Optimize forward rendering and add benchmark
Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -12,6 +12,7 @@ export type GraphTexture = {
|
||||
format?: string;
|
||||
size?: [number | "canvas", number | "canvas", number?];
|
||||
usage?: string[];
|
||||
mipLevelCount?: number;
|
||||
transient?: boolean;
|
||||
};
|
||||
|
||||
@@ -19,6 +20,7 @@ export type GraphSampler = {
|
||||
id: string;
|
||||
magFilter?: "nearest" | "linear";
|
||||
minFilter?: "nearest" | "linear";
|
||||
mipmapFilter?: "nearest" | "linear";
|
||||
addressModeU?: "clamp-to-edge" | "repeat" | "mirror-repeat";
|
||||
addressModeV?: "clamp-to-edge" | "repeat" | "mirror-repeat";
|
||||
};
|
||||
|
||||
+28
-34
@@ -38,6 +38,7 @@ const rows = [
|
||||
["meshInfo", 16, "u32"],
|
||||
["bounds", 32, "f32"],
|
||||
["cameras", 80, "f32"],
|
||||
["cameraMatrices", 80, "f32"],
|
||||
["materials", 48, "f32"],
|
||||
["materialTextures", 32, "u32"],
|
||||
["pointLights", 32, "f32"],
|
||||
@@ -94,7 +95,7 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
||||
const basicForwardShader = /* wgsl */ `
|
||||
struct Accent { color: vec4<f32> }
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@invariant @builtin(position) position: vec4<f32>,
|
||||
@location(0) normal: vec3<f32>,
|
||||
@location(1) @interpolate(flat) mesh: u32,
|
||||
@location(2) @interpolate(flat) material: u32,
|
||||
@@ -107,7 +108,7 @@ struct VertexOutput {
|
||||
@group(0) @binding(4) var<storage, read> scales: array<vec4<f32>>;
|
||||
@group(0) @binding(5) var<storage, read> meshInfo: array<u32>;
|
||||
@group(0) @binding(6) var<storage, read> materials: array<vec4<f32>>;
|
||||
@group(0) @binding(7) var<storage, read> cameras: array<vec4<f32>>;
|
||||
@group(0) @binding(7) var<storage, read> cameraMatrices: array<vec4<f32>>;
|
||||
|
||||
fn rotate(q: vec4<f32>, value: vec3<f32>) -> vec3<f32> {
|
||||
return value + 2.0 * cross(q.xyz, cross(q.xyz, value) + q.w * value);
|
||||
@@ -119,18 +120,14 @@ fn vertex(@location(0) point: vec3<f32>, @builtin(instance_index) packed: u32) -
|
||||
let visible = meshInfo[instance * 4u + 2u];
|
||||
let transformed = rotate(quaternions[instance], point * scales[instance].xyz) + positions[instance].xyz;
|
||||
var clip = vec4<f32>(transformed, 1.0);
|
||||
if (cameras[2].w != 0.0) {
|
||||
let cameraNode = u32(cameras[1].x);
|
||||
let inverse = vec4<f32>(-quaternions[cameraNode].xyz, quaternions[cameraNode].w);
|
||||
let view = rotate(inverse, transformed - positions[cameraNode].xyz);
|
||||
if (cameras[1].y == 1.0) {
|
||||
let size = max(cameras[1].z, 0.0001);
|
||||
clip = vec4<f32>(view.x / (size * cameras[0].y * 0.5), view.y / (size * 0.5), -view.z / cameras[0].w, 1.0);
|
||||
} else {
|
||||
let focal = 1.0 / tan(cameras[0].x * 0.5);
|
||||
let depth = (-view.z * cameras[0].w - cameras[0].z * cameras[0].w) / (cameras[0].w - cameras[0].z);
|
||||
clip = vec4<f32>(view.x * focal / cameras[0].y, view.y * focal, depth, -view.z);
|
||||
}
|
||||
if (cameraMatrices[4].w != 0.0) {
|
||||
let world = vec4(transformed, 1.0);
|
||||
clip = vec4(
|
||||
dot(cameraMatrices[0], world),
|
||||
dot(cameraMatrices[1], world),
|
||||
dot(cameraMatrices[2], world),
|
||||
dot(cameraMatrices[3], world),
|
||||
);
|
||||
}
|
||||
var output: VertexOutput;
|
||||
output.position = select(vec4<f32>(2.0, 2.0, 2.0, 1.0), clip, visible != 0u);
|
||||
@@ -159,7 +156,7 @@ function pbrShader(mask: number) {
|
||||
const normalTexture = mask & 4;
|
||||
return /* wgsl */ `
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@invariant @builtin(position) position: vec4<f32>,
|
||||
@location(0) world: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
@@ -175,7 +172,7 @@ struct VertexOutput {
|
||||
@group(0) @binding(4) var<storage, read> scales: array<vec4<f32>>;
|
||||
@group(0) @binding(5) var<storage, read> meshInfo: array<u32>;
|
||||
@group(0) @binding(6) var<storage, read> materials: array<vec4<f32>>;
|
||||
@group(0) @binding(7) var<storage, read> cameras: array<vec4<f32>>;
|
||||
@group(0) @binding(7) var<storage, read> cameraMatrices: 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>;" : ""}
|
||||
@@ -197,18 +194,14 @@ fn vertex(
|
||||
let scale = scales[instance].xyz;
|
||||
let world = rotate(quaternions[instance], point * scale) + positions[instance].xyz;
|
||||
var clip = vec4<f32>(world, 1.0);
|
||||
if (cameras[2].w != 0.0) {
|
||||
let cameraNode = u32(cameras[1].x);
|
||||
let inverse = vec4<f32>(-quaternions[cameraNode].xyz, quaternions[cameraNode].w);
|
||||
let view = rotate(inverse, world - positions[cameraNode].xyz);
|
||||
if (cameras[1].y == 1.0) {
|
||||
let size = max(cameras[1].z, 0.0001);
|
||||
clip = vec4<f32>(view.x / (size * cameras[0].y * 0.5), view.y / (size * 0.5), -view.z / cameras[0].w, 1.0);
|
||||
} else {
|
||||
let focal = 1.0 / tan(cameras[0].x * 0.5);
|
||||
let depth = (-view.z * cameras[0].w - cameras[0].z * cameras[0].w) / (cameras[0].w - cameras[0].z);
|
||||
clip = vec4<f32>(view.x * focal / cameras[0].y, view.y * focal, depth, -view.z);
|
||||
}
|
||||
if (cameraMatrices[4].w != 0.0) {
|
||||
let homogeneous = vec4(world, 1.0);
|
||||
clip = vec4(
|
||||
dot(cameraMatrices[0], homogeneous),
|
||||
dot(cameraMatrices[1], homogeneous),
|
||||
dot(cameraMatrices[2], homogeneous),
|
||||
dot(cameraMatrices[3], homogeneous),
|
||||
);
|
||||
}
|
||||
var output: VertexOutput;
|
||||
output.position = select(vec4<f32>(2.0, 2.0, 2.0, 1.0), clip, meshInfo[instance * 4u + 2u] != 0u);
|
||||
@@ -234,8 +227,7 @@ fn fragment(input: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let roughness = clamp(properties.y * ${materialTexture ? "packedMaterial.g" : "1.0"}, 0.04, 1.0);
|
||||
var normal = normalize(input.normal);
|
||||
${normalTexture ? "let tangent = normalize(input.tangent.xyz); let bitangent = normalize(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));" : ""}
|
||||
let cameraNode = u32(cameras[1].x);
|
||||
let view = normalize(positions[cameraNode].xyz - input.world);
|
||||
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);
|
||||
@@ -683,7 +675,6 @@ export class Scene {
|
||||
])
|
||||
addBuffer({ id, array, usage: ["storage"] });
|
||||
addBuffer({ id: "accent", array: "sceneAccent", usage: ["uniform"] });
|
||||
|
||||
computePipelines.push({
|
||||
id: "cluster-lights",
|
||||
code: clusterShader,
|
||||
@@ -747,6 +738,7 @@ export class Scene {
|
||||
id: "material-linear",
|
||||
magFilter: "linear",
|
||||
minFilter: "linear",
|
||||
mipmapFilter: "linear",
|
||||
addressModeU: "repeat",
|
||||
addressModeV: "repeat",
|
||||
});
|
||||
@@ -778,7 +770,7 @@ export class Scene {
|
||||
["node-scales", "nodeScales"],
|
||||
["mesh-info", "meshInfo"],
|
||||
["materials", "materials"],
|
||||
["cameras", "cameras"],
|
||||
["camera-matrices", "cameraMatrices"],
|
||||
])
|
||||
addBuffer({ id, array, usage: ["storage"] });
|
||||
const forwardPipelines = new Set<string>();
|
||||
@@ -841,7 +833,9 @@ export class Scene {
|
||||
const material =
|
||||
draw.material ?? Number(this.array("meshInfo").row(mesh.id)[1]);
|
||||
if (mesh.id > 65535 || material > 65534)
|
||||
throw new RangeError("Scene handle limit");
|
||||
throw new RangeError(
|
||||
`Scene handle limit: mesh ${mesh.id}, material ${material}`,
|
||||
);
|
||||
const pointers = this.array("materialTextures").row(material);
|
||||
const texture = (lane: number) =>
|
||||
pointers[lane]
|
||||
@@ -955,7 +949,7 @@ export class Scene {
|
||||
"node-scales",
|
||||
"mesh-info",
|
||||
"materials",
|
||||
"cameras",
|
||||
"camera-matrices",
|
||||
].map((resource, binding) => ({
|
||||
group: 0,
|
||||
binding,
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
import { Node, type NodeOptions } from "../Node";
|
||||
import type { Scene } from "../Scene";
|
||||
|
||||
function rotate(q: ArrayLike<number>, value: number[]) {
|
||||
const [x, y, z] = value;
|
||||
const tx = 2 * (q[1] * z - q[2] * y);
|
||||
const ty = 2 * (q[2] * x - q[0] * z);
|
||||
const tz = 2 * (q[0] * y - q[1] * x);
|
||||
return [
|
||||
x + q[3] * tx + q[1] * tz - q[2] * ty,
|
||||
y + q[3] * ty + q[2] * tx - q[0] * tz,
|
||||
z + q[3] * tz + q[0] * ty - q[1] * tx,
|
||||
];
|
||||
}
|
||||
|
||||
function dot(left: number[], right: ArrayLike<number>) {
|
||||
return left[0] * right[0] + left[1] * right[1] + left[2] * right[2];
|
||||
}
|
||||
|
||||
export type CameraOptions = NodeOptions & {
|
||||
fov?: number;
|
||||
near?: number;
|
||||
@@ -24,6 +40,7 @@ export class Camera extends Node {
|
||||
const nodeReady = this.ready;
|
||||
this.ready = nodeReady.then(async () => {
|
||||
this.cameraId = await scene.core.allocateObject("cameras");
|
||||
await scene.ensureRows("cameraMatrices", this.cameraId + 1, 80, "f32");
|
||||
const row = scene.array("cameras").row(this.cameraId);
|
||||
row.set([
|
||||
options.fov ?? Math.PI / 3,
|
||||
@@ -39,6 +56,7 @@ export class Camera extends Node {
|
||||
options.sensorWidth ?? 36,
|
||||
1,
|
||||
]);
|
||||
this.refreshMatrix();
|
||||
return this;
|
||||
});
|
||||
}
|
||||
@@ -49,17 +67,20 @@ export class Camera extends Node {
|
||||
}
|
||||
|
||||
get fov() { return this.cameraRow()[0]; }
|
||||
set fov(value: number) { this.cameraRow()[0] = value; }
|
||||
set fov(value: number) { this.cameraRow()[0] = value; this.refreshMatrix(); }
|
||||
get aspect() { return this.cameraRow()[1]; }
|
||||
set aspect(value: number) { this.cameraRow()[1] = value; }
|
||||
set aspect(value: number) { this.cameraRow()[1] = value; this.refreshMatrix(); }
|
||||
get near() { return this.cameraRow()[2]; }
|
||||
set near(value: number) { this.cameraRow()[2] = value; }
|
||||
set near(value: number) { this.cameraRow()[2] = value; this.refreshMatrix(); }
|
||||
get far() { return this.cameraRow()[3]; }
|
||||
set far(value: number) { this.cameraRow()[3] = value; }
|
||||
set far(value: number) { this.cameraRow()[3] = value; this.refreshMatrix(); }
|
||||
get projection() { return this.cameraRow()[5] === 1 ? "orthographic" : "perspective"; }
|
||||
set projection(value: "perspective" | "orthographic") { this.cameraRow()[5] = value === "orthographic" ? 1 : 0; }
|
||||
set projection(value: "perspective" | "orthographic") {
|
||||
this.cameraRow()[5] = value === "orthographic" ? 1 : 0;
|
||||
this.refreshMatrix();
|
||||
}
|
||||
get orthoSize() { return this.cameraRow()[6]; }
|
||||
set orthoSize(value: number) { this.cameraRow()[6] = value; }
|
||||
set orthoSize(value: number) { this.cameraRow()[6] = value; this.refreshMatrix(); }
|
||||
get focalLength() { return this.cameraRow()[7]; }
|
||||
set focalLength(value: number) { this.cameraRow()[7] = value; }
|
||||
get aperture() { return this.cameraRow()[8]; }
|
||||
@@ -69,6 +90,18 @@ export class Camera extends Node {
|
||||
get sensorWidth() { return this.cameraRow()[10]; }
|
||||
set sensorWidth(value: number) { this.cameraRow()[10] = value; }
|
||||
|
||||
override get position(): Float32Array { return super.position; }
|
||||
override set position(value: ArrayLike<number>) {
|
||||
super.position = value;
|
||||
this.refreshMatrix();
|
||||
}
|
||||
|
||||
override get quaternion(): Float32Array { return super.quaternion; }
|
||||
override set quaternion(value: ArrayLike<number>) {
|
||||
super.quaternion = value;
|
||||
this.refreshMatrix();
|
||||
}
|
||||
|
||||
lookAt(target: ArrayLike<number>) {
|
||||
if (target.length !== 3) throw new RangeError("camera target");
|
||||
const position = this.position;
|
||||
@@ -86,6 +119,47 @@ export class Camera extends Node {
|
||||
return this;
|
||||
}
|
||||
|
||||
protected refreshMatrix() {
|
||||
if (this.id < 0 || this.cameraId < 0) return;
|
||||
const camera = this.cameraRow();
|
||||
const position = this.position;
|
||||
const quaternion = this.quaternion;
|
||||
const right = rotate(quaternion, [1, 0, 0]);
|
||||
const up = rotate(quaternion, [0, 1, 0]);
|
||||
const forward = rotate(quaternion, [0, 0, 1]);
|
||||
let rows: number[][];
|
||||
if (camera[5] === 1) {
|
||||
const size = Math.max(camera[6], 0.0001);
|
||||
const x = 2 / (size * camera[1]);
|
||||
const y = 2 / size;
|
||||
const z = -1 / camera[3];
|
||||
rows = [
|
||||
[...right.map((value) => value * x), -dot(right, position) * x],
|
||||
[...up.map((value) => value * y), -dot(up, position) * y],
|
||||
[...forward.map((value) => value * z), -dot(forward, position) * z],
|
||||
[0, 0, 0, 1],
|
||||
];
|
||||
} else {
|
||||
const focal = 1 / Math.tan(camera[0] * 0.5);
|
||||
const x = focal / camera[1];
|
||||
const z = -camera[3] / (camera[3] - camera[2]);
|
||||
const translation = (-camera[2] * camera[3]) / (camera[3] - camera[2]);
|
||||
rows = [
|
||||
[...right.map((value) => value * x), -dot(right, position) * x],
|
||||
[...up.map((value) => value * focal), -dot(up, position) * focal],
|
||||
[
|
||||
...forward.map((value) => value * z),
|
||||
-dot(forward, position) * z + translation,
|
||||
],
|
||||
[...forward.map((value) => -value), dot(forward, position)],
|
||||
];
|
||||
}
|
||||
this.scene
|
||||
.array("cameraMatrices")
|
||||
.row(this.cameraId)
|
||||
.set([...rows.flat(), ...position, camera[11]]);
|
||||
}
|
||||
|
||||
override async dispose() {
|
||||
await this.ready;
|
||||
if (this.#cameraDisposed) return;
|
||||
|
||||
@@ -91,6 +91,7 @@ export class FreeCamera extends Camera {
|
||||
this.position[0] += (x * Math.cos(yaw) + z * Math.sin(yaw)) * speed;
|
||||
this.position[1] += y * speed;
|
||||
this.position[2] += (x * -Math.sin(yaw) + z * Math.cos(yaw)) * speed;
|
||||
this.refreshMatrix();
|
||||
this.#frame = requestAnimationFrame(this.#update);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ export type TextureOptions = {
|
||||
size?: [number | "canvas", number | "canvas", number?];
|
||||
format?: string;
|
||||
usage?: string[];
|
||||
mipmaps?: boolean;
|
||||
transient?: boolean;
|
||||
};
|
||||
|
||||
@@ -29,6 +30,10 @@ export class Texture {
|
||||
typeof options.source === "string"
|
||||
? await createImageBitmap(await (await fetch(options.source)).blob())
|
||||
: options.source;
|
||||
const mipLevelCount =
|
||||
image instanceof ImageBitmap && options.mipmaps !== false
|
||||
? Math.floor(Math.log2(Math.max(image.width, image.height))) + 1
|
||||
: 1;
|
||||
const registration = scene.registerTexture({
|
||||
id: resource,
|
||||
source: image,
|
||||
@@ -38,6 +43,7 @@ export class Texture {
|
||||
? [image.width, image.height, 1]
|
||||
: [1, 1, 1]),
|
||||
format: options.format ?? "rgba8unorm",
|
||||
mipLevelCount,
|
||||
usage: [
|
||||
...new Set([
|
||||
...(options.usage ?? ["copyDst"]),
|
||||
@@ -49,8 +55,22 @@ export class Texture {
|
||||
});
|
||||
this.id = registration.number;
|
||||
await registration.ready;
|
||||
if (image instanceof ImageBitmap)
|
||||
await scene.core.uploadTexture(resource, image);
|
||||
if (image instanceof ImageBitmap) {
|
||||
const levels = [image];
|
||||
for (let level = 1; level < mipLevelCount; level++)
|
||||
levels.push(
|
||||
await createImageBitmap(image, {
|
||||
resizeWidth: Math.max(1, image.width >> level),
|
||||
resizeHeight: Math.max(1, image.height >> level),
|
||||
resizeQuality: "high",
|
||||
}),
|
||||
);
|
||||
await Promise.all(
|
||||
levels.map((level, index) =>
|
||||
scene.core.uploadTexture(resource, level, index),
|
||||
),
|
||||
);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user