117 lines
5.5 KiB
TypeScript
117 lines
5.5 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
test("M9-09 derives the same occlusion set from real main-thread and Offscreen GPU depth passes", async ({ page }) => {
|
|
test.setTimeout(60_000);
|
|
await page.goto("/");
|
|
const result = await page.evaluate(async () => {
|
|
const { ViewportRenderer } = await import("/src/three-adapter/viewport.ts");
|
|
const { OffscreenViewportRenderer } = await import("/src/three-adapter/offscreen-viewport.ts");
|
|
|
|
const normalize = (value: number[]) => {
|
|
const length = Math.hypot(...value);
|
|
return value.map((component) => component / length);
|
|
};
|
|
const cross = (left: number[], right: number[]) => [
|
|
left[1] * right[2] - left[2] * right[1],
|
|
left[2] * right[0] - left[0] * right[2],
|
|
left[0] * right[1] - left[1] * right[0],
|
|
];
|
|
const add = (...values: number[][]) => values[0].map((_, axis) => values.reduce((sum, value) => sum + value[axis], 0));
|
|
const scale = (value: number[], factor: number) => value.map((component) => component * factor);
|
|
const camera = [
|
|
7 * Math.cos(0.55) * Math.cos(-Math.PI / 4),
|
|
7 * Math.cos(0.55) * Math.sin(-Math.PI / 4),
|
|
7 * Math.sin(0.55),
|
|
];
|
|
const direction = normalize(camera.map((component) => -component));
|
|
const horizontal = normalize(cross(direction, [0, 0, 1]));
|
|
const vertical = normalize(cross(direction, horizontal));
|
|
const front = [
|
|
add(scale(horizontal, -2), scale(vertical, -2)),
|
|
add(scale(horizontal, -2), scale(vertical, 2)),
|
|
add(scale(horizontal, 2), scale(vertical, 2)),
|
|
add(scale(horizontal, 2), scale(vertical, -2)),
|
|
];
|
|
const behind = scale(direction, 1.5);
|
|
const back = [
|
|
add(behind, scale(horizontal, -0.12), scale(vertical, -0.08)),
|
|
add(behind, scale(horizontal, 0.12), scale(vertical, -0.08)),
|
|
add(behind, scale(vertical, 0.12)),
|
|
];
|
|
const toBlender = (value: number[]) => [value[0], -value[2], value[1]];
|
|
const positions = new Float32Array([...front, ...back].flatMap(toBlender));
|
|
const indices = new Uint32Array([0, 1, 2, 0, 2, 3, 4, 6, 5]);
|
|
const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
const snapshot = {
|
|
schemaVersion: 1 as const,
|
|
revision: 9,
|
|
sceneId: "scene:paint-depth",
|
|
source: { kind: "mock" as const },
|
|
coordinateSystem: { upAxis: "Z" as const, forwardAxis: "-Y" as const, handedness: "RIGHT" as const, unitSystem: 0, unitScale: 1 },
|
|
nodes: [{
|
|
id: "object:PaintDepth",
|
|
name: "PaintDepth",
|
|
type: "MESH" as const,
|
|
parentId: null,
|
|
dataId: "mesh:PaintDepth",
|
|
visible: true,
|
|
selectable: true,
|
|
localMatrix: identity,
|
|
worldMatrix: identity,
|
|
transform: { translation: [0, 0, 0] as [number, number, number], rotationEuler: [0, 0, 0] as [number, number, number], scale: [1, 1, 1] as [number, number, number], rotationMode: 1 },
|
|
}],
|
|
meshes: [{ id: "mesh:PaintDepth", name: "PaintDepth", vertexCount: 7, edgeCount: 0, faceCount: 3, cornerCount: 9, triangleCount: 3, geometryStatus: "binary" as const }],
|
|
materials: [], cameras: [], lights: [], worlds: [], images: [], animations: [], collections: [], scenes: [],
|
|
activeObjectId: "object:PaintDepth",
|
|
frame: { current: 1, start: 1, end: 1 },
|
|
};
|
|
const geometry = {
|
|
schemaVersion: 1 as const,
|
|
meshId: "mesh:PaintDepth",
|
|
byteLength: positions.byteLength + indices.byteLength,
|
|
positions: positions.buffer,
|
|
indices: indices.buffer,
|
|
};
|
|
const request = { schemaVersion: 1 as const, objectId: "object:PaintDepth", meshId: "mesh:PaintDepth", revision: 9, vertexIndices: [0, 1, 2, 3, 4, 5, 6] };
|
|
|
|
const createCanvas = () => {
|
|
const canvas = document.createElement("canvas");
|
|
canvas.style.width = "320px";
|
|
canvas.style.height = "240px";
|
|
document.body.append(canvas);
|
|
return canvas;
|
|
};
|
|
|
|
const mainCanvas = createCanvas();
|
|
const mainRenderer = new ViewportRenderer(mainCanvas);
|
|
mainRenderer.setSnapshot(snapshot, [geometry]);
|
|
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
const main = await mainRenderer.samplePaintVisibility(request);
|
|
mainRenderer.dispose();
|
|
mainCanvas.remove();
|
|
|
|
const offscreenCanvas = createCanvas();
|
|
const offscreenRenderer = new OffscreenViewportRenderer(offscreenCanvas);
|
|
offscreenRenderer.setSnapshot(snapshot, [geometry]);
|
|
let stale = "";
|
|
try { await offscreenRenderer.samplePaintVisibility({ ...request, revision: 8 }); }
|
|
catch (error) { stale = error instanceof Error ? error.message : String(error); }
|
|
const offscreen = await offscreenRenderer.samplePaintVisibility(request);
|
|
offscreenRenderer.dispose();
|
|
offscreenCanvas.remove();
|
|
|
|
return { main, offscreen, stale };
|
|
});
|
|
|
|
expect(result.main.backend).toBe("MAIN_THREAD_WEBGL2");
|
|
expect(result.offscreen.backend).toBe("OFFSCREEN_WEBGL2");
|
|
expect(result.main.source).toBe("GPU_RGBA_DEPTH_READBACK");
|
|
expect(result.main.occluderPixelCount).toBeGreaterThan(0);
|
|
expect(result.offscreen.occluderPixelCount).toBeGreaterThan(0);
|
|
expect(result.main.depthReadbackBytes).toBe(result.main.width * result.main.height * 4);
|
|
expect(result.offscreen.depthReadbackBytes).toBe(result.offscreen.width * result.offscreen.height * 4);
|
|
expect(result.main.visibleVertexIndices).toEqual([0, 1, 2, 3]);
|
|
expect(result.offscreen.visibleVertexIndices).toEqual(result.main.visibleVertexIndices);
|
|
expect(result.stale).toContain("REVISION_CONFLICT");
|
|
});
|