pbr viewing
This commit is contained in:
@@ -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)"):
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user