172 lines
6.8 KiB
TypeScript
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);
|
|
}
|