Files

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, []);
});