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
+174 -32
View File
@@ -1,6 +1,13 @@
import type { Scene } from "../Scene";
import type { Texture } from "./Texture";
export type PBRTextureInfo = {
texCoord?: number;
offset?: ArrayLike<number>;
scale?: ArrayLike<number>;
rotation?: number;
};
export type PBRMaterialOptions = {
baseColor?: ArrayLike<number>;
metallic?: number;
@@ -12,9 +19,40 @@ export type PBRMaterialOptions = {
metallicRoughnessTexture?: Texture;
normalTexture?: Texture;
emissiveTexture?: Texture;
occlusionTexture?: Texture;
transmissionTexture?: Texture;
thicknessTexture?: Texture;
clearcoatTexture?: Texture;
iridescenceTexture?: Texture;
iridescenceThicknessTexture?: Texture;
anisotropyTexture?: Texture;
specularTexture?: Texture;
specularColorTexture?: Texture;
textureInfos?: ArrayLike<PBRTextureInfo | undefined>;
flags?: number;
occlusionStrength?: number;
transmission?: number;
ior?: number;
thickness?: number;
attenuationDistance?: number;
attenuationColor?: ArrayLike<number>;
emissiveStrength?: number;
clearcoat?: number;
clearcoatRoughness?: number;
specular?: number;
sheenColor?: ArrayLike<number>;
sheenRoughness?: number;
specularColor?: ArrayLike<number>;
iridescence?: number;
iridescenceIor?: number;
iridescenceThicknessMinimum?: number;
iridescenceThicknessMaximum?: number;
anisotropy?: number;
anisotropyRotation?: number;
specularExtensionColor?: ArrayLike<number>;
};
/** Conventional PBR values and texture IDs stored in two shared SOA rows. */
/** Conventional PBR values, extensions, texture IDs, and UV transforms. */
export class PBRMaterial {
readonly scene: Scene;
id = -1;
@@ -28,48 +66,152 @@ export class PBRMaterial {
options.metallicRoughnessTexture,
options.normalTexture,
options.emissiveTexture,
options.occlusionTexture,
options.transmissionTexture,
options.thicknessTexture,
options.clearcoatTexture,
options.iridescenceTexture,
options.iridescenceThicknessTexture,
options.anisotropyTexture,
options.specularTexture,
options.specularColorTexture,
].filter((texture): texture is Texture => texture !== undefined);
this.ready = Promise.all(textures.map((texture) => texture.ready)).then(async () => {
const id = await scene.allocateMaterial();
this.id = id;
const color = Array.from(options.baseColor ?? [1, 1, 1, 1]);
const emissive = Array.from(options.emissive ?? [0, 0, 0]);
if (color.length !== 4 || emissive.length !== 3) throw new TypeError("PBR material vectors");
scene.array("materials").write(id, [
...color,
options.metallic ?? 0,
options.roughness ?? 0.7,
...emissive,
options.normalScale ?? 1,
options.alphaCutoff ?? 0.5,
0,
]);
scene.array("materialTextures").write(id, [
(options.baseColorTexture?.id ?? -1) + 1,
(options.metallicRoughnessTexture?.id ?? -1) + 1,
(options.normalTexture?.id ?? -1) + 1,
(options.emissiveTexture?.id ?? -1) + 1,
0, 0, 0, 0,
]);
return this;
});
this.ready = Promise.all(textures.map((texture) => texture.ready)).then(
async () => {
const id = await scene.allocateMaterial();
this.id = id;
const color = Array.from(options.baseColor ?? [1, 1, 1, 1]);
const emissive = Array.from(options.emissive ?? [0, 0, 0]);
const attenuation = Array.from(options.attenuationColor ?? [1, 1, 1]);
const secondary = Array.from(
options.flags && options.flags & 16
? (options.specularColor ?? [1, 1, 1])
: (options.sheenColor ?? [0, 0, 0]),
);
const specularExtensionColor = Array.from(
options.specularExtensionColor ?? [1, 1, 1],
);
if (
color.length !== 4 ||
emissive.length !== 3 ||
attenuation.length !== 3 ||
secondary.length !== 3 ||
specularExtensionColor.length !== 3
)
throw new TypeError("PBR material vectors");
scene
.array("materials")
.write(id, [
...color,
options.metallic ?? 0,
options.roughness ?? 0.7,
...emissive,
options.normalScale ?? 1,
options.alphaCutoff ?? 0.5,
options.flags ?? 0,
]);
scene
.array("materialTextures")
.write(id, [
(options.baseColorTexture?.id ?? -1) + 1,
(options.metallicRoughnessTexture?.id ?? -1) + 1,
(options.normalTexture?.id ?? -1) + 1,
(options.emissiveTexture?.id ?? -1) + 1,
(options.occlusionTexture?.id ?? -1) + 1,
(options.transmissionTexture?.id ?? -1) + 1,
(options.thicknessTexture?.id ?? -1) + 1,
(options.clearcoatTexture?.id ?? -1) + 1,
(options.iridescenceTexture?.id ?? -1) + 1,
(options.iridescenceThicknessTexture?.id ?? -1) + 1,
(options.anisotropyTexture?.id ?? -1) + 1,
(options.specularTexture?.id ?? -1) + 1,
(options.specularColorTexture?.id ?? -1) + 1,
0,
0,
0,
]);
scene
.array("materialExtensions")
.write(id, [
options.transmission ?? 0,
options.ior ?? 1.5,
options.thickness ?? 0,
options.attenuationDistance ?? 0,
...attenuation,
options.emissiveStrength ?? 1,
options.clearcoat ?? 0,
options.clearcoatRoughness ?? 0,
options.specular ?? 1,
options.occlusionStrength ?? 1,
...secondary,
options.sheenRoughness ?? 0,
options.iridescence ?? 0,
options.iridescenceIor ?? 1.3,
options.iridescenceThicknessMinimum ?? 100,
options.iridescenceThicknessMaximum ?? 400,
options.anisotropy ?? 0,
options.anisotropyRotation ?? 0,
0,
0,
...specularExtensionColor,
0,
]);
const transforms = new Float32Array(104);
for (let slot = 0; slot < 13; slot++) {
const info = options.textureInfos?.[slot];
const offset = Array.from(info?.offset ?? [0, 0]);
const scale = Array.from(info?.scale ?? [1, 1]);
if (offset.length !== 2 || scale.length !== 2)
throw new TypeError("PBR texture transform");
const rotation = info?.rotation ?? 0;
transforms.set(
[
offset[0],
offset[1],
scale[0],
scale[1],
Math.sin(rotation),
Math.cos(rotation),
info?.texCoord ?? 0,
0,
],
slot * 8,
);
}
scene.array("materialTextureInfo").write(id, transforms);
return this;
},
);
}
#values() {
if (this.id < 0) throw new Error("Await material.ready before reading or writing it");
if (this.id < 0)
throw new Error("Await material.ready before reading or writing it");
return this.scene.array("materials").row(this.id);
}
get baseColor() { return this.#values().subarray(0, 4); }
get baseColor() {
return this.#values().subarray(0, 4);
}
set baseColor(value: ArrayLike<number>) {
if (value.length !== 4) throw new RangeError("baseColor");
this.#values().set(value, 0);
}
get metallic() { return this.#values()[4]; }
set metallic(value: number) { this.#values()[4] = value; }
get roughness() { return this.#values()[5]; }
set roughness(value: number) { this.#values()[5] = value; }
get emissive() { return this.#values().subarray(6, 9); }
get metallic() {
return this.#values()[4];
}
set metallic(value: number) {
this.#values()[4] = value;
}
get roughness() {
return this.#values()[5];
}
set roughness(value: number) {
this.#values()[5] = value;
}
get emissive() {
return this.#values().subarray(6, 9);
}
set emissive(value: ArrayLike<number>) {
if (value.length !== 3) throw new RangeError("emissive");
this.#values().set(value, 6);