diff --git a/nodes/visualization/preview_mesh_vtk.py b/nodes/visualization/preview_mesh_vtk.py
index 03f4a6a8..8fa26b15 100644
--- a/nodes/visualization/preview_mesh_vtk.py
+++ b/nodes/visualization/preview_mesh_vtk.py
@@ -41,9 +41,12 @@ class PreviewMeshVTKNode:
def INPUT_TYPES(cls):
return {
"required": {
- "trimesh": ("TRIMESH",),
"mode": (["fields", "texture", "texture (PBR)"], {"default": "fields"}),
},
+ "optional": {
+ "trimesh": ("TRIMESH",),
+ "voxelgrid": ("VOXELGRID",),
+ },
}
RETURN_TYPES = ()
@@ -51,7 +54,7 @@ class PreviewMeshVTKNode:
FUNCTION = "preview_mesh_vtk"
CATEGORY = "geompack/visualization"
- def preview_mesh_vtk(self, trimesh, mode="fields"):
+ def preview_mesh_vtk(self, mode="fields", trimesh=None, voxelgrid=None):
"""
Export mesh and prepare for VTK.js preview.
@@ -60,17 +63,26 @@ class PreviewMeshVTKNode:
- texture: Textured mesh visualization with materials (GLB export)
Args:
- trimesh: Input trimesh_module.Trimesh object or VOXEL_GRID tensor
mode: Visualization mode - "fields" or "texture"
+ trimesh: Input trimesh_module.Trimesh object (optional)
+ voxelgrid: Input trimesh.VoxelGrid object (optional)
Returns:
dict: UI data for frontend widget
"""
+ # Use whichever input is provided
+ if trimesh is None and voxelgrid is None:
+ raise ValueError("Either trimesh or voxelgrid input is required")
+
+ mesh_input = trimesh if trimesh is not None else voxelgrid
+
# Handle VOXEL_GRID input (trimesh.VoxelGrid from MeshToVoxel node)
- if hasattr(trimesh, 'as_boxes'): # It's a trimesh.VoxelGrid
- voxel_shape = trimesh.matrix.shape
- trimesh = trimesh.as_boxes()
+ if hasattr(mesh_input, 'as_boxes'): # It's a trimesh.VoxelGrid
+ voxel_shape = mesh_input.matrix.shape
+ trimesh = mesh_input.as_boxes()
print(f"[PreviewMeshVTK] Converted voxel grid {voxel_shape} to box mesh: {len(trimesh.vertices)} vertices")
+ else:
+ trimesh = mesh_input
print(f"[PreviewMeshVTK] Preparing preview: {get_geometry_type(trimesh)} - {len(trimesh.vertices)} vertices, {get_face_count(trimesh)} faces")
@@ -133,6 +145,20 @@ class PreviewMeshVTKNode:
else:
filepath = os.path.join(tempfile.gettempdir(), filename)
+ # Set alpha mode based on viewing mode
+ if mode == "texture":
+ # Texture mode: force opaque for solid color viewing (RGB only)
+ if hasattr(trimesh, 'visual') and hasattr(trimesh.visual, 'material'):
+ if hasattr(trimesh.visual.material, 'alphaMode'):
+ trimesh.visual.material.alphaMode = 'OPAQUE'
+ print(f"[PreviewMeshVTK] Set alphaMode to OPAQUE for texture mode")
+ elif mode == "texture (PBR)":
+ # PBR mode: use BLEND for transparency support
+ if hasattr(trimesh, 'visual') and hasattr(trimesh.visual, 'material'):
+ if hasattr(trimesh.visual.material, 'alphaMode'):
+ trimesh.visual.material.alphaMode = 'BLEND'
+ print(f"[PreviewMeshVTK] Set alphaMode to BLEND for PBR mode")
+
# Export mesh
try:
if mode in ("texture", "texture (PBR)"):
diff --git a/web/js/gaussian_preview.js b/web/js/gaussian_preview.js
index 646700ee..5c6bcd79 100644
--- a/web/js/gaussian_preview.js
+++ b/web/js/gaussian_preview.js
@@ -68,12 +68,31 @@ app.registerExtension({
setValue(v) { }
});
- widget.computeSize = () => [512, 580];
+ // Store reference to node for dynamic resizing
+ const node = this;
+ let currentNodeSize = [512, 580];
+
+ widget.computeSize = () => currentNodeSize;
// Store references
this.gaussianViewerIframe = iframe;
this.gaussianInfoPanel = infoPanel;
+ // Function to resize node dynamically
+ this.resizeToAspectRatio = function(imageWidth, imageHeight) {
+ const aspectRatio = imageWidth / imageHeight;
+ const nodeWidth = 512;
+ const viewerHeight = Math.round(nodeWidth / aspectRatio);
+ const nodeHeight = viewerHeight + 60; // Add space for info panel
+
+ currentNodeSize = [nodeWidth, nodeHeight];
+ node.setSize(currentNodeSize);
+ node.setDirtyCanvas(true, true);
+ app.graph.setDirtyCanvas(true, true);
+
+ console.log("[GeomPack Gaussian] Resized node to:", nodeWidth, "x", nodeHeight, "(aspect ratio:", aspectRatio.toFixed(2), ")");
+ };
+
// Track iframe load state
let iframeLoaded = false;
iframe.addEventListener('load', () => {
@@ -158,6 +177,13 @@ app.registerExtension({
const extrinsics = message.extrinsics?.[0] || null;
const intrinsics = message.intrinsics?.[0] || null;
+ // Resize node to match image aspect ratio from intrinsics
+ if (intrinsics && intrinsics[0] && intrinsics[1]) {
+ const imageWidth = intrinsics[0][2] * 2; // cx * 2
+ const imageHeight = intrinsics[1][2] * 2; // cy * 2
+ this.resizeToAspectRatio(imageWidth, imageHeight);
+ }
+
// Update info panel
infoPanel.innerHTML = `
diff --git a/web/viewer_gaussian.html b/web/viewer_gaussian.html
index aec0b3a5..6123d4a3 100644
--- a/web/viewer_gaussian.html
+++ b/web/viewer_gaussian.html
@@ -221,15 +221,16 @@
const canvasWidth = canvas.clientWidth || 512;
const canvasHeight = canvas.clientHeight || 512;
- // Scale focal lengths proportionally to canvas size
- const scaledFx = fx * (canvasWidth / imageWidth);
- const scaledFy = fy * (canvasHeight / imageHeight);
+ // Use uniform scale factor (canvas should have same aspect ratio as image)
+ // This keeps fx = fy for undistorted view
+ const scale = canvasWidth / imageWidth;
+ const scaledF = fx * scale; // fx and fy are equal in SHARP
- camera.data.fx = scaledFx;
- camera.data.fy = scaledFy;
+ camera.data.fx = scaledF;
+ camera.data.fy = scaledF;
console.log('[GaussianViewer] Canvas size:', canvasWidth, 'x', canvasHeight);
- console.log('[GaussianViewer] Scaled focal lengths: fx=', scaledFx, 'fy=', scaledFy);
+ console.log('[GaussianViewer] Uniform scale:', scale, 'scaled focal length:', scaledF);
// Calculate vertical FOV in radians for logging
const fovY = 2 * Math.atan(imageHeight / (2 * fy));
@@ -245,23 +246,38 @@
}
// Determine the target position (what the camera looks at)
- // Use scene bounds center if available, otherwise use calculated targetZ
+ // For the INITIAL view (matching original image), look STRAIGHT AHEAD along Z axis
+ // NOT at the scene center - that would tilt the camera
let targetX = 0, targetY = 0;
+ let orbitZ = targetZ; // For orbiting, use a closer point
if (splat && splat.bounds) {
const center = splat.bounds.center();
- targetX = center.x;
- targetY = center.y;
+ const size = splat.bounds.size();
+
+ // Use scene center Z for the view direction
targetZ = center.z;
- console.log('[GaussianViewer] Using scene center as target:', targetX, targetY, targetZ);
+
+ // For orbit center, use distance to closest object + 50%
+ // Camera is at origin, so minZ is the distance to closest point
+ const minZ = center.z - size.z / 2;
+ orbitZ = minZ * 1.5; // 50% past the closest point
+
+ console.log('[GaussianViewer] Scene center:', center.x, -center.y, center.z);
+ console.log('[GaussianViewer] Closest point Z:', minZ.toFixed(1));
+ console.log('[GaussianViewer] Orbit center Z:', orbitZ.toFixed(1), '(closest + 50%)');
}
+ // SHARP uses Y-down coordinate system, so flip camera Y as well
+ camPosY = -camPosY;
+
// For SHARP: camera is at origin, scene is at positive Z
// The orbit controls work by having the camera orbit around a target point.
// To get the "original image view", we set camera at the computed position
// and look at the scene center.
const camPos = new SPLAT.Vector3(camPosX, camPosY, camPosZ);
- const targetPos = new SPLAT.Vector3(targetX, targetY, targetZ);
+ // Use orbitZ for the target - closer to camera for better orbit feel
+ const targetPos = new SPLAT.Vector3(targetX, targetY, orbitZ);
// Store for reset
initialCameraData = {
@@ -271,10 +287,15 @@
fy: camera.data.fy
};
- // Set camera position
- camera.data.position = camPos;
+ // Set camera position - IMPORTANT: must set camera.position, not camera.data.position
+ // camera.position is used by OrbitControls
+ // camera.data.position is used for rendering
+ camera.position.x = camPosX;
+ camera.position.y = camPosY;
+ camera.position.z = camPosZ;
// Update orbit controls to orbit around the target point
+ // This recalculates the internal orbit state from current camera.position
controls.setCameraTarget(targetPos);
console.log('[GaussianViewer] Camera set to position:', camPosX, camPosY, camPosZ);
@@ -285,11 +306,15 @@
// Load a PLY file from ArrayBuffer data
async function loadPLYFromData(arrayBuffer, filename, extrinsics, intrinsics) {
try {
- // Clear existing splat
- if (currentSplat) {
- scene.removeObject(currentSplat);
- currentSplat = null;
+ // Clear ALL existing objects from scene
+ if (scene.objects && scene.objects.length > 0) {
+ console.log('[GaussianViewer] Clearing', scene.objects.length, 'existing objects from scene');
+ // Remove all objects (iterate backwards to avoid index issues)
+ while (scene.objects.length > 0) {
+ scene.removeObject(scene.objects[0]);
+ }
}
+ currentSplat = null;
console.log('[GaussianViewer] Loading from data, size:', arrayBuffer.byteLength);
@@ -351,20 +376,32 @@
function resetCamera() {
if (camera && controls) {
if (initialCameraData) {
- camera.data.position = initialCameraData.position;
- if (initialCameraData.target) {
- controls.setCameraTarget(initialCameraData.target);
- }
+ // Set camera.position (used by OrbitControls), not camera.data.position
+ const pos = initialCameraData.position;
+ camera.position.x = pos.x;
+ camera.position.y = pos.y;
+ camera.position.z = pos.z;
+
+ // Restore focal lengths
if (initialCameraData.fx) {
camera.data.fx = initialCameraData.fx;
}
if (initialCameraData.fy) {
camera.data.fy = initialCameraData.fy;
}
- console.log('[GaussianViewer] Reset camera to initial position');
+
+ // Set the target - this recalculates orbit state from current camera.position
+ if (initialCameraData.target) {
+ controls.setCameraTarget(initialCameraData.target);
+ }
+
+ console.log('[GaussianViewer] Reset camera to position:', pos.x, pos.y, pos.z);
+ console.log('[GaussianViewer] Reset camera target:', initialCameraData.target?.x, initialCameraData.target?.y, initialCameraData.target?.z);
} else {
// Default fallback
- camera.data.position = new SPLAT.Vector3(0, 0, 5);
+ camera.position.x = 0;
+ camera.position.y = 0;
+ camera.position.z = 5;
console.log('[GaussianViewer] Reset camera to default position (0, 0, 5)');
}
controls.update();
diff --git a/web/viewer_pbr.html b/web/viewer_pbr.html
index 0d012852..0bd4ec0d 100644
--- a/web/viewer_pbr.html
+++ b/web/viewer_pbr.html
@@ -78,6 +78,7 @@
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
+ renderer.sortObjects = true; // Enable proper transparency sorting
// Controls
const controls = new OrbitControls(camera, canvas);
@@ -106,18 +107,26 @@
const extPath = getExtensionPath();
const exrLoader = new EXRLoader();
+ const exrPath = `${extPath}/assets/studio.exr`;
+ console.log('[GeomPack PBR Viewer] Loading environment map from:', exrPath);
+
exrLoader.load(
- `${extPath}/assets/studio.exr`,
+ exrPath,
(texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture;
envMapLoaded = true;
- console.log('[GeomPack PBR Viewer] Environment map loaded');
+ console.log('[GeomPack PBR Viewer] Environment map loaded successfully');
resolve(texture);
},
- undefined,
+ (progress) => {
+ if (progress.lengthComputable) {
+ console.log('[GeomPack PBR Viewer] Env map loading:', Math.round(progress.loaded / progress.total * 100) + '%');
+ }
+ },
(error) => {
- console.warn('[GeomPack PBR Viewer] Could not load EXR environment, using fallback');
+ console.error('[GeomPack PBR Viewer] Failed to load EXR environment:', error);
+ console.warn('[GeomPack PBR Viewer] Using fallback gradient environment');
// Fallback: create a simple gradient environment
createFallbackEnvironment();
resolve(null);
@@ -252,6 +261,20 @@
materials.forEach(mat => {
if (mat.isMeshStandardMaterial || mat.isMeshPhysicalMaterial) {
mat.envMapIntensity = 1.0;
+
+ // Enable transparency support
+ // Check if baseColorTexture has alpha channel
+ const hasAlphaMap = mat.map && mat.map.format === THREE.RGBAFormat;
+ const hasAlphaTest = mat.alphaTest > 0;
+ const needsTransparency = mat.transparent || mat.opacity < 1.0 || hasAlphaMap;
+
+ if (needsTransparency || hasAlphaMap) {
+ mat.transparent = true;
+ mat.alphaTest = 0.01; // Discard nearly-transparent pixels
+ mat.side = THREE.DoubleSide; // Render both sides
+ mat.depthWrite = true; // Keep depth writing for proper sorting
+ }
+
mat.needsUpdate = true;
// Check if has textures
@@ -263,9 +286,14 @@
name: mat.name,
metalness: mat.metalness,
roughness: mat.roughness,
+ opacity: mat.opacity,
+ transparent: mat.transparent,
+ alphaTest: mat.alphaTest,
hasMap: !!mat.map,
+ mapFormat: mat.map ? mat.map.format : 'none',
hasMetalnessMap: !!mat.metalnessMap,
- hasRoughnessMap: !!mat.roughnessMap
+ hasRoughnessMap: !!mat.roughnessMap,
+ hasAlphaMap: hasAlphaMap
});
}
});
diff --git a/web/viewer_vtk.html b/web/viewer_vtk.html
index 4e990eb4..305a3006 100644
--- a/web/viewer_vtk.html
+++ b/web/viewer_vtk.html
@@ -285,8 +285,12 @@
} else {
// For meshes, use normals filter for proper shading
const normalsFilter = vtkPolyDataNormals.newInstance();
- normalsFilter.setInputConnection(currentReader.getOutputPort());
- mapper.setInputConnection(normalsFilter.getOutputPort());
+ normalsFilter.setInputData(polydata);
+ normalsFilter.update();
+ // Use the filtered polydata so scalars are preserved
+ const filteredPolyData = normalsFilter.getOutputData();
+ mapper.setInputData(filteredPolyData);
+ currentPolyData = filteredPolyData; // Update reference for field visualization
}
mapper.update();