pbr viewing

This commit is contained in:
Andrea Pozzetti
2026-02-12 21:48:40 +01:00
parent a2faf93715
commit b10d8a8b0e
5 changed files with 158 additions and 37 deletions
+32 -6
View File
@@ -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)"):
+27 -1
View File
@@ -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 = `
<div style="display: grid; grid-template-columns: auto 1fr; gap: 2px 8px;">
+60 -23
View File
@@ -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();
+33 -5
View File
@@ -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
});
}
});
+6 -2
View File
@@ -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();