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:
Amp
2026-08-24 10:51:26 +00:00
co-authored by heaust
parent df8b69e09d
commit 951449db0b
99 changed files with 4004 additions and 376 deletions
+32 -4
View File
@@ -1,5 +1,8 @@
import { Mesh } from "../Mesh";
import type { Scene } from "../Scene";
import { DirectionalLight } from "../lights/DirectionalLight";
import { PointLight } from "../lights/PointLight";
import { SpotLight } from "../lights/SpotLight";
import { PBRMaterial } from "../materials/PBRMaterial";
import { Texture } from "../materials/Texture";
@@ -49,12 +52,20 @@ export async function importGltf(scene: Scene, url: string | URL) {
for (const material of result.materials) {
if (material.baseColorTexture >= 0) srgb.add(material.baseColorTexture);
if (material.emissiveTexture >= 0) srgb.add(material.emissiveTexture);
if (material.flags & 16 && material.metallicRoughnessTexture >= 0)
srgb.add(material.metallicRoughnessTexture);
if (material.specularColorTexture >= 0)
srgb.add(material.specularColorTexture);
}
textures = result.textures.map(
({ image }: { image: ImageBitmap }, index: number) =>
(
{ image, sampler }: { image: ImageBitmap; sampler: any },
index: number,
) =>
new Texture(scene, {
source: image,
format: srgb.has(index) ? "rgba8unorm-srgb" : "rgba8unorm",
sampler,
}),
);
await Promise.all(textures.map((texture: Texture) => texture.ready));
@@ -63,10 +74,19 @@ export async function importGltf(scene: Scene, url: string | URL) {
new PBRMaterial(scene, {
...options,
baseColorTexture: textures[options.baseColorTexture],
metallicRoughnessTexture:
textures[options.metallicRoughnessTexture],
metallicRoughnessTexture: textures[options.metallicRoughnessTexture],
normalTexture: textures[options.normalTexture],
emissiveTexture: textures[options.emissiveTexture],
occlusionTexture: textures[options.occlusionTexture],
transmissionTexture: textures[options.transmissionTexture],
thicknessTexture: textures[options.thicknessTexture],
clearcoatTexture: textures[options.clearcoatTexture],
iridescenceTexture: textures[options.iridescenceTexture],
iridescenceThicknessTexture:
textures[options.iridescenceThicknessTexture],
anisotropyTexture: textures[options.anisotropyTexture],
specularTexture: textures[options.specularTexture],
specularColorTexture: textures[options.specularColorTexture],
}),
);
await Promise.all(materials.map((material: PBRMaterial) => material.ready));
@@ -83,12 +103,20 @@ export async function importGltf(scene: Scene, url: string | URL) {
...(primitive.normals ? { normals: primitive.normals } : {}),
...(primitive.tangents ? { tangents: primitive.tangents } : {}),
...(primitive.uvs ? { uvs: primitive.uvs } : {}),
...(primitive.uvs2 ? { uvs2: primitive.uvs2 } : {}),
...(primitive.colors ? { colors: primitive.colors } : {}),
},
}),
);
await Promise.all(meshes.map((mesh) => mesh.ready));
return meshes;
const lights = (result.lights ?? []).map((options: any) => {
if (options.type === "directional")
return new DirectionalLight(scene, options);
if (options.type === "spot") return new SpotLight(scene, options);
return new PointLight(scene, options);
});
await Promise.all(lights.map((light: any) => light.ready));
return Object.assign(meshes, { camera: result.camera });
});
await Promise.all(
result.textures.map((_: unknown, index: number) =>
+747 -163
View File
@@ -1,7 +1,15 @@
export {};
const decoder = new TextDecoder();
const widths: Record<string, number> = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4 };
const widths: Record<string, number> = {
SCALAR: 1,
VEC2: 2,
VEC3: 3,
VEC4: 4,
MAT2: 4,
MAT3: 9,
MAT4: 16,
};
const sizes: Record<number, number> = {
5120: 1,
5121: 1,
@@ -29,79 +37,165 @@ function component(view: DataView, offset: number, type: number) {
throw new Error("GLTF_COMPONENT");
}
function rotate(rotor: number[], value: number[]) {
const [rx, ry, rz, rw] = rotor;
const [x, y, z] = value;
const tx = 2 * (ry * z - rz * y);
const ty = 2 * (rz * x - rx * z);
const tz = 2 * (rx * y - ry * x);
return [
x + rw * tx + ry * tz - rz * ty,
y + rw * ty + rz * tx - rx * tz,
z + rw * tz + rx * ty - ry * tx,
];
}
const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
function multiply(a: number[], b: number[]) {
return [
a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],
];
}
function decompose(matrix: number[]) {
const scale = [
Math.hypot(matrix[0], matrix[1], matrix[2]),
Math.hypot(matrix[4], matrix[5], matrix[6]),
Math.hypot(matrix[8], matrix[9], matrix[10]),
];
const [sx, sy, sz] = scale;
const [m00, m01, m02] = [matrix[0] / sx, matrix[4] / sy, matrix[8] / sz];
const [m10, m11, m12] = [matrix[1] / sx, matrix[5] / sy, matrix[9] / sz];
const [m20, m21, m22] = [matrix[2] / sx, matrix[6] / sy, matrix[10] / sz];
let rotor: number[];
if (m00 + m11 + m22 > 0) {
const s = Math.sqrt(1 + m00 + m11 + m22) * 2;
rotor = [(m21 - m12) / s, (m02 - m20) / s, (m10 - m01) / s, s / 4];
} else if (m00 > m11 && m00 > m22) {
const s = Math.sqrt(1 + m00 - m11 - m22) * 2;
rotor = [s / 4, (m01 + m10) / s, (m02 + m20) / s, (m21 - m12) / s];
} else if (m11 > m22) {
const s = Math.sqrt(1 + m11 - m00 - m22) * 2;
rotor = [(m01 + m10) / s, s / 4, (m12 + m21) / s, (m02 - m20) / s];
} else {
const s = Math.sqrt(1 + m22 - m00 - m11) * 2;
rotor = [(m02 + m20) / s, (m12 + m21) / s, s / 4, (m10 - m01) / s];
}
return { position: matrix.slice(12, 15), rotor, scale };
function multiply(a: ArrayLike<number>, b: ArrayLike<number>) {
const result = new Array<number>(16);
for (let column = 0; column < 4; column++)
for (let row = 0; row < 4; row++) {
let value = 0;
for (let lane = 0; lane < 4; lane++)
value += a[lane * 4 + row] * b[column * 4 + lane];
result[column * 4 + row] = value;
}
return result;
}
function transform(node: any) {
return node.matrix
? decompose(node.matrix)
: {
position: node.translation ?? [0, 0, 0],
rotor: node.rotation ?? [0, 0, 0, 1],
scale: node.scale ?? [1, 1, 1],
};
if (node.matrix) return Array.from(node.matrix);
const [x, y, z, w] = node.rotation ?? [0, 0, 0, 1];
const [sx, sy, sz] = node.scale ?? [1, 1, 1];
const [tx, ty, tz] = node.translation ?? [0, 0, 0];
const xx = x * x;
const yy = y * y;
const zz = z * z;
const xy = x * y;
const xz = x * z;
const yz = y * z;
const wx = w * x;
const wy = w * y;
const wz = w * z;
return [
(1 - 2 * (yy + zz)) * sx,
2 * (xy + wz) * sx,
2 * (xz - wy) * sx,
0,
2 * (xy - wz) * sy,
(1 - 2 * (xx + zz)) * sy,
2 * (yz + wx) * sy,
0,
2 * (xz + wy) * sz,
2 * (yz - wx) * sz,
(1 - 2 * (xx + yy)) * sz,
0,
tx,
ty,
tz,
1,
];
}
function compose(parent: any, local: any) {
const position = rotate(
parent.rotor,
local.position.map(
(value: number, lane: number) => value * parent.scale[lane],
),
function determinant(matrix: ArrayLike<number>) {
return (
matrix[0] * (matrix[5] * matrix[10] - matrix[6] * matrix[9]) -
matrix[4] * (matrix[1] * matrix[10] - matrix[2] * matrix[9]) +
matrix[8] * (matrix[1] * matrix[6] - matrix[2] * matrix[5])
);
return {
position: position.map((value, lane) => value + parent.position[lane]),
rotor: multiply(parent.rotor, local.rotor),
scale: local.scale.map(
(value: number, lane: number) => value * parent.scale[lane],
),
};
}
function point(matrix: ArrayLike<number>, source: ArrayLike<number>) {
return [
matrix[0] * source[0] +
matrix[4] * source[1] +
matrix[8] * source[2] +
matrix[12],
matrix[1] * source[0] +
matrix[5] * source[1] +
matrix[9] * source[2] +
matrix[13],
matrix[2] * source[0] +
matrix[6] * source[1] +
matrix[10] * source[2] +
matrix[14],
];
}
function vector(matrix: ArrayLike<number>, source: ArrayLike<number>) {
const x =
matrix[0] * source[0] + matrix[4] * source[1] + matrix[8] * source[2];
const y =
matrix[1] * source[0] + matrix[5] * source[1] + matrix[9] * source[2];
const z =
matrix[2] * source[0] + matrix[6] * source[1] + matrix[10] * source[2];
const length = Math.hypot(x, y, z) || 1;
return [x / length, y / length, z / length];
}
function direction(matrix: ArrayLike<number>, source: ArrayLike<number>) {
const a = matrix[0];
const b = matrix[4];
const c = matrix[8];
const d = matrix[1];
const e = matrix[5];
const f = matrix[9];
const g = matrix[2];
const h = matrix[6];
const i = matrix[10];
const inverse = 1 / determinant(matrix);
const x =
((e * i - f * h) * source[0] +
(f * g - d * i) * source[1] +
(d * h - e * g) * source[2]) *
inverse;
const y =
((c * h - b * i) * source[0] +
(a * i - c * g) * source[1] +
(b * g - a * h) * source[2]) *
inverse;
const z =
((b * f - c * e) * source[0] +
(c * d - a * f) * source[1] +
(a * e - b * d) * source[2]) *
inverse;
const length = Math.hypot(x, y, z) || 1;
return [x / length, y / length, z / length];
}
function rotor(matrix: ArrayLike<number>) {
const sx = Math.hypot(matrix[0], matrix[1], matrix[2]) || 1;
const sy = Math.hypot(matrix[4], matrix[5], matrix[6]) || 1;
const sz = Math.hypot(matrix[8], matrix[9], matrix[10]) || 1;
const m00 = matrix[0] / sx;
const m01 = matrix[4] / sy;
const m02 = matrix[8] / sz;
const m10 = matrix[1] / sx;
const m11 = matrix[5] / sy;
const m12 = matrix[9] / sz;
const m20 = matrix[2] / sx;
const m21 = matrix[6] / sy;
const m22 = matrix[10] / sz;
const trace = m00 + m11 + m22;
let x: number;
let y: number;
let z: number;
let w: number;
if (trace > 0) {
const scale = Math.sqrt(trace + 1) * 2;
x = (m21 - m12) / scale;
y = (m02 - m20) / scale;
z = (m10 - m01) / scale;
w = scale / 4;
} else if (m00 > m11 && m00 > m22) {
const scale = Math.sqrt(1 + m00 - m11 - m22) * 2;
x = scale / 4;
y = (m01 + m10) / scale;
z = (m02 + m20) / scale;
w = (m21 - m12) / scale;
} else if (m11 > m22) {
const scale = Math.sqrt(1 + m11 - m00 - m22) * 2;
x = (m01 + m10) / scale;
y = scale / 4;
z = (m12 + m21) / scale;
w = (m02 - m20) / scale;
} else {
const scale = Math.sqrt(1 + m22 - m00 - m11) * 2;
x = (m02 + m20) / scale;
y = (m12 + m21) / scale;
z = scale / 4;
w = (m10 - m01) / scale;
}
const length = Math.hypot(x, y, z, w) || 1;
return [x / length, y / length, z / length, w / length];
}
function parse(bytes: Uint8Array) {
@@ -127,7 +221,127 @@ function parse(bytes: Uint8Array) {
return { document, binary };
}
async function load(url: string) {
function computeNormals(positions: Float32Array, indices: Uint32Array) {
const normals = new Float32Array(positions.length);
for (let face = 0; face + 2 < indices.length; face += 3) {
const a = indices[face] * 3;
const b = indices[face + 1] * 3;
const c = indices[face + 2] * 3;
const ab = [
positions[b] - positions[a],
positions[b + 1] - positions[a + 1],
positions[b + 2] - positions[a + 2],
];
const ac = [
positions[c] - positions[a],
positions[c + 1] - positions[a + 1],
positions[c + 2] - positions[a + 2],
];
const cross = [
ab[1] * ac[2] - ab[2] * ac[1],
ab[2] * ac[0] - ab[0] * ac[2],
ab[0] * ac[1] - ab[1] * ac[0],
];
for (const vertex of [a, b, c])
for (let lane = 0; lane < 3; lane++)
normals[vertex + lane] += cross[lane];
}
for (let vertex = 0; vertex < normals.length; vertex += 3) {
const length =
Math.hypot(normals[vertex], normals[vertex + 1], normals[vertex + 2]) ||
1;
for (let lane = 0; lane < 3; lane++) normals[vertex + lane] /= length;
}
return normals;
}
function computeTangents(
positions: Float32Array,
normals: Float32Array,
uvs: Float32Array,
indices: Uint32Array,
) {
const first = new Float32Array(positions.length);
const second = new Float32Array(positions.length);
for (let face = 0; face + 2 < indices.length; face += 3) {
const ia = indices[face];
const ib = indices[face + 1];
const ic = indices[face + 2];
const a = ia * 3;
const b = ib * 3;
const c = ic * 3;
const au = ia * 2;
const bu = ib * 2;
const cu = ic * 2;
const x1 = positions[b] - positions[a];
const x2 = positions[c] - positions[a];
const y1 = positions[b + 1] - positions[a + 1];
const y2 = positions[c + 1] - positions[a + 1];
const z1 = positions[b + 2] - positions[a + 2];
const z2 = positions[c + 2] - positions[a + 2];
const s1 = uvs[bu] - uvs[au];
const s2 = uvs[cu] - uvs[au];
const t1 = uvs[bu + 1] - uvs[au + 1];
const t2 = uvs[cu + 1] - uvs[au + 1];
const divisor = s1 * t2 - s2 * t1;
if (Math.abs(divisor) < 1e-10) continue;
const r = 1 / divisor;
const tangent = [
(t2 * x1 - t1 * x2) * r,
(t2 * y1 - t1 * y2) * r,
(t2 * z1 - t1 * z2) * r,
];
const bitangent = [
(s1 * x2 - s2 * x1) * r,
(s1 * y2 - s2 * y1) * r,
(s1 * z2 - s2 * z1) * r,
];
for (const vertex of [a, b, c])
for (let lane = 0; lane < 3; lane++) {
first[vertex + lane] += tangent[lane];
second[vertex + lane] += bitangent[lane];
}
}
const tangents = new Float32Array((positions.length / 3) * 4);
for (let vertex = 0; vertex < positions.length / 3; vertex++) {
const at = vertex * 3;
const output = vertex * 4;
const normal = normals.subarray(at, at + 3);
const source = first.subarray(at, at + 3);
const projection =
normal[0] * source[0] + normal[1] * source[1] + normal[2] * source[2];
let x = source[0] - normal[0] * projection;
let y = source[1] - normal[1] * projection;
let z = source[2] - normal[2] * projection;
const length = Math.hypot(x, y, z) || 1;
x /= length;
y /= length;
z /= length;
tangents[output] = x;
tangents[output + 1] = y;
tangents[output + 2] = z;
tangents[output + 3] =
normal[0] * (y * second[at + 2] - z * second[at + 1]) +
normal[1] * (z * second[at] - x * second[at + 2]) +
normal[2] * (x * second[at + 1] - y * second[at]) <
0
? -1
: 1;
}
return tangents;
}
function color4(values: Float32Array, width: number) {
if (width === 4) return values;
const result = new Float32Array((values.length / width) * 4);
for (let vertex = 0; vertex < values.length / width; vertex++) {
result.set(values.subarray(vertex * width, vertex * width + 3), vertex * 4);
result[vertex * 4 + 3] = 1;
}
return result;
}
export async function loadGltf(url: string) {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP_${response.status}`);
const { document, binary } = parse(
@@ -159,152 +373,522 @@ async function load(url: string) {
return result.blob();
}),
);
const samplerDescriptor = (id: number | undefined) => {
const sampler = id === undefined ? {} : (document.samplers?.[id] ?? {});
const addressMode = (mode: number | undefined) =>
mode === 33071
? "clamp-to-edge"
: mode === 33648
? "mirror-repeat"
: "repeat";
return {
magFilter: sampler.magFilter === 9728 ? "nearest" : "linear",
minFilter:
sampler.minFilter === 9728 ||
sampler.minFilter === 9984 ||
sampler.minFilter === 9986
? "nearest"
: "linear",
mipmapFilter:
sampler.minFilter === 9984 || sampler.minFilter === 9985
? "nearest"
: "linear",
addressModeU: addressMode(sampler.wrapS),
addressModeV: addressMode(sampler.wrapT),
};
};
const textures = await Promise.all(
(document.textures ?? []).map(async (texture: any) => ({
image: await createImageBitmap(imageBlobs[texture.source]),
sampler: samplerDescriptor(texture.sampler),
})),
);
const normalized = (value: number, type: number) => {
if (type === 5120) return Math.max(value / 127, -1);
if (type === 5121) return value / 255;
if (type === 5122) return Math.max(value / 32_767, -1);
if (type === 5123) return value / 65_535;
return value;
};
const accessor = (id: number, integer = false) => {
const source = document.accessors[id];
const width = widths[source.type];
const size = sizes[source.componentType];
const bufferView = document.bufferViews[source.bufferView];
const bytes = buffers[bufferView.buffer];
const start = (bufferView.byteOffset ?? 0) + (source.byteOffset ?? 0);
const stride = bufferView.byteStride ?? width * size;
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const values = integer
? new Uint32Array(source.count * width)
: new Float32Array(source.count * width);
if (
!source.normalized &&
stride === width * size &&
(bytes.byteOffset + start) % size === 0
) {
const packed = new arrays[source.componentType](
if (source.bufferView !== undefined) {
const bufferView = document.bufferViews[source.bufferView];
const bytes = buffers[bufferView.buffer];
const start = (bufferView.byteOffset ?? 0) + (source.byteOffset ?? 0);
const stride = bufferView.byteStride ?? width * size;
const view = new DataView(
bytes.buffer,
bytes.byteOffset + start,
source.count * width,
bytes.byteOffset,
bytes.byteLength,
);
values.set(packed);
return values;
}
for (let item = 0; item < source.count; item++)
for (let lane = 0; lane < width; lane++) {
let value = component(
view,
start + item * stride + lane * size,
source.componentType,
if (
!source.sparse &&
!source.normalized &&
stride === width * size &&
(bytes.byteOffset + start) % size === 0
) {
values.set(
new arrays[source.componentType](
bytes.buffer,
bytes.byteOffset + start,
source.count * width,
),
);
if (!integer && source.normalized) {
const maximum =
source.componentType === 5121
? 255
: source.componentType === 5123
? 65535
: 1;
value /= maximum;
} else
for (let item = 0; item < source.count; item++)
for (let lane = 0; lane < width; lane++) {
let value = component(
view,
start + item * stride + lane * size,
source.componentType,
);
if (!integer && source.normalized)
value = normalized(value, source.componentType);
values[item * width + lane] = value;
}
}
if (source.sparse) {
const sparse = source.sparse;
const indexView = document.bufferViews[sparse.indices.bufferView];
const indexBytes = buffers[indexView.buffer];
const indexStart =
(indexView.byteOffset ?? 0) + (sparse.indices.byteOffset ?? 0);
const indexData = new DataView(
indexBytes.buffer,
indexBytes.byteOffset,
indexBytes.byteLength,
);
const indexSize = sizes[sparse.indices.componentType];
const valueView = document.bufferViews[sparse.values.bufferView];
const valueBytes = buffers[valueView.buffer];
const valueStart =
(valueView.byteOffset ?? 0) + (sparse.values.byteOffset ?? 0);
const valueData = new DataView(
valueBytes.buffer,
valueBytes.byteOffset,
valueBytes.byteLength,
);
for (let item = 0; item < sparse.count; item++) {
const target = component(
indexData,
indexStart + item * indexSize,
sparse.indices.componentType,
);
for (let lane = 0; lane < width; lane++) {
let value = component(
valueData,
valueStart + (item * width + lane) * size,
source.componentType,
);
if (!integer && source.normalized)
value = normalized(value, source.componentType);
values[target * width + lane] = value;
}
values[item * width + lane] = value;
}
}
return values;
};
const materials = (document.materials ?? []).map((material: any) => {
const pbr = material.pbrMetallicRoughness ?? {};
const textureInfo = (info: any) => {
if (!info) return undefined;
const extension = info.extensions?.KHR_texture_transform ?? {};
return {
baseColor: pbr.baseColorFactor ?? [1, 1, 1, 1],
metallic: pbr.metallicFactor ?? 0,
roughness: pbr.roughnessFactor ?? 0.7,
texture: info.index,
texCoord: extension.texCoord ?? info.texCoord ?? 0,
offset: extension.offset ?? [0, 0],
scale: extension.scale ?? [1, 1],
rotation: extension.rotation ?? 0,
};
};
const materials = (document.materials ?? []).map((material: any) => {
const extensions = material.extensions ?? {};
const specGloss = extensions.KHR_materials_pbrSpecularGlossiness;
const pbr = material.pbrMetallicRoughness ?? {};
const transmission = extensions.KHR_materials_transmission ?? {};
const volume = extensions.KHR_materials_volume ?? {};
const clearcoat = extensions.KHR_materials_clearcoat ?? {};
const sheen = extensions.KHR_materials_sheen ?? {};
const specular = extensions.KHR_materials_specular ?? {};
const iridescence = extensions.KHR_materials_iridescence ?? {};
const anisotropy = extensions.KHR_materials_anisotropy ?? {};
const alphaMode =
material.alphaMode === "MASK"
? 1
: material.alphaMode === "BLEND"
? 2
: 0;
const flags =
alphaMode |
(material.doubleSided ? 4 : 0) |
(extensions.KHR_materials_unlit ? 8 : 0) |
(specGloss ? 16 : 0);
const infos = [
textureInfo(specGloss?.diffuseTexture ?? pbr.baseColorTexture),
textureInfo(
specGloss?.specularGlossinessTexture ?? pbr.metallicRoughnessTexture,
),
textureInfo(material.normalTexture),
textureInfo(material.emissiveTexture),
textureInfo(material.occlusionTexture),
textureInfo(transmission.transmissionTexture),
textureInfo(volume.thicknessTexture),
textureInfo(clearcoat.clearcoatTexture),
textureInfo(iridescence.iridescenceTexture),
textureInfo(iridescence.iridescenceThicknessTexture),
textureInfo(anisotropy.anisotropyTexture),
textureInfo(specular.specularTexture),
textureInfo(specular.specularColorTexture),
];
return {
baseColor: specGloss?.diffuseFactor ??
pbr.baseColorFactor ?? [1, 1, 1, 1],
metallic: specGloss ? 0 : (pbr.metallicFactor ?? 1),
roughness: specGloss
? 1 - (specGloss.glossinessFactor ?? 1)
: (pbr.roughnessFactor ?? 1),
emissive: material.emissiveFactor ?? [0, 0, 0],
normalScale: material.normalTexture?.scale ?? 1,
alphaCutoff: material.alphaCutoff ?? 0.5,
baseColorTexture: pbr.baseColorTexture?.index ?? -1,
metallicRoughnessTexture: pbr.metallicRoughnessTexture?.index ?? -1,
normalTexture: material.normalTexture?.index ?? -1,
emissiveTexture: material.emissiveTexture?.index ?? -1,
flags,
occlusionStrength: material.occlusionTexture?.strength ?? 1,
transmission: transmission.transmissionFactor ?? 0,
ior: extensions.KHR_materials_ior?.ior ?? 1.5,
thickness: volume.thicknessFactor ?? 0,
attenuationDistance: volume.attenuationDistance ?? 0,
attenuationColor: volume.attenuationColor ?? [1, 1, 1],
emissiveStrength:
extensions.KHR_materials_emissive_strength?.emissiveStrength ?? 1,
clearcoat: clearcoat.clearcoatFactor ?? 0,
clearcoatRoughness: clearcoat.clearcoatRoughnessFactor ?? 0,
specular: specular.specularFactor ?? 1,
sheenColor: sheen.sheenColorFactor ?? [0, 0, 0],
sheenRoughness: sheen.sheenRoughnessFactor ?? 0,
specularColor: specGloss?.specularFactor ?? [1, 1, 1],
iridescence: iridescence.iridescenceFactor ?? 0,
iridescenceIor: iridescence.iridescenceIor ?? 1.3,
iridescenceThicknessMinimum:
iridescence.iridescenceThicknessMinimum ?? 100,
iridescenceThicknessMaximum:
iridescence.iridescenceThicknessMaximum ?? 400,
anisotropy: anisotropy.anisotropyStrength ?? 0,
anisotropyRotation: anisotropy.anisotropyRotation ?? 0,
specularExtensionColor: specular.specularColorFactor ?? [1, 1, 1],
textureInfos: infos,
baseColorTexture: infos[0]?.texture ?? -1,
metallicRoughnessTexture: infos[1]?.texture ?? -1,
normalTexture: infos[2]?.texture ?? -1,
emissiveTexture: infos[3]?.texture ?? -1,
occlusionTexture: infos[4]?.texture ?? -1,
transmissionTexture: infos[5]?.texture ?? -1,
thicknessTexture: infos[6]?.texture ?? -1,
clearcoatTexture: infos[7]?.texture ?? -1,
iridescenceTexture: infos[8]?.texture ?? -1,
iridescenceThicknessTexture: infos[9]?.texture ?? -1,
anisotropyTexture: infos[10]?.texture ?? -1,
specularTexture: infos[11]?.texture ?? -1,
specularColorTexture: infos[12]?.texture ?? -1,
};
});
const nodes = document.nodes ?? [];
const parents = new Map<number, number>();
for (let id = 0; id < nodes.length; id++)
for (const child of nodes[id].children ?? []) parents.set(child, id);
const worlds = new Map<number, number[]>();
const world = (id: number): number[] => {
const existing = worlds.get(id);
if (existing) return existing;
const local = transform(nodes[id] ?? {});
const parent = parents.get(id);
const result =
parent === undefined ? local : multiply(world(parent), local);
worlds.set(id, result);
return result;
};
for (let id = 0; id < nodes.length; id++) world(id);
const primitives: any[] = [];
const emitMesh = (meshId: number, transform: any) => {
const emitMesh = (meshId: number, nodeId: number, matrix: number[]) => {
const mesh = document.meshes?.[meshId];
const node = nodes[nodeId] ?? {};
const skin =
node.skin === undefined ? undefined : document.skins?.[node.skin];
const inverseBindMatrices =
skin?.inverseBindMatrices === undefined
? undefined
: (accessor(skin.inverseBindMatrices) as Float32Array);
const jointMatrices = skin?.joints.map((joint: number, index: number) =>
multiply(
world(joint),
inverseBindMatrices?.subarray(index * 16, index * 16 + 16) ?? identity,
),
);
for (const primitive of mesh?.primitives ?? []) {
if (
(primitive.mode ?? 4) !== 4 ||
primitive.attributes.POSITION === undefined
)
continue;
const positions = accessor(primitive.attributes.POSITION);
const positions = accessor(primitive.attributes.POSITION) as Float32Array;
let normals =
primitive.attributes.NORMAL === undefined
? undefined
: (accessor(primitive.attributes.NORMAL) as Float32Array);
let tangents =
primitive.attributes.TANGENT === undefined
? undefined
: (accessor(primitive.attributes.TANGENT) as Float32Array);
const uvs =
primitive.attributes.TEXCOORD_0 === undefined
? new Float32Array((positions.length / 3) * 2)
: (accessor(primitive.attributes.TEXCOORD_0) as Float32Array);
const uvs2 =
primitive.attributes.TEXCOORD_1 === undefined
? undefined
: (accessor(primitive.attributes.TEXCOORD_1) as Float32Array);
const indices =
primitive.indices === undefined
? Uint32Array.from(
{ length: positions.length / 3 },
(_, index) => index,
)
: accessor(primitive.indices, true);
: (accessor(primitive.indices, true) as Uint32Array);
const weights = node.weights ?? mesh.weights ?? [];
for (
let target = 0;
target < (primitive.targets?.length ?? 0);
target++
) {
const weight = weights[target] ?? 0;
if (!weight) continue;
for (const [semantic, values] of [
["POSITION", positions],
["NORMAL", normals],
["TANGENT", tangents],
] as const) {
const targetAccessor = primitive.targets[target][semantic];
if (targetAccessor === undefined || !values) continue;
const delta = accessor(targetAccessor) as Float32Array;
if (semantic === "TANGENT") {
for (let vertex = 0; vertex < delta.length / 3; vertex++)
for (let lane = 0; lane < 3; lane++)
values[vertex * 4 + lane] += delta[vertex * 3 + lane] * weight;
} else {
for (let index = 0; index < delta.length; index++)
values[index] += delta[index] * weight;
}
}
}
const jointAccessor = primitive.attributes.JOINTS_0;
const weightAccessor = primitive.attributes.WEIGHTS_0;
if (
jointMatrices &&
jointAccessor !== undefined &&
weightAccessor !== undefined
) {
const joints = accessor(jointAccessor, true) as Uint32Array;
const weights = accessor(weightAccessor) as Float32Array;
for (let vertex = 0; vertex < positions.length / 3; vertex++) {
const position = [0, 0, 0];
const normal = [0, 0, 0];
const tangent = [0, 0, 0];
let total = 0;
for (let influence = 0; influence < 4; influence++) {
const weight = weights[vertex * 4 + influence];
const joint = jointMatrices[joints[vertex * 4 + influence]];
if (!weight || !joint) continue;
const skinnedPosition = point(
joint,
positions.subarray(vertex * 3, vertex * 3 + 3),
);
const skinnedNormal = normals
? direction(joint, normals.subarray(vertex * 3, vertex * 3 + 3))
: undefined;
const skinnedTangent = tangents
? vector(joint, tangents.subarray(vertex * 4, vertex * 4 + 3))
: undefined;
for (let lane = 0; lane < 3; lane++) {
position[lane] += skinnedPosition[lane] * weight;
if (skinnedNormal) normal[lane] += skinnedNormal[lane] * weight;
if (skinnedTangent)
tangent[lane] += skinnedTangent[lane] * weight;
}
total += weight;
}
if (!total) continue;
positions.set(
position.map((value) => value / total),
vertex * 3,
);
if (normals) {
const length = Math.hypot(...normal) || 1;
normals.set(
normal.map((value) => value / length),
vertex * 3,
);
}
if (tangents) {
const length = Math.hypot(...tangent) || 1;
tangents.set(
tangent.map((value) => value / length),
vertex * 4,
);
}
}
matrix = identity;
}
const mirrored = determinant(matrix) < 0;
for (let vertex = 0; vertex < positions.length; vertex += 3)
positions.set(
point(matrix, positions.subarray(vertex, vertex + 3)),
vertex,
);
if (normals)
for (let vertex = 0; vertex < normals.length; vertex += 3)
normals.set(
direction(matrix, normals.subarray(vertex, vertex + 3)),
vertex,
);
if (tangents)
for (let vertex = 0; vertex < tangents.length; vertex += 4) {
tangents.set(
vector(matrix, tangents.subarray(vertex, vertex + 3)),
vertex,
);
if (mirrored) tangents[vertex + 3] *= -1;
}
if (mirrored)
for (let face = 0; face + 2 < indices.length; face += 3)
[indices[face + 1], indices[face + 2]] = [
indices[face + 2],
indices[face + 1],
];
normals ??= computeNormals(positions, indices);
const material = materials[primitive.material ?? -1];
if (!tangents && material?.normalTexture >= 0)
tangents = computeTangents(positions, normals, uvs, indices);
const colorAccessor = primitive.attributes.COLOR_0;
primitives.push({
positions,
normals,
tangents,
uvs,
uvs2,
indices,
...(primitive.attributes.NORMAL === undefined
...(colorAccessor === undefined
? {}
: { normals: accessor(primitive.attributes.NORMAL) }),
...(primitive.attributes.TANGENT === undefined
? {}
: { tangents: accessor(primitive.attributes.TANGENT) }),
...(primitive.attributes.TEXCOORD_0 === undefined
? {}
: { uvs: accessor(primitive.attributes.TEXCOORD_0) }),
...(primitive.attributes.COLOR_0 === undefined
? {}
: { colors: accessor(primitive.attributes.COLOR_0) }),
: {
colors: color4(
accessor(colorAccessor) as Float32Array,
widths[document.accessors[colorAccessor].type],
),
}),
material: primitive.material ?? -1,
...transform,
position: [0, 0, 0],
rotor: [0, 0, 0, 1],
scale: [1, 1, 1],
});
}
};
const nodes = document.nodes ?? [];
const scene = document.scenes?.[document.scene ?? 0];
const children = new Set(nodes.flatMap((node: any) => node.children ?? []));
const roots =
scene?.nodes ??
nodes
.map((_: any, id: number) => id)
.filter((id: number) => !children.has(id));
const visit = (
id: number,
parent = {
position: [0, 0, 0],
rotor: [0, 0, 0, 1],
scale: [1, 1, 1],
},
) => {
const node = nodes[id] ?? {};
const world = compose(parent, transform(node));
if (node.mesh !== undefined) emitMesh(node.mesh, world);
for (const child of node.children ?? []) visit(child, world);
const emitNode = (id: number) => {
const node = nodes[id];
if (node.mesh === undefined) return;
const instances = node.extensions?.EXT_mesh_gpu_instancing?.attributes;
if (!instances) return emitMesh(node.mesh, id, world(id));
const translations =
instances.TRANSLATION === undefined
? undefined
: (accessor(instances.TRANSLATION) as Float32Array);
const rotations =
instances.ROTATION === undefined
? undefined
: (accessor(instances.ROTATION) as Float32Array);
const scales =
instances.SCALE === undefined
? undefined
: (accessor(instances.SCALE) as Float32Array);
const count = translations
? translations.length / 3
: rotations
? rotations.length / 4
: scales
? scales.length / 3
: 0;
for (let instance = 0; instance < count; instance++)
emitMesh(
node.mesh,
id,
multiply(
world(id),
transform({
translation: translations?.subarray(instance * 3, instance * 3 + 3),
rotation: rotations?.subarray(instance * 4, instance * 4 + 4),
scale: scales?.subarray(instance * 3, instance * 3 + 3),
}),
),
);
};
for (const root of roots) visit(root);
for (let id = 0; id < nodes.length; id++) emitNode(id);
if (!nodes.length)
for (let id = 0; id < (document.meshes ?? []).length; id++)
emitMesh(id, {});
return { materials, primitives, textures };
emitMesh(id, -1, identity);
const lightDefinitions =
document.extensions?.KHR_lights_punctual?.lights ?? [];
const lights = nodes.flatMap((node: any, id: number) => {
const lightId = node.extensions?.KHR_lights_punctual?.light;
const light = lightId === undefined ? undefined : lightDefinitions[lightId];
if (!light) return [];
const matrix = world(id);
return [
{
type: light.type,
color: light.color ?? [1, 1, 1],
intensity: light.intensity ?? 1,
range: light.range ?? 1_000_000,
innerAngle: light.spot?.innerConeAngle ?? 0,
outerAngle: light.spot?.outerConeAngle ?? Math.PI / 4,
position: [matrix[12], matrix[13], matrix[14]],
rotor: rotor(matrix),
},
];
});
const cameraNode = nodes.findIndex((node: any) => node.camera !== undefined);
const cameraDefinition =
cameraNode < 0 ? undefined : document.cameras?.[nodes[cameraNode].camera];
const camera = cameraDefinition
? {
direction: vector(world(cameraNode), [0, 0, 1]),
fov: cameraDefinition.perspective?.yfov,
}
: undefined;
return { camera, lights, materials, primitives, textures };
}
addEventListener("message", async ({ data }) => {
try {
const result = await load(data.url);
const transfers = result.primitives.flatMap((primitive: any) =>
["positions", "indices", "normals", "tangents", "uvs", "colors"]
.map((name) => primitive[name]?.buffer)
.filter(Boolean),
);
transfers.push(...result.textures.map((texture: any) => texture.image));
(postMessage as any)({ request: data.request, result }, transfers);
} catch (error) {
postMessage({
request: data.request,
error: error instanceof Error ? error.message : "GLTF_IMPORT",
});
}
});
if (typeof addEventListener === "function")
addEventListener("message", async ({ data }) => {
try {
const result = await loadGltf(data.url);
const transfers = result.primitives.flatMap((primitive: any) =>
["positions", "indices", "normals", "tangents", "uvs", "uvs2", "colors"]
.map((name) => primitive[name]?.buffer)
.filter(Boolean),
);
transfers.push(...result.textures.map((texture: any) => texture.image));
(postMessage as any)({ request: data.request, result }, transfers);
} catch (error) {
postMessage({
request: data.request,
error: error instanceof Error ? error.message : "GLTF_IMPORT",
});
}
});