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
+55 -17
View File
@@ -2,7 +2,15 @@ import { Node, type NodeOptions } from "./Node";
import type { Scene } from "./Scene";
import type { PBRMaterial } from "./materials/PBRMaterial";
export const VertexKinds = Object.freeze(["positions", "normals", "tangents", "uvs", "colors", "indices"] as const);
export const VertexKinds = Object.freeze([
"positions",
"normals",
"tangents",
"uvs",
"uvs2",
"colors",
"indices",
] as const);
export type VertexKind = (typeof VertexKinds)[number];
export type MeshOptions = NodeOptions & {
geometryId?: number;
@@ -27,15 +35,22 @@ export class Mesh extends Node {
this.ready = nodeReady.then(async () => {
if (options.material) await options.material.ready;
await scene.ensureRows(`mesh.${this.id}.faceMaterials`, 1, 16, "u32");
this.vertexCount = Number(scene.geometryData(this.geometryId, "positions")?.length ?? 0) / 3;
this.indexCount = Number(scene.geometryData(this.geometryId, "indices")?.length ?? 0);
this.instanceOf = options.geometryId === undefined ? this.id : this.geometryId;
scene.array("meshInfo").write(this.id, [
this.geometryId,
options.material?.id ?? 0,
options.visible === false ? 0 : 1,
this.instanceOf,
]);
this.vertexCount =
Number(scene.geometryData(this.geometryId, "positions")?.length ?? 0) /
3;
this.indexCount = Number(
scene.geometryData(this.geometryId, "indices")?.length ?? 0,
);
this.instanceOf =
options.geometryId === undefined ? this.id : this.geometryId;
scene
.array("meshInfo")
.write(this.id, [
this.geometryId,
options.material?.id ?? 0,
options.visible === false ? 0 : 1,
this.instanceOf,
]);
for (const [kind, data] of Object.entries(options.vertexData ?? {}))
await this.#setVertexData(kind as VertexKind, data!, false);
this.#writeBounds();
@@ -60,7 +75,8 @@ export class Mesh extends Node {
}
set material(value: PBRMaterial) {
if (value.scene !== this.scene || value.id < 0) throw new Error("Await a material from the same Scene");
if (value.scene !== this.scene || value.id < 0)
throw new Error("Await a material from the same Scene");
this.scene.markDirty(true);
this.scene.array("meshInfo").row(this.id)[1] = value.id;
void this.scene.updateRenderGraph().catch(() => undefined);
@@ -72,13 +88,18 @@ export class Mesh extends Node {
}
async setVertexData(kind: VertexKind, data: ArrayLike<number>) {
if (!VertexKinds.includes(kind)) throw new TypeError(`Unknown vertex kind: ${kind}`);
if (!VertexKinds.includes(kind))
throw new TypeError(`Unknown vertex kind: ${kind}`);
await this.ready;
await this.#setVertexData(kind, data, true);
return this;
}
async #setVertexData(kind: VertexKind, data: ArrayLike<number>, makeUnique: boolean) {
async #setVertexData(
kind: VertexKind,
data: ArrayLike<number>,
makeUnique: boolean,
) {
if (makeUnique && this.scene.geometryReferences(this.geometryId) > 1) {
const original = this.geometryId;
this.geometryId = await this.scene.cloneGeometry(original);
@@ -86,7 +107,15 @@ export class Mesh extends Node {
this.scene.referenceGeometry(this.geometryId);
this.instanceOf = this.id;
this.scene.markDirty(true);
this.scene.array("meshInfo").row(this.id).set([this.geometryId, this.materialId, this.isVisible ? 1 : 0, this.id]);
this.scene
.array("meshInfo")
.row(this.id)
.set([
this.geometryId,
this.materialId,
this.isVisible ? 1 : 0,
this.id,
]);
}
if (kind === "positions") this.vertexCount = data.length / 3;
if (kind === "indices") this.indexCount = data.length;
@@ -96,10 +125,16 @@ export class Mesh extends Node {
async setMaterialForFaces(material: PBRMaterial, faces: number | number[]) {
await Promise.all([this.ready, material.ready]);
if (material.scene !== this.scene) throw new Error("Material must belong to the same Scene");
if (material.scene !== this.scene)
throw new Error("Material must belong to the same Scene");
const list = Array.isArray(faces) ? faces : [faces];
const maximum = Math.max(...list);
const array = await this.scene.ensureRows(`mesh.${this.id}.faceMaterials`, maximum + 1, 16, "u32");
const array = await this.scene.ensureRows(
`mesh.${this.id}.faceMaterials`,
maximum + 1,
16,
"u32",
);
this.scene.markDirty(true);
for (const face of list) {
if (!Number.isInteger(face) || face < 0) throw new RangeError("face");
@@ -120,7 +155,10 @@ export class Mesh extends Node {
minimum[lane] = Math.min(minimum[lane], positions[index + lane]);
maximum[lane] = Math.max(maximum[lane], positions[index + lane]);
}
this.scene.array("bounds").row(this.id).set([...minimum, 0, ...maximum, 0]);
this.scene
.array("bounds")
.row(this.id)
.set([...minimum, 0, ...maximum, 0]);
}
override async dispose() {
+592 -130
View File
@@ -28,6 +28,7 @@ type PostProcessState = {
type TextureState = GraphTexture & {
number: number;
source?: string | ImageBitmap;
sampler?: Omit<GraphSampler, "id">;
};
const rows = [
@@ -40,7 +41,9 @@ const rows = [
["cameras", 80, "f32"],
["cameraMatrices", 80, "f32"],
["materials", 48, "f32"],
["materialTextures", 32, "u32"],
["materialTextures", 64, "u32"],
["materialExtensions", 112, "f32"],
["materialTextureInfo", 416, "f32"],
["pointLights", 32, "f32"],
["rectAreaLights", 48, "f32"],
["spotLights", 32, "f32"],
@@ -49,6 +52,11 @@ const rows = [
["sceneAccent", 16, "f32"],
] as const;
const compactLightLimit = 500;
const compactPointStart = 11;
const compactSpotCount = compactPointStart + compactLightLimit;
const compactSpotStart = compactSpotCount + 1;
const clusterShader = /* wgsl */ `
@group(0) @binding(0) var<storage, read> pointLights: array<vec4<f32>>;
@group(0) @binding(1) var<storage, read> rectLights: array<vec4<f32>>;
@@ -61,12 +69,20 @@ const clusterShader = /* wgsl */ `
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
if (id.x != 0u) { return; }
var count = 0u;
var pointCount = 0u;
var spotCount = 0u;
var light = vec3<f32>(0.0);
var pointLight = vec3<f32>(0.0);
var spotLight = vec3<f32>(0.0);
for (var i = 0u; i < arrayLength(&pointLights) / 2u; i++) {
let enabled = pointLights[i * 2u + 1u].y;
count += select(0u, 1u, enabled != 0.0);
if (enabled != 0.0) {
if (pointCount < ${compactLightLimit}u) {
clusters[${compactPointStart}u + pointCount] = i;
}
pointCount += 1u;
}
let contribution = pointLights[i * 2u].rgb * pointLights[i * 2u].a * enabled * 0.02;
light += contribution;
pointLight += contribution;
@@ -79,6 +95,12 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
for (var i = 0u; i < arrayLength(&spotLights) / 2u; i++) {
let enabled = spotLights[i * 2u + 1u].w;
count += select(0u, 1u, enabled != 0.0);
if (enabled != 0.0) {
if (spotCount < ${compactLightLimit}u) {
clusters[${compactSpotStart}u + spotCount] = i;
}
spotCount += 1u;
}
let contribution = spotLights[i * 2u].rgb * spotLights[i * 2u].a * enabled * 0.02;
light += contribution;
spotLight += contribution;
@@ -102,6 +124,8 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
clusters[7] = bitcast<u32>(spotLight.r);
clusters[8] = bitcast<u32>(spotLight.g);
clusters[9] = bitcast<u32>(spotLight.b);
clusters[10] = pointCount;
clusters[${compactSpotCount}] = spotCount;
}`;
const basicForwardShader = /* wgsl */ `
@@ -162,10 +186,70 @@ fn fragment(input: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(color, base.a);
}`;
function pbrShader(mask: number) {
const baseTexture = mask & 1;
const materialTexture = mask & 2;
const normalTexture = mask & 4;
function pbrShader(
mask: number,
flags: number,
hasColors: boolean,
hasUvs2: boolean,
transmission: boolean,
textureInfo: ArrayLike<number>,
extensionInfo: ArrayLike<number>,
) {
const names = [
"baseTexture",
"materialTexture",
"normalTexture",
"emissiveTexture",
"occlusionTexture",
"transmissionTexture",
"thicknessTexture",
"clearcoatTexture",
"iridescenceTexture",
"iridescenceThicknessTexture",
"anisotropyTexture",
"specularTexture",
"specularColorTexture",
];
const texture = (slot: number) => mask & (1 << slot);
const number = (value: number) =>
Number.isFinite(value)
? Number.isInteger(value)
? `${value}.0`
: `${value}`
: "0.0";
const textureUv = (slot: number) => {
const at = slot * 8;
const source = textureInfo[at + 6] > 0.5 ? "input.uv2" : "input.uv";
if (
textureInfo[at] === 0 &&
textureInfo[at + 1] === 0 &&
textureInfo[at + 2] === 1 &&
textureInfo[at + 3] === 1 &&
textureInfo[at + 4] === 0 &&
textureInfo[at + 5] === 1
)
return source;
const x = `(${source}.x * ${number(textureInfo[at + 2])})`;
const y = `(${source}.y * ${number(textureInfo[at + 3])})`;
return `vec2(${number(textureInfo[at + 5])} * ${x} - ${number(textureInfo[at + 4])} * ${y}, ${number(textureInfo[at + 4])} * ${x} + ${number(textureInfo[at + 5])} * ${y}) + vec2(${number(textureInfo[at])}, ${number(textureInfo[at + 1])})`;
};
const sample = (slot: number, fallback = "vec4(1.0)") =>
texture(slot)
? `textureSample(${names[slot]}, materialSampler, ${textureUv(slot)})`
: fallback;
const normalTexture = texture(2);
const alphaMode = flags & 3;
const doubleSided = flags & 4;
const unlit = flags & 8;
const specularGlossiness = flags & 16;
const clearcoat = extensionInfo[8] > 0;
const sheen =
extensionInfo[12] > 0 || extensionInfo[13] > 0 || extensionInfo[14] > 0;
const iridescence = extensionInfo[16] > 0;
const anisotropy = extensionInfo[20] > 0;
const attenuation = transmission && extensionInfo[3] > 0;
const ior = Math.max(extensionInfo[1], 1);
const dielectric = ((ior - 1) / (ior + 1)) ** 2;
return /* wgsl */ `
struct VertexOutput {
@invariant @builtin(position) position: vec4<f32>,
@@ -173,8 +257,9 @@ struct VertexOutput {
@location(1) normal: vec3<f32>,
@location(2) uv: vec2<f32>,
@location(3) tangent: vec4<f32>,
@location(4) @interpolate(flat) mesh: u32,
@location(5) @interpolate(flat) material: u32,
@location(4) color: vec4<f32>,
@location(5) uv2: vec2<f32>,
@location(6) @interpolate(flat) material: u32,
}
@group(0) @binding(0) var<storage, read> clusters: array<u32>;
@@ -188,9 +273,8 @@ struct VertexOutput {
@group(0) @binding(8) var<storage, read> pointLights: array<vec4<f32>>;
@group(0) @binding(9) var<storage, read> spotLights: array<vec4<f32>>;
${mask ? "@group(1) @binding(0) var materialSampler: sampler;" : ""}
${baseTexture ? "@group(1) @binding(1) var baseTexture: texture_2d<f32>;" : ""}
${materialTexture ? "@group(1) @binding(2) var materialTexture: texture_2d<f32>;" : ""}
${normalTexture ? "@group(1) @binding(3) var normalTexture: texture_2d<f32>;" : ""}
${names.map((name, slot) => (texture(slot) ? `@group(1) @binding(${slot + 1}) var ${name}: texture_2d<f32>;` : "")).join("\n")}
${transmission ? "@group(2) @binding(0) var sceneColor: texture_2d<f32>;\n@group(2) @binding(1) var sceneSampler: sampler;" : ""}
fn rotate(rotor: vec4<f32>, value: vec3<f32>) -> vec3<f32> {
return value + 2.0 * cross(rotor.xyz, cross(rotor.xyz, value) + rotor.w * value);
@@ -202,6 +286,8 @@ fn vertex(
@location(1) localNormal: vec3<f32>,
@location(2) uv: vec2<f32>,
${normalTexture ? "@location(3) localTangent: vec4<f32>," : ""}
${hasColors ? "@location(4) color: vec4<f32>," : ""}
${hasUvs2 ? "@location(5) uv2: vec2<f32>," : ""}
@builtin(instance_index) packed: u32,
) -> VertexOutput {
let instance = packed & 65535u;
@@ -223,79 +309,184 @@ fn vertex(
output.normal = normalize(rotate(rotors[instance], localNormal / scale));
output.uv = uv;
output.tangent = ${normalTexture ? "vec4(normalize(rotate(rotors[instance], localTangent.xyz * scale)), localTangent.w)" : "vec4(1.0, 0.0, 0.0, 1.0)"};
output.mesh = instance;
output.material = packed >> 16u;
output.color = ${hasColors ? "color" : "vec4(1.0)"};
output.uv2 = ${hasUvs2 ? "uv2" : "uv"};
output.material = (packed >> 16u) - 1u;
return output;
}
fn studioEnvironment(direction: vec3<f32>) -> vec3<f32> {
let ray = direction;
let horizon = mix(vec3(0.14, 0.15, 0.16), vec3(0.62, 0.59, 0.54), clamp(ray.y * 0.5 + 0.5, 0.0, 1.0));
let key = max(dot(ray, normalize(vec3(-0.65, 0.7, 0.35))), 0.0);
let key2 = key * key;
let key4 = key2 * key2;
let key8 = key4 * key4;
let key16 = key8 * key8;
let key32 = key16 * key16;
let key64 = key32 * key32;
return horizon + vec3(5.0, 4.2, 3.1) * key64 * key32;
}
fn pow5(value: f32) -> f32 {
let squared = value * value;
return squared * squared * value;
}
fn pointContribution(index: u32, normal: vec3<f32>, world: vec3<f32>) -> vec3<f32> {
let colorIntensity = pointLights[index * 2u];
let state = pointLights[index * 2u + 1u];
if (state.y == 0.0 || state.x <= 0.0) { return vec3(0.0); }
let offset = positions[index].xyz - world;
let distance = length(offset);
let falloff = max(1.0 - distance / state.x, 0.0);
let incidence = max(dot(normal, offset / max(distance, 0.0001)), 0.0);
return colorIntensity.rgb * colorIntensity.a * falloff * falloff * incidence * 0.08;
}
fn spotContribution(index: u32, normal: vec3<f32>, world: vec3<f32>) -> vec3<f32> {
let colorIntensity = spotLights[index * 2u];
let state = spotLights[index * 2u + 1u];
if (state.w == 0.0 || state.x <= 0.0) { return vec3(0.0); }
let fromLight = world - positions[index].xyz;
let distance = length(fromLight);
let direction = normalize(rotate(rotors[index], vec3<f32>(0.0, 0.0, -1.0)));
let cone = smoothstep(cos(state.z), cos(state.y), dot(direction, fromLight / max(distance, 0.0001)));
let falloff = max(1.0 - distance / state.x, 0.0);
let incidence = max(dot(normal, -fromLight / max(distance, 0.0001)), 0.0);
return colorIntensity.rgb * colorIntensity.a * cone * falloff * falloff * incidence * 0.08;
}
@fragment
fn fragment(input: VertexOutput) -> @location(0) vec4<f32> {
let fallback = meshInfo[input.mesh * 4u + 1u];
let material = select(fallback, input.material - 1u, input.material != 0u);
let factor = materials[material * 3u];
let properties = materials[material * 3u + 1u];
let extra = materials[material * 3u + 2u];
let base = factor * ${baseTexture ? "textureSample(baseTexture, materialSampler, input.uv)" : "vec4(1.0)"};
let packedMaterial = ${materialTexture ? "textureSample(materialTexture, materialSampler, input.uv)" : "vec4(1.0)"};
let metallic = clamp(properties.x * ${materialTexture ? "packedMaterial.b" : "1.0"}, 0.0, 1.0);
let roughness = clamp(properties.y * ${materialTexture ? "packedMaterial.g" : "1.0"}, 0.04, 1.0);
fn fragment(input: VertexOutput, @builtin(front_facing) frontFacing: bool) -> @location(0) vec4<f32> {
let factor = materials[input.material * 3u];
let properties = materials[input.material * 3u + 1u];
let extra = materials[input.material * 3u + 2u];
let extension0 = vec4(${Array.from(extensionInfo).slice(0, 4).map(number).join(", ")});
let extension1 = vec4(${Array.from(extensionInfo).slice(4, 8).map(number).join(", ")});
let extension2 = vec4(${Array.from(extensionInfo).slice(8, 12).map(number).join(", ")});
let extension3 = vec4(${Array.from(extensionInfo).slice(12, 16).map(number).join(", ")});
let extension4 = vec4(${Array.from(extensionInfo).slice(16, 20).map(number).join(", ")});
let extension5 = vec4(${Array.from(extensionInfo).slice(20, 24).map(number).join(", ")});
let extension6 = vec4(${Array.from(extensionInfo).slice(24, 28).map(number).join(", ")});
let base = factor * input.color * ${sample(0)};
${alphaMode === 1 ? "if (base.a < extra.z) { discard; }" : ""}
let packedMaterial = ${sample(1)};
let metallic = clamp(properties.x * ${specularGlossiness ? "0.0" : texture(1) ? "packedMaterial.b" : "1.0"}, 0.0, 1.0);
${anisotropy ? `let anisotropyAmount = clamp(extension5.x * ${sample(10)}.b, 0.0, 1.0);` : ""}
let roughness = clamp(${specularGlossiness ? `1.0 - (1.0 - properties.y) * ${texture(1) ? "packedMaterial.a" : "1.0"}` : `properties.y * ${texture(1) ? "packedMaterial.g" : "1.0"}`}${anisotropy ? " * mix(1.0, 0.7, anisotropyAmount)" : ""}, 0.04, 1.0);
var normal = normalize(input.normal);
${normalTexture ? "let tangent = normalize(input.tangent.xyz - normal * dot(input.tangent.xyz, normal)); let bitangent = cross(normal, tangent) * input.tangent.w; let mapped = textureSample(normalTexture, materialSampler, input.uv).xyz * 2.0 - 1.0; normal = normalize(mat3x3<f32>(tangent, bitangent, normal) * vec3(mapped.xy * extra.y, mapped.z));" : ""}
${doubleSided ? "normal *= select(-1.0, 1.0, frontFacing);" : ""}
${normalTexture ? `let tangent = normalize(input.tangent.xyz - normal * dot(input.tangent.xyz, normal)); let bitangent = cross(normal, tangent) * input.tangent.w; let mapped = ${sample(2)}.xyz * 2.0 - 1.0; normal = normalize(mat3x3<f32>(tangent, bitangent, normal) * vec3(mapped.xy * extra.y, mapped.z));` : ""}
let view = normalize(cameraMatrices[4].xyz - input.world);
let lightDirection = normalize(vec3<f32>(0.4, 0.7, 0.6));
let halfVector = normalize(lightDirection + view);
let nDotL = max(dot(normal, lightDirection), 0.0);
let nDotV = max(dot(normal, view), 0.0001);
let nDotH = max(dot(normal, halfVector), 0.0);
let f0 = mix(vec3(0.04), base.rgb, metallic);
let specular = f0 * pow(nDotH, max(2.0, 2.0 / (roughness * roughness) - 2.0));
let vDotH = max(dot(view, halfVector), 0.0);
let specularWeight = extension2.z * ${sample(11)}.a;
let dielectricColor = vec3(${number(dielectric)} * specularWeight) * extension6.rgb * ${sample(12)}.rgb;
var f0 = ${specularGlossiness ? "packedMaterial.rgb * extension3.rgb" : "mix(dielectricColor, base.rgb, metallic)"};
${
iridescence
? `let iridescenceAmount = clamp(extension4.x * ${sample(8)}.r, 0.0, 1.0);
let iridescenceThickness = mix(extension4.z, extension4.w, ${sample(9)}.g);
let iridescencePhase = iridescenceThickness * 0.025 + (1.0 - nDotV) * 6.0;
let iridescenceTint = 0.12 + 0.88 * (vec3(0.5) + vec3(0.5) * cos(vec3(iridescencePhase, iridescencePhase + 2.0944, iridescencePhase + 4.1888)));
let iridescenceLevel = max(max(f0.r, f0.g), max(f0.b, 0.04));
f0 = mix(f0, iridescenceTint * iridescenceLevel, iridescenceAmount * (1.0 - roughness * 0.5));`
: ""
}
let fresnelPower = pow5(1.0 - vDotH);
let fresnel = f0 + (vec3(1.0) - f0) * fresnelPower;
let alpha = roughness * roughness;
let alpha2 = alpha * alpha;
let denominator = nDotH * nDotH * (alpha2 - 1.0) + 1.0;
let distribution = alpha2 / max(3.14159265 * denominator * denominator, 0.0001);
let geometryV = nDotV / mix(nDotV, 1.0, alpha * 0.5);
let geometryL = nDotL / mix(nDotL, 1.0, alpha * 0.5);
let specular = distribution * geometryV * geometryL * fresnel / max(4.0 * nDotV * nDotL, 0.0001);
let clusterLight = vec3<f32>(bitcast<f32>(clusters[1]), bitcast<f32>(clusters[2]), bitcast<f32>(clusters[3]));
let pointAmbient = vec3<f32>(bitcast<f32>(clusters[4]), bitcast<f32>(clusters[5]), bitcast<f32>(clusters[6]));
let spotAmbient = vec3<f32>(bitcast<f32>(clusters[7]), bitcast<f32>(clusters[8]), bitcast<f32>(clusters[9]));
let ambient = vec3(0.08) + max(clusterLight - pointAmbient - spotAmbient, vec3(0.0));
let ambient = vec3(0.14) + max(clusterLight - pointAmbient - spotAmbient, vec3(0.0));
var pointDiffuse = vec3<f32>(0.0);
for (var i = 0u; i < arrayLength(&pointLights) / 2u; i++) {
let colorIntensity = pointLights[i * 2u];
let state = pointLights[i * 2u + 1u];
if (state.y == 0.0 || state.x <= 0.0) { continue; }
let offset = positions[i].xyz - input.world;
let distance = length(offset);
let attenuation = pow(max(1.0 - distance / state.x, 0.0), 2.0);
let incidence = max(dot(normal, offset / max(distance, 0.0001)), 0.0);
pointDiffuse += colorIntensity.rgb * colorIntensity.a * attenuation * incidence * 0.08;
let pointCount = clusters[10];
if (pointCount <= ${compactLightLimit}u) {
for (var light = 0u; light < pointCount; light++) {
pointDiffuse += pointContribution(clusters[${compactPointStart}u + light], normal, input.world);
}
} else {
for (var light = 0u; light < arrayLength(&pointLights) / 2u; light++) {
pointDiffuse += pointContribution(light, normal, input.world);
}
}
var spotDiffuse = vec3<f32>(0.0);
for (var i = 0u; i < arrayLength(&spotLights) / 2u; i++) {
let colorIntensity = spotLights[i * 2u];
let state = spotLights[i * 2u + 1u];
if (state.w == 0.0 || state.x <= 0.0) { continue; }
let fromLight = input.world - positions[i].xyz;
let distance = length(fromLight);
let lightDirection = normalize(rotate(rotors[i], vec3<f32>(0.0, 0.0, -1.0)));
let cone = smoothstep(cos(state.z), cos(state.y), dot(lightDirection, fromLight / max(distance, 0.0001)));
let attenuation = pow(max(1.0 - distance / state.x, 0.0), 2.0);
let incidence = max(dot(normal, -fromLight / max(distance, 0.0001)), 0.0);
spotDiffuse += colorIntensity.rgb * colorIntensity.a * cone * attenuation * incidence * 0.08;
let spotCount = clusters[${compactSpotCount}];
if (spotCount <= ${compactLightLimit}u) {
for (var light = 0u; light < spotCount; light++) {
spotDiffuse += spotContribution(clusters[${compactSpotStart}u + light], normal, input.world);
}
} else {
for (var light = 0u; light < arrayLength(&spotLights) / 2u; light++) {
spotDiffuse += spotContribution(light, normal, input.world);
}
}
let defaultLight = select(1.0, 0.18, clusters[0] != 0u);
let diffuse = base.rgb * (1.0 - metallic) * nDotL * defaultLight;
let emissive = vec3(properties.z, properties.w, extra.x);
let occlusion = mix(1.0, ${sample(4)}.r, extension2.w);
let diffuseColor = base.rgb * (vec3(1.0) - fresnel) * (1.0 - metallic) / 3.14159265;
let diffuse = (diffuseColor + specular) * nDotL * defaultLight;
let reflected = studioEnvironment(reflect(-view, normal));
let environmentFresnel = f0 + (vec3(1.0) - f0) * pow5(1.0 - nDotV);
let environment = (base.rgb * (1.0 - metallic) * studioEnvironment(normal) * 0.5 + reflected * environmentFresnel * (1.0 - roughness * 0.75)) * occlusion;
let emissive = vec3(properties.z, properties.w, extra.x) * ${sample(3, "vec4(1.0)")}.rgb * extension1.w;
let localDiffuse = base.rgb * (1.0 - metallic) * (pointDiffuse + spotDiffuse);
return vec4((base.rgb * ambient + diffuse + localDiffuse + specular * nDotL * defaultLight + emissive) * accent.a, base.a);
${sheen ? "let sheen = extension3.rgb * pow(1.0 - nDotV, mix(2.0, 8.0, extension3.w));" : ""}
${
clearcoat
? `let clearcoatFresnel = 0.04 + 0.96 * fresnelPower;
let clearcoat = extension2.x * ${sample(7)}.r * clearcoatFresnel * pow(nDotH, mix(256.0, 4.0, extension2.y));`
: ""
}
var color = base.rgb * ambient * occlusion + diffuse + environment + localDiffuse${sheen ? " + sheen" : ""}${clearcoat ? " + vec3(clearcoat)" : ""} + emissive;
${unlit ? "color = base.rgb + emissive;" : ""}
${
transmission
? `let transmissionAmount = clamp(extension0.x * ${sample(5)}.r, 0.0, 1.0);
let thickness = max(extension0.z * ${sample(6)}.g, 0.0);
let screenUv = input.position.xy / vec2<f32>(textureDimensions(sceneColor));
let refractionOffset = normal.xy * ${number(1 - 1 / ior)} * mix(0.006, 0.035, clamp(thickness, 0.0, 1.0));
let refractedUv = clamp(screenUv + refractionOffset, vec2(0.001), vec2(0.999));
let transmissionBlur = vec2(0.02078461 * roughness);
var transmitted = textureSampleLevel(sceneColor, sceneSampler, refractedUv, 0.0).rgb * 0.2;
transmitted += textureSampleLevel(sceneColor, sceneSampler, clamp(refractedUv + transmissionBlur, vec2(0.001), vec2(0.999)), 0.0).rgb * 0.2;
transmitted += textureSampleLevel(sceneColor, sceneSampler, clamp(refractedUv - transmissionBlur, vec2(0.001), vec2(0.999)), 0.0).rgb * 0.2;
transmitted += textureSampleLevel(sceneColor, sceneSampler, clamp(refractedUv + vec2(transmissionBlur.x, -transmissionBlur.y), vec2(0.001), vec2(0.999)), 0.0).rgb * 0.2;
transmitted += textureSampleLevel(sceneColor, sceneSampler, clamp(refractedUv + vec2(-transmissionBlur.x, transmissionBlur.y), vec2(0.001), vec2(0.999)), 0.0).rgb * 0.2;
${
attenuation
? `{
let travel = thickness / max(abs(dot(normal, view)), 0.15);
transmitted *= pow(clamp(extension1.rgb, vec3(0.001), vec3(1.0)), vec3(travel / extension0.w));
}`
: ""
}
let normalizedTransmissionTint = base.rgb / max(max(base.r, base.g), max(base.b, 0.001));
transmitted *= normalizedTransmissionTint;
color = mix(color, transmitted + reflected * environmentFresnel, transmissionAmount);`
: ""
}
return vec4(color * accent.a, ${alphaMode === 2 ? "base.a" : "1.0"});
}`;
}
const emptyForwardShader = /* wgsl */ `
struct Accent { color: vec4<f32> }
@group(0) @binding(0) var<uniform> accent: Accent;
struct VertexOutput { @builtin(position) position: vec4<f32> }
@vertex fn vertex(@builtin(vertex_index) index: u32) -> VertexOutput {
let points = array(vec2(-0.72, -0.6), vec2(0.72, -0.6), vec2(0.0, 0.72));
var output: VertexOutput;
output.position = vec4(points[index], 0.0, 1.0);
return output;
const clearShader = /* wgsl */ `
@vertex fn vertex() -> @builtin(position) vec4<f32> {
return vec4(0.0);
}
@fragment fn fragment() -> @location(0) vec4<f32> {
return vec4(accent.color.rgb, 1.0);
return vec4(0.0);
}`;
const fullscreenVertex = /* wgsl */ `
@@ -311,6 +502,28 @@ struct VertexOutput {
return output;
}`;
const copyShader = `${fullscreenVertex}
@group(0) @binding(0) var source: texture_2d<f32>;
@group(0) @binding(1) var sourceSampler: sampler;
@fragment fn fragment(input: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(source, sourceSampler, input.uv);
}`;
const studioBackground = /* wgsl */ `
fn studioBackground(uv: vec2<f32>) -> vec3<f32> {
let upper = vec3(0.42, 0.43, 0.44);
let lower = vec3(0.16, 0.17, 0.18);
let vertical = smoothstep(0.0, 1.0, uv.y);
let keyBase = max(1.0 - distance(uv, vec2(0.05, 0.05)) / 0.72, 0.0);
return mix(upper, lower, vertical) + vec3(4.0, 3.8, 3.45) * keyBase * keyBase;
}`;
const studioBackgroundShader = `${fullscreenVertex}
${studioBackground}
@fragment fn fragment(input: VertexOutput) -> @location(0) vec4<f32> {
return vec4(studioBackground(input.uv), 1.0);
}`;
function effectFragment(kind: string, options: Record<string, unknown>) {
const amount = Number(options.amount ?? options.exposure ?? 1);
const safeAmount = Number.isFinite(amount) ? amount : 1;
@@ -330,7 +543,10 @@ function effectFragment(kind: string, options: Record<string, unknown>) {
}`;
}
function presentShader(toneMap: string) {
function presentShader(
toneMap: string,
composite: "studio" | "scene" | undefined,
) {
const tone =
toneMap === "reinhard"
? "color / (color + vec3(1.0))"
@@ -338,12 +554,14 @@ function presentShader(toneMap: string) {
? "clamp(color, vec3(0.0), vec3(1.0))"
: "clamp((color * (2.51 * color + vec3(0.03))) / (color * (2.43 * color + vec3(0.59)) + vec3(0.14)), vec3(0.0), vec3(1.0))";
return `${fullscreenVertex}
${composite === "studio" ? studioBackground : ""}
@group(0) @binding(0) var source: texture_2d<f32>;
@group(0) @binding(1) var sourceSampler: sampler;
${composite === "scene" ? "@group(0) @binding(2) var scene: texture_2d<f32>;" : ""}
@fragment fn fragment(input: VertexOutput) -> @location(0) vec4<f32> {
let value = textureSample(source, sourceSampler, input.uv);
let color = max(value.rgb, vec3(0.0));
return vec4(${tone}, value.a);
let color = max(${composite === "studio" ? "value.rgb + studioBackground(input.uv) * (1.0 - value.a)" : composite === "scene" ? "value.rgb + textureSample(scene, sourceSampler, input.uv).rgb * (1.0 - value.a)" : "value.rgb"}, vec3(0.0));
return vec4(${tone}, ${composite ? "1.0" : "value.a"});
}`;
}
@@ -402,7 +620,12 @@ export class Scene {
constructor(
canvas: HTMLCanvasElement,
options: { arenaBytes?: number; debug?: boolean; fps?: number; hdr?: boolean } = {},
options: {
arenaBytes?: number;
debug?: boolean;
fps?: number;
hdr?: boolean;
} = {},
) {
this.hdr = options.hdr ?? true;
this.core = new YawnCore(canvas, {
@@ -429,7 +652,22 @@ export class Scene {
const material = await this.core.allocateObject("materials");
this.core
.array("materials")
.write(material, [1, 1, 1, 1, 0, 0.7, 0, 0, 0, 0, 1, 0.5]);
.write(material, [1, 1, 1, 1, 0, 0.7, 0, 0, 0, 1, 0.5, 0]);
this.core
.array("materialExtensions")
.write(
material,
[
0, 1.5, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1.3, 100, 400, 0,
0, 0, 0, 1, 1, 1, 0,
],
);
const textureInfo = this.core.array("materialTextureInfo").row(material);
for (let slot = 0; slot < 13; slot++) {
textureInfo[slot * 8 + 2] = 1;
textureInfo[slot * 8 + 3] = 1;
textureInfo[slot * 8 + 5] = 1;
}
if (fps !== undefined) await this.core.setFps(fps);
await this.#compileRenderGraph();
return this;
@@ -471,7 +709,8 @@ export class Scene {
const value = Reflect.get(target, property, target);
if (typeof value !== "function") return value;
if (property === "constructor") return value;
if (!typedArrayMutators.has(String(property))) return value.bind(target);
if (!typedArrayMutators.has(String(property)))
return value.bind(target);
return (...arguments_: unknown[]) => {
const result = Reflect.apply(value, target, arguments_);
this.markDirty();
@@ -570,9 +809,21 @@ export class Scene {
{
name: "materialTextures",
rows: materialCapacity,
stride: 32,
stride: 64,
format: "u32" as const,
},
{
name: "materialExtensions",
rows: materialCapacity,
stride: 112,
format: "f32" as const,
},
{
name: "materialTextureInfo",
rows: materialCapacity,
stride: 416,
format: "f32" as const,
},
].filter((request) => this.array(request.name).rows < request.rows);
if (growth.length) await this.core.createRowsBatch(growth);
}
@@ -623,7 +874,11 @@ export class Scene {
async allocateMaterial() {
await this.ready;
const id = await this.core.allocateObject("materials");
await this.ensureRows("materialTextures", id + 1, 32, "u32");
await Promise.all([
this.ensureRows("materialTextures", id + 1, 64, "u32"),
this.ensureRows("materialExtensions", id + 1, 112, "f32"),
this.ensureRows("materialTextureInfo", id + 1, 416, "f32"),
]);
return id;
}
@@ -713,6 +968,7 @@ export class Scene {
normals: 3,
tangents: 4,
uvs: 2,
uvs2: 2,
colors: 4,
indices: 1,
};
@@ -850,9 +1106,34 @@ export class Scene {
}
const computeIds = [...this.#computePasses.keys()];
const renderedMeshes = [...this.#meshes.values()].filter(
(mesh) => mesh.vertexCount > 0,
);
const renderCategory = (material: number) => {
const flags = Math.round(this.array("materials").row(material)[11]);
const transmitted = this.array("materialExtensions").row(material)[0] > 0;
return transmitted ? 1 : (flags & 3) === 2 ? 2 : 0;
};
const renderedMeshes = [...this.#meshes.values()]
.filter((mesh) => mesh.vertexCount > 0)
.sort(
(left, right) =>
renderCategory(Number(this.array("meshInfo").row(left.id)[1])) -
renderCategory(Number(this.array("meshInfo").row(right.id)[1])),
);
const hasTransmission = renderedMeshes.some((mesh) => {
const materials = new Set([
Number(this.array("meshInfo").row(mesh.id)[1]),
...mesh.faceMaterials.values(),
]);
return [...materials].some(
(material) => this.array("materialExtensions").row(material)[0] > 0,
);
});
const compositeStudioBackground =
renderedMeshes.length > 0 &&
!hasTransmission &&
this.#effects.size === 0 &&
this.#shaders.size === 0;
const compositeSceneColor =
hasTransmission && this.#effects.size === 0 && this.#shaders.size === 0;
const hdrFormat = this.hdr ? "rgba16float" : "rgba8unorm";
addTexture({
id: "hdr",
@@ -868,6 +1149,14 @@ export class Scene {
usage: ["render"],
transient: true,
});
if (compositeSceneColor)
addTexture({
id: "scene-color",
format: hdrFormat,
size: ["canvas", "canvas", 1],
usage: ["render", "sampled"],
transient: true,
});
addSampler({ id: "linear", magFilter: "linear", minFilter: "linear" });
addSampler({
id: "material-linear",
@@ -878,14 +1167,21 @@ export class Scene {
addressModeV: "repeat",
anisotropyClamp: 16,
});
for (const { number: _, source: __, ...texture } of this.#textures.values())
for (const {
number: _,
source: __,
sampler,
...texture
} of this.#textures.values()) {
addTexture(texture);
if (sampler) addSampler({ id: `${texture.id}-sampler`, ...sampler });
}
let previous = computeIds.length ? computeIds : ["cluster-lights"];
if (!renderedMeshes.length) {
renderPipelines.push({
id: "empty-forward",
code: emptyForwardShader,
code: clearShader,
vertex: { entry: "vertex" },
fragment: { entry: "fragment", targets: [{ format: hdrFormat }] },
});
@@ -894,9 +1190,8 @@ export class Scene {
type: "render",
pipeline: "empty-forward",
after: previous,
bindings: [{ group: 0, binding: 0, resource: "accent" }],
color: [{ resource: "hdr", clear: [0.015, 0.025, 0.05, 1] }],
draw: { vertices: 3 },
draw: { vertices: 0 },
});
previous = ["forward-empty"];
} else {
@@ -909,8 +1204,33 @@ export class Scene {
["camera-matrices", "cameraMatrices"],
])
addBuffer({ id, array, usage: ["storage"] });
const forwardPipelines = new Set<string>();
let firstRender = true;
if (!compositeStudioBackground) {
renderPipelines.push({
id: "studio-background",
code: studioBackgroundShader,
vertex: { entry: "vertex" },
fragment: { entry: "fragment", targets: [{ format: hdrFormat }] },
});
passes.push({
id: "studio-background",
type: "render",
pipeline: "studio-background",
after: previous,
color: [
{
resource: compositeSceneColor ? "scene-color" : "hdr",
clear: [0, 0, 0, 1],
},
],
draw: { vertices: 3 },
});
previous = ["studio-background"];
}
const forwardPipelines = new Map<string, string>();
let firstColor = true;
let firstDepth = true;
let sceneColorReady = false;
let forwardTarget = compositeSceneColor ? "scene-color" : "hdr";
for (const mesh of renderedMeshes) {
const vertex = `geometry-${mesh.geometryId}-positions`;
addBuffer({
@@ -921,14 +1241,20 @@ export class Scene {
});
const normal = `geometry-${mesh.geometryId}-normals`;
const uv = `geometry-${mesh.geometryId}-uvs`;
const uv2 = `geometry-${mesh.geometryId}-uvs2`;
const tangent = `geometry-${mesh.geometryId}-tangents`;
const color = `geometry-${mesh.geometryId}-colors`;
const hasNormals = !!this.geometryData(mesh.geometryId, "normals");
const hasUvs = !!this.geometryData(mesh.geometryId, "uvs");
const hasUvs2 = !!this.geometryData(mesh.geometryId, "uvs2");
const hasTangents = !!this.geometryData(mesh.geometryId, "tangents");
const hasColors = !!this.geometryData(mesh.geometryId, "colors");
for (const [present, id, kind] of [
[hasNormals, normal, "normals"],
[hasUvs, uv, "uvs"],
[hasUvs2, uv2, "uvs2"],
[hasTangents, tangent, "tangents"],
[hasColors, color, "colors"],
] as const)
if (present)
addBuffer({
@@ -977,21 +1303,91 @@ export class Scene {
pointers[lane]
? this.#textures.get(Number(pointers[lane]) - 1)
: undefined;
const baseTexture = texture(0);
const materialTexture = texture(1);
const normalTexture = hasTangents ? texture(2) : undefined;
const materialTextures = Array.from({ length: 13 }, (_, lane) =>
lane === 2 && !hasTangents ? undefined : texture(lane),
);
const normalTexture = materialTextures[2];
const detailed = hasNormals && hasUvs;
const mask = detailed
? (baseTexture ? 1 : 0) |
(materialTexture ? 2 : 0) |
(normalTexture ? 4 : 0)
const flags = Math.round(this.array("materials").row(material)[11]);
const extensionInfo = this.array("materialExtensions").row(material);
const transmitted = extensionInfo[0] > 0;
let mask = detailed
? materialTextures.reduce(
(value, current, lane) =>
value | (current === undefined ? 0 : 1 << lane),
0,
)
: 0;
const pipeline = detailed ? `forward-pbr-${mask}` : "forward-basic";
if (!forwardPipelines.has(pipeline)) {
forwardPipelines.add(pipeline);
if (!transmitted) mask &= ~((1 << 5) | (1 << 6));
else if (extensionInfo[2] <= 0) mask &= ~(1 << 6);
if (extensionInfo[8] <= 0) mask &= ~(1 << 7);
if (extensionInfo[16] <= 0) mask &= ~((1 << 8) | (1 << 9));
if (extensionInfo[20] <= 0) mask &= ~(1 << 10);
if (extensionInfo[11] <= 0) mask &= ~(1 << 4);
if ((flags & 8) !== 0 && !transmitted) mask &= (1 << 0) | (1 << 3);
const tangentSlot = normalTexture ? 3 : -1;
const colorSlot = hasColors ? 3 + Number(!!normalTexture) : -1;
const uv2Slot = hasUvs2
? 3 + Number(!!normalTexture) + Number(hasColors)
: -1;
const textureInfo = this.array("materialTextureInfo").row(material);
let clearCompositeLayer = false;
if (transmitted && !sceneColorReady) {
sceneColorReady = true;
if (compositeSceneColor) {
forwardTarget = "hdr";
clearCompositeLayer = true;
} else {
addTexture({
id: "scene-color",
format: hdrFormat,
size: ["canvas", "canvas", 1],
usage: ["render", "sampled"],
transient: true,
});
renderPipelines.push({
id: "copy-scene-color",
code: copyShader,
vertex: { entry: "vertex" },
fragment: {
entry: "fragment",
targets: [{ format: hdrFormat }],
},
});
passes.push({
id: "copy-scene-color",
type: "render",
pipeline: "copy-scene-color",
after: previous,
bindings: [
{ group: 0, binding: 0, resource: "hdr" },
{ group: 0, binding: 1, resource: "linear" },
],
color: [{ resource: "scene-color", clear: [0, 0, 0, 1] }],
draw: { vertices: 3 },
});
previous = ["copy-scene-color"];
}
}
const code = detailed
? pbrShader(
mask,
flags,
hasColors,
hasUvs2,
transmitted,
textureInfo,
extensionInfo,
)
: basicForwardShader;
const pipelineKey = `${flags}-${transmitted ? 1 : 0}\n${code}`;
let pipeline = forwardPipelines.get(pipelineKey);
if (!pipeline) {
pipeline = `forward-${detailed ? "pbr" : "basic"}-${forwardPipelines.size}`;
forwardPipelines.set(pipelineKey, pipeline);
renderPipelines.push({
id: pipeline,
code: detailed ? pbrShader(mask) : basicForwardShader,
code,
vertex: {
entry: "vertex",
buffers: detailed
@@ -1040,6 +1436,34 @@ export class Scene {
},
]
: []),
...(hasColors
? [
{
arrayStride: 16,
attributes: [
{
format: "float32x4",
offset: 0,
shaderLocation: 4,
},
],
},
]
: []),
...(hasUvs2
? [
{
arrayStride: 16,
attributes: [
{
format: "float32x2",
offset: 0,
shaderLocation: 5,
},
],
},
]
: []),
]
: [
{
@@ -1056,21 +1480,45 @@ export class Scene {
},
fragment: {
entry: "fragment",
targets: [{ format: hdrFormat }],
targets: [
{
format: hdrFormat,
...((flags & 3) === 2
? {
blend: {
color: {
srcFactor: "src-alpha",
dstFactor: "one-minus-src-alpha",
operation: "add",
},
alpha: {
srcFactor: "one",
dstFactor: "one-minus-src-alpha",
operation: "add",
},
},
}
: {}),
},
],
},
depthStencil: {
format: "depth24plus",
depth_write_enabled: true,
depth_write_enabled: (flags & 3) !== 2 && !transmitted,
depth_compare: "less",
},
primitive: {
topology: "triangle-list",
frontFace: "ccw",
cullMode: (flags & 4) !== 0 ? null : "back",
conservative: false,
},
});
}
const instance =
(mesh.id +
(draw.material === undefined
? 0
: (draw.material + 1) * 65536)) >>>
0;
const instance = (mesh.id + (material + 1) * 65536) >>> 0;
const samplerTexture = materialTextures.find(
(value, lane) => value !== undefined && (mask & (1 << lane)) !== 0,
);
passes.push({
id,
type: "render",
@@ -1099,48 +1547,45 @@ export class Scene {
: []),
...(detailed && mask
? [
{ group: 1, binding: 0, resource: "material-linear" },
...(baseTexture
? [
{
group: 1,
binding: 1,
resource: baseTexture.id,
},
]
: []),
...(materialTexture
? [
{
group: 1,
binding: 2,
resource: materialTexture.id,
},
]
: []),
...(normalTexture
? [
{
group: 1,
binding: 3,
resource: normalTexture.id,
},
]
: []),
{
group: 1,
binding: 0,
resource: samplerTexture?.sampler
? `${samplerTexture.id}-sampler`
: "material-linear",
},
...materialTextures.flatMap((value, lane) =>
value && (mask & (1 << lane)) !== 0
? [
{
group: 1,
binding: lane + 1,
resource: value.id,
},
]
: [],
),
]
: []),
...(detailed && transmitted
? [
{ group: 2, binding: 0, resource: "scene-color" },
{ group: 2, binding: 1, resource: "linear" },
]
: []),
],
color: [
{
resource: "hdr",
...(firstRender
? { clear: [0.015, 0.025, 0.05, 1] }
resource: forwardTarget,
...((firstColor && compositeStudioBackground) ||
clearCompositeLayer
? { clear: [0, 0, 0, 0] }
: { load: "load" }),
},
],
depth: {
resource: "depth",
...(firstRender ? { clear: 1 } : { load: "load" }),
...(firstDepth ? { clear: 1 } : { load: "load" }),
},
vertexBuffers: [
{ slot: 0, resource: vertex },
@@ -1148,7 +1593,13 @@ export class Scene {
? [
{ slot: 1, resource: normal },
{ slot: 2, resource: uv },
...(normalTexture ? [{ slot: 3, resource: tangent }] : []),
...(normalTexture
? [{ slot: tangentSlot, resource: tangent }]
: []),
...(hasColors
? [{ slot: colorSlot, resource: color }]
: []),
...(hasUvs2 ? [{ slot: uv2Slot, resource: uv2 }] : []),
]
: []),
],
@@ -1169,7 +1620,8 @@ export class Scene {
}
: { vertices: draw.count, instances: 1, firstInstance: instance },
});
firstRender = false;
firstColor = false;
firstDepth = false;
previous = [id];
}
}
@@ -1237,7 +1689,14 @@ export class Scene {
);
renderPipelines.push({
id: "present",
code: presentShader(toneMap),
code: presentShader(
toneMap,
compositeStudioBackground
? "studio"
: compositeSceneColor
? "scene"
: undefined,
),
vertex: { entry: "vertex" },
fragment: { entry: "fragment", targets: [{ format: "canvas" }] },
});
@@ -1249,6 +1708,9 @@ export class Scene {
bindings: [
{ group: 0, binding: 0, resource: input },
{ group: 0, binding: 1, resource: "linear" },
...(compositeSceneColor
? [{ group: 0, binding: 2, resource: "scene-color" }]
: []),
],
color: [{ resource: "canvas", clear: [0, 0, 0, 1] }],
draw: { vertices: 3 },
+51 -28
View File
@@ -45,30 +45,45 @@ export class ArcRotateCamera extends Camera {
});
}
get alpha() { return this.cameraRow()[13]; }
get alpha() {
return this.cameraRow()[13];
}
set alpha(value: number) {
const row = this.cameraRow();
this.#updateTransform(() => { row[13] = value; });
this.#updateTransform(() => {
row[13] = value;
});
}
get beta() {
return this.cameraRow()[14];
}
get beta() { return this.cameraRow()[14]; }
set beta(value: number) {
const row = this.cameraRow();
this.#updateTransform(() => {
row[14] = Math.min(Math.PI - 0.001, Math.max(0.001, value));
});
}
get radius() { return this.cameraRow()[15]; }
get radius() {
return this.cameraRow()[15];
}
set radius(value: number) {
const row = this.cameraRow();
this.#updateTransform(() => { row[15] = Math.max(0.01, value); });
this.#updateTransform(() => {
row[15] = Math.max(0.01, value);
});
}
get target() { return this.#target; }
get target() {
return this.#target;
}
set target(value: Node | undefined) {
if (value && (value.scene !== this.scene || value.id < 0)) throw new Error("Target must be a ready Node in this Scene");
if (value && (value.scene !== this.scene || value.id < 0))
throw new Error("Target must be a ready Node in this Scene");
this.#target = value;
const row = this.cameraRow();
this.#updateTransform(() => { row[12] = value ? value.id + 1 : 0; });
this.#updateTransform(() => {
row[12] = value ? value.id + 1 : 0;
});
}
attachControls(controls: ArcRotateControls) {
@@ -80,9 +95,12 @@ export class ArcRotateCamera extends Camera {
controls.element.addEventListener("pointerup", this.#up);
controls.element.addEventListener("pointercancel", this.#up);
controls.element.addEventListener("contextmenu", this.#contextMenu);
controls.element.addEventListener("wheel", this.#wheel, { passive: false });
controls.element.addEventListener("wheel", this.#wheel, {
passive: false,
});
}
if (controls.controller) this.#frame = requestAnimationFrame(this.#pollController);
if (controls.controller)
this.#frame = requestAnimationFrame(this.#pollController);
return this;
}
@@ -96,7 +114,7 @@ export class ArcRotateCamera extends Camera {
element.removeEventListener("contextmenu", this.#contextMenu);
element.removeEventListener("wheel", this.#wheel);
}
cancelAnimationFrame(this.#frame);
if (this.#frame) cancelAnimationFrame(this.#frame);
this.#frame = 0;
this.#pointer = undefined;
this.#controls = undefined;
@@ -106,11 +124,7 @@ export class ArcRotateCamera extends Camera {
const row = this.cameraRow();
if (!this.#target) return row.subarray(16, 19);
const position = this.#target.position;
return [
position.x + row[16],
position.y + row[17],
position.z + row[18],
];
return [position.x + row[16], position.y + row[17], position.z + row[18]];
}
#updateTransform(change?: () => void) {
@@ -130,10 +144,16 @@ export class ArcRotateCamera extends Camera {
#down = (event: PointerEvent) => {
event.preventDefault();
this.#pointer = { x: event.clientX, y: event.clientY, button: event.button };
this.#pointer = {
x: event.clientX,
y: event.clientY,
button: event.button,
};
this.#controls?.element.setPointerCapture?.(event.pointerId);
};
#up = () => { this.#pointer = undefined; };
#up = () => {
this.#pointer = undefined;
};
#contextMenu = (event: Event) => event.preventDefault();
#move = (event: PointerEvent) => {
if (!this.#pointer || !this.#controls) return;
@@ -144,9 +164,17 @@ export class ArcRotateCamera extends Camera {
if (this.#pointer.button === 2) {
const row = this.cameraRow();
this.#updateTransform(() => {
const speed = this.#controls?.panSpeed ?? 0.005;
const horizontal = -x * speed;
const vertical = y * speed;
const element = this.#controls!.element;
const verticalExtent =
this.projection === "orthographic"
? this.orthoSize
: 2 * this.radius * Math.tan(this.fov / 2);
const speed = this.#controls?.panSpeed ?? 1;
const horizontal =
(-x * verticalExtent * this.aspect * speed) /
Math.max(1, element.clientWidth);
const vertical =
(y * verticalExtent * speed) / Math.max(1, element.clientHeight);
const sinAlpha = Math.sin(row[13]);
const cosAlpha = Math.cos(row[13]);
const sinBeta = Math.sin(row[14]);
@@ -185,17 +213,12 @@ export class ArcRotateCamera extends Camera {
row[13] += (pad.axes[2] ?? pad.axes[0] ?? 0) * 0.03;
row[14] = Math.min(
Math.PI - 0.001,
Math.max(
0.001,
row[14] + (pad.axes[3] ?? pad.axes[1] ?? 0) * 0.03,
),
Math.max(0.001, row[14] + (pad.axes[3] ?? pad.axes[1] ?? 0) * 0.03),
);
row[15] = Math.max(
0.01,
row[15] +
((pad.buttons[6]?.value ?? 0) -
(pad.buttons[7]?.value ?? 0)) *
0.1,
((pad.buttons[6]?.value ?? 0) - (pad.buttons[7]?.value ?? 0)) * 0.1,
);
});
}
+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",
});
}
});
+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);
+3
View File
@@ -1,4 +1,5 @@
import type { Scene } from "../Scene";
import type { GraphSampler } from "../ComputePass";
export type TextureOptions = {
source?: string | ImageBitmap;
@@ -7,6 +8,7 @@ export type TextureOptions = {
usage?: string[];
mipmaps?: boolean;
transient?: boolean;
sampler?: Omit<GraphSampler, "id">;
};
let nextTextureName = 1;
@@ -52,6 +54,7 @@ export class Texture {
]),
],
transient: options.transient ?? false,
sampler: options.sampler,
});
this.id = registration.number;
await registration.ready;