reze-engine 0.43.0 → 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 -438
- 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 +841 -53
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +3498 -451
- 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/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/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 +28 -22
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +165 -138
- 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 +10 -2
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +37 -109
- 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 +14 -2
- package/dist/shaders/passes/trails.d.ts.map +1 -1
- package/dist/shaders/passes/trails.js +55 -90
- 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 +4015 -556
- 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 +3 -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/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 +182 -139
- 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 +54 -112
- package/src/shaders/passes/scene-contract.ts +266 -0
- package/src/shaders/passes/trails.ts +77 -93
- package/src/shadow-cascades.ts +97 -0
- package/src/vmd-loader.ts +2 -2
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { audioApi } from "../audio-api"
|
|
2
|
+
import { lyricsApi } from "../lyrics-api"
|
|
3
|
+
import { anchorAliasWgsl } from "../anchor-table"
|
|
4
|
+
import { midiApi } from "../midi-api"
|
|
5
|
+
import { EFFECT_SCENE_API } from "./composite"
|
|
6
|
+
import { type CastLayout } from "./particles"
|
|
7
|
+
import { clockApi, trailSlotsApi, viewportApi } from "./hosted-api"
|
|
8
|
+
|
|
9
|
+
// A persistent grid an effect can step and read: the one thing an effect could
|
|
10
|
+
// not have before, which is MEMORY.
|
|
11
|
+
//
|
|
12
|
+
// Every other mount is a pure function of position and time. A field shader
|
|
13
|
+
// recomputes its noise from scratch every frame; a trail reads history the
|
|
14
|
+
// engine happens to keep. Neither can build on what it produced last frame, and
|
|
15
|
+
// a whole class of effect is nothing but that:
|
|
16
|
+
//
|
|
17
|
+
// Fog that a dancer walks through does not "look displaced near her" — it IS
|
|
18
|
+
// displaced, and stays displaced, and creeps back over seconds. Water does not
|
|
19
|
+
// look rippled where a foot landed; a ripple leaves and propagates and
|
|
20
|
+
// reflects off the far edge long after the foot is gone. Both are the same
|
|
21
|
+
// shape of thing — a grid, stepped by its own previous value.
|
|
22
|
+
//
|
|
23
|
+
// It is also the only way to get the LOOK of a fluid, not merely its motion.
|
|
24
|
+
// Advection compounds: each frame's field is the last one pushed sideways, and a
|
|
25
|
+
// few hundred frames of that stretches and folds a smooth field into filaments
|
|
26
|
+
// and vortex sheets finer than the grid storing them. No noise function
|
|
27
|
+
// produces that, at any octave count, because the structure is not a function of
|
|
28
|
+
// position at all — it is a function of history.
|
|
29
|
+
//
|
|
30
|
+
// The engine owns the ping-pong, the dispatch and the sampling. The author owns
|
|
31
|
+
// the KERNEL, which is the part that differs: advection for smoke, the wave
|
|
32
|
+
// equation for water, a decaying stamp for footprints. That split is the same
|
|
33
|
+
// one the particle mounts make, and for the same reason — the loop is identical
|
|
34
|
+
// in every such effect and the physics never is.
|
|
35
|
+
|
|
36
|
+
// A megatexel of rgba16float is 8MB, and two of those is the whole cost. What
|
|
37
|
+
// actually bounds this is the STEP: one invocation per texel per frame, each
|
|
38
|
+
// taking a dozen samples, so doubling the side quadruples the work.
|
|
39
|
+
export const GRID_MAX = 1024
|
|
40
|
+
/** rgba16float, ping-ponged. Two of these is the whole memory cost. */
|
|
41
|
+
export const SIM_FORMAT: GPUTextureFormat = "rgba16float"
|
|
42
|
+
|
|
43
|
+
/** `// @grid 256` — the grid's resolution, in texels per side. */
|
|
44
|
+
export function parseGridSize(wgsl: string, max: number): number {
|
|
45
|
+
const m = /^\s*\/\/\s*@grid\s+(\d+)\s*$/m.exec(wgsl)
|
|
46
|
+
if (!m) return 0
|
|
47
|
+
// Clamped rather than rejected, as the particle count is: an author asking for
|
|
48
|
+
// 4096 gets the most the engine will give and a scene that still runs.
|
|
49
|
+
return Math.max(8, Math.min(max, parseInt(m[1], 10)))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Whether this effect drives a grid at all. */
|
|
53
|
+
export function gridEntryPoint(wgsl: string): boolean {
|
|
54
|
+
return /\bfn\s+gridStep\s*\(/.test(wgsl)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `rzGrid(uv)`, for a shader that READS the grid rather than steps it.
|
|
59
|
+
*
|
|
60
|
+
* Always compiled in, even for an effect with no grid — it then samples a 1×1
|
|
61
|
+
* of zeroes. An accessor that exists only sometimes is one an author has to
|
|
62
|
+
* guard, and a missing function is a compile error rather than a blank result.
|
|
63
|
+
*/
|
|
64
|
+
export function gridReadApi(group: number, tex: number, samp: number, size: number): string {
|
|
65
|
+
return /* wgsl */ `
|
|
66
|
+
const RZ_GRID_SIZE: f32 = ${size > 0 ? size : 1}.0;
|
|
67
|
+
@group(${group}) @binding(${tex}) var _rzGridTex: texture_2d<f32>;
|
|
68
|
+
@group(${group}) @binding(${samp}) var _rzGridSamp: sampler;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The grid, bilinearly sampled. uv is 0..1 across it, and what that MEANS in
|
|
72
|
+
* the world is the effect's own business — the engine deliberately does not
|
|
73
|
+
* impose a mapping, because a fog laid over a stage and a ripple field around a
|
|
74
|
+
* character want different ones and both are two lines of arithmetic.
|
|
75
|
+
*/
|
|
76
|
+
fn rzGrid(uv: vec2f) -> vec4f {
|
|
77
|
+
return textureSampleLevel(_rzGridTex, _rzGridSamp, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0);
|
|
78
|
+
}
|
|
79
|
+
fn rzGridSize() -> f32 { return RZ_GRID_SIZE; }
|
|
80
|
+
fn rzGridTexel() -> f32 { return 1.0 / RZ_GRID_SIZE; }
|
|
81
|
+
|
|
82
|
+
// ── The step-only half, defined here too and never called here ──
|
|
83
|
+
//
|
|
84
|
+
// One effect file is spliced into EVERY module it has a mount in, so a file
|
|
85
|
+
// with a grid compiles its gridStep inside the field and particle shaders as
|
|
86
|
+
// well — where it is dead code that nothing calls, but still code that has to
|
|
87
|
+
// resolve. Leaving these out is a compile error on a function the author was
|
|
88
|
+
// right to write. rzGridPrev reads the current grid rather than a previous one,
|
|
89
|
+
// which is the honest answer outside the step: there is no previous frame here.
|
|
90
|
+
fn rzGridPrev(uv: vec2f) -> vec4f { return rzGrid(uv); }
|
|
91
|
+
fn rzGridFrame() -> i32 { return 1; }
|
|
92
|
+
`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const SIM_UNIFORMS = /* wgsl */ `
|
|
96
|
+
struct SimU {
|
|
97
|
+
time: f32,
|
|
98
|
+
dt: f32,
|
|
99
|
+
size: f32,
|
|
100
|
+
frame: f32,
|
|
101
|
+
}
|
|
102
|
+
`
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The step shader: one invocation per texel, once per frame.
|
|
106
|
+
*
|
|
107
|
+
* The previous grid is bound as a SAMPLED texture and the next as a storage
|
|
108
|
+
* texture, which is what makes the two different resources and the whole thing
|
|
109
|
+
* legal — a shader cannot read and write one texture coherently, and the
|
|
110
|
+
* ping-pong is not an optimisation but the only correct way to do this.
|
|
111
|
+
*
|
|
112
|
+
* Sampled rather than only fetched, because advection needs to read BETWEEN
|
|
113
|
+
* texels: a semi-Lagrangian step asks "what was at the place this parcel came
|
|
114
|
+
* from", and that place is almost never a texel centre. Point-sampling it is
|
|
115
|
+
* what turns smoke into a staircase.
|
|
116
|
+
*/
|
|
117
|
+
export function buildSimShader(wgsl: string, size: number, cast: CastLayout): string {
|
|
118
|
+
return (
|
|
119
|
+
SIM_UNIFORMS +
|
|
120
|
+
/* wgsl */ `
|
|
121
|
+
@group(0) @binding(0) var<uniform> su: SimU;
|
|
122
|
+
@group(0) @binding(1) var _rzGridPrevTex: texture_2d<f32>;
|
|
123
|
+
@group(0) @binding(2) var _rzGridSamp: sampler;
|
|
124
|
+
@group(0) @binding(3) var _rzGridOut: texture_storage_2d<${SIM_FORMAT}, write>;
|
|
125
|
+
@group(0) @binding(4) var<storage, read> _rzCast: array<vec4f>;
|
|
126
|
+
// The same view uniform the composite reads, so the scene API below is the real
|
|
127
|
+
// thing here rather than a stub — a kernel can ask where a bone is, and a
|
|
128
|
+
// foreground spliced into this module resolves its camera calls.
|
|
129
|
+
@group(0) @binding(6) var<uniform> viewU: array<vec4<f32>, 15>;
|
|
130
|
+
${clockApi("su.time", "su.dt")}${viewportApi("viewU[6].w")}${trailSlotsApi(cast.trailCount)}
|
|
131
|
+
/** Texels per side. */
|
|
132
|
+
fn rzGridSize() -> f32 { return su.size; }
|
|
133
|
+
/** One texel, in uv — the step a neighbour lookup takes. */
|
|
134
|
+
fn rzGridTexel() -> f32 { return 1.0 / su.size; }
|
|
135
|
+
/**
|
|
136
|
+
* Steps since this effect was installed. Zero on the very first one, which is
|
|
137
|
+
* the only chance to seed a grid: everything after it builds on what is there.
|
|
138
|
+
*/
|
|
139
|
+
fn rzGridFrame() -> i32 { return i32(su.frame); }
|
|
140
|
+
|
|
141
|
+
/** The grid as it was LAST frame, bilinear. The only input a kernel really has. */
|
|
142
|
+
fn rzGridPrev(uv: vec2f) -> vec4f {
|
|
143
|
+
return textureSampleLevel(_rzGridPrevTex, _rzGridSamp, clamp(uv, vec2f(0.0), vec2f(1.0)), 0.0);
|
|
144
|
+
}
|
|
145
|
+
` +
|
|
146
|
+
EFFECT_SCENE_API +
|
|
147
|
+
// The alias _rzSlot, which the scene API's accessors route through. Every
|
|
148
|
+
// module embedding EFFECT_SCENE_API must splice this; the sim module was
|
|
149
|
+
// the one that did not, and every gridStep effect failed to install with
|
|
150
|
+
// "unresolved call target" from the day the identity fallback was removed.
|
|
151
|
+
// Found by tools/validate-wgsl.mjs on its first run.
|
|
152
|
+
anchorAliasWgsl(cast.alias) +
|
|
153
|
+
audioApi(0, 5) +
|
|
154
|
+
midiApi(0, 7) +
|
|
155
|
+
lyricsApi(0, 8) +
|
|
156
|
+
// rzGrid itself, so a kernel may read the grid it is writing — through the
|
|
157
|
+
// PREVIOUS frame's texture, which is the only version of it that exists
|
|
158
|
+
// while the current one is still being written.
|
|
159
|
+
/* wgsl */ `
|
|
160
|
+
fn rzGrid(uv: vec2f) -> vec4f { return rzGridPrev(uv); }
|
|
161
|
+
` +
|
|
162
|
+
"\n// ── user effect (setEffect) ──\n" +
|
|
163
|
+
wgsl +
|
|
164
|
+
/* wgsl */ `
|
|
165
|
+
|
|
166
|
+
@compute @workgroup_size(8, 8)
|
|
167
|
+
fn main(@builtin(global_invocation_id) gid: vec3u) {
|
|
168
|
+
let n = u32(${size});
|
|
169
|
+
if (gid.x >= n || gid.y >= n) { return; }
|
|
170
|
+
let xy = vec2i(i32(gid.x), i32(gid.y));
|
|
171
|
+
// The texel CENTRE, so a kernel that reads its own uv back through rzGridPrev
|
|
172
|
+
// lands on itself rather than a quarter-texel off.
|
|
173
|
+
let uv = (vec2f(f32(gid.x), f32(gid.y)) + vec2f(0.5)) / su.size;
|
|
174
|
+
textureStore(_rzGridOut, xy, gridStep(uv, textureLoad(_rzGridPrevTex, xy, 0), su.dt));
|
|
175
|
+
}
|
|
176
|
+
`
|
|
177
|
+
)
|
|
178
|
+
}
|
|
@@ -1,23 +1,49 @@
|
|
|
1
|
+
import { sceneFsOutWgsl, sceneIdWriteWgsl } from "./scene-contract"
|
|
2
|
+
import { lightsApi } from "../lights"
|
|
3
|
+
import { SHADOW_CASCADES } from "../../shadow-cascades"
|
|
4
|
+
import { WORLD_AMBIENT_WGSL } from "../lights"
|
|
5
|
+
|
|
1
6
|
// Ground shadow-catcher: receives directional shadow, grid lines, frosted noise,
|
|
2
7
|
// radial distance fade. Writes bloom mask = 0 (ground never bloom-bleeds).
|
|
8
|
+
//
|
|
9
|
+
// A FUNCTION, not a constant, since the id attachment is a device capability:
|
|
10
|
+
// the struct this returns depends on what the probe at init found, and a string
|
|
11
|
+
// baked at import time cannot know that. Called once, when the module is built.
|
|
3
12
|
|
|
4
|
-
export
|
|
13
|
+
export function groundShaderWgsl(): string {
|
|
14
|
+
return /* wgsl */ `
|
|
5
15
|
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
|
|
6
16
|
struct Light { direction: vec4f, color: vec4f, };
|
|
7
|
-
struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, };
|
|
17
|
+
struct LightUniforms { ambientColor: vec4f, lights: array<Light, 4>, sh: array<vec4f, 9>, };
|
|
8
18
|
struct GroundShadowMat {
|
|
9
19
|
diffuseColor: vec3f, fadeStart: f32,
|
|
10
20
|
fadeEnd: f32, shadowStrength: f32, pcfTexel: f32, gridSpacing: f32,
|
|
11
21
|
gridLineWidth: f32, gridLineOpacity: f32, noiseStrength: f32, opacity: f32,
|
|
12
|
-
gridLineColor: vec3f,
|
|
22
|
+
gridLineColor: vec3f, mirror: f32,
|
|
23
|
+
mirrorBlur: f32, _mb0: f32, _mb1: f32, _mb2: f32,
|
|
13
24
|
};
|
|
14
|
-
|
|
25
|
+
// One view-projection per shadow cascade, inner to outer — same buffer and
|
|
26
|
+
// same order the materials read.
|
|
27
|
+
struct LightVP { viewProj: array<mat4x4f, ${SHADOW_CASCADES.length}>, };
|
|
15
28
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
16
29
|
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
17
30
|
@group(0) @binding(2) var shadowMap: texture_depth_2d;
|
|
18
31
|
@group(0) @binding(3) var shadowSampler: sampler_comparison;
|
|
19
32
|
@group(0) @binding(4) var<uniform> material: GroundShadowMat;
|
|
20
33
|
@group(0) @binding(5) var<uniform> lightVP: LightVP;
|
|
34
|
+
// The far cascade's map, binding 7 as in the materials' layout.
|
|
35
|
+
@group(0) @binding(7) var shadowMapFar: texture_depth_2d;
|
|
36
|
+
// The floor mirror (step 7D): the reflection target, the camera that rendered
|
|
37
|
+
// it, and an ordinary sampler — the two shadow bindings above are comparison.
|
|
38
|
+
// params = (projA, projB, _, _) — the depth-linearisation pair of the SHARED
|
|
39
|
+
// projection, refreshed per frame beside the matrix.
|
|
40
|
+
struct MirrorVP { viewProj: mat4x4f, params: vec4f, };
|
|
41
|
+
@group(0) @binding(8) var<uniform> mirrorVP: MirrorVP;
|
|
42
|
+
@group(0) @binding(9) var mirrorTex: texture_2d<f32>;
|
|
43
|
+
@group(0) @binding(10) var linearSampler: sampler;
|
|
44
|
+
@group(0) @binding(11) var mirrorDepth: texture_depth_multisampled_2d;
|
|
45
|
+
${WORLD_AMBIENT_WGSL}
|
|
46
|
+
${lightsApi(0, 6)}
|
|
21
47
|
|
|
22
48
|
fn hash2(p: vec2f) -> f32 {
|
|
23
49
|
var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
|
|
@@ -48,8 +74,7 @@ struct VO { @builtin(position) position: vec4f, @location(0) worldPos: vec3f, @l
|
|
|
48
74
|
var o: VO; o.worldPos = position; o.normal = normal;
|
|
49
75
|
o.position = camera.projection * camera.view * vec4f(position, 1.0); return o;
|
|
50
76
|
}
|
|
51
|
-
|
|
52
|
-
@fragment fn fs(i: VO) -> FSOut {
|
|
77
|
+
${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
|
|
53
78
|
// Derivatives first, unconditionally: WGSL requires fwidth in uniform control
|
|
54
79
|
// flow and everything below branches on world position.
|
|
55
80
|
let gp = i.worldPos.xz / material.gridSpacing;
|
|
@@ -68,15 +93,20 @@ struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
|
|
|
68
93
|
return out;
|
|
69
94
|
}
|
|
70
95
|
|
|
71
|
-
|
|
72
|
-
let ndc = lclip.xyz / max(lclip.w, 1e-6);
|
|
73
|
-
// Outside the light's frustum there IS no shadow information — the clamped
|
|
96
|
+
// Outside a cascade's frustum there IS no shadow information — the clamped
|
|
74
97
|
// border samples compare against unrelated depths and read "shadowed",
|
|
75
98
|
// darkening the whole far plane with a visible band at the frustum edge
|
|
76
99
|
// (masked by the opaque surface normally, glaring in green-screen mode where
|
|
77
|
-
// only the shadow-catcher term renders).
|
|
100
|
+
// only the shadow-catcher term renders). So each cascade fades out near its
|
|
101
|
+
// border — the near one INTO the far one's answer, the far one into lit.
|
|
102
|
+
let l0 = lightVP.viewProj[0] * vec4f(i.worldPos, 1.0);
|
|
103
|
+
let ndc = l0.xyz / max(l0.w, 1e-6);
|
|
78
104
|
let inZ = select(0.0, 1.0, ndc.z > 0.0 && ndc.z < 1.0);
|
|
79
105
|
let frustum = (1.0 - smoothstep(0.88, 0.96, abs(ndc.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc.y))) * inZ;
|
|
106
|
+
let l1 = lightVP.viewProj[1] * vec4f(i.worldPos, 1.0);
|
|
107
|
+
let ndc1 = l1.xyz / max(l1.w, 1e-6);
|
|
108
|
+
let inZ1 = select(0.0, 1.0, ndc1.z > 0.0 && ndc1.z < 1.0);
|
|
109
|
+
let frustum1 = (1.0 - smoothstep(0.88, 0.96, abs(ndc1.x))) * (1.0 - smoothstep(0.88, 0.96, abs(ndc1.y))) * inZ1;
|
|
80
110
|
|
|
81
111
|
// THE cost of this shader, measured: a full-screen ground ran 25 shadow
|
|
82
112
|
// comparisons per pixel, and each one is hardware-bilinear, so ~100 depth
|
|
@@ -88,10 +118,23 @@ struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
|
|
|
88
118
|
// almost entirely once the comparison sampler's own 2x2 filter is counted.
|
|
89
119
|
// Nine taps instead of twenty-five for the same blur radius.
|
|
90
120
|
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// and
|
|
121
|
+
// The far cascade's taps run ONLY where the near one is fading or absent
|
|
122
|
+
// (frustum < 1), so a pixel in the near core costs exactly what it did with
|
|
123
|
+
// one cascade — and that core is where the camera usually looks.
|
|
94
124
|
var vis = 1.0;
|
|
125
|
+
if (frustum < 1.0 && frustum1 > 0.0) {
|
|
126
|
+
let suv1 = vec2f(ndc1.x * 0.5 + 0.5, 0.5 - ndc1.y * 0.5);
|
|
127
|
+
let suv1_c = clamp(suv1, vec2f(0.02), vec2f(0.98));
|
|
128
|
+
let st1 = ${1 / SHADOW_CASCADES[SHADOW_CASCADES.length - 1].mapSize} * 2.0;
|
|
129
|
+
let compareZ1 = ndc1.z - 0.0035;
|
|
130
|
+
var acc1 = 0.0;
|
|
131
|
+
for (var y = -1; y <= 1; y++) {
|
|
132
|
+
for (var x = -1; x <= 1; x++) {
|
|
133
|
+
acc1 += textureSampleCompareLevel(shadowMapFar, shadowSampler, suv1_c + vec2f(f32(x), f32(y)) * st1, compareZ1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
vis = mix(1.0, acc1 * (1.0 / 9.0), frustum1);
|
|
137
|
+
}
|
|
95
138
|
if (frustum > 0.0) {
|
|
96
139
|
let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
|
|
97
140
|
let suv_c = clamp(suv, vec2f(0.02), vec2f(0.98));
|
|
@@ -105,7 +148,9 @@ struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
|
|
|
105
148
|
acc += textureSampleCompareLevel(shadowMap, shadowSampler, suv_c + vec2f(f32(x), f32(y)) * st, compareZ);
|
|
106
149
|
}
|
|
107
150
|
}
|
|
108
|
-
|
|
151
|
+
// The base is whatever the far cascade decided, so the near border blends
|
|
152
|
+
// cascade to cascade rather than snapping to lit mid-floor.
|
|
153
|
+
vis = mix(vis, acc * (1.0 / 9.0), frustum);
|
|
109
154
|
}
|
|
110
155
|
|
|
111
156
|
// Frosted/matte micro-texture. Scenes leaving it at zero were still paying
|
|
@@ -123,24 +168,126 @@ struct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };
|
|
|
123
168
|
smoothstep(halfLine - gridDeriv.x, halfLine + gridDeriv.x, gridFrac.x),
|
|
124
169
|
smoothstep(halfLine - gridDeriv.y, halfLine + gridDeriv.y, gridFrac.y)
|
|
125
170
|
);
|
|
126
|
-
let sun =
|
|
171
|
+
let sun = rzWorldAmbient(n) + light.lights[0].color.xyz * light.lights[0].color.w * max(dot(n, -light.lights[0].direction.xyz), 0.0);
|
|
172
|
+
// The positional layer reaches the floor too. A stage light that lit the
|
|
173
|
+
// cast and not the ground under her would read as a sticker, which is the
|
|
174
|
+
// same failure the shadow catcher exists to prevent.
|
|
175
|
+
let lamps = rzLightsDiffuse(i.worldPos, n);
|
|
127
176
|
let dark = (1.0 - vis) * material.shadowStrength;
|
|
128
|
-
|
|
177
|
+
// The shadow darkens the SUN only. The dark term comes from the sun's shadow
|
|
178
|
+
// map, so applying it to the lamps too would have a lamp on the far side of
|
|
179
|
+
// the stage dimmed by a shadow cast away from it — the one light in the scene
|
|
180
|
+
// that provably is not blocking it.
|
|
181
|
+
// (No backticks in here: this string is a TS template literal and one ends it
|
|
182
|
+
// mid-shader, which is exactly how this line broke the build a moment ago.)
|
|
183
|
+
var baseColor = material.diffuseColor * (sun * (1.0 - dark * 0.65) + lamps);
|
|
129
184
|
baseColor *= noiseTint;
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
185
|
+
// The floor mirror — its OWN LAYER, not a blend into the surface. The
|
|
186
|
+
// reflection was rendered by the MIRROR camera, so projecting this
|
|
187
|
+
// fragment's world position through that camera yields exactly the texel
|
|
188
|
+
// where its reflection landed — projective mapping, no screen-space guess.
|
|
189
|
+
// There is deliberately NO strength dial: mirror is on or off, and how much
|
|
190
|
+
// of it shows is the SURFACE's own opacity covering it — the same
|
|
191
|
+
// independence the grid won earlier. The branch is on a uniform, so the
|
|
192
|
+
// whole cost vanishes for the scenes that leave it off.
|
|
193
|
+
var reflShadowed = vec3f(0.0);
|
|
194
|
+
if (material.mirror > 0.0) {
|
|
195
|
+
let mc = mirrorVP.viewProj * vec4f(i.worldPos, 1.0);
|
|
196
|
+
let mndc = mc.xyz / max(mc.w, 1e-6);
|
|
197
|
+
let muv = clamp(vec2f(mndc.x * 0.5 + 0.5, 0.5 - mndc.y * 0.5), vec2f(0.0), vec2f(1.0));
|
|
198
|
+
var lod = 0.0;
|
|
199
|
+
if (material.mirrorBlur > 0.0) {
|
|
200
|
+
// DEPTH-PROPORTIONAL softness: the real driver is how far BEHIND the
|
|
201
|
+
// mirror surface the reflected geometry sits — a foot on the floor
|
|
202
|
+
// reflects sharp, a head reflects soft, the empty backdrop softest.
|
|
203
|
+
//
|
|
204
|
+
// Read the mirror pass's own depth at this texel, linearise it with the
|
|
205
|
+
// shared projection's pair (viewZ = projB / (z - projA); the formula
|
|
206
|
+
// inverts both depth conventions, see the composite's linearDepth), and
|
|
207
|
+
// reconstruct the reflected image point along the ray from the MIRRORED
|
|
208
|
+
// eye through this fragment. The plane is y = 0, so mirroring the eye
|
|
209
|
+
// and the camera forward is a sign flip on y, and the image point's
|
|
210
|
+
// depth below the plane IS the reflected object's height above it.
|
|
211
|
+
let dims = vec2f(textureDimensions(mirrorDepth));
|
|
212
|
+
let texel = clamp(vec2i(muv * dims), vec2i(0), vec2i(dims) - vec2i(1));
|
|
213
|
+
let z = textureLoad(mirrorDepth, texel, 0);
|
|
214
|
+
let viewZ = clamp(mirrorVP.params.y / (z - mirrorVP.params.x), 0.05, 100000.0);
|
|
215
|
+
let eyeM = vec3f(camera.viewPos.x, -camera.viewPos.y, camera.viewPos.z);
|
|
216
|
+
let fwd = vec3f(camera.view[0][2], camera.view[1][2], camera.view[2][2]);
|
|
217
|
+
let fwdM = vec3f(fwd.x, -fwd.y, fwd.z);
|
|
218
|
+
let toFrag = i.worldPos - eyeM;
|
|
219
|
+
let dir = toFrag * inverseSqrt(max(dot(toFrag, toFrag), 1e-8));
|
|
220
|
+
let t = viewZ / max(dot(dir, fwdM), 1e-4);
|
|
221
|
+
let height = max(-(eyeM.y + dir.y * t), 0.0);
|
|
222
|
+
// Full softness by BLUR_SPAN units above the floor — about a character's
|
|
223
|
+
// height. The hardware clamps the level, so the nominal 5.0 needs no
|
|
224
|
+
// knowledge of the real chain length, and blur 0 reads exactly level 0.
|
|
225
|
+
let BLUR_SPAN = 14.0;
|
|
226
|
+
lod = material.mirrorBlur * 5.0 * clamp(height / BLUR_SPAN, 0.0, 1.0);
|
|
227
|
+
}
|
|
228
|
+
let refl = textureSampleLevel(mirrorTex, linearSampler, muv, lod).rgb;
|
|
229
|
+
// Still under the received shadow: a polished floor in shade shows a dim
|
|
230
|
+
// reflection, and a mirror that ignored the shadow would glow in it.
|
|
231
|
+
reflShadowed = refl * (1.0 - dark * 0.65);
|
|
232
|
+
}
|
|
233
|
+
// THREE LAYERS, composited premultiplied, bottom to top: the shadow the
|
|
234
|
+
// catcher receives, the ground surface, and the grid.
|
|
235
|
+
//
|
|
236
|
+
// The grid used to be MIXED INTO the surface and then weighted by the
|
|
237
|
+
// surface's own alpha, so turning the ground down to nothing took the grid
|
|
238
|
+
// with it — you could not have a bare grid over a photo, which is most of
|
|
239
|
+
// what a grid is for. It is its own layer now: the ground plane keeps an
|
|
240
|
+
// alpha, and the grid is simply on or off.
|
|
135
241
|
let surfA = edgeFade * material.opacity;
|
|
136
|
-
let
|
|
137
|
-
|
|
138
|
-
|
|
242
|
+
let gridA = gridLine * material.gridLineOpacity * edgeFade;
|
|
243
|
+
// The mirror is opaque across the ground's extent when on — it always shows
|
|
244
|
+
// SOMETHING, because the backdrop rides in the reflection target itself.
|
|
245
|
+
let mirrA = material.mirror * edgeFade;
|
|
246
|
+
// As opacity drops the received shadow becomes a translucent dark layer
|
|
247
|
+
// (Blender's Shadow Catcher), so models still feel grounded on a photo or a
|
|
248
|
+
// 360 backdrop. Colourless by construction: it darkens by covering. The
|
|
249
|
+
// MIRROR subsumes it: the reflection already carries the received shadow,
|
|
250
|
+
// and a catcher stacked on top would darken the same shadow twice.
|
|
251
|
+
let catchA = dark * 0.65 * edgeFade * (1.0 - material.opacity) * (1.0 - material.mirror);
|
|
252
|
+
|
|
253
|
+
// FOUR LAYERS, premultiplied, bottom to top: reflection, received shadow,
|
|
254
|
+
// ground surface, grid. Each with its own coverage, none scaling another —
|
|
255
|
+
// ground opacity REVEALS the mirror rather than the mirror carrying a
|
|
256
|
+
// strength of its own.
|
|
257
|
+
var pm = reflShadowed * mirrA;
|
|
258
|
+
var cov = mirrA;
|
|
259
|
+
// Shadow catcher over the reflection-or-nothing: black, it covers.
|
|
260
|
+
pm = pm * (1.0 - catchA);
|
|
261
|
+
cov = catchA + cov * (1.0 - catchA);
|
|
262
|
+
// Surface over shadow.
|
|
263
|
+
pm = baseColor * surfA + pm * (1.0 - surfA);
|
|
264
|
+
cov = surfA + cov * (1.0 - surfA);
|
|
265
|
+
// Grid over surface. Unshadowed, as it has always been — a painted line reads
|
|
266
|
+
// as paint, and darkening it was never what the mix did either.
|
|
267
|
+
pm = material.gridLineColor * gridA + pm * (1.0 - gridA);
|
|
268
|
+
cov = gridA + cov * (1.0 - gridA);
|
|
269
|
+
|
|
270
|
+
let outA = cov;
|
|
271
|
+
out.color = vec4f(pm, outA);
|
|
139
272
|
// mask.r = 0: ground never contributes to bloom. mask.g = 1.0 with src.a =
|
|
140
273
|
// outA turns the aux blend into alpha-over, so the drawable alpha goes
|
|
141
274
|
// from outA at the center to 0 at the radial edge — letting the page
|
|
142
275
|
// background show through under the premultiplied canvas alphaMode.
|
|
143
276
|
out.mask = vec4f(0.0, 1.0, 0.0, outA);
|
|
144
|
-
return out;
|
|
277
|
+
${sceneIdWriteWgsl("out", `${GROUND_MATERIAL_ID}u`, `${GROUND_OBJECT_ID}u`)} return out;
|
|
145
278
|
}
|
|
146
279
|
`
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The ground belongs to no model instance, so it takes ids of its own — at the
|
|
284
|
+
* TOP of the u16 range, not at the bottom.
|
|
285
|
+
*
|
|
286
|
+
* Both of the engine's counters are 1-based (model id is instance count + 1,
|
|
287
|
+
* material id counts from one), so 1 is the first model's first material and
|
|
288
|
+
* the floor would have shared it. 0 stays unclaimed in the other direction: it
|
|
289
|
+
* is what the attachment clears to, and so what every pixel nothing drew
|
|
290
|
+
* reports.
|
|
291
|
+
*/
|
|
292
|
+
export const GROUND_MATERIAL_ID = 0xffff
|
|
293
|
+
export const GROUND_OBJECT_ID = 0xffff
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// The half of the effect API whose VALUES are module-local.
|
|
2
|
+
//
|
|
3
|
+
// An effect file is spliced WHOLE into every module it has a mount in. A file
|
|
4
|
+
// with a trail and a lightEmit compiles its trail code inside the light module,
|
|
5
|
+
// where it never runs but still has to resolve — so `rzTime()` has to exist
|
|
6
|
+
// there even though a compute pass that writes lamp positions has no obvious
|
|
7
|
+
// clock, and `rzFalloff()` has to exist there even though nothing calls it.
|
|
8
|
+
//
|
|
9
|
+
// EFFECT_SCENE_API (composite.ts) is the other half: names that mean the same
|
|
10
|
+
// thing everywhere, written once. These are the ones that cannot be, because
|
|
11
|
+
// each module keeps its clock and its viewport in a different uniform. So they
|
|
12
|
+
// are written once HERE and the expression is the parameter — which is the
|
|
13
|
+
// same trick the grid pass already used for one of them, generalised to all.
|
|
14
|
+
//
|
|
15
|
+
// The rule this file exists to make keepable: a hosting module includes every
|
|
16
|
+
// block below. Not the ones its own mount happens to need — every one, because
|
|
17
|
+
// what lands in it is the author's whole file.
|
|
18
|
+
//
|
|
19
|
+
// EXCEPT that EFFECT_SCENE_API already carries EFFECT_MATH_API and the Particle
|
|
20
|
+
// struct, so the three modules built on it take those two by inheritance and
|
|
21
|
+
// must not add them again. tests/hosted-api.test.mjs fails on a name declared
|
|
22
|
+
// twice for exactly this reason — it is the mistake sharing blocks invites.
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The particle record, laid out by hand — declared in EVERY hosting module.
|
|
26
|
+
*
|
|
27
|
+
* Not only in the particle ones: `fn particleStep(p: Particle)` is part of the
|
|
28
|
+
* author's file, so the trail, field and lightEmit modules compile that
|
|
29
|
+
* signature as dead code and a missing struct is a compile error on a function
|
|
30
|
+
* the author was right to write. The trail module used to carry a hand-copied
|
|
31
|
+
* duplicate for exactly this reason.
|
|
32
|
+
*
|
|
33
|
+
* `age` and `life` sit in the padding that vec3f alignment would waste anyway
|
|
34
|
+
* (a vec3f occupies 12 bytes but aligns the next field to 16), so the struct is
|
|
35
|
+
* 48 bytes rather than the 64 a naive ordering costs. At 4096 particles that is
|
|
36
|
+
* 192KB instead of 256KB, and it is read every frame by both stages.
|
|
37
|
+
*
|
|
38
|
+
* `life <= 0` means "not alive" and is what the pool checks to recycle a slot,
|
|
39
|
+
* so a freshly zeroed buffer is entirely dead and every particle is born on the
|
|
40
|
+
* first step rather than needing a separate seeding pass.
|
|
41
|
+
*
|
|
42
|
+
* RzLight is the same kind of thing and lives in lights.ts, because it is that
|
|
43
|
+
* module's own type; every hosting module splices it for this same reason.
|
|
44
|
+
*/
|
|
45
|
+
export const PARTICLE_STRUCT_WGSL = /* wgsl */ `
|
|
46
|
+
struct Particle {
|
|
47
|
+
pos: vec3f,
|
|
48
|
+
age: f32,
|
|
49
|
+
vel: vec3f,
|
|
50
|
+
life: f32,
|
|
51
|
+
size: f32,
|
|
52
|
+
rot: f32,
|
|
53
|
+
seed: f32,
|
|
54
|
+
// Aspect along the direction of travel. 1 or less is a square billboard; a
|
|
55
|
+
// raindrop is 10 or 20. Zero-initialised, so an effect that never sets it gets
|
|
56
|
+
// the square it expects.
|
|
57
|
+
stretch: f32,
|
|
58
|
+
}
|
|
59
|
+
`
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `rzTime`/`rzDt`, from whatever the module calls its clock.
|
|
63
|
+
*
|
|
64
|
+
* A module with no clock of its own passes a literal. Zero dt is honest there:
|
|
65
|
+
* nothing spliced into it is being stepped, so a kernel that integrates would
|
|
66
|
+
* integrate by nothing rather than by a plausible-looking wrong number.
|
|
67
|
+
*/
|
|
68
|
+
export function clockApi(timeExpr: string, dtExpr: string): string {
|
|
69
|
+
return /* wgsl */ `
|
|
70
|
+
fn rzTime() -> f32 { return ${timeExpr}; }
|
|
71
|
+
fn rzDt() -> f32 { return ${dtExpr}; }
|
|
72
|
+
`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** `rzViewportHeight` — the render target's height in pixels, which is what a
|
|
76
|
+
* point size in world units has to be divided by to become a pixel radius. */
|
|
77
|
+
export function viewportApi(heightExpr: string): string {
|
|
78
|
+
return /* wgsl */ `
|
|
79
|
+
fn rzViewportHeight() -> f32 { return ${heightExpr}; }
|
|
80
|
+
`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The one cast number that is PER EFFECT rather than per engine.
|
|
85
|
+
*
|
|
86
|
+
* Author-visible, and published effects loop over it, so its meaning is pinned:
|
|
87
|
+
* how many of THIS effect's anchors asked for a trail. Deliberately not the
|
|
88
|
+
* anchor address space — that is RZ_MAX_ANCHORS in CAST_API, and the two being
|
|
89
|
+
* one number was the old trail bug. Everything else about the cast's shape is a
|
|
90
|
+
* constant and lives there.
|
|
91
|
+
*/
|
|
92
|
+
export function trailSlotsApi(trailCount: number): string {
|
|
93
|
+
return /* wgsl */ `
|
|
94
|
+
const RZ_TRAIL_SLOTS: i32 = ${trailCount};
|
|
95
|
+
`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Pure math — no bindings, no uniforms, identical in every module.
|
|
100
|
+
*
|
|
101
|
+
* These were duplicated in the particle and trail modules, byte for byte in the
|
|
102
|
+
* case of rzFalloff and to a `+ vec3f(0.0)` in the case of rzValueNoise. Two
|
|
103
|
+
* copies of a noise function is one bad merge away from an effect that looks
|
|
104
|
+
* different depending on which mount drew it, which is not a bug anyone would
|
|
105
|
+
* think to look for.
|
|
106
|
+
*
|
|
107
|
+
* EVERY hosting module includes this and none defines any of these itself, so
|
|
108
|
+
* "which module am I in" can never change what a helper returns.
|
|
109
|
+
*/
|
|
110
|
+
export const EFFECT_MATH_API = /* wgsl */ `
|
|
111
|
+
fn rzHash11(x: f32) -> f32 {
|
|
112
|
+
var p = fract(x * 0.1031);
|
|
113
|
+
p = p * (p + 33.33);
|
|
114
|
+
return fract(p * (p + p));
|
|
115
|
+
}
|
|
116
|
+
fn rzHash21(p: vec2f) -> f32 {
|
|
117
|
+
var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
|
|
118
|
+
p3 = p3 + dot(p3, p3.yzx + 33.33);
|
|
119
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
120
|
+
}
|
|
121
|
+
/** Three independent randoms from one seed — the usual need when spawning. */
|
|
122
|
+
fn rzHash13(x: f32) -> vec3f {
|
|
123
|
+
return vec3f(rzHash11(x), rzHash11(x + 17.13), rzHash11(x + 41.71));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Compact-support falloff: 1 at the centre, exactly 0 at r, smooth between.
|
|
127
|
+
*
|
|
128
|
+
* It reaches exactly zero rather than merely getting small, because a glow that
|
|
129
|
+
* never quite ends has to be culled somewhere, and culling it wherever it looks
|
|
130
|
+
* close enough is what put a visible hard edge on the first halo effect.
|
|
131
|
+
*/
|
|
132
|
+
fn rzFalloff(d: f32, r: f32) -> f32 {
|
|
133
|
+
let x = clamp(d / max(r, 1e-6), 0.0, 1.0);
|
|
134
|
+
let f = 1.0 - x;
|
|
135
|
+
return f * f * f;
|
|
136
|
+
}
|
|
137
|
+
fn rzHash31(p: vec3f) -> f32 {
|
|
138
|
+
var p3 = fract(p * 0.1031);
|
|
139
|
+
p3 = p3 + dot(p3, p3.zyx + 31.32);
|
|
140
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
141
|
+
}
|
|
142
|
+
fn rzValueNoise(p: vec3f) -> f32 {
|
|
143
|
+
let i = floor(p);
|
|
144
|
+
let f = fract(p);
|
|
145
|
+
let u = f * f * (3.0 - 2.0 * f);
|
|
146
|
+
let n000 = rzHash31(i);
|
|
147
|
+
let n100 = rzHash31(i + vec3f(1.0, 0.0, 0.0));
|
|
148
|
+
let n010 = rzHash31(i + vec3f(0.0, 1.0, 0.0));
|
|
149
|
+
let n110 = rzHash31(i + vec3f(1.0, 1.0, 0.0));
|
|
150
|
+
let n001 = rzHash31(i + vec3f(0.0, 0.0, 1.0));
|
|
151
|
+
let n101 = rzHash31(i + vec3f(1.0, 0.0, 1.0));
|
|
152
|
+
let n011 = rzHash31(i + vec3f(0.0, 1.0, 1.0));
|
|
153
|
+
let n111 = rzHash31(i + vec3f(1.0, 1.0, 1.0));
|
|
154
|
+
let x00 = mix(n000, n100, u.x);
|
|
155
|
+
let x10 = mix(n010, n110, u.x);
|
|
156
|
+
let x01 = mix(n001, n101, u.x);
|
|
157
|
+
let x11 = mix(n011, n111, u.x);
|
|
158
|
+
return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);
|
|
159
|
+
}
|
|
160
|
+
/** Divergence-free flow — the field a wisp of smoke follows without a solver. */
|
|
161
|
+
fn rzCurlNoise(p: vec3f) -> vec3f {
|
|
162
|
+
let e = 0.1;
|
|
163
|
+
let dx = vec3f(e, 0.0, 0.0);
|
|
164
|
+
let dy = vec3f(0.0, e, 0.0);
|
|
165
|
+
let dz = vec3f(0.0, 0.0, e);
|
|
166
|
+
let x0 = rzValueNoise(p - dx); let x1 = rzValueNoise(p + dx);
|
|
167
|
+
let y0 = rzValueNoise(p - dy); let y1 = rzValueNoise(p + dy);
|
|
168
|
+
let z0 = rzValueNoise(p - dz); let z1 = rzValueNoise(p + dz);
|
|
169
|
+
return normalize(vec3f((y1 - y0) - (z1 - z0), (z1 - z0) - (x1 - x0), (x1 - x0) - (y1 - y0)) + vec3f(1e-6));
|
|
170
|
+
}
|
|
171
|
+
`
|