reze-engine 0.42.3 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -410
- package/dist/camera.d.ts +3 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +33 -8
- package/dist/engine.d.ts +924 -29
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +4043 -278
- package/dist/graph/registry.d.ts +2 -2
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +1 -1
- package/dist/graph/slots.d.ts +0 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +37 -9
- package/dist/hdr.d.ts +18 -0
- package/dist/hdr.d.ts.map +1 -0
- package/dist/hdr.js +162 -0
- package/dist/ibl.d.ts +19 -0
- package/dist/ibl.d.ts.map +1 -0
- package/dist/ibl.js +113 -0
- package/dist/ik-solver.d.ts +2 -1
- package/dist/ik-solver.d.ts.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/math.d.ts +20 -1
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +23 -16
- package/dist/midi-loader.d.ts +10 -0
- package/dist/midi-loader.d.ts.map +1 -0
- package/dist/midi-loader.js +247 -0
- package/dist/model.d.ts +2 -13
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +29 -0
- package/dist/param-track.d.ts +48 -0
- package/dist/param-track.d.ts.map +1 -0
- package/dist/param-track.js +80 -0
- package/dist/physics/types.d.ts.map +1 -1
- package/dist/physics/types.js +3 -0
- package/dist/reflection.d.ts +27 -0
- package/dist/reflection.d.ts.map +1 -0
- package/dist/reflection.js +93 -0
- package/dist/shaders/anchor-table.d.ts +56 -0
- package/dist/shaders/anchor-table.d.ts.map +1 -0
- package/dist/shaders/anchor-table.js +128 -0
- package/dist/shaders/audio-api.d.ts +3 -0
- package/dist/shaders/audio-api.d.ts.map +1 -0
- package/dist/shaders/audio-api.js +81 -0
- package/dist/shaders/cast-api.d.ts +2 -0
- package/dist/shaders/cast-api.d.ts.map +1 -0
- package/dist/shaders/cast-api.js +121 -0
- package/dist/shaders/cast-layout.d.ts +21 -0
- package/dist/shaders/cast-layout.d.ts.map +1 -0
- package/dist/shaders/cast-layout.js +20 -0
- package/dist/shaders/lights.d.ts +79 -0
- package/dist/shaders/lights.d.ts.map +1 -0
- package/dist/shaders/lights.js +269 -0
- package/dist/shaders/lyrics-api.d.ts +39 -0
- package/dist/shaders/lyrics-api.d.ts.map +1 -0
- package/dist/shaders/lyrics-api.js +187 -0
- package/dist/shaders/materials/common.d.ts +2 -4
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +87 -35
- package/dist/shaders/midi-api.d.ts +10 -0
- package/dist/shaders/midi-api.d.ts.map +1 -0
- package/dist/shaders/midi-api.js +114 -0
- package/dist/shaders/passes/composite.d.ts +31 -15
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +225 -135
- package/dist/shaders/passes/cull.d.ts +2 -0
- package/dist/shaders/passes/cull.d.ts.map +1 -0
- package/dist/shaders/passes/cull.js +138 -0
- package/dist/shaders/passes/field-blit.d.ts +26 -0
- package/dist/shaders/passes/field-blit.d.ts.map +1 -0
- package/dist/shaders/passes/field-blit.js +65 -0
- package/dist/shaders/passes/grid.d.ts +31 -0
- package/dist/shaders/passes/grid.d.ts.map +1 -0
- package/dist/shaders/passes/grid.js +169 -0
- package/dist/shaders/passes/ground.d.ts +13 -1
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +170 -25
- package/dist/shaders/passes/hosted-api.d.ts +57 -0
- package/dist/shaders/passes/hosted-api.d.ts.map +1 -0
- package/dist/shaders/passes/hosted-api.js +166 -0
- package/dist/shaders/passes/id-debug.d.ts +28 -0
- package/dist/shaders/passes/id-debug.d.ts.map +1 -0
- package/dist/shaders/passes/id-debug.js +74 -0
- package/dist/shaders/passes/particles.d.ts +66 -0
- package/dist/shaders/passes/particles.d.ts.map +1 -0
- package/dist/shaders/passes/particles.js +279 -0
- package/dist/shaders/passes/scene-contract.d.ts +128 -0
- package/dist/shaders/passes/scene-contract.d.ts.map +1 -0
- package/dist/shaders/passes/scene-contract.js +207 -0
- package/dist/shaders/passes/sim.d.ts +34 -0
- package/dist/shaders/passes/sim.d.ts.map +1 -0
- package/dist/shaders/passes/sim.js +169 -0
- package/dist/shaders/passes/trails.d.ts +59 -0
- package/dist/shaders/passes/trails.d.ts.map +1 -0
- package/dist/shaders/passes/trails.js +340 -0
- package/dist/shaders/score-api.d.ts +10 -0
- package/dist/shaders/score-api.d.ts.map +1 -0
- package/dist/shaders/score-api.js +114 -0
- package/dist/shadow-cascades.d.ts +45 -0
- package/dist/shadow-cascades.d.ts.map +1 -0
- package/dist/shadow-cascades.js +70 -0
- package/dist/vmd-loader.d.ts +3 -2
- package/dist/vmd-loader.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/camera.ts +31 -8
- package/src/engine.ts +4535 -296
- package/src/graph/registry.ts +2 -2
- package/src/graph/slots.ts +37 -9
- package/src/hdr.ts +156 -0
- package/src/ibl.ts +115 -0
- package/src/ik-solver.ts +1 -1
- package/src/index.ts +12 -0
- package/src/math.ts +23 -17
- package/src/midi-loader.ts +246 -0
- package/src/model.ts +31 -3
- package/src/param-track.ts +83 -0
- package/src/physics/types.ts +4 -1
- package/src/reflection.ts +94 -0
- package/src/shaders/anchor-table.ts +147 -0
- package/src/shaders/audio-api.ts +82 -0
- package/src/shaders/cast-api.ts +123 -0
- package/src/shaders/cast-layout.ts +20 -0
- package/src/shaders/lights.ts +280 -0
- package/src/shaders/lyrics-api.ts +202 -0
- package/src/shaders/materials/common.ts +89 -35
- package/src/shaders/midi-api.ts +116 -0
- package/src/shaders/passes/composite.ts +244 -136
- package/src/shaders/passes/cull.ts +139 -0
- package/src/shaders/passes/grid.ts +178 -0
- package/src/shaders/passes/ground.ts +172 -25
- package/src/shaders/passes/hosted-api.ts +171 -0
- package/src/shaders/passes/id-debug.ts +75 -0
- package/src/shaders/passes/particles.ts +340 -0
- package/src/shaders/passes/scene-contract.ts +266 -0
- package/src/shaders/passes/trails.ts +390 -0
- package/src/shadow-cascades.ts +97 -0
- package/src/vmd-loader.ts +2 -2
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { GROUND_MATERIAL_ID } from "./ground"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The id attachment, drawn so a person can look at it.
|
|
5
|
+
*
|
|
6
|
+
* The id buffer is correct or incorrect in ways nothing else in the frame can
|
|
7
|
+
* show: with no consumer, a perfect id buffer and a completely wrong one
|
|
8
|
+
* produce exactly the same picture. This pass exists so that "it works" is
|
|
9
|
+
* something seen rather than inferred.
|
|
10
|
+
*
|
|
11
|
+
* WHAT CORRECT LOOKS LIKE, and what each failure looks like instead:
|
|
12
|
+
*
|
|
13
|
+
* - Every material is ONE FLAT COLOUR, with hard edges. Ids are names, not
|
|
14
|
+
* quantities: a gradient anywhere, or fringing along an edge, means
|
|
15
|
+
* something is interpolating or resolving them — the two failures the
|
|
16
|
+
* no-resolve/no-blend rules exist to prevent.
|
|
17
|
+
* - Parts differ from each other. A whole model in one colour means the
|
|
18
|
+
* material index never made it into the uniform.
|
|
19
|
+
* - The FLOOR is a fixed light grey, the reserved id, and never shares a
|
|
20
|
+
* colour with a body part.
|
|
21
|
+
* - Where nothing was drawn is BLACK — id 0, the reserved nothing, which is
|
|
22
|
+
* also what the attachment clears to. Black *over geometry* means that draw
|
|
23
|
+
* wrote no id; colour where the sky should be means the clear is wrong.
|
|
24
|
+
*
|
|
25
|
+
* Colours come from a hash of the pair, so neighbouring materials land on
|
|
26
|
+
* unrelated hues rather than adjacent shades of one — the point is telling them
|
|
27
|
+
* apart, not ordering them.
|
|
28
|
+
*/
|
|
29
|
+
export const ID_DEBUG_SHADER_WGSL = /* wgsl */ `
|
|
30
|
+
// Multisampled and NEVER resolved, so it is read the way it is written: one
|
|
31
|
+
// sample, by texel. Sample 0 is what every consumer of this attachment reads.
|
|
32
|
+
@group(0) @binding(0) var idTex: texture_multisampled_2d<u32>;
|
|
33
|
+
|
|
34
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
35
|
+
let x = f32((vi & 1u) << 2u) - 1.0;
|
|
36
|
+
let y = f32((vi & 2u) << 1u) - 1.0;
|
|
37
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Integer hash → 0..1. Distinct inputs land on unrelated outputs, which is the
|
|
41
|
+
* whole requirement: adjacent ids must not read as adjacent colours. */
|
|
42
|
+
fn hashId(x: u32) -> f32 {
|
|
43
|
+
var h = x * 747796405u + 2891336453u;
|
|
44
|
+
h = ((h >> ((h >> 28u) + 4u)) ^ h) * 277803737u;
|
|
45
|
+
return f32((h >> 22u) ^ h) / 4294967295.0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@fragment fn fs(@builtin(position) fragCoord: vec4f) -> @location(0) vec4f {
|
|
49
|
+
let ids = textureLoad(idTex, vec2<i32>(fragCoord.xy), 0);
|
|
50
|
+
let materialId = ids.x;
|
|
51
|
+
let objectId = ids.y;
|
|
52
|
+
|
|
53
|
+
// Nothing drawn here. Black, and it must cover exactly the empty space.
|
|
54
|
+
if (materialId == 0u && objectId == 0u) {
|
|
55
|
+
return vec4f(0.0, 0.0, 0.0, 1.0);
|
|
56
|
+
}
|
|
57
|
+
// The floor, at the top of the range. Flat light grey so it is unmistakable
|
|
58
|
+
// and cannot be confused with a hashed colour.
|
|
59
|
+
if (materialId == ${GROUND_MATERIAL_ID}u) {
|
|
60
|
+
return vec4f(0.72, 0.72, 0.75, 1.0);
|
|
61
|
+
}
|
|
62
|
+
// Hue from the material, brightness from the object, so two models wearing
|
|
63
|
+
// the same material index still read apart.
|
|
64
|
+
let h = hashId(materialId * 1973u + 9277u);
|
|
65
|
+
let shade = 0.55 + 0.45 * hashId(objectId * 6151u + 1u);
|
|
66
|
+
// Cheap hue ramp: three offset cosines. Saturated on purpose — this is a
|
|
67
|
+
// diagnostic, not a look.
|
|
68
|
+
let rgb = vec3f(
|
|
69
|
+
0.5 + 0.5 * cos(6.28318 * (h + 0.00)),
|
|
70
|
+
0.5 + 0.5 * cos(6.28318 * (h + 0.33)),
|
|
71
|
+
0.5 + 0.5 * cos(6.28318 * (h + 0.67)),
|
|
72
|
+
);
|
|
73
|
+
return vec4f(rgb * shade, 1.0);
|
|
74
|
+
}
|
|
75
|
+
`
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { RZ_LIGHT_STRUCT_WGSL } from "../lights"
|
|
2
|
+
import { audioApi } from "../audio-api"
|
|
3
|
+
import { lyricsApi } from "../lyrics-api"
|
|
4
|
+
import { anchorAliasWgsl } from "../anchor-table"
|
|
5
|
+
import { midiApi } from "../midi-api"
|
|
6
|
+
import { CAST_API } from "../cast-api"
|
|
7
|
+
import { clockApi, EFFECT_MATH_API, PARTICLE_STRUCT_WGSL, trailSlotsApi, viewportApi } from "./hosted-api"
|
|
8
|
+
// GPU particles for user effects: a compute step and an instanced quad draw.
|
|
9
|
+
//
|
|
10
|
+
// Its own shader MODULE rather than more source spliced into composite.ts, for
|
|
11
|
+
// three reasons that all point the same way. Cost: a fullscreen effect pays for
|
|
12
|
+
// every pixel on screen whether it drew anything there or not, so a ribbon walked
|
|
13
|
+
// as a field cost O(pixels × samples) where the same picture as geometry costs
|
|
14
|
+
// O(segments) — the difference between one effect and a scene full of them.
|
|
15
|
+
// Occlusion: geometry drawn inside the scene pass is depth-tested, so a particle
|
|
16
|
+
// behind the character is simply hidden, which a composited field can only fake.
|
|
17
|
+
// And names: separate modules cannot collide, so two effects may both define
|
|
18
|
+
// `hash21` and never meet — which is what makes running several at once safe.
|
|
19
|
+
//
|
|
20
|
+
// The author writes three functions and the engine owns the loop. That split is
|
|
21
|
+
// deliberate: spawning, ageing, recycling, billboarding and sorting are the same
|
|
22
|
+
// in every particle effect ever written, and an author who has to re-derive them
|
|
23
|
+
// gets them subtly wrong in a way that only shows up on someone else's machine.
|
|
24
|
+
|
|
25
|
+
/** Where the cast/trail history sits in the shared storage buffer. */
|
|
26
|
+
export type CastLayout = {
|
|
27
|
+
subjects: number
|
|
28
|
+
samples: number
|
|
29
|
+
base: number
|
|
30
|
+
trailBase: number
|
|
31
|
+
/** The scene's anchor CAP — the address space, not how many asked for trails. */
|
|
32
|
+
slots: number
|
|
33
|
+
/** How many of this effect's anchors asked for a trail. AUTHOR-VISIBLE as
|
|
34
|
+
* RZ_TRAIL_SLOTS: published effects loop over it, so its meaning is pinned
|
|
35
|
+
* even though the address space it used to share is now a separate number. */
|
|
36
|
+
trailCount: number
|
|
37
|
+
/** This effect's local slot → scene slot. Identity while one effect exists. */
|
|
38
|
+
alias: number[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The trail accessors, in the PARTICLE module.
|
|
43
|
+
*
|
|
44
|
+
* Sparks are the reason: the original hand ribbon shed sparks along its path,
|
|
45
|
+
* and as real particles they need to SPAWN on that path — which means
|
|
46
|
+
* particleInit reading the same recorded history the trail draws from. One
|
|
47
|
+
* effect file, two mounts, one buffer.
|
|
48
|
+
*/
|
|
49
|
+
/** Shared with the grid pass, which reads the same buffer for the same reason:
|
|
50
|
+
* a kernel that displaces fog has to know where the dancer's feet are. */
|
|
51
|
+
function castApi(cast: CastLayout): string {
|
|
52
|
+
return (
|
|
53
|
+
// rzSubjectCount FIRST, because CAST_API is written against it and this
|
|
54
|
+
// module has no view uniform to read the engine's count out of. Scanning
|
|
55
|
+
// for a subject whose bounding sphere has a radius is the same answer by a
|
|
56
|
+
// different route — the seam is named at the top of cast-api.ts.
|
|
57
|
+
`
|
|
58
|
+
fn rzSubjectCount() -> i32 {
|
|
59
|
+
var n = 0;
|
|
60
|
+
for (var i = 0; i < RZ_SUBJECTS; i++) {
|
|
61
|
+
if (_rzCast[i * 3 + 2].w > 0.0) { n = i + 1; }
|
|
62
|
+
}
|
|
63
|
+
return n;
|
|
64
|
+
}
|
|
65
|
+
` +
|
|
66
|
+
CAST_API +
|
|
67
|
+
trailSlotsApi(cast.trailCount) +
|
|
68
|
+
anchorAliasWgsl(cast.alias)
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** How the author's quads combine with the scene. */
|
|
73
|
+
type ParticleBlend = "alpha" | "additive"
|
|
74
|
+
|
|
75
|
+
type ParticleSource = {
|
|
76
|
+
/** The author's WGSL verbatim. */
|
|
77
|
+
wgsl: string
|
|
78
|
+
/** Simultaneous particles. Fixed at install; the pool recycles rather than grows. */
|
|
79
|
+
count: number
|
|
80
|
+
blend: ParticleBlend
|
|
81
|
+
/** Feed the bloom pyramid. Off by default — rain should not glow. */
|
|
82
|
+
bloom: boolean
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Bytes per particle. Explicitly padded — see the struct below. */
|
|
86
|
+
export const PARTICLE_STRIDE = 48
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
const CAMERA_STRUCT = /* wgsl */ `
|
|
90
|
+
struct CameraU {
|
|
91
|
+
view: mat4x4f,
|
|
92
|
+
proj: mat4x4f,
|
|
93
|
+
camPos: vec3f,
|
|
94
|
+
targetHeight: f32,
|
|
95
|
+
}
|
|
96
|
+
`
|
|
97
|
+
|
|
98
|
+
const PARTICLE_UNIFORMS = /* wgsl */ `
|
|
99
|
+
struct ParticleU {
|
|
100
|
+
time: f32,
|
|
101
|
+
dt: f32,
|
|
102
|
+
count: u32,
|
|
103
|
+
frame: u32,
|
|
104
|
+
}
|
|
105
|
+
`
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* This module's half of the prelude: the camera, from its own uniform.
|
|
109
|
+
*
|
|
110
|
+
* The rest — hashes, noise, falloff, clock, viewport — is shared with every
|
|
111
|
+
* other module that hosts an effect's source, because an author's helper must
|
|
112
|
+
* mean the same thing in whichever one it lands in.
|
|
113
|
+
*/
|
|
114
|
+
const PRELUDE =
|
|
115
|
+
// The hashes, the noise and rzFalloff, identical in every module that hosts an
|
|
116
|
+
// effect's source; then the clock and the viewport, which cannot be, because
|
|
117
|
+
// this module keeps both in its own uniforms.
|
|
118
|
+
EFFECT_MATH_API +
|
|
119
|
+
clockApi("pu.time", "pu.dt") +
|
|
120
|
+
viewportApi("cam.targetHeight") +
|
|
121
|
+
/* wgsl */ `
|
|
122
|
+
${RZ_LIGHT_STRUCT_WGSL}
|
|
123
|
+
fn rzCameraPos() -> vec3f { return cam.camPos; }
|
|
124
|
+
fn rzCameraRight() -> vec3f { return vec3f(cam.view[0][0], cam.view[1][0], cam.view[2][0]); }
|
|
125
|
+
fn rzCameraUp() -> vec3f { return vec3f(cam.view[0][1], cam.view[1][1], cam.view[2][1]); }
|
|
126
|
+
fn rzCameraForward() -> vec3f { return vec3f(cam.view[0][2], cam.view[1][2], cam.view[2][2]); }
|
|
127
|
+
/** World point → (uv, view distance), same contract as the field mounts' rzProject. */
|
|
128
|
+
fn rzProject(p: vec3f) -> vec3f {
|
|
129
|
+
let clip = cam.proj * cam.view * vec4f(p, 1.0);
|
|
130
|
+
let w = max(clip.w, 1e-4);
|
|
131
|
+
return vec3f(clip.xy / w * 0.5 + 0.5, clip.w);
|
|
132
|
+
}
|
|
133
|
+
fn rzCamPos() -> vec3f { return cam.camPos; }
|
|
134
|
+
`
|
|
135
|
+
|
|
136
|
+
/** `// @particles 4096` — how many live at once. */
|
|
137
|
+
export function parseParticleCount(wgsl: string, max: number): number {
|
|
138
|
+
const m = /^\s*\/\/\s*@particles\s+(\d+)\s*$/m.exec(wgsl)
|
|
139
|
+
if (!m) return 0
|
|
140
|
+
// Clamped rather than rejected: an author asking for a million gets the most
|
|
141
|
+
// the engine will give and a scene that still runs, which is a better failure
|
|
142
|
+
// than a compile error naming a number they had no way to know.
|
|
143
|
+
return Math.max(1, Math.min(max, parseInt(m[1], 10)))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** `// @bloom` — opt in to the bloom pyramid. Sparks want it; rain does not. */
|
|
147
|
+
export function parseParticleBloom(wgsl: string): boolean {
|
|
148
|
+
return /^\s*\/\/\s*@bloom\s*$/m.test(wgsl)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** `// @blend additive` — default is straight alpha. */
|
|
152
|
+
export function parseParticleBlend(wgsl: string): ParticleBlend {
|
|
153
|
+
return /^\s*\/\/\s*@blend\s+additive\s*$/m.test(wgsl) ? "additive" : "alpha"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Does the source define the particle contract? All three are required together. */
|
|
157
|
+
export function particleEntryPoints(wgsl: string): { init: boolean; step: boolean; shade: boolean } {
|
|
158
|
+
return {
|
|
159
|
+
init: /\bfn\s+particleInit\s*\(/.test(wgsl),
|
|
160
|
+
step: /\bfn\s+particleStep\s*\(/.test(wgsl),
|
|
161
|
+
shade: /\bfn\s+particleShade\s*\(/.test(wgsl),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Spawn, age, recycle.
|
|
167
|
+
*
|
|
168
|
+
* One kernel for both birth and update, because a dead slot and a new particle
|
|
169
|
+
* are the same write — a pool that recycles has no allocation and therefore no
|
|
170
|
+
* spawn-rate bookkeeping to get wrong. The cost is that lifetimes are staggered
|
|
171
|
+
* only by whatever the author randomises in `particleInit`, which for rain and
|
|
172
|
+
* snow is exactly right, and for a burst is what the age offset is for.
|
|
173
|
+
*/
|
|
174
|
+
export function buildParticleComputeShader(src: ParticleSource, cast: CastLayout): string {
|
|
175
|
+
return (
|
|
176
|
+
PARTICLE_STRUCT_WGSL +
|
|
177
|
+
CAMERA_STRUCT +
|
|
178
|
+
PARTICLE_UNIFORMS +
|
|
179
|
+
`
|
|
180
|
+
@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
|
|
181
|
+
@group(0) @binding(1) var<uniform> pu: ParticleU;
|
|
182
|
+
@group(0) @binding(2) var<uniform> cam: CameraU;
|
|
183
|
+
@group(0) @binding(3) var<storage, read> _rzCast: array<vec4f>;
|
|
184
|
+
` +
|
|
185
|
+
castApi(cast) +
|
|
186
|
+
audioApi(0, 4) +
|
|
187
|
+
midiApi(0, 5) +
|
|
188
|
+
lyricsApi(0, 6) +
|
|
189
|
+
PRELUDE +
|
|
190
|
+
"\n// ── user effect ──\n" +
|
|
191
|
+
src.wgsl +
|
|
192
|
+
/* wgsl */ `
|
|
193
|
+
@compute @workgroup_size(64)
|
|
194
|
+
fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
195
|
+
let i = gid.x;
|
|
196
|
+
if (i >= pu.count) { return; }
|
|
197
|
+
var p = particles[i];
|
|
198
|
+
if (p.life <= 0.0 || p.age >= p.life) {
|
|
199
|
+
// The seed is stable per SLOT and per generation, so a particle looks the
|
|
200
|
+
// same every time the scene is replayed at the same moment — which is what
|
|
201
|
+
// keeps an exported video identical to the preview.
|
|
202
|
+
let generation = floor(pu.time * 0.37) + f32(i) * 0.618;
|
|
203
|
+
p = particleInit(i, rzHash11(generation));
|
|
204
|
+
// age is NOT reset here. WGSL zero-initialises a var, so an author who
|
|
205
|
+
// ignores it starts at zero anyway — while one who sets it to a fraction of
|
|
206
|
+
// its life staggers the pool, which is the difference between snow and a
|
|
207
|
+
// pulse of snow arriving all at once every few seconds.
|
|
208
|
+
} else {
|
|
209
|
+
p = particleStep(p, pu.dt);
|
|
210
|
+
p.age = p.age + pu.dt;
|
|
211
|
+
}
|
|
212
|
+
particles[i] = p;
|
|
213
|
+
}
|
|
214
|
+
`
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* One camera-facing quad per live particle.
|
|
220
|
+
*
|
|
221
|
+
* Six vertices, no index or vertex buffer: the corners are derived from
|
|
222
|
+
* `vertex_index` and the particle is read from storage by `instance_index`, so a
|
|
223
|
+
* draw is `draw(6, count)` and there is nothing to upload per frame. The billboard
|
|
224
|
+
* basis comes from the VIEW matrix's rows rather than from a look-at, which keeps
|
|
225
|
+
* the quad square on screen no matter where the camera rolls.
|
|
226
|
+
*
|
|
227
|
+
* A dead particle collapses to a degenerate quad instead of being culled on the
|
|
228
|
+
* CPU — the alternative is a compacted draw list, which costs a prefix sum and a
|
|
229
|
+
* readback every frame to save vertices the rasteriser was going to reject anyway.
|
|
230
|
+
*/
|
|
231
|
+
export function buildParticleRenderShader(src: ParticleSource, cast: CastLayout): string {
|
|
232
|
+
return (
|
|
233
|
+
`override BLOOM: bool = ${src.bloom ? "true" : "false"};
|
|
234
|
+
override ADDITIVE: bool = ${src.blend === "additive" ? "true" : "false"};\n` +
|
|
235
|
+
PARTICLE_STRUCT_WGSL +
|
|
236
|
+
CAMERA_STRUCT +
|
|
237
|
+
PARTICLE_UNIFORMS +
|
|
238
|
+
`
|
|
239
|
+
@group(0) @binding(0) var<storage, read> particles: array<Particle>;
|
|
240
|
+
@group(0) @binding(1) var<uniform> pu: ParticleU;
|
|
241
|
+
@group(0) @binding(2) var<uniform> cam: CameraU;
|
|
242
|
+
@group(0) @binding(3) var<storage, read> _rzCast: array<vec4f>;
|
|
243
|
+
` +
|
|
244
|
+
castApi(cast) +
|
|
245
|
+
audioApi(0, 4) +
|
|
246
|
+
midiApi(0, 5) +
|
|
247
|
+
lyricsApi(0, 6) +
|
|
248
|
+
PRELUDE +
|
|
249
|
+
"\n// ── user effect ──\n" +
|
|
250
|
+
src.wgsl +
|
|
251
|
+
/* wgsl */ `
|
|
252
|
+
struct VSOut {
|
|
253
|
+
@builtin(position) clip: vec4f,
|
|
254
|
+
@location(0) uv: vec2f,
|
|
255
|
+
@location(1) @interpolate(flat) id: u32,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@vertex
|
|
259
|
+
fn vs(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut {
|
|
260
|
+
var out: VSOut;
|
|
261
|
+
out.id = ii;
|
|
262
|
+
let p = particles[ii];
|
|
263
|
+
// Two triangles, corners in the order 0,1,2, 2,1,3.
|
|
264
|
+
let quad = array<vec2f, 6>(
|
|
265
|
+
vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(-1.0, 1.0),
|
|
266
|
+
vec2f(-1.0, 1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0),
|
|
267
|
+
);
|
|
268
|
+
let c = quad[vi];
|
|
269
|
+
out.uv = c * 0.5 + 0.5;
|
|
270
|
+
if (p.life <= 0.0) {
|
|
271
|
+
// Degenerate: off the near plane, rasterises nothing.
|
|
272
|
+
out.clip = vec4f(0.0, 0.0, -2.0, 1.0);
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
let right = vec3f(cam.view[0][0], cam.view[1][0], cam.view[2][0]);
|
|
276
|
+
let up = vec3f(cam.view[0][1], cam.view[1][1], cam.view[2][1]);
|
|
277
|
+
let s = sin(p.rot);
|
|
278
|
+
let k = cos(p.rot);
|
|
279
|
+
var r = vec2f(c.x * k - c.y * s, c.x * s + c.y * k);
|
|
280
|
+
// Stretched along the direction of travel ON SCREEN — which is not the world
|
|
281
|
+
// direction once the camera is off-axis. Rain falling straight down is nearly
|
|
282
|
+
// a point when viewed from above and a long streak from the side, and taking
|
|
283
|
+
// the velocity's components in the camera's own basis is what gets both right.
|
|
284
|
+
// Rotation is ignored while stretched: the velocity IS the orientation.
|
|
285
|
+
if (p.stretch > 1.0) {
|
|
286
|
+
let vr = dot(p.vel, right);
|
|
287
|
+
let vu = dot(p.vel, up);
|
|
288
|
+
let vlen = length(vec2f(vr, vu));
|
|
289
|
+
if (vlen > 1e-5) {
|
|
290
|
+
let d = vec2f(vr, vu) / vlen;
|
|
291
|
+
r = vec2f(d.y, -d.x) * c.x + d * (c.y * p.stretch);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
let world = p.pos + (right * r.x + up * r.y) * p.size;
|
|
295
|
+
out.clip = cam.proj * cam.view * vec4f(world, 1.0);
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
struct FSOut {
|
|
300
|
+
@location(0) color: vec4f,
|
|
301
|
+
// The scene's aux target: (bloom mask, coverage). Materials write it, so a
|
|
302
|
+
// particle that skipped it would punch a hole in the mask of whatever it drew
|
|
303
|
+
// over.
|
|
304
|
+
//
|
|
305
|
+
// vec4f even though the target is rg8unorm and only .rg land: that target's
|
|
306
|
+
// blend factors reference SrcAlpha, and a fragment with no alpha channel is
|
|
307
|
+
// rejected outright — "reading alpha but it is missing from fragment output".
|
|
308
|
+
// The material shaders declare vec4f here for the same reason.
|
|
309
|
+
@location(1) mask: vec4f,
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@fragment
|
|
313
|
+
fn fs(in: VSOut) -> FSOut {
|
|
314
|
+
let p = particles[in.id];
|
|
315
|
+
let c = particleShade(p, in.uv);
|
|
316
|
+
if (c.a <= 0.0) { discard; }
|
|
317
|
+
var out: FSOut;
|
|
318
|
+
// PREMULTIPLIED: the scene's colour target blends with srcFactor \"one\", so a
|
|
319
|
+
// straight-alpha fragment would come out over-bright wherever it is
|
|
320
|
+
// translucent — which is most of a soft particle.
|
|
321
|
+
out.color = vec4f(c.rgb * c.a, c.a);
|
|
322
|
+
// The mask's OPERATOR must match the colour's, or the composite invents bands.
|
|
323
|
+
//
|
|
324
|
+
// The composite divides the HDR colour by this coverage to un-premultiply
|
|
325
|
+
// before tone mapping. An ALPHA effect writes (gate, 1.0) and lets the
|
|
326
|
+
// src-alpha blend make alpha-over coverage, exactly as the materials do. An
|
|
327
|
+
// ADDITIVE effect's pipeline blends this target with factor ONE instead, and
|
|
328
|
+
// writes its values directly — coverage SUMS like the colour does, so
|
|
329
|
+
// Σ(rgb·a)/Σa returns the true colour even where the effect overlaps itself.
|
|
330
|
+
// With summed colour over alpha-over coverage, every self-overlap divided into
|
|
331
|
+
// a bright white bar — visible only over the background, because the model's
|
|
332
|
+
// own coverage is already 1 there and the divide is a no-op. That mismatch,
|
|
333
|
+
// not geometry, was the banding that survived every geometry fix.
|
|
334
|
+
let mg = select(vec2f(select(0.0, 1.0, BLOOM), 1.0), vec2f(select(0.0, c.a, BLOOM), c.a), ADDITIVE);
|
|
335
|
+
out.mask = vec4f(mg.x, mg.y, 0.0, c.a);
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
`
|
|
339
|
+
)
|
|
340
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scene pass's attachment contract, in one place.
|
|
3
|
+
*
|
|
4
|
+
* Everything drawn INSIDE the scene pass — models, ground, outline hulls,
|
|
5
|
+
* particles, ribbons, and the transparent depth prepass — shares one set of
|
|
6
|
+
* attachments, and therefore one set of formats, blends and write masks. That
|
|
7
|
+
* agreement used to be restated at every pipeline that joins the pass and in
|
|
8
|
+
* every shader that writes to it, which is why adding an attachment was
|
|
9
|
+
* dangerous rather than tedious: a class left behind does not fail loudly, it
|
|
10
|
+
* fails as a validation error naming a pipeline that was never edited.
|
|
11
|
+
*
|
|
12
|
+
* So the contract is DATA here, and the pipelines ask for it by render class.
|
|
13
|
+
* Adding the id attachment (MRT) becomes one edit in this file plus a per-class
|
|
14
|
+
* decision about whether that class writes it — which is the shape of the
|
|
15
|
+
* question, and now the shape of the code.
|
|
16
|
+
*
|
|
17
|
+
* WHAT IS NOT HERE. Two passes look like they belong and do not:
|
|
18
|
+
* - the FIELD pass has its own pair of rgba16float targets, its own blend and
|
|
19
|
+
* no MSAA. It is a different pass with a different contract.
|
|
20
|
+
* - the gizmo and selection-edge pipelines draw to the SWAPCHAIN in their own
|
|
21
|
+
* unmultisampled pass, so the presentation format is the whole of their
|
|
22
|
+
* contract. The plan listed gizmo as a scene render class; the code says
|
|
23
|
+
* otherwise, and the code is right.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** The scene pass's attachments, as the engine has them at init. Passed in
|
|
27
|
+
* rather than imported: hdr is chosen per device (rg11b10ufloat where it is
|
|
28
|
+
* available and blendable, rgba16float otherwise). */
|
|
29
|
+
export type SceneFormats = {
|
|
30
|
+
/** The HDR colour attachment, @location(0). */
|
|
31
|
+
hdr: GPUTextureFormat
|
|
32
|
+
/** The aux attachment, @location(1) — (bloom mask, coverage). */
|
|
33
|
+
aux: GPUTextureFormat
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The id attachment's format: (material index, object index), one u16 each.
|
|
38
|
+
*
|
|
39
|
+
* Two 16-bit channels rather than one 32-bit: 32-bit formats are not
|
|
40
|
+
* multisamplable, and this attachment is multisampled with the rest of the
|
|
41
|
+
* pass. Uint targets take no blend at all per spec, which is exactly right —
|
|
42
|
+
* an averaged id is not an id.
|
|
43
|
+
*/
|
|
44
|
+
export const SCENE_ID_FORMAT: GPUTextureFormat = "rg16uint"
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether the scene pass carries the id attachment.
|
|
48
|
+
*
|
|
49
|
+
* Runtime rather than a compile-time constant, and mutable, because it is not
|
|
50
|
+
* only a decision — it is a CAPABILITY. Multisampled rg16uint has to be probed
|
|
51
|
+
* on the device (see the engine's init), and a device that cannot do it must
|
|
52
|
+
* leave this off. That forces the shaders to be assembled after the probe,
|
|
53
|
+
* which is why the two shader modules that gain an output stopped being
|
|
54
|
+
* module-level constants: a string baked at import cannot know what the device
|
|
55
|
+
* said.
|
|
56
|
+
*
|
|
57
|
+
* Set ONCE at init, before any pipeline or shader module is built. Nothing
|
|
58
|
+
* reads it per frame.
|
|
59
|
+
*/
|
|
60
|
+
let mrtIds = false
|
|
61
|
+
|
|
62
|
+
/** Called by the engine at init, after probing the device. */
|
|
63
|
+
export function setMrtIds(on: boolean): void {
|
|
64
|
+
mrtIds = on
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function mrtIdsEnabled(): boolean {
|
|
68
|
+
return mrtIds
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* What is being drawn, which is the only thing that varies.
|
|
73
|
+
*
|
|
74
|
+
* The classes differ ONLY in blend and write mask; formats are the pass's, not
|
|
75
|
+
* the draw's. A class here is a thing with a reason, not a pipeline name — two
|
|
76
|
+
* pipelines that blend the same way share one.
|
|
77
|
+
*/
|
|
78
|
+
type SceneRenderClass =
|
|
79
|
+
/** Models, opaque and transparent. Straight alpha over. */
|
|
80
|
+
| "material"
|
|
81
|
+
/** The shadow-catcher floor. Blends PREMULTIPLIED, not like a material: its
|
|
82
|
+
* coverage is a lit surface plus a colourless shadow layer, so it weights
|
|
83
|
+
* its own colour before the blend sees it. */
|
|
84
|
+
| "ground"
|
|
85
|
+
/** Backface-expanded hulls. Also a material blend — it is geometry. */
|
|
86
|
+
| "outline"
|
|
87
|
+
/** Particles and ribbons in their default, non-additive mode. */
|
|
88
|
+
| "particle"
|
|
89
|
+
/** Particles declaring `// @blend additive` — LIGHT rather than matter, so
|
|
90
|
+
* colour sums and alpha is left alone: a glow must not claim coverage it
|
|
91
|
+
* never occluded. The aux target sums with it, which is what lets an
|
|
92
|
+
* additive effect reach the bloom gate at all. */
|
|
93
|
+
| "particle-additive"
|
|
94
|
+
/** Ribbons. Additive colour like the above, but premultiplied by the
|
|
95
|
+
* fragment's own alpha on the way in (src-alpha, one) rather than added
|
|
96
|
+
* whole, and an ordinary alpha-over aux so a ribbon's mask does not
|
|
97
|
+
* saturate along every overlap. */
|
|
98
|
+
| "trail"
|
|
99
|
+
/** The transparent depth prepass: it exists to write DEPTH after the fabric's
|
|
100
|
+
* colour blended, so an outline drawn later is occluded behind it. It must
|
|
101
|
+
* therefore write no colour at all — the targets exist only to make the
|
|
102
|
+
* pipeline compatible with the pass it joins. */
|
|
103
|
+
| "depth-prepass"
|
|
104
|
+
|
|
105
|
+
const ALPHA_OVER: GPUBlendState = {
|
|
106
|
+
color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha", operation: "add" },
|
|
107
|
+
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Colour sums; alpha is not touched (src zero, dst one). */
|
|
111
|
+
const ADD_KEEP_ALPHA: GPUBlendState = {
|
|
112
|
+
color: { srcFactor: "one", dstFactor: "one", operation: "add" },
|
|
113
|
+
alpha: { srcFactor: "zero", dstFactor: "one", operation: "add" },
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Both channels sum. rg8unorm clamps at 1, which is the saturation alpha-over
|
|
117
|
+
* would have reached anyway. */
|
|
118
|
+
const ADD_BOTH: GPUBlendState = {
|
|
119
|
+
color: { srcFactor: "one", dstFactor: "one", operation: "add" },
|
|
120
|
+
alpha: { srcFactor: "one", dstFactor: "one", operation: "add" },
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* OVER for something that arrives ALREADY premultiplied: take the source whole
|
|
125
|
+
* and let it displace the destination by its own coverage.
|
|
126
|
+
*
|
|
127
|
+
* The ground needs this and nothing else does. Every other class writes a
|
|
128
|
+
* straight colour and an alpha, and the src-alpha factor premultiplies it once
|
|
129
|
+
* on the way in. The ground cannot: its coverage is the SUM of a lit surface
|
|
130
|
+
* and a colourless shadow-catcher layer, so it has to weight its own colour by
|
|
131
|
+
* the surface's share before it gets here. Handed to the src-alpha blend, that
|
|
132
|
+
* weighting happened a second time.
|
|
133
|
+
*/
|
|
134
|
+
const PREMULTIPLIED_OVER: GPUBlendState = {
|
|
135
|
+
color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
|
|
136
|
+
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Additive, premultiplied by the fragment's alpha as it writes. */
|
|
140
|
+
const ADD_PREMULTIPLIED: GPUBlendState = {
|
|
141
|
+
color: { srcFactor: "src-alpha", dstFactor: "one", operation: "add" },
|
|
142
|
+
alpha: { srcFactor: "zero", dstFactor: "one", operation: "add" },
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Which classes actually WRITE an id, and so gain a fragment output for it.
|
|
147
|
+
*
|
|
148
|
+
* Everything else keeps its shader exactly as it is and takes the id target at
|
|
149
|
+
* writeMask 0 — legal, specified, and free. The alternative (leaving the target
|
|
150
|
+
* off those pipelines) is not available: every pipeline in a pass must agree
|
|
151
|
+
* with the pass's attachments.
|
|
152
|
+
*
|
|
153
|
+
* The ground is in because a mark placed by id needs the floor to have one.
|
|
154
|
+
* Transparent fabric writes ids too — dissolving a dress needs the dress's own
|
|
155
|
+
* pixels, and last write wins there, which is a documented choice rather than a
|
|
156
|
+
* consequence. Outline hulls, particles and ribbons are OUT: they are not
|
|
157
|
+
* things you would ever address by id, and a hull would overwrite the id of the
|
|
158
|
+
* body it traces.
|
|
159
|
+
*/
|
|
160
|
+
const WRITES_ID = new Set<SceneRenderClass>(["material", "ground"])
|
|
161
|
+
|
|
162
|
+
/** The blends each class writes its two attachments with. */
|
|
163
|
+
const BLENDS: Record<Exclude<SceneRenderClass, "depth-prepass">, [GPUBlendState, GPUBlendState]> = {
|
|
164
|
+
material: [ALPHA_OVER, ALPHA_OVER],
|
|
165
|
+
// PREMULTIPLIED colour, alone among the classes — see the blend's own note.
|
|
166
|
+
// The aux is ordinary alpha-over: the ground writes its mask unweighted, like
|
|
167
|
+
// everything else, and coverage is what the blend applies.
|
|
168
|
+
ground: [PREMULTIPLIED_OVER, ALPHA_OVER],
|
|
169
|
+
outline: [ALPHA_OVER, ALPHA_OVER],
|
|
170
|
+
particle: [ALPHA_OVER, ALPHA_OVER],
|
|
171
|
+
"particle-additive": [ADD_KEEP_ALPHA, ADD_BOTH],
|
|
172
|
+
trail: [ADD_PREMULTIPLIED, ALPHA_OVER],
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The colour targets a scene-pass pipeline of this class declares, in
|
|
177
|
+
* attachment order.
|
|
178
|
+
*
|
|
179
|
+
* Fresh objects every call, deliberately: a caller that mutated a shared
|
|
180
|
+
* descriptor would change every pipeline built after it, and the ones built
|
|
181
|
+
* before would keep the old value — a difference that only shows up as one
|
|
182
|
+
* pipeline blending unlike its neighbours.
|
|
183
|
+
*/
|
|
184
|
+
export function sceneTargets(cls: SceneRenderClass, formats: SceneFormats): GPUColorTargetState[] {
|
|
185
|
+
const targets: GPUColorTargetState[] =
|
|
186
|
+
cls === "depth-prepass"
|
|
187
|
+
? // Format only, and writeMask 0. Note the asymmetry this leans on, which
|
|
188
|
+
// is the same one the id target leans on below: a target the shader has
|
|
189
|
+
// no output for is legal at writeMask 0 (gpuweb#1918), while an output
|
|
190
|
+
// with no target is NOT governed (gpuweb#5341). This is the specified
|
|
191
|
+
// direction, and it is the only one this file ever uses.
|
|
192
|
+
[
|
|
193
|
+
{ format: formats.hdr, writeMask: 0 },
|
|
194
|
+
{ format: formats.aux, writeMask: 0 },
|
|
195
|
+
]
|
|
196
|
+
: (() => {
|
|
197
|
+
const [color, aux] = BLENDS[cls]
|
|
198
|
+
return [
|
|
199
|
+
{ format: formats.hdr, blend: { color: { ...color.color }, alpha: { ...color.alpha } } },
|
|
200
|
+
{ format: formats.aux, blend: { color: { ...aux.color }, alpha: { ...aux.alpha } } },
|
|
201
|
+
]
|
|
202
|
+
})()
|
|
203
|
+
if (mrtIds) targets.push({ format: SCENE_ID_FORMAT, writeMask: WRITES_ID.has(cls) ? 0xf : 0 })
|
|
204
|
+
return targets
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The pass's colour attachments, in order — for the things that describe the
|
|
209
|
+
* pass itself rather than a draw within it.
|
|
210
|
+
*
|
|
211
|
+
* A render bundle declares the formats it will be replayed into and is rejected
|
|
212
|
+
* against a pass that does not match, so the bundle encoder has to move in
|
|
213
|
+
* lockstep with the targets above. It restated the list independently until
|
|
214
|
+
* this existed, which made it the one consumer an MRT change would have missed
|
|
215
|
+
* — a bundle is recorded once and replayed, so the failure would have arrived
|
|
216
|
+
* at replay, naming the bundle rather than the attachment that changed.
|
|
217
|
+
*/
|
|
218
|
+
export function sceneColorFormats(formats: SceneFormats): GPUTextureFormat[] {
|
|
219
|
+
return mrtIds ? [formats.hdr, formats.aux, SCENE_ID_FORMAT] : [formats.hdr, formats.aux]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The fragment-output struct a scene-pass shader returns.
|
|
224
|
+
*
|
|
225
|
+
* Emitted rather than written out per file so that the attachment list has one
|
|
226
|
+
* author. The struct and the targets above have to agree on count and order,
|
|
227
|
+
* and they now disagree in one file rather than in five.
|
|
228
|
+
*
|
|
229
|
+
* Only the shaders that will GAIN an output take this today — the materials
|
|
230
|
+
* (hand-written and graph-generated alike, through COMMON_FS_OUT_WGSL) and the
|
|
231
|
+
* ground. Outline, particles and ribbons keep their own declarations on
|
|
232
|
+
* purpose: they never write the id, so they would take an `id: false` argument
|
|
233
|
+
* forever, and their structs carry comments about their own blend that belong
|
|
234
|
+
* where they are.
|
|
235
|
+
*/
|
|
236
|
+
export function sceneFsOutWgsl(opts?: { name?: string; aux?: string }): string {
|
|
237
|
+
const name = opts?.name ?? "FSOut"
|
|
238
|
+
const aux = opts?.aux ?? "mask"
|
|
239
|
+
// No @interpolate here, deliberately. The plan called for
|
|
240
|
+
// `@location(2) @interpolate(flat)`, and that attribute is only legal on a
|
|
241
|
+
// vertex OUTPUT or a fragment INPUT — a fragment output is neither, so it
|
|
242
|
+
// would not compile. It is also unnecessary: the id is read from the per-draw
|
|
243
|
+
// uniform, not carried across the triangle as a varying, so there is no
|
|
244
|
+
// interpolation to suppress. (A varying carrying it WOULD need flat, since an
|
|
245
|
+
// integer varying must be.)
|
|
246
|
+
//
|
|
247
|
+
// vec2u for rg16uint: the output type must be compatible with the format, and
|
|
248
|
+
// uint targets take no blend, which is what makes last-write-wins the rule.
|
|
249
|
+
const id = mrtIds ? ` @location(2) id: vec2u,\n` : ""
|
|
250
|
+
return `struct ${name} {
|
|
251
|
+
@location(0) color: vec4f,
|
|
252
|
+
@location(1) ${aux}: vec4f,
|
|
253
|
+
${id}};
|
|
254
|
+
`
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The line a fragment shader assigns its id with, or nothing when ids are off.
|
|
259
|
+
*
|
|
260
|
+
* Emitted rather than written into each shader for the same reason as the
|
|
261
|
+
* struct: with ids off there must be no assignment either, and a shader cannot
|
|
262
|
+
* ask the device what it supports.
|
|
263
|
+
*/
|
|
264
|
+
export function sceneIdWriteWgsl(out: string, material: string, object: string): string {
|
|
265
|
+
return mrtIds ? ` ${out}.id = vec2u(${material}, ${object});\n` : ""
|
|
266
|
+
}
|