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
+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 },