Files
workinf_Blender_Wasm/web/app/src/three-adapter/paint-depth-visibility.ts
mes123456 0fe8d2bb56
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

172 lines
6.8 KiB
TypeScript

import {
Color,
InstancedMesh,
Matrix4,
MeshDepthMaterial,
RGBADepthPacking,
Vector2,
Vector3,
WebGLRenderTarget,
type Object3D,
type PerspectiveCamera,
type Scene,
type WebGLRenderer,
} from "../vendor/three/three.module.js";
import {
PAINT_DEPTH_VISIBILITY_BUDGET,
PaintDepthVisibilityError,
validatePaintDepthVisibilityResult,
type PaintDepthVisibilityBackend,
type PaintDepthVisibilityRequestIR,
type PaintDepthVisibilityResultIR,
} from "../../../protocol/paint-depth-visibility";
const PACK_DOWNSCALE = 255 / 256;
const DEPTH_TOLERANCE_RATIO = 0.002;
function unpackRGBADepth(pixels: Uint8Array, offset: number): number {
return (pixels[offset] / 255) * PACK_DOWNSCALE +
(pixels[offset + 1] / 255) * (PACK_DOWNSCALE / 256) +
(pixels[offset + 2] / 255) * (PACK_DOWNSCALE / 65_536) +
(pixels[offset + 3] / 255) / 16_777_216;
}
function perspectiveDistance(depth: number, near: number, far: number): number {
const viewZ = (near * far) / ((far - near) * depth - far);
return -viewZ;
}
function depthTargetSize(renderer: WebGLRenderer): { width: number; height: number } {
const size = renderer.getDrawingBufferSize(new Vector2());
const sourceWidth = Math.max(1, Math.floor(size.x));
const sourceHeight = Math.max(1, Math.floor(size.y));
const maxPixels = Math.floor(PAINT_DEPTH_VISIBILITY_BUDGET.maxReadbackBytes / 4);
const scale = Math.min(
1,
PAINT_DEPTH_VISIBILITY_BUDGET.maxDimension / sourceWidth,
PAINT_DEPTH_VISIBILITY_BUDGET.maxDimension / sourceHeight,
Math.sqrt(maxPixels / (sourceWidth * sourceHeight)),
);
return {
width: Math.max(1, Math.floor(sourceWidth * scale)),
height: Math.max(1, Math.floor(sourceHeight * scale)),
};
}
function candidateMatrix(object: Object3D, objectId: string): Matrix4 {
object.updateWorldMatrix(true, false);
if (!(object instanceof InstancedMesh)) return object.matrixWorld.clone();
const instanceIds = object.userData.instanceNodeIds as string[] | undefined;
const instanceIndex = instanceIds?.indexOf(objectId) ?? -1;
if (instanceIndex < 0) throw new PaintDepthVisibilityError("PAINT_DEPTH_UNAVAILABLE", "Paint object instance is unavailable");
const instance = new Matrix4();
object.getMatrixAt(instanceIndex, instance);
return new Matrix4().multiplyMatrices(object.matrixWorld, instance);
}
export function samplePaintDepthVisibilityGPU({
renderer,
scene,
camera,
object,
request,
backend,
}: {
renderer: WebGLRenderer;
scene: Scene;
camera: PerspectiveCamera;
object: Object3D;
request: PaintDepthVisibilityRequestIR;
backend: PaintDepthVisibilityBackend;
}): PaintDepthVisibilityResultIR {
if (!renderer.capabilities.isWebGL2) throw new PaintDepthVisibilityError("PAINT_DEPTH_UNAVAILABLE", "Paint visibility requires a WebGL2 depth pass");
const meshId = object.userData.meshId;
const sourcePositions = object.userData.sourcePositions as number[] | undefined;
if (meshId !== request.meshId || !sourcePositions || sourcePositions.length % 3 !== 0) {
throw new PaintDepthVisibilityError("PAINT_DEPTH_UNAVAILABLE", "Paint source geometry is unavailable");
}
const vertexCount = sourcePositions.length / 3;
if (request.vertexIndices.some((index) => index >= vertexCount)) {
throw new PaintDepthVisibilityError("PAINT_SCHEMA_INVALID", "Paint depth request references an unknown vertex");
}
const { width, height } = depthTargetSize(renderer);
const target = new WebGLRenderTarget(width, height, { depthBuffer: true, stencilBuffer: false });
const depthMaterial = new MeshDepthMaterial({ depthPacking: RGBADepthPacking });
const pixels = new Uint8Array(width * height * 4);
const previousTarget = renderer.getRenderTarget();
const previousOverride = scene.overrideMaterial;
const previousBackground = scene.background;
const previousClearColor = renderer.getClearColor(new Color()).clone();
const previousClearAlpha = renderer.getClearAlpha();
const hidden: Object3D[] = [];
try {
scene.traverse((candidate) => {
const renderable = candidate as Object3D & { isLine?: boolean; isPoints?: boolean; isSprite?: boolean };
if (candidate.visible && (renderable.isLine || renderable.isPoints || renderable.isSprite || candidate.userData.nanoVDBVolume)) {
hidden.push(candidate);
candidate.visible = false;
}
});
scene.background = null;
scene.overrideMaterial = depthMaterial;
renderer.setClearColor(0xffffff, 1);
renderer.setRenderTarget(target);
renderer.clear(true, true, true);
camera.updateMatrixWorld(true);
scene.updateMatrixWorld(true);
renderer.render(scene, camera);
renderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);
}
catch (error) {
throw new PaintDepthVisibilityError("PAINT_DEPTH_UNAVAILABLE", error instanceof Error ? error.message : "GPU depth readback failed");
}
finally {
renderer.setRenderTarget(previousTarget);
renderer.setClearColor(previousClearColor, previousClearAlpha);
scene.overrideMaterial = previousOverride;
scene.background = previousBackground;
for (const candidate of hidden) candidate.visible = true;
depthMaterial.dispose();
target.dispose();
}
let occluderPixelCount = 0;
for (let offset = 0; offset < pixels.length; offset += 4) {
if (unpackRGBADepth(pixels, offset) < 1 - 1e-7) occluderPixelCount++;
}
if (occluderPixelCount === 0) throw new PaintDepthVisibilityError("PAINT_DEPTH_UNAVAILABLE", "GPU depth pass produced no mesh coverage");
const modelMatrix = candidateMatrix(object, request.objectId);
const visibleVertexIndices: number[] = [];
for (const index of request.vertexIndices) {
const sourceOffset = index * 3;
const world = new Vector3(
sourcePositions[sourceOffset],
sourcePositions[sourceOffset + 2],
-sourcePositions[sourceOffset + 1],
).applyMatrix4(modelMatrix);
const viewDistance = -world.clone().applyMatrix4(camera.matrixWorldInverse).z;
const projected = world.project(camera);
if (projected.x < -1 || projected.x > 1 || projected.y < -1 || projected.y > 1 || projected.z < -1 || projected.z > 1 || viewDistance <= 0) continue;
const x = Math.max(0, Math.min(width - 1, Math.round((projected.x * 0.5 + 0.5) * (width - 1))));
const y = Math.max(0, Math.min(height - 1, Math.round((projected.y * 0.5 + 0.5) * (height - 1))));
const depth = unpackRGBADepth(pixels, (y * width + x) * 4);
const sampledDistance = perspectiveDistance(depth, camera.near, camera.far);
const tolerance = Math.max(1e-4, viewDistance * DEPTH_TOLERANCE_RATIO);
if (viewDistance <= sampledDistance + tolerance) visibleVertexIndices.push(index);
}
return validatePaintDepthVisibilityResult({
...request,
backend,
source: "GPU_RGBA_DEPTH_READBACK",
width,
height,
depthReadbackBytes: pixels.byteLength,
occluderPixelCount,
visibleVertexIndices,
}, request);
}