reze-engine 0.55.16 → 0.56.1
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/dist/camera.d.ts +12 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +20 -3
- package/dist/engine.d.ts +253 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1078 -104
- package/dist/model.d.ts +10 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +16 -0
- package/dist/overlay.d.ts +0 -5
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +0 -5
- package/dist/reflection.d.ts +17 -4
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +93 -38
- package/dist/shaders/directives.d.ts +15 -0
- package/dist/shaders/directives.d.ts.map +1 -1
- package/dist/shaders/directives.js +5 -0
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +42 -6
- package/dist/shaders/passes/mirror.d.ts +32 -0
- package/dist/shaders/passes/mirror.d.ts.map +1 -0
- package/dist/shaders/passes/mirror.js +284 -0
- package/dist/shaders/passes/scene-contract.d.ts +6 -0
- package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
- package/dist/shaders/passes/scene-contract.js +8 -0
- package/package.json +1 -1
- package/src/camera.ts +21 -4
- package/src/engine.ts +1158 -108
- package/src/model.ts +16 -0
- package/src/overlay.ts +0 -5
- package/src/reflection.ts +103 -37
- package/src/shaders/directives.ts +20 -0
- package/src/shaders/passes/ground.ts +42 -6
- package/src/shaders/passes/mirror.ts +289 -0
- package/src/shaders/passes/scene-contract.ts +14 -0
package/src/model.ts
CHANGED
|
@@ -1024,6 +1024,22 @@ export class Model {
|
|
|
1024
1024
|
return this.skeleton
|
|
1025
1025
|
}
|
|
1026
1026
|
|
|
1027
|
+
/**
|
|
1028
|
+
* Overwrites BASE (pre-morph) positions in place — a permanent geometry
|
|
1029
|
+
* edit (a bone scale's own vertex half), not a transient morph. Only the
|
|
1030
|
+
* CPU copy: the caller (Engine.setVertexPositions) owns pushing the same
|
|
1031
|
+
* change to whichever GPU buffer(s) actually feed the render this frame.
|
|
1032
|
+
*/
|
|
1033
|
+
setVertexPositions(updates: readonly { index: number; position: readonly [number, number, number] }[]): void {
|
|
1034
|
+
for (const { index, position } of updates) {
|
|
1035
|
+
if (index < 0 || index >= this.vertexCount) continue
|
|
1036
|
+
const vi = index * VERTEX_STRIDE
|
|
1037
|
+
this.baseVertexData[vi] = position[0]
|
|
1038
|
+
this.baseVertexData[vi + 1] = position[1]
|
|
1039
|
+
this.baseVertexData[vi + 2] = position[2]
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1027
1043
|
// Direct bone local-transform accessors (used by interactive gizmo drag).
|
|
1028
1044
|
// Readers return the live runtime state — callers that want a snapshot for
|
|
1029
1045
|
// later comparison should `.clone()` the returned Quat / copy the Vec3.
|
package/src/overlay.ts
CHANGED
|
@@ -502,11 +502,6 @@ export const OVERLAY_STYLE = {
|
|
|
502
502
|
|
|
503
503
|
/** Stroke for the material borders drawn over the mesh wireframe. */
|
|
504
504
|
seamStrokePx: 3,
|
|
505
|
-
/** Stroke for the material under the pointer — the same self-occluding reveal
|
|
506
|
-
* a pick draws, layered over the section-wide view rather than replacing it,
|
|
507
|
-
* and heavier than either a pick's own edges or a seam so it still pops out
|
|
508
|
-
* of a view that already has two other things drawn in it. */
|
|
509
|
-
hoverStrokePx: 4,
|
|
510
505
|
/** How strongly the triangulation draws under those borders.
|
|
511
506
|
*
|
|
512
507
|
* Both at full strength is one undifferentiated field of yellow — forty
|
package/src/reflection.ts
CHANGED
|
@@ -2,18 +2,34 @@
|
|
|
2
2
|
// shadow-cascades precedent.
|
|
3
3
|
//
|
|
4
4
|
// A planar reflection is not a second camera aimed by hand; it is the SAME
|
|
5
|
-
// camera with the world reflected about the
|
|
5
|
+
// camera with the world reflected about the mirror's plane. Fold the reflection
|
|
6
6
|
// into the view matrix and everything downstream is untouched: world positions
|
|
7
7
|
// stay TRUE world positions, so sun, shadows and positional lights evaluate at
|
|
8
8
|
// the unmirrored point — which is exactly what a mirror shows, an object lit
|
|
9
9
|
// as it is, seen from a mirrored eye. The only other value that must mirror is
|
|
10
10
|
// the eye itself, because specular reads the view direction from it.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
12
|
+
// The plane is carried as (n, d) with n a UNIT normal and the surface at
|
|
13
|
+
// n·p + d = 0 — general from the start, because a mirror standing on the floor
|
|
14
|
+
// is the ordinary case and a floor is only the one plane that happens to be
|
|
15
|
+
// horizontal. The ground is (0, 1, 0, 0).
|
|
16
|
+
//
|
|
17
|
+
// WINDING: a reflection has determinant -1, so triangle orientation flips, and
|
|
18
|
+
// any pipeline that culls a face culls the WRONG one in the mirror pass.
|
|
19
|
+
//
|
|
20
|
+
// This note used to claim every scene-pass pipeline culls "none". That was
|
|
21
|
+
// never true, and each exception cost a round of "the mirror looks broken":
|
|
22
|
+
//
|
|
23
|
+
// ground culls "back" -> the floor vanished whole, and with it the grid
|
|
24
|
+
// and the received shadow, since the shadow is a
|
|
25
|
+
// layer of the ground shader
|
|
26
|
+
// outline culls "back" -> no ink line on the reflection at all
|
|
27
|
+
// eye culls "front" -> the back of the eyeball kept, its front discarded
|
|
28
|
+
//
|
|
29
|
+
// All three now build a mirror twin with the cull answered (the ground goes to
|
|
30
|
+
// "none", the outline to "front", the eye to "back"). Anything ADDED to the
|
|
31
|
+
// scene pass with a cullMode must do the same — grep cullMode in engine.ts,
|
|
32
|
+
// which is the whole list.
|
|
17
33
|
|
|
18
34
|
/**
|
|
19
35
|
* The debug view: the reflection target drawn over the finished frame — the
|
|
@@ -44,20 +60,66 @@ struct VSOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f, };
|
|
|
44
60
|
`
|
|
45
61
|
|
|
46
62
|
/**
|
|
47
|
-
*
|
|
63
|
+
* A plane as the mirror wants it: 4 numbers, (nx, ny, nz, d), surface at
|
|
64
|
+
* n·p + d = 0.
|
|
65
|
+
*
|
|
66
|
+
* Written into a caller-owned array rather than returned, because the engine
|
|
67
|
+
* holds one per mirror and rebuilds it whenever the mirror moves — which is
|
|
68
|
+
* every frame a gizmo is being dragged.
|
|
69
|
+
*
|
|
70
|
+
* `n` is normalized here rather than trusted: it arrives from a rotated quad's
|
|
71
|
+
* basis, and a scale baked into that basis would otherwise scale `d` against
|
|
72
|
+
* it and put the plane somewhere else entirely.
|
|
73
|
+
*/
|
|
74
|
+
export function planeFromPointNormal(
|
|
75
|
+
px: number, py: number, pz: number,
|
|
76
|
+
nx: number, ny: number, nz: number,
|
|
77
|
+
out: Float32Array,
|
|
78
|
+
): Float32Array {
|
|
79
|
+
const len = Math.hypot(nx, ny, nz) || 1
|
|
80
|
+
const x = nx / len
|
|
81
|
+
const y = ny / len
|
|
82
|
+
const z = nz / len
|
|
83
|
+
out[0] = x
|
|
84
|
+
out[1] = y
|
|
85
|
+
out[2] = z
|
|
86
|
+
// d places the plane through the point: n·p + d = 0.
|
|
87
|
+
out[3] = -(x * px + y * py + z * pz)
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The Householder reflection about (n, d), column-major.
|
|
48
93
|
*
|
|
49
|
-
* p' =
|
|
94
|
+
* p' = p - 2 (n·p + d) n
|
|
50
95
|
*/
|
|
51
|
-
export function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
]
|
|
96
|
+
export function reflectionAboutPlane(plane: ArrayLike<number>, out?: Float32Array): Float32Array {
|
|
97
|
+
const nx = plane[0], ny = plane[1], nz = plane[2], d = plane[3]
|
|
98
|
+
const m = out ?? new Float32Array(16)
|
|
99
|
+
m[0] = 1 - 2 * nx * nx
|
|
100
|
+
m[1] = -2 * ny * nx
|
|
101
|
+
m[2] = -2 * nz * nx
|
|
102
|
+
m[3] = 0
|
|
103
|
+
m[4] = -2 * nx * ny
|
|
104
|
+
m[5] = 1 - 2 * ny * ny
|
|
105
|
+
m[6] = -2 * nz * ny
|
|
106
|
+
m[7] = 0
|
|
107
|
+
m[8] = -2 * nx * nz
|
|
108
|
+
m[9] = -2 * ny * nz
|
|
109
|
+
m[10] = 1 - 2 * nz * nz
|
|
110
|
+
m[11] = 0
|
|
111
|
+
m[12] = -2 * d * nx
|
|
112
|
+
m[13] = -2 * d * ny
|
|
113
|
+
m[14] = -2 * d * nz
|
|
114
|
+
m[15] = 1
|
|
115
|
+
return m
|
|
59
116
|
}
|
|
60
117
|
|
|
118
|
+
/** Scratch for the product below — this module is called once per mirror per
|
|
119
|
+
* frame and allocating a matrix each time is a garbage-collector visit for
|
|
120
|
+
* nothing. */
|
|
121
|
+
const R = new Float32Array(16)
|
|
122
|
+
|
|
61
123
|
/**
|
|
62
124
|
* Fill a camera-uniform block for the mirror pass from the live one.
|
|
63
125
|
*
|
|
@@ -69,26 +131,30 @@ export function reflectionAboutY(h: number): Float32Array {
|
|
|
69
131
|
* view' = view × R (column-vector convention, matching `projection * view *
|
|
70
132
|
* pos` in the vertex shaders); projection unchanged; eye reflected.
|
|
71
133
|
*/
|
|
72
|
-
export function buildMirrorCamera(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
for (let
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
134
|
+
export function buildMirrorCamera(
|
|
135
|
+
camera: Float32Array,
|
|
136
|
+
plane: ArrayLike<number>,
|
|
137
|
+
out: Float32Array,
|
|
138
|
+
): Float32Array {
|
|
139
|
+
reflectionAboutPlane(plane, R)
|
|
140
|
+
// view × R, column-major: out.col_j = Σ_k view.col_k · R[j*4 + k].
|
|
141
|
+
for (let j = 0; j < 4; j++) {
|
|
142
|
+
const b0 = R[j * 4 + 0], b1 = R[j * 4 + 1], b2 = R[j * 4 + 2], b3 = R[j * 4 + 3]
|
|
143
|
+
for (let r = 0; r < 4; r++) {
|
|
144
|
+
out[j * 4 + r] =
|
|
145
|
+
camera[0 + r] * b0 + camera[4 + r] * b1 + camera[8 + r] * b2 + camera[12 + r] * b3
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (let i = 16; i < 32; i++) out[i] = camera[i]
|
|
149
|
+
// The eye through the same reflection, so specular reads the mirrored view
|
|
150
|
+
// direction. Written from the plane directly rather than through R: it is one
|
|
151
|
+
// dot product, and going via the matrix would round-trip it through nine
|
|
152
|
+
// multiplies to reach the same number.
|
|
153
|
+
const nx = plane[0], ny = plane[1], nz = plane[2], d = plane[3]
|
|
154
|
+
const t = 2 * (nx * camera[32] + ny * camera[33] + nz * camera[34] + d)
|
|
155
|
+
out[32] = camera[32] - t * nx
|
|
156
|
+
out[33] = camera[33] - t * ny
|
|
157
|
+
out[34] = camera[34] - t * nz
|
|
158
|
+
out[35] = camera[35]
|
|
93
159
|
return out
|
|
94
160
|
}
|
|
@@ -55,6 +55,21 @@ export type EffectDirectives = {
|
|
|
55
55
|
bloom: boolean
|
|
56
56
|
/** This effect takes the cast apart — the host reads the timing. */
|
|
57
57
|
dissolve: boolean
|
|
58
|
+
/**
|
|
59
|
+
* This effect IS a mirror: a plane in the scene showing a true reflection of
|
|
60
|
+
* it, folded and re-rendered by the engine.
|
|
61
|
+
*
|
|
62
|
+
* A mount with no shader, and the second of them — `#lights` was the first.
|
|
63
|
+
* The reasoning is identical: a planar reflection re-renders every model from
|
|
64
|
+
* a folded camera, which is a PASS, and no mount is a pass. So the effect
|
|
65
|
+
* declares the mirror and the engine reflects, exactly as a lighting rig
|
|
66
|
+
* declares lamps and the engine shades.
|
|
67
|
+
*
|
|
68
|
+
* The plane comes from the effect's own `#param` dials, by name — see the
|
|
69
|
+
* engine's MIRROR_DIALS. That is what makes it an effect rather than a switch:
|
|
70
|
+
* it publishes, forks, schedules and fades like every other one.
|
|
71
|
+
*/
|
|
72
|
+
mirror: boolean
|
|
58
73
|
/**
|
|
59
74
|
* How long ONE firing of this effect lasts, in seconds. 0 = undeclared.
|
|
60
75
|
*
|
|
@@ -84,6 +99,7 @@ const SPEC = {
|
|
|
84
99
|
grid: 1,
|
|
85
100
|
bloom: 0,
|
|
86
101
|
dissolve: 0,
|
|
102
|
+
mirror: 0,
|
|
87
103
|
duration: 1,
|
|
88
104
|
} as const
|
|
89
105
|
|
|
@@ -139,6 +155,7 @@ export function parseDirectives(wgsl: string): DirectiveResult {
|
|
|
139
155
|
grid: 0,
|
|
140
156
|
bloom: false,
|
|
141
157
|
dissolve: false,
|
|
158
|
+
mirror: false,
|
|
142
159
|
duration: 0,
|
|
143
160
|
}
|
|
144
161
|
const errors: string[] = []
|
|
@@ -235,6 +252,9 @@ export function parseDirectives(wgsl: string): DirectiveResult {
|
|
|
235
252
|
case "dissolve":
|
|
236
253
|
d.dissolve = true
|
|
237
254
|
return
|
|
255
|
+
case "mirror":
|
|
256
|
+
d.mirror = true
|
|
257
|
+
return
|
|
238
258
|
case "duration": {
|
|
239
259
|
// SECONDS, like every other time a directive states (see #dissolve).
|
|
240
260
|
// The document above works in frames and converts once; an author
|
|
@@ -123,6 +123,26 @@ struct MirrorVP { viewProj: mat4x4f, params: vec4f, };
|
|
|
123
123
|
// The frost noise, PRE-BAKED — see GROUND_NOISE_BAKE_WGSL below. Sampled with
|
|
124
124
|
// the repeat sampler already at binding 10.
|
|
125
125
|
@group(0) @binding(12) var noiseTex: texture_2d<f32>;
|
|
126
|
+
// The mirror pass's resolved aux. .g is accumulated alpha — the reflection's
|
|
127
|
+
// coverage. It lives here rather than in the colour target's own alpha because
|
|
128
|
+
// the HDR format is rg11b10ufloat wherever the device allows it, and that
|
|
129
|
+
// format has no alpha channel at all.
|
|
130
|
+
@group(0) @binding(13) var mirrorMask: texture_2d<f32>;
|
|
131
|
+
// THE MIRROR'S OWN PLANE, and whether this draw is the one inside the mirror
|
|
132
|
+
// pass. A mirror shows nothing behind itself, and the floor is the geometry
|
|
133
|
+
// most often behind one: stand a pane up and half the floor is behind it, lay
|
|
134
|
+
// it facing UP and the whole floor is. Without this the reflection carries a
|
|
135
|
+
// grid that the mirror cannot see.
|
|
136
|
+
//
|
|
137
|
+
// A uniform rather than a branch on some global, because the two ground bind
|
|
138
|
+
// groups already differ — the camera's binds a block set to 0 and the
|
|
139
|
+
// mirror's binds the live plane. No per-pass write, and nothing to get wrong
|
|
140
|
+
// about which pass is running.
|
|
141
|
+
// The flag is named on, not active: WGSL RESERVES active, and a reserved
|
|
142
|
+
// word in a struct field fails the whole module — taking the ground
|
|
143
|
+
// pipeline, and every style group and effect compiled after it, with it.
|
|
144
|
+
struct MirrorClip { plane: vec4f, on: f32, _p0: f32, _p1: f32, _p2: f32, };
|
|
145
|
+
@group(0) @binding(14) var<uniform> clip: MirrorClip;
|
|
126
146
|
${WORLD_AMBIENT_WGSL}
|
|
127
147
|
${lightsApi(0, 6)}
|
|
128
148
|
|
|
@@ -137,6 +157,14 @@ ${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
|
|
|
137
157
|
let gp = i.worldPos.xz / material.gridSpacing;
|
|
138
158
|
let gridDeriv = fwidth(gp);
|
|
139
159
|
|
|
160
|
+
// The clip, straight after the derivatives and before anything else branches.
|
|
161
|
+
// EPSILON rather than zero: a pane lying flat on the floor is coplanar with
|
|
162
|
+
// it, and a floor reflecting itself at zero distance is z-fighting, not a
|
|
163
|
+
// reflection.
|
|
164
|
+
if (clip.on > 0.5 && dot(clip.plane.xyz, i.worldPos) + clip.plane.w < 0.01) {
|
|
165
|
+
discard;
|
|
166
|
+
}
|
|
167
|
+
|
|
140
168
|
var out: FSOut;
|
|
141
169
|
let n = normalize(i.normal);
|
|
142
170
|
let centerDist = length(i.worldPos.xz);
|
|
@@ -315,6 +343,11 @@ ${pcfWgsl("shadowMap", "suv_c", "material.pcfTexel", "compareZ", "acc", " ",
|
|
|
315
343
|
// independence the grid won earlier. The branch is on a uniform, so the
|
|
316
344
|
// whole cost vanishes for the scenes that leave it off.
|
|
317
345
|
var reflShadowed = vec3f(0.0);
|
|
346
|
+
// How much of this texel the reflection pass actually covered. The target
|
|
347
|
+
// clears TRANSPARENT, so this is 1 where something reflected and 0 over the
|
|
348
|
+
// empty sky — which is what stops a floor mirror from painting the backdrop
|
|
349
|
+
// onto itself and wiping out the ground's own colour.
|
|
350
|
+
var reflA = 0.0;
|
|
318
351
|
if (material.mirror > 0.0) {
|
|
319
352
|
let mc = mirrorVP.viewProj * vec4f(i.worldPos, 1.0);
|
|
320
353
|
let mndc = mc.xyz / max(mc.w, 1e-6);
|
|
@@ -349,10 +382,11 @@ ${pcfWgsl("shadowMap", "suv_c", "material.pcfTexel", "compareZ", "acc", " ",
|
|
|
349
382
|
let BLUR_SPAN = 14.0;
|
|
350
383
|
lod = material.mirrorBlur * 5.0 * clamp(height / BLUR_SPAN, 0.0, 1.0);
|
|
351
384
|
}
|
|
352
|
-
let refl = textureSampleLevel(mirrorTex, linearSampler, muv, lod)
|
|
385
|
+
let refl = textureSampleLevel(mirrorTex, linearSampler, muv, lod);
|
|
353
386
|
// Still under the received shadow: a polished floor in shade shows a dim
|
|
354
387
|
// reflection, and a mirror that ignored the shadow would glow in it.
|
|
355
|
-
reflShadowed = refl * (1.0 - dark * 0.65);
|
|
388
|
+
reflShadowed = refl.rgb * (1.0 - dark * 0.65);
|
|
389
|
+
reflA = textureSampleLevel(mirrorMask, linearSampler, muv, 0.0).g;
|
|
356
390
|
}
|
|
357
391
|
// THREE LAYERS, composited premultiplied, bottom to top: the shadow the
|
|
358
392
|
// catcher receives, the ground surface, and the grid.
|
|
@@ -364,15 +398,17 @@ ${pcfWgsl("shadowMap", "suv_c", "material.pcfTexel", "compareZ", "acc", " ",
|
|
|
364
398
|
// alpha, and the grid is simply on or off.
|
|
365
399
|
let surfA = edgeFade * material.opacity;
|
|
366
400
|
let gridA = gridLine * material.gridLineOpacity * edgeFade;
|
|
367
|
-
// The mirror
|
|
368
|
-
//
|
|
369
|
-
|
|
401
|
+
// The mirror covers only what it actually REFLECTS. The backdrop no longer
|
|
402
|
+
// rides in the reflection target — it clears transparent — so the floor keeps
|
|
403
|
+
// its own colour everywhere the cast is not standing, which is the difference
|
|
404
|
+
// between an MMD stage floor and a mirror of the sky.
|
|
405
|
+
let mirrA = material.mirror * edgeFade * reflA;
|
|
370
406
|
// As opacity drops the received shadow becomes a translucent dark layer
|
|
371
407
|
// (Blender's Shadow Catcher), so models still feel grounded on a photo or a
|
|
372
408
|
// 360 backdrop. Colourless by construction: it darkens by covering. The
|
|
373
409
|
// MIRROR subsumes it: the reflection already carries the received shadow,
|
|
374
410
|
// and a catcher stacked on top would darken the same shadow twice.
|
|
375
|
-
let catchA = dark * 0.65 * edgeFade * (1.0 - material.opacity) * (1.0 -
|
|
411
|
+
let catchA = dark * 0.65 * edgeFade * (1.0 - material.opacity) * (1.0 - mirrA);
|
|
376
412
|
|
|
377
413
|
// FOUR LAYERS, premultiplied, bottom to top: reflection, received shadow,
|
|
378
414
|
// ground surface, grid. Each with its own coverage, none scaling another —
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// The mirror surface — the one thing that consumes the reflection pass as a
|
|
2
|
+
// SURFACE rather than as a layer under the floor.
|
|
3
|
+
//
|
|
4
|
+
// The ground has sampled the mirror target since step 7D, and it does it the
|
|
5
|
+
// only way a planar reflection can be sampled correctly: project the fragment's
|
|
6
|
+
// world position through the camera that rendered the reflection, and the texel
|
|
7
|
+
// you land on is exactly where that fragment's reflection was drawn. No
|
|
8
|
+
// screen-space ray, no guess. This file is that same projection on a quad you
|
|
9
|
+
// can put anywhere, which is all a standing mirror is.
|
|
10
|
+
//
|
|
11
|
+
// What it deliberately does NOT do:
|
|
12
|
+
// - light. A mirror shows what it reflects; a sun term on top of that is a
|
|
13
|
+
// second surface nobody asked for.
|
|
14
|
+
// - receive shadow. The reflection already carries the shadow the reflected
|
|
15
|
+
// geometry is standing in.
|
|
16
|
+
// - carry a strength. Coverage is 1 — the tint is how a mirror falls short of
|
|
17
|
+
// perfect, the same way real glass does, and it is a colour rather than a
|
|
18
|
+
// dial that fades the surface out into nothing.
|
|
19
|
+
|
|
20
|
+
import { sceneFsOutWgsl, sceneIdPadWgsl } from "./scene-contract"
|
|
21
|
+
|
|
22
|
+
/** Bytes in MirrorMat below: mat4x4f + three 16-byte rows. */
|
|
23
|
+
export const MIRROR_MAT_BYTES = 64 + 16 * 3
|
|
24
|
+
|
|
25
|
+
export function mirrorShaderWgsl(): string {
|
|
26
|
+
return /* wgsl */ `
|
|
27
|
+
struct CameraUniforms { view: mat4x4f, projection: mat4x4f, viewPos: vec3f, _p: f32, };
|
|
28
|
+
// params = (projA, projB, _, _) — the depth-linearisation pair, carried beside
|
|
29
|
+
// the matrix exactly as the ground reads it. Unused here (the surface takes a
|
|
30
|
+
// flat blur level, not a depth-proportional one) and kept so the two consumers
|
|
31
|
+
// bind one buffer rather than two shapes of the same thing.
|
|
32
|
+
struct MirrorVP { viewProj: mat4x4f, params: vec4f, };
|
|
33
|
+
struct MirrorMat {
|
|
34
|
+
model: mat4x4f,
|
|
35
|
+
tint: vec3f, blur: f32,
|
|
36
|
+
// frame = the band's width in WORLD units, 0 for a bare pane. World rather
|
|
37
|
+
// than a fraction of the quad so that a wide mirror and a narrow one in the
|
|
38
|
+
// same scene are framed in the same moulding.
|
|
39
|
+
frameColor: vec3f, frame: f32,
|
|
40
|
+
size: vec2f, _pad: vec2f,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
44
|
+
@group(0) @binding(1) var<uniform> mirrorVP: MirrorVP;
|
|
45
|
+
@group(0) @binding(2) var<uniform> material: MirrorMat;
|
|
46
|
+
@group(0) @binding(3) var mirrorTex: texture_2d<f32>;
|
|
47
|
+
@group(0) @binding(4) var linearSampler: sampler;
|
|
48
|
+
// The mirror pass's aux, resolved. .g is accumulated alpha — the reflection's
|
|
49
|
+
// COVERAGE, and the only place it exists: the HDR attachment is rg11b10ufloat
|
|
50
|
+
// wherever the device allows it, and that format has no alpha channel, so a
|
|
51
|
+
// mirror reading the colour target's own alpha reads 1 everywhere and paints
|
|
52
|
+
// the empty black of the reflection across the whole pane.
|
|
53
|
+
@group(0) @binding(5) var mirrorMask: texture_2d<f32>;
|
|
54
|
+
|
|
55
|
+
struct VO {
|
|
56
|
+
@builtin(position) position: vec4f,
|
|
57
|
+
@location(0) worldPos: vec3f,
|
|
58
|
+
@location(1) normal: vec3f,
|
|
59
|
+
/** Across the quad, 0..1 — what the frame is measured in. */
|
|
60
|
+
@location(2) uv: vec2f,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> VO {
|
|
64
|
+
// The quad is generated, not buffered: it is six vertices of a unit square
|
|
65
|
+
// and the model matrix carries every difference between one mirror and
|
|
66
|
+
// another, so a vertex buffer would be an allocation per mirror holding the
|
|
67
|
+
// same twelve numbers.
|
|
68
|
+
//
|
|
69
|
+
// Local XY, facing local +Z. A mirror at identity therefore faces the way the
|
|
70
|
+
// default camera looks FROM, which is the orientation someone dropping one
|
|
71
|
+
// into a scene means by "in front of her".
|
|
72
|
+
// Local Y runs 0..1 from the BASE, not -0.5..0.5 from the centre. A mirror
|
|
73
|
+
// stands on the floor: shortening it has to take the height off the top and
|
|
74
|
+
// leave the foot where it is, and turning it has to pivot on that foot. With
|
|
75
|
+
// a centred quad both of those move the whole mirror.
|
|
76
|
+
var q = array<vec2f, 6>(
|
|
77
|
+
vec2f(-0.5, 0.0), vec2f(0.5, 0.0), vec2f(0.5, 1.0),
|
|
78
|
+
vec2f(-0.5, 0.0), vec2f(0.5, 1.0), vec2f(-0.5, 1.0),
|
|
79
|
+
);
|
|
80
|
+
let local = vec4f(q[vi], 0.0, 1.0);
|
|
81
|
+
let uv = q[vi] + vec2f(0.5, 0.0);
|
|
82
|
+
let world = material.model * local;
|
|
83
|
+
var o: VO;
|
|
84
|
+
o.worldPos = world.xyz;
|
|
85
|
+
o.uv = uv;
|
|
86
|
+
// The local +Z axis through the model's rotation. Column 2 of a matrix whose
|
|
87
|
+
// scale is uniform per axis is that axis scaled — normalized here because the
|
|
88
|
+
// side test below is a sign, and a scaled normal would still give the right
|
|
89
|
+
// sign but a wrong-length one is a trap for whatever reads this next.
|
|
90
|
+
o.normal = normalize((material.model * vec4f(0.0, 0.0, 1.0, 0.0)).xyz);
|
|
91
|
+
o.position = camera.projection * camera.view * world;
|
|
92
|
+
return o;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
${sceneFsOutWgsl()}@fragment fn fs(i: VO) -> FSOut {
|
|
96
|
+
var out: FSOut;
|
|
97
|
+
${sceneIdPadWgsl("out")}
|
|
98
|
+
let toEye = camera.viewPos - i.worldPos;
|
|
99
|
+
// Facing the viewer whichever side is up: the quad has one normal and two
|
|
100
|
+
// sides, and the moulding is on both.
|
|
101
|
+
let nrm = select(-i.normal, i.normal, dot(toEye, i.normal) > 0.0);
|
|
102
|
+
|
|
103
|
+
// ── The moulding ──
|
|
104
|
+
//
|
|
105
|
+
// Measured in world units off each edge, so the width means the same thing on
|
|
106
|
+
// a tall mirror as on a wide one.
|
|
107
|
+
let dx = min(i.uv.x, 1.0 - i.uv.x) * material.size.x;
|
|
108
|
+
let dy = min(i.uv.y, 1.0 - i.uv.y) * material.size.y;
|
|
109
|
+
let edge = min(dx, dy);
|
|
110
|
+
if (edge < material.frame) {
|
|
111
|
+
// A MOULDING with a cross-section, not a painted border. The band's normal
|
|
112
|
+
// rolls from facing outward at the rim to facing the viewer at the inner
|
|
113
|
+
// lip, and that roll is the whole trick: a flat rectangle of colour reads as
|
|
114
|
+
// a decal on a plane, while a normal that turns catches the view and reads
|
|
115
|
+
// as metal bent around glass.
|
|
116
|
+
let t = clamp(edge / max(material.frame, 1e-4), 0.0, 1.0);
|
|
117
|
+
// Which way this point of the band faces, in the plane. The axes come off
|
|
118
|
+
// the model matrix, so a rotated mirror's moulding rotates with it.
|
|
119
|
+
let ax = normalize(material.model[0].xyz);
|
|
120
|
+
let ay = normalize(material.model[1].xyz);
|
|
121
|
+
var outward = ax * select(-1.0, 1.0, i.uv.x > 0.5);
|
|
122
|
+
if (dy < dx) { outward = ay * select(-1.0, 1.0, i.uv.y > 0.5); }
|
|
123
|
+
let roll = cos(t * 1.5707963);
|
|
124
|
+
let n = normalize(nrm * (1.0 - roll) + outward * roll);
|
|
125
|
+
let v = normalize(toEye);
|
|
126
|
+
// Two FIXED studio lights, not the scene's. A frame lit by the scene goes
|
|
127
|
+
// black whenever the sun is behind it, and a black frame reads as a hole cut
|
|
128
|
+
// in the picture rather than as metal in shadow.
|
|
129
|
+
let l1 = normalize(vec3f(0.40, 0.80, 0.45));
|
|
130
|
+
let l2 = normalize(vec3f(-0.50, 0.20, -0.60));
|
|
131
|
+
let spec = pow(max(dot(n, normalize(l1 + v)), 0.0), 64.0) * 2.6
|
|
132
|
+
+ pow(max(dot(n, normalize(l2 + v)), 0.0), 20.0) * 0.7;
|
|
133
|
+
let diff = max(dot(n, l1), 0.0) * 0.55 + max(dot(n, l2), 0.0) * 0.25;
|
|
134
|
+
// The grazing rim is what makes a metal edge read as an edge.
|
|
135
|
+
let fres = pow(1.0 - abs(dot(n, v)), 4.0);
|
|
136
|
+
// A METAL TINTS ITS OWN HIGHLIGHT. White specular on a gold frame reads as
|
|
137
|
+
// cream plastic; carrying the frame's colour into the highlight and the rim
|
|
138
|
+
// is what separates gold from a yellow-painted thing.
|
|
139
|
+
let sheen = mix(vec3f(1.0), material.frameColor, 0.65);
|
|
140
|
+
out.color = vec4f(material.frameColor * (0.45 + diff) + sheen * (spec + fres * 0.40), 1.0);
|
|
141
|
+
out.mask = vec4f(0.0, 1.0, 0.0, 1.0);
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Which side is being looked at. Behind a mirror there is no reflection to
|
|
146
|
+
// show — the fold puts the mirror camera on the viewer's own side and the
|
|
147
|
+
// projection returns a smear — so the back reads as what the back of a mirror
|
|
148
|
+
// is: a flat panel. Without this a mirror rotated the wrong way is not
|
|
149
|
+
// wrong-looking, it is broken-looking, and the fix is invisible from the
|
|
150
|
+
// transform panel.
|
|
151
|
+
if (dot(toEye, i.normal) <= 0.0) {
|
|
152
|
+
// The BACKING BOARD. A mirror seen from behind is a board in a frame, and
|
|
153
|
+
// it is opaque: the scene must not show through it, or the pane reads as a
|
|
154
|
+
// pane of dark glass with the cast standing inside it.
|
|
155
|
+
out.color = vec4f(material.frameColor * 0.55, 1.0);
|
|
156
|
+
out.mask = vec4f(0.0, 1.0, 0.0, 1.0);
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The projective sample: this fragment's world position through the mirror
|
|
161
|
+
// camera IS the texel its reflection landed on.
|
|
162
|
+
let mc = mirrorVP.viewProj * vec4f(i.worldPos, 1.0);
|
|
163
|
+
let mndc = mc.xyz / max(mc.w, 1e-6);
|
|
164
|
+
let muv = clamp(vec2f(mndc.x * 0.5 + 0.5, 0.5 - mndc.y * 0.5), vec2f(0.0), vec2f(1.0));
|
|
165
|
+
// A flat level, not the ground's depth-proportional one. That trick
|
|
166
|
+
// reconstructs how far behind the surface the reflected geometry sits by
|
|
167
|
+
// mirroring the eye through y — arithmetic that is only true for a floor.
|
|
168
|
+
// The general form belongs here eventually; a uniform frosting is the honest
|
|
169
|
+
// thing to ship before it, and blur 0 (the default) reads level 0 exactly.
|
|
170
|
+
let refl = textureSampleLevel(mirrorTex, linearSampler, muv, material.blur * 5.0);
|
|
171
|
+
// THE SAME LEVEL as the colour, and that is the whole of it. refl.rgb is
|
|
172
|
+
// premultiplied by this coverage, so the two are one quantity in two
|
|
173
|
+
// textures: blur the colour into the empty part of the reflection while
|
|
174
|
+
// reading coverage sharp, and the pane divides a spread-out colour by a
|
|
175
|
+
// coverage that never spread — which is a reflection that goes dark as the
|
|
176
|
+
// frosting comes up, exactly backwards.
|
|
177
|
+
let cov = textureSampleLevel(mirrorMask, linearSampler, muv, material.blur * 5.0).g;
|
|
178
|
+
|
|
179
|
+
// ALPHA IS COVERAGE, and it is what keeps the glass clear.
|
|
180
|
+
//
|
|
181
|
+
// The reflection target clears TRANSPARENT, so coverage is 1 exactly where
|
|
182
|
+
// the mirror pass drew something and 0 where it drew nothing. Filling the empty
|
|
183
|
+
// part with the scene's background colour is the obvious thing and it is
|
|
184
|
+
// wrong: that colour would then ride the view transform a second time (the
|
|
185
|
+
// real backdrop composites after it), and the panel comes out paler and
|
|
186
|
+
// flatter than the backdrop it sits against — a mirror that fogs.
|
|
187
|
+
//
|
|
188
|
+
// Letting it through instead costs nothing and is also what a mirror does: an
|
|
189
|
+
// empty mirror shows the room, and here the room IS the backdrop.
|
|
190
|
+
// PREMULTIPLIED, and it must not be premultiplied twice. refl.rgb came out of
|
|
191
|
+
// the HDR target, which stores colour already weighted by its own alpha, and
|
|
192
|
+
// the mirror class blends premultiplied for exactly that reason. Multiplying
|
|
193
|
+
// by cov here as well is what made the reflection a ghost.
|
|
194
|
+
//
|
|
195
|
+
// ALPHA 1, ALWAYS — the pane REPLACES what is behind it. A mirror is opaque;
|
|
196
|
+
// blending here let the real floor show through the glass beside the
|
|
197
|
+
// reflected one, which is two grids in one pane. Premultiplied over at alpha
|
|
198
|
+
// 1 is a straight overwrite, and refl.rgb is already premultiplied, so the
|
|
199
|
+
// empty sky of the reflection writes 0 rather than black-over-something.
|
|
200
|
+
out.color = vec4f(refl.rgb * material.tint, 1.0);
|
|
201
|
+
// .g is the ACCUMULATED CANVAS ALPHA the composite un-premultiplies by, and a
|
|
202
|
+
// surface that writes nothing here is a surface the composite treats as
|
|
203
|
+
// absent — the reflection then survived only where something else had already
|
|
204
|
+
// written alpha under it, which is to say only against the reflected floor.
|
|
205
|
+
// .r is the bloom gate, left at 0: the glow of a reflected ribbon is already
|
|
206
|
+
// in refl.rgb, and gating it again would bloom the one thing in the scene
|
|
207
|
+
// most likely to be over the threshold twice.
|
|
208
|
+
// COVERAGE goes here instead, and it REPLACES too (.a = 1, so the alpha-over
|
|
209
|
+
// blend takes the source whole). The composite reads .g as accumulated alpha:
|
|
210
|
+
// it un-premultiplies the colour by it and paints the scene's backdrop where
|
|
211
|
+
// it falls short. That is what puts the true sky behind the reflection at the
|
|
212
|
+
// true colour — the pane cannot paint the backdrop itself, because anything
|
|
213
|
+
// it writes here goes through the view transform a second time.
|
|
214
|
+
out.mask = vec4f(0.0, cov, 0.0, 1.0);
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
`
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The mirror in the SHADOW pass — the frame throwing shade on the floor.
|
|
222
|
+
*
|
|
223
|
+
* Its own shader because the mirror has no vertex buffer and no skeleton: the
|
|
224
|
+
* quad is six generated vertices and the model matrix is the whole of it, while
|
|
225
|
+
* the ordinary shadow shader is built for skinned geometry with an alpha test.
|
|
226
|
+
*
|
|
227
|
+
* Depth only, no fragment stage. The pane is opaque across its whole rectangle
|
|
228
|
+
* — glass included, because this glass is not see-through — so there is nothing
|
|
229
|
+
* to alpha-test and nothing to colour.
|
|
230
|
+
*
|
|
231
|
+
* The cascade index arrives in its own tiny uniform rather than as a dynamic
|
|
232
|
+
* offset: the cascade matrices sit 64 bytes apart in one array, and a dynamic
|
|
233
|
+
* uniform offset has to be 256-aligned.
|
|
234
|
+
*/
|
|
235
|
+
/**
|
|
236
|
+
* Downsample for the reflection's COVERAGE chain.
|
|
237
|
+
*
|
|
238
|
+
* Colour and coverage are a premultiplied PAIR, and a blur has to move them
|
|
239
|
+
* together. Blurring colour alone drags it toward zero wherever it spreads into
|
|
240
|
+
* the empty part of the reflection, while a coverage still sampled sharp says
|
|
241
|
+
* "fully covered" — so the reflection simply got darker as the dial came up.
|
|
242
|
+
*
|
|
243
|
+
* One bilinear tap at the destination texel centre averages the source's 2x2
|
|
244
|
+
* exactly, which is the box filter this wants and the cheapest way to write it.
|
|
245
|
+
*/
|
|
246
|
+
export const MIRROR_MASK_DOWNSAMPLE_WGSL = /* wgsl */ `
|
|
247
|
+
@group(0) @binding(0) var src: texture_2d<f32>;
|
|
248
|
+
@group(0) @binding(1) var samp: sampler;
|
|
249
|
+
|
|
250
|
+
struct VSOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f, };
|
|
251
|
+
|
|
252
|
+
@vertex fn vs(@builtin(vertex_index) i: u32) -> VSOut {
|
|
253
|
+
var out: VSOut;
|
|
254
|
+
let x = f32(i32(i / 2u) * 4 - 1);
|
|
255
|
+
let y = f32(i32(i % 2u) * 4 - 1);
|
|
256
|
+
out.pos = vec4f(x, y, 0.0, 1.0);
|
|
257
|
+
out.uv = vec2f(x * 0.5 + 0.5, 0.5 - y * 0.5);
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@fragment fn fs(in: VSOut) -> @location(0) vec4f {
|
|
262
|
+
return textureSampleLevel(src, samp, in.uv, 0.0);
|
|
263
|
+
}
|
|
264
|
+
`
|
|
265
|
+
|
|
266
|
+
export function mirrorShadowWgsl(cascades: number): string {
|
|
267
|
+
return /* wgsl */ `
|
|
268
|
+
struct LightVP { viewProj: array<mat4x4f, ${cascades}>, };
|
|
269
|
+
struct Cascade { index: u32, _p0: u32, _p1: u32, _p2: u32, };
|
|
270
|
+
struct MirrorMat {
|
|
271
|
+
model: mat4x4f,
|
|
272
|
+
tint: vec3f, blur: f32,
|
|
273
|
+
frameColor: vec3f, frame: f32,
|
|
274
|
+
size: vec2f, _pad: vec2f,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
@group(0) @binding(0) var<uniform> lightVP: LightVP;
|
|
278
|
+
@group(0) @binding(1) var<uniform> cascade: Cascade;
|
|
279
|
+
@group(0) @binding(2) var<uniform> material: MirrorMat;
|
|
280
|
+
|
|
281
|
+
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
282
|
+
var q = array<vec2f, 6>(
|
|
283
|
+
vec2f(-0.5, 0.0), vec2f(0.5, 0.0), vec2f(0.5, 1.0),
|
|
284
|
+
vec2f(-0.5, 0.0), vec2f(0.5, 1.0), vec2f(-0.5, 1.0),
|
|
285
|
+
);
|
|
286
|
+
return lightVP.viewProj[cascade.index] * material.model * vec4f(q[vi], 0.0, 1.0);
|
|
287
|
+
}
|
|
288
|
+
`
|
|
289
|
+
}
|