Add local glTF viewer and visual coverage
Amp-Thread-ID: https://ampcode.com/threads/T-01a02d1c-27fc-7761-9f51-ed97b2dd4e3e Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { Mesh } from "../src/Mesh";
|
||||
import { Node } from "../src/Node";
|
||||
import { ArcRotateCamera } from "../src/camera/ArcRotateCamera";
|
||||
import { PointLight } from "../src/lights/PointLight";
|
||||
import { PBRMaterial } from "../src/materials/PBRMaterial";
|
||||
import type { Texture } from "../src/materials/Texture";
|
||||
import { createScene, testCore, triangle } from "./scene-helpers";
|
||||
|
||||
function close(actual: ArrayLike<number>, expected: ArrayLike<number>) {
|
||||
assert.equal(actual.length, expected.length);
|
||||
for (let index = 0; index < actual.length; index++)
|
||||
assert.ok(
|
||||
Math.abs(actual[index] - expected[index]) < 1e-5,
|
||||
`${actual[index]} != ${expected[index]} at ${index}`,
|
||||
);
|
||||
}
|
||||
|
||||
test("PBRMaterial packs factors, extensions, texture pointers, and UV transforms", async () => {
|
||||
const scene = await createScene();
|
||||
const textures = Array.from(
|
||||
{ length: 13 },
|
||||
(_, id) => ({ id, ready: Promise.resolve() }) as unknown as Texture,
|
||||
);
|
||||
const material = new PBRMaterial(scene, {
|
||||
baseColor: [0.1, 0.2, 0.3, 0.4],
|
||||
metallic: 0.5,
|
||||
roughness: 0.6,
|
||||
emissive: [0.7, 0.8, 0.9],
|
||||
normalScale: 0.75,
|
||||
alphaCutoff: 0.25,
|
||||
flags: 6,
|
||||
baseColorTexture: textures[0],
|
||||
metallicRoughnessTexture: textures[1],
|
||||
normalTexture: textures[2],
|
||||
emissiveTexture: textures[3],
|
||||
occlusionTexture: textures[4],
|
||||
transmissionTexture: textures[5],
|
||||
thicknessTexture: textures[6],
|
||||
clearcoatTexture: textures[7],
|
||||
iridescenceTexture: textures[8],
|
||||
iridescenceThicknessTexture: textures[9],
|
||||
anisotropyTexture: textures[10],
|
||||
specularTexture: textures[11],
|
||||
specularColorTexture: textures[12],
|
||||
transmission: 0.8,
|
||||
ior: 1.6,
|
||||
thickness: 0.4,
|
||||
attenuationDistance: 3,
|
||||
attenuationColor: [0.1, 0.2, 0.3],
|
||||
emissiveStrength: 4,
|
||||
clearcoat: 0.5,
|
||||
clearcoatRoughness: 0.6,
|
||||
specular: 0.7,
|
||||
occlusionStrength: 0.8,
|
||||
sheenColor: [0.9, 0.1, 0.2],
|
||||
sheenRoughness: 0.3,
|
||||
iridescence: 0.4,
|
||||
iridescenceIor: 1.4,
|
||||
iridescenceThicknessMinimum: 120,
|
||||
iridescenceThicknessMaximum: 380,
|
||||
anisotropy: 0.5,
|
||||
anisotropyRotation: 0.6,
|
||||
specularExtensionColor: [0.7, 0.8, 0.9],
|
||||
textureInfos: [
|
||||
{
|
||||
offset: [0.1, 0.2],
|
||||
scale: [0.3, 0.4],
|
||||
rotation: 0.5,
|
||||
texCoord: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
await material.ready;
|
||||
|
||||
close(
|
||||
scene.array("materials").row(material.id),
|
||||
[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.75, 0.25, 6],
|
||||
);
|
||||
assert.deepEqual(
|
||||
Array.from(scene.array("materialTextures").row(material.id)),
|
||||
[...Array.from({ length: 13 }, (_, index) => index + 1), 0, 0, 0],
|
||||
);
|
||||
close(
|
||||
scene.array("materialExtensions").row(material.id),
|
||||
[
|
||||
0.8, 1.6, 0.4, 3, 0.1, 0.2, 0.3, 4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1, 0.2,
|
||||
0.3, 0.4, 1.4, 120, 380, 0.5, 0.6, 0, 0, 0.7, 0.8, 0.9, 0,
|
||||
],
|
||||
);
|
||||
const transforms = scene.array("materialTextureInfo").row(material.id);
|
||||
close(transforms.subarray(0, 8), [
|
||||
0.1,
|
||||
0.2,
|
||||
0.3,
|
||||
0.4,
|
||||
Math.sin(0.5),
|
||||
Math.cos(0.5),
|
||||
1,
|
||||
0,
|
||||
]);
|
||||
close(transforms.subarray(8, 16), [0, 0, 1, 1, 0, 1, 0, 0]);
|
||||
});
|
||||
|
||||
test("hot material setters write shared rows without rebuilding the graph", async () => {
|
||||
const scene = await createScene();
|
||||
const material = new PBRMaterial(scene);
|
||||
await material.ready;
|
||||
const core = testCore(scene);
|
||||
const compiled = core.graphs.length;
|
||||
core.array("signals").row(0).fill(0);
|
||||
|
||||
material.baseColor = [0.2, 0.3, 0.4, 0.5];
|
||||
material.metallic = 0.6;
|
||||
material.roughness = 0.7;
|
||||
material.emissive = [0.8, 0.9, 1];
|
||||
|
||||
close(
|
||||
scene.array("materials").row(material.id),
|
||||
[0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1, 0.5, 0],
|
||||
);
|
||||
assert.equal(core.array("signals").row(0)[5], 1);
|
||||
assert.equal(core.graphs.length, compiled);
|
||||
});
|
||||
|
||||
test("Mesh packs geometry and bounds, then clones with copy-on-write", async () => {
|
||||
const scene = await createScene();
|
||||
const firstMaterial = new PBRMaterial(scene);
|
||||
const secondMaterial = new PBRMaterial(scene, { baseColor: [1, 0, 0, 1] });
|
||||
await Promise.all([firstMaterial.ready, secondMaterial.ready]);
|
||||
const mesh = new Mesh(scene, {
|
||||
material: firstMaterial,
|
||||
vertexData: triangle,
|
||||
});
|
||||
await mesh.ready;
|
||||
|
||||
assert.equal(mesh.vertexCount, 3);
|
||||
assert.equal(mesh.indexCount, 3);
|
||||
close(scene.geometryData(mesh.geometryId, "positions")!, triangle.positions);
|
||||
close(scene.array("bounds").row(mesh.id), [-1, -1, 0, 0, 1, 1, 0, 0]);
|
||||
|
||||
const clone = mesh.clone({ position: [2, 0, 0] });
|
||||
await clone.ready;
|
||||
assert.equal(clone.geometryId, mesh.geometryId);
|
||||
await clone.setVertexData("positions", [0, 0, 0, 2, 0, 0, 0, 3, 0]);
|
||||
assert.notEqual(clone.geometryId, mesh.geometryId);
|
||||
close(scene.geometryData(mesh.geometryId, "positions")!, triangle.positions);
|
||||
close(
|
||||
scene.geometryData(clone.geometryId, "positions")!,
|
||||
[0, 0, 0, 2, 0, 0, 0, 3, 0],
|
||||
);
|
||||
|
||||
clone.material = secondMaterial;
|
||||
await clone.setMaterialForFaces(firstMaterial, 0);
|
||||
assert.equal(clone.materialId, secondMaterial.id);
|
||||
assert.equal(clone.faceMaterials.get(0), firstMaterial.id);
|
||||
assert.equal(
|
||||
scene.array(`mesh.${clone.id}.faceMaterials`).row(0)[0],
|
||||
firstMaterial.id + 1,
|
||||
);
|
||||
});
|
||||
|
||||
test("Node, ArcRotateCamera, and light handles update their shared rows", async () => {
|
||||
const scene = await createScene();
|
||||
const target = new Node(scene, { position: [1, 2, 3], scale: [2, 3, 4] });
|
||||
await target.ready;
|
||||
target.translate([1, -1, 2]).rotateZ(Math.PI);
|
||||
close(target.position, [2, 1, 5]);
|
||||
close(target.scale, [2, 3, 4]);
|
||||
close(target.rotor, [0, 0, 1, 0]);
|
||||
|
||||
const child = new Node(scene, { parent: target });
|
||||
await child.ready;
|
||||
assert.equal(child.parentId, target.id);
|
||||
child.enabled = false;
|
||||
assert.equal(scene.array("nodes").row(child.id)[0], 0);
|
||||
|
||||
const camera = new ArcRotateCamera(scene, {
|
||||
target,
|
||||
alpha: 0,
|
||||
beta: Math.PI / 2,
|
||||
radius: 5,
|
||||
});
|
||||
await camera.ready;
|
||||
close(camera.position, [2, 1, 10]);
|
||||
assert.equal(scene.array("cameraMatrices").row(camera.cameraId)[19], 1);
|
||||
camera.radius = 3;
|
||||
close(camera.position, [2, 1, 8]);
|
||||
|
||||
const light = new PointLight(scene, {
|
||||
position: [4, 5, 6],
|
||||
color: [0.2, 0.4, 0.6],
|
||||
intensity: 7,
|
||||
range: 8,
|
||||
});
|
||||
await light.ready;
|
||||
close(
|
||||
scene.array("pointLights").row(light.id),
|
||||
[0.2, 0.4, 0.6, 7, 8, 1, 0, 0],
|
||||
);
|
||||
light.intensity = 9;
|
||||
light.range = 10;
|
||||
close(scene.array("pointLights").row(light.id).subarray(3, 5), [9, 10]);
|
||||
});
|
||||
|
||||
test("ArcRotateCamera pans one projected world pixel per pointer pixel", async () => {
|
||||
const listeners = new Map<string, Set<(event: any) => void>>();
|
||||
const element = {
|
||||
clientWidth: 400,
|
||||
clientHeight: 200,
|
||||
addEventListener(type: string, listener: (event: any) => void) {
|
||||
const registered = listeners.get(type) ?? new Set();
|
||||
registered.add(listener);
|
||||
listeners.set(type, registered);
|
||||
},
|
||||
removeEventListener(type: string, listener: (event: any) => void) {
|
||||
listeners.get(type)?.delete(listener);
|
||||
},
|
||||
setPointerCapture() {},
|
||||
} as unknown as HTMLElement;
|
||||
const dispatch = (type: string, event: Record<string, unknown>) => {
|
||||
for (const listener of listeners.get(type) ?? [])
|
||||
listener({ preventDefault() {}, ...event });
|
||||
};
|
||||
const scene = await createScene();
|
||||
const camera = new ArcRotateCamera(scene, {
|
||||
alpha: 0,
|
||||
beta: Math.PI / 2,
|
||||
radius: 10,
|
||||
fov: Math.PI / 2,
|
||||
aspect: 2,
|
||||
controls: { element },
|
||||
});
|
||||
await camera.ready;
|
||||
|
||||
dispatch("pointerdown", {
|
||||
button: 2,
|
||||
clientX: 100,
|
||||
clientY: 100,
|
||||
pointerId: 1,
|
||||
});
|
||||
dispatch("pointermove", { clientX: 120, clientY: 110 });
|
||||
|
||||
close(camera.position, [-2, 1, 10]);
|
||||
camera.detachControls();
|
||||
});
|
||||
Reference in New Issue
Block a user