Amp-Thread-ID: https://ampcode.com/threads/T-01a02d1c-27fc-7761-9f51-ed97b2dd4e3e Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
360 lines
11 KiB
TypeScript
360 lines
11 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { loadGltf } from "../src/importers/worker";
|
|
|
|
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}`,
|
|
);
|
|
}
|
|
|
|
function dataUrl(type: string, value: Uint8Array | string) {
|
|
return `data:${type};base64,${Buffer.from(value).toString("base64")}`;
|
|
}
|
|
|
|
function binaryBuilder() {
|
|
let bytes = new Uint8Array();
|
|
return {
|
|
append(source: ArrayBufferView, alignment: number) {
|
|
const input = new Uint8Array(
|
|
source.buffer,
|
|
source.byteOffset,
|
|
source.byteLength,
|
|
);
|
|
const offset = Math.ceil(bytes.length / alignment) * alignment;
|
|
const next = new Uint8Array(offset + input.length);
|
|
next.set(bytes);
|
|
next.set(input, offset);
|
|
bytes = next;
|
|
return { byteOffset: offset, byteLength: input.length };
|
|
},
|
|
get bytes() {
|
|
return bytes;
|
|
},
|
|
};
|
|
}
|
|
|
|
function glb(document: object, binary: Uint8Array) {
|
|
const json = new TextEncoder().encode(JSON.stringify(document));
|
|
const jsonLength = Math.ceil(json.length / 4) * 4;
|
|
const binaryLength = Math.ceil(binary.length / 4) * 4;
|
|
const result = new Uint8Array(12 + 8 + jsonLength + 8 + binaryLength);
|
|
const view = new DataView(result.buffer);
|
|
view.setUint32(0, 0x46546c67, true);
|
|
view.setUint32(4, 2, true);
|
|
view.setUint32(8, result.length, true);
|
|
view.setUint32(12, jsonLength, true);
|
|
view.setUint32(16, 0x4e4f534a, true);
|
|
result.fill(0x20, 20, 20 + jsonLength);
|
|
result.set(json, 20);
|
|
const binaryHeader = 20 + jsonLength;
|
|
view.setUint32(binaryHeader, binaryLength, true);
|
|
view.setUint32(binaryHeader + 4, 0x004e4942, true);
|
|
result.set(binary, binaryHeader + 8);
|
|
return result;
|
|
}
|
|
|
|
test("JSON glTF parsing covers accessors, transforms, materials, lights, and cameras", async () => {
|
|
const binary = binaryBuilder();
|
|
const positions = binary.append(
|
|
new Float32Array([0, 0, 0, 99, 1, 0, 0, 99, 0, 1, 0, 99]),
|
|
4,
|
|
);
|
|
const uvs = binary.append(new Uint16Array([0, 0, 65_535, 0, 0, 65_535]), 2);
|
|
const colors = binary.append(
|
|
new Uint8Array([255, 0, 0, 0, 255, 0, 0, 0, 255]),
|
|
1,
|
|
);
|
|
const indices = binary.append(new Uint16Array([0, 1, 2]), 2);
|
|
const sparseIndices = binary.append(new Uint8Array([1]), 1);
|
|
const sparsePositions = binary.append(new Float32Array([2, 0, 0]), 4);
|
|
const texture = { index: 0 };
|
|
const document = {
|
|
asset: { version: "2.0" },
|
|
extensions: {
|
|
KHR_lights_punctual: {
|
|
lights: [
|
|
{
|
|
type: "spot",
|
|
color: [0.2, 0.4, 0.6],
|
|
intensity: 7,
|
|
range: 8,
|
|
spot: { innerConeAngle: 0.1, outerConeAngle: 0.5 },
|
|
},
|
|
],
|
|
},
|
|
},
|
|
buffers: [
|
|
{
|
|
byteLength: binary.bytes.length,
|
|
uri: dataUrl("application/octet-stream", binary.bytes),
|
|
},
|
|
],
|
|
bufferViews: [
|
|
{ buffer: 0, ...positions, byteStride: 16 },
|
|
{ buffer: 0, ...uvs },
|
|
{ buffer: 0, ...colors },
|
|
{ buffer: 0, ...indices },
|
|
{ buffer: 0, ...sparseIndices },
|
|
{ buffer: 0, ...sparsePositions },
|
|
],
|
|
accessors: [
|
|
{
|
|
bufferView: 0,
|
|
componentType: 5126,
|
|
count: 3,
|
|
type: "VEC3",
|
|
sparse: {
|
|
count: 1,
|
|
indices: { bufferView: 4, componentType: 5121 },
|
|
values: { bufferView: 5 },
|
|
},
|
|
},
|
|
{
|
|
bufferView: 1,
|
|
componentType: 5123,
|
|
normalized: true,
|
|
count: 3,
|
|
type: "VEC2",
|
|
},
|
|
{
|
|
bufferView: 2,
|
|
componentType: 5121,
|
|
normalized: true,
|
|
count: 3,
|
|
type: "VEC3",
|
|
},
|
|
{
|
|
bufferView: 3,
|
|
componentType: 5123,
|
|
count: 3,
|
|
type: "SCALAR",
|
|
},
|
|
],
|
|
images: [
|
|
{
|
|
uri: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
|
|
},
|
|
],
|
|
samplers: [
|
|
{ magFilter: 9728, minFilter: 9984, wrapS: 33071, wrapT: 33648 },
|
|
],
|
|
textures: [{ source: 0, sampler: 0 }],
|
|
materials: [
|
|
{
|
|
alphaMode: "BLEND",
|
|
doubleSided: true,
|
|
normalTexture: { ...texture, scale: 0.75 },
|
|
emissiveTexture: texture,
|
|
emissiveFactor: [0.4, 0.5, 0.6],
|
|
occlusionTexture: { ...texture, strength: 0.7 },
|
|
extensions: {
|
|
KHR_materials_unlit: {},
|
|
KHR_materials_pbrSpecularGlossiness: {
|
|
diffuseFactor: [0.1, 0.2, 0.3, 0.4],
|
|
specularFactor: [0.5, 0.6, 0.7],
|
|
glossinessFactor: 0.75,
|
|
diffuseTexture: {
|
|
...texture,
|
|
extensions: {
|
|
KHR_texture_transform: {
|
|
offset: [0.1, 0.2],
|
|
scale: [0.3, 0.4],
|
|
rotation: 0.5,
|
|
texCoord: 1,
|
|
},
|
|
},
|
|
},
|
|
specularGlossinessTexture: texture,
|
|
},
|
|
KHR_materials_transmission: {
|
|
transmissionFactor: 0.8,
|
|
transmissionTexture: texture,
|
|
},
|
|
KHR_materials_volume: {
|
|
thicknessFactor: 0.9,
|
|
thicknessTexture: texture,
|
|
attenuationDistance: 3,
|
|
attenuationColor: [0.2, 0.3, 0.4],
|
|
},
|
|
KHR_materials_clearcoat: {
|
|
clearcoatFactor: 0.45,
|
|
clearcoatRoughnessFactor: 0.25,
|
|
clearcoatTexture: texture,
|
|
},
|
|
KHR_materials_sheen: {
|
|
sheenColorFactor: [0.3, 0.2, 0.1],
|
|
sheenRoughnessFactor: 0.35,
|
|
},
|
|
KHR_materials_specular: {
|
|
specularFactor: 0.55,
|
|
specularColorFactor: [0.7, 0.8, 0.9],
|
|
specularTexture: texture,
|
|
specularColorTexture: texture,
|
|
},
|
|
KHR_materials_iridescence: {
|
|
iridescenceFactor: 0.65,
|
|
iridescenceIor: 1.4,
|
|
iridescenceThicknessMinimum: 120,
|
|
iridescenceThicknessMaximum: 360,
|
|
iridescenceTexture: texture,
|
|
iridescenceThicknessTexture: texture,
|
|
},
|
|
KHR_materials_anisotropy: {
|
|
anisotropyStrength: 0.75,
|
|
anisotropyRotation: 0.15,
|
|
anisotropyTexture: texture,
|
|
},
|
|
KHR_materials_emissive_strength: { emissiveStrength: 4 },
|
|
KHR_materials_ior: { ior: 1.6 },
|
|
},
|
|
},
|
|
],
|
|
meshes: [
|
|
{
|
|
primitives: [
|
|
{
|
|
attributes: {
|
|
POSITION: 0,
|
|
TEXCOORD_0: 1,
|
|
TEXCOORD_1: 1,
|
|
COLOR_0: 2,
|
|
},
|
|
indices: 3,
|
|
material: 0,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
cameras: [{ type: "perspective", perspective: { yfov: 1.1 } }],
|
|
nodes: [
|
|
{
|
|
mesh: 0,
|
|
camera: 0,
|
|
translation: [5, 0, 0],
|
|
scale: [-1, 1, 1],
|
|
extensions: { KHR_lights_punctual: { light: 0 } },
|
|
},
|
|
],
|
|
};
|
|
const previousCreateImageBitmap = globalThis.createImageBitmap;
|
|
Object.defineProperty(globalThis, "createImageBitmap", {
|
|
configurable: true,
|
|
value: async () => ({ width: 1, height: 1 }),
|
|
});
|
|
try {
|
|
const result = await loadGltf(
|
|
dataUrl("model/gltf+json", JSON.stringify(document)),
|
|
);
|
|
assert.equal(result.primitives.length, 1);
|
|
const primitive = result.primitives[0];
|
|
close(primitive.positions, [5, 0, 0, 3, 0, 0, 5, 1, 0]);
|
|
assert.deepEqual(Array.from(primitive.indices), [0, 2, 1]);
|
|
close(primitive.normals, [0, 0, 1, 0, 0, 1, 0, 0, 1]);
|
|
close(primitive.uvs, [0, 0, 1, 0, 0, 1]);
|
|
close(primitive.uvs2, [0, 0, 1, 0, 0, 1]);
|
|
close(primitive.colors, [1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1]);
|
|
assert.equal(primitive.tangents.length, 12);
|
|
assert.equal(Array.from(primitive.tangents).every(Number.isFinite), true);
|
|
|
|
const material = result.materials[0];
|
|
close(material.baseColor, [0.1, 0.2, 0.3, 0.4]);
|
|
assert.equal(material.metallic, 0);
|
|
assert.equal(material.roughness, 0.25);
|
|
assert.equal(material.flags, 30);
|
|
assert.equal(material.transmission, 0.8);
|
|
assert.equal(material.ior, 1.6);
|
|
assert.equal(material.thickness, 0.9);
|
|
assert.equal(material.attenuationDistance, 3);
|
|
close(material.attenuationColor, [0.2, 0.3, 0.4]);
|
|
assert.equal(material.emissiveStrength, 4);
|
|
assert.equal(material.clearcoat, 0.45);
|
|
assert.equal(material.clearcoatRoughness, 0.25);
|
|
assert.equal(material.specular, 0.55);
|
|
close(material.specularColor, [0.5, 0.6, 0.7]);
|
|
close(material.specularExtensionColor, [0.7, 0.8, 0.9]);
|
|
assert.equal(material.iridescence, 0.65);
|
|
assert.equal(material.anisotropy, 0.75);
|
|
assert.deepEqual(
|
|
[
|
|
material.baseColorTexture,
|
|
material.metallicRoughnessTexture,
|
|
material.normalTexture,
|
|
material.emissiveTexture,
|
|
material.occlusionTexture,
|
|
material.transmissionTexture,
|
|
material.thicknessTexture,
|
|
material.clearcoatTexture,
|
|
material.iridescenceTexture,
|
|
material.iridescenceThicknessTexture,
|
|
material.anisotropyTexture,
|
|
material.specularTexture,
|
|
material.specularColorTexture,
|
|
],
|
|
Array(13).fill(0),
|
|
);
|
|
assert.deepEqual(material.textureInfos[0], {
|
|
texture: 0,
|
|
texCoord: 1,
|
|
offset: [0.1, 0.2],
|
|
scale: [0.3, 0.4],
|
|
rotation: 0.5,
|
|
});
|
|
|
|
assert.equal(result.textures.length, 1);
|
|
assert.deepEqual(result.textures[0].sampler, {
|
|
magFilter: "nearest",
|
|
minFilter: "nearest",
|
|
mipmapFilter: "nearest",
|
|
addressModeU: "clamp-to-edge",
|
|
addressModeV: "mirror-repeat",
|
|
});
|
|
assert.deepEqual(result.camera, { direction: [0, 0, 1], fov: 1.1 });
|
|
assert.equal(result.lights.length, 1);
|
|
const { rotor, ...light } = result.lights[0];
|
|
assert.deepEqual(light, {
|
|
type: "spot",
|
|
color: [0.2, 0.4, 0.6],
|
|
intensity: 7,
|
|
range: 8,
|
|
innerAngle: 0.1,
|
|
outerAngle: 0.5,
|
|
position: [5, 0, 0],
|
|
});
|
|
close(rotor, [0, 0, 0, 1]);
|
|
} finally {
|
|
if (previousCreateImageBitmap)
|
|
Object.defineProperty(globalThis, "createImageBitmap", {
|
|
configurable: true,
|
|
value: previousCreateImageBitmap,
|
|
});
|
|
else delete (globalThis as any).createImageBitmap;
|
|
}
|
|
});
|
|
|
|
test("binary GLB parsing consumes JSON and BIN chunks", async () => {
|
|
const binary = new Uint8Array(
|
|
new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]).buffer,
|
|
);
|
|
const document = {
|
|
asset: { version: "2.0" },
|
|
buffers: [{ byteLength: binary.length }],
|
|
bufferViews: [{ buffer: 0, byteOffset: 0, byteLength: binary.length }],
|
|
accessors: [{ bufferView: 0, componentType: 5126, count: 3, type: "VEC3" }],
|
|
meshes: [{ primitives: [{ attributes: { POSITION: 0 } }] }],
|
|
};
|
|
|
|
const result = await loadGltf(
|
|
dataUrl("model/gltf-binary", glb(document, binary)),
|
|
);
|
|
assert.equal(result.primitives.length, 1);
|
|
close(result.primitives[0].positions, [0, 0, 0, 1, 0, 0, 0, 1, 0]);
|
|
close(result.primitives[0].normals, [0, 0, 1, 0, 0, 1, 0, 0, 1]);
|
|
assert.deepEqual(Array.from(result.primitives[0].indices), [0, 1, 2]);
|
|
assert.deepEqual(result.materials, []);
|
|
assert.deepEqual(result.textures, []);
|
|
});
|