@driftengine/core 4.13.6 → 4.14.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/CHANGELOG.json +51 -0
- package/README.md +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/render/backend/api.d.ts +19 -3
- package/dist/render/backend/webgl2/motionTarget.d.ts +39 -0
- package/dist/render/backend/webgl2/motionTarget.js +254 -0
- package/dist/render/backend/webgl2/pointSkinFeedback.d.ts +13 -3
- package/dist/render/backend/webgl2/pointSkinFeedback.js +45 -12
- package/dist/render/backend/webgl2/renderer.d.ts +101 -7
- package/dist/render/backend/webgl2/renderer.js +337 -9
- package/dist/render/backend/webgpu/pointSkinPass.d.ts +16 -3
- package/dist/render/backend/webgpu/pointSkinPass.js +79 -21
- package/dist/render/backend/webgpu/postPass.d.ts +6 -1
- package/dist/render/backend/webgpu/renderer.d.ts +41 -0
- package/dist/render/backend/webgpu/renderer.js +143 -5
- package/dist/render/baseDecals.d.ts +48 -0
- package/dist/render/baseDecals.js +71 -0
- package/dist/render/emissiveFlow.d.ts +40 -0
- package/dist/render/emissiveFlow.js +51 -0
- package/dist/render/fixtureAtlas.d.ts +8 -3
- package/dist/render/fixtureAtlas.js +17 -6
- package/dist/render/globalMedium.d.ts +17 -0
- package/dist/render/globalMedium.js +14 -0
- package/dist/render/globalMediumPass.js +3 -2
- package/dist/render/iridescentOverlay.d.ts +78 -0
- package/dist/render/iridescentOverlay.js +102 -0
- package/dist/render/mesh.d.ts +24 -0
- package/dist/render/mesh.js +79 -0
- package/dist/render/pointSkin.d.ts +18 -0
- package/dist/render/pointSkin.js +50 -5
- package/dist/render/recon/mover.d.ts +3 -2
- package/dist/render/recon/mover.js +3 -2
- package/dist/render/sceneTarget.d.ts +2 -0
- package/dist/render/sceneTarget.js +1 -1
- package/dist/render/shaders/flat/baseDecals.d.ts +17 -0
- package/dist/render/shaders/flat/baseDecals.js +71 -0
- package/dist/render/shaders/flat/emissiveFlow.d.ts +15 -0
- package/dist/render/shaders/flat/emissiveFlow.js +45 -0
- package/dist/render/shaders/flat/glassTint.d.ts +1 -1
- package/dist/render/shaders/flat/glassTint.js +3 -0
- package/dist/render/shaders/flat/index.d.ts +16 -0
- package/dist/render/shaders/flat/index.js +9 -0
- package/dist/render/shaders/flat/iridescentOverlay.d.ts +15 -0
- package/dist/render/shaders/flat/iridescentOverlay.js +65 -0
- package/dist/render/shaders/flat/layered.js +4 -4
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +12 -1
- package/dist/render/shaders/flat/mapReads.d.ts +1 -1
- package/dist/render/shaders/flat/mapReads.js +7 -3
- package/dist/render/shaders/flat/worldUv.js +6 -5
- package/dist/render/shaders/generated/flat.wgsl.d.ts +480 -16
- package/dist/render/shaders/generated/flat.wgsl.js +519 -55
- package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +15 -5
- package/dist/render/shaders/generated/temporalResolve.wgsl.js +15 -5
- package/dist/render/shaders/temporalResolve.js +25 -9
- package/dist/render/surfaceTexture.d.ts +32 -0
- package/dist/render/temporalPass.d.ts +3 -1
- package/dist/render/temporalPass.js +9 -1
- package/package.json +6 -6
- package/src/index.ts +3 -0
- package/src/render/backend/api.ts +19 -2
- package/src/render/backend/webgl2/motionTarget.ts +299 -0
- package/src/render/backend/webgl2/pointSkinFeedback.ts +49 -14
- package/src/render/backend/webgl2/renderer.ts +411 -9
- package/src/render/backend/webgpu/pointSkinPass.ts +90 -23
- package/src/render/backend/webgpu/renderer.ts +163 -3
- package/src/render/baseDecals.ts +93 -0
- package/src/render/emissiveFlow.ts +66 -0
- package/src/render/fixtureAtlas.ts +26 -7
- package/src/render/globalMedium.ts +31 -0
- package/src/render/globalMediumPass.ts +3 -7
- package/src/render/iridescentOverlay.ts +177 -0
- package/src/render/mesh.ts +82 -0
- package/src/render/pointSkin.ts +64 -5
- package/src/render/recon/mover.ts +3 -2
- package/src/render/sceneTarget.ts +3 -0
- package/src/render/shaders/flat/baseDecals.ts +71 -0
- package/src/render/shaders/flat/emissiveFlow.ts +45 -0
- package/src/render/shaders/flat/glassTint.ts +7 -1
- package/src/render/shaders/flat/index.ts +25 -0
- package/src/render/shaders/flat/iridescentOverlay.ts +65 -0
- package/src/render/shaders/flat/layered.ts +4 -4
- package/src/render/shaders/flat/main.ts +12 -1
- package/src/render/shaders/flat/mapReads.ts +7 -3
- package/src/render/shaders/flat/worldUv.ts +6 -5
- package/src/render/shaders/generated/flat.wgsl.ts +519 -55
- package/src/render/shaders/generated/temporalResolve.wgsl.ts +15 -5
- package/src/render/shaders/temporalResolve.ts +25 -9
- package/src/render/surfaceTexture.ts +32 -0
- package/src/render/temporalPass.ts +8 -0
|
@@ -116,6 +116,16 @@ export interface GlobalMediumOptions {
|
|
|
116
116
|
readonly sunScatter?: number;
|
|
117
117
|
/** The same for the sky's fill, which lights a shadowed medium from every side. Default 1. */
|
|
118
118
|
readonly skyScatter?: number;
|
|
119
|
+
/**
|
|
120
|
+
* The sky's fill the medium scatters, a colour of its own in the units of
|
|
121
|
+
* `Environment.ambient`, which `skyScatter` then scales. Absent or null, the frame's ambient, as
|
|
122
|
+
* it always was. **For a scene whose surfaces are lit by more than its flat ambient**, a sky's
|
|
123
|
+
* cubemap, probes and a bake, which keeps that ambient for what falls outside them: the ambient
|
|
124
|
+
* is then the wrong colour for the sky's light in the air, and setting it to the right one would
|
|
125
|
+
* relight those surfaces too. What it gives up is the sky's direction, as the ambient does: one
|
|
126
|
+
* colour from every side.
|
|
127
|
+
*/
|
|
128
|
+
readonly skyFill?: readonly [number, number, number] | null;
|
|
119
129
|
/**
|
|
120
130
|
* The same for the lamps, each through its own shadow and times its own `lightMediumScatter`.
|
|
121
131
|
* **Default 0**: a medium was the sun's and the sky's until there was this, and a scene that
|
|
@@ -142,6 +152,8 @@ export interface ResolvedGlobalMedium {
|
|
|
142
152
|
readonly sunScatter: number;
|
|
143
153
|
readonly skyScatter: number;
|
|
144
154
|
readonly lampScatter: number;
|
|
155
|
+
/** The sky's fill of the medium's own, or null for the frame's ambient: see `mediumSkyFill`. */
|
|
156
|
+
readonly skyFill: readonly [number, number, number] | null;
|
|
145
157
|
}
|
|
146
158
|
|
|
147
159
|
/**
|
|
@@ -192,9 +204,28 @@ export function resolveGlobalMedium(options: Partial<GlobalMediumOptions>): Reso
|
|
|
192
204
|
sunScatter: Math.max(0, finite(options.sunScatter, 1)),
|
|
193
205
|
skyScatter: Math.max(0, finite(options.skyScatter, 1)),
|
|
194
206
|
lampScatter: Math.max(0, finite(options.lampScatter, 0)),
|
|
207
|
+
skyFill:
|
|
208
|
+
options.skyFill === undefined || options.skyFill === null
|
|
209
|
+
? null
|
|
210
|
+
: [
|
|
211
|
+
Math.max(0, finite(options.skyFill[0], 0)),
|
|
212
|
+
Math.max(0, finite(options.skyFill[1], 0)),
|
|
213
|
+
Math.max(0, finite(options.skyFill[2], 0)),
|
|
214
|
+
],
|
|
195
215
|
};
|
|
196
216
|
}
|
|
197
217
|
|
|
218
|
+
/**
|
|
219
|
+
* The colour the medium scatters as the sky's fill: its own where it has one, else the frame's
|
|
220
|
+
* ambient. One decision both backends upload, so the two cannot disagree about which.
|
|
221
|
+
*/
|
|
222
|
+
export function mediumSkyFill(
|
|
223
|
+
medium: Pick<ResolvedGlobalMedium, 'skyFill'>,
|
|
224
|
+
ambient: ArrayLike<number>,
|
|
225
|
+
): ArrayLike<number> {
|
|
226
|
+
return medium.skyFill ?? ambient;
|
|
227
|
+
}
|
|
228
|
+
|
|
198
229
|
/** `setGlobalMedium`'s object form: a density, and any of the rest. */
|
|
199
230
|
export type GlobalMediumSettings = Partial<GlobalMediumOptions> & { readonly density: number };
|
|
200
231
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mat4 } from 'gl-matrix';
|
|
2
2
|
import type { ReadonlyMat4 } from 'gl-matrix';
|
|
3
3
|
|
|
4
|
-
import { mediumTargetSize } from './globalMedium.ts';
|
|
4
|
+
import { mediumSkyFill, mediumTargetSize } from './globalMedium.ts';
|
|
5
5
|
import type { ResolvedGlobalMedium, ResolvedMediumLamps } from './globalMedium.ts';
|
|
6
6
|
import { DEPTH_01_TO_CLIP } from './lightVolumeDraw.ts';
|
|
7
7
|
import { compileProgram, uniformLocations } from './shader.ts';
|
|
@@ -236,12 +236,8 @@ export class GlobalMediumPass {
|
|
|
236
236
|
light.sunColor[1] ?? 0,
|
|
237
237
|
light.sunColor[2] ?? 0,
|
|
238
238
|
);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
light.ambient[0] ?? 0,
|
|
242
|
-
light.ambient[1] ?? 0,
|
|
243
|
-
light.ambient[2] ?? 0,
|
|
244
|
-
);
|
|
239
|
+
const sky = mediumSkyFill(options, light.ambient);
|
|
240
|
+
gl.uniform3f(u['uAmbient'] ?? null, sky[0] ?? 0, sky[1] ?? 0, sky[2] ?? 0);
|
|
245
241
|
gl.uniform1f(u['uDensity'] ?? null, options.density);
|
|
246
242
|
gl.uniform2f(u['uHeight'] ?? null, options.heightFalloff, options.baseY);
|
|
247
243
|
gl.uniform3f(u['uLayer2'] ?? null, options.density2, options.heightFalloff2, options.baseY2);
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A surface's iridescent overlay: a hue that shifts with the view, laid over the base colour before
|
|
3
|
+
* it is lit. `SurfaceMaterial.iridescentOverlay`, packed into `uIridescence` for the lit stage's
|
|
4
|
+
* `IRIDESCENT_OVERLAY` switch, and the same arithmetic on the CPU, which the tests hold the shader to.
|
|
5
|
+
*
|
|
6
|
+
* Per pixel, with `N` the mapped normal, `V` toward the eye and `uv` the mesh's own first
|
|
7
|
+
* coordinates, as many material systems shade a sheen on cloth or a coat:
|
|
8
|
+
*
|
|
9
|
+
* h = saturate((N·H)^contrast), 0 where N·H ≤ 0, H = normalize(V + direction)
|
|
10
|
+
* x = N·V + uv.x + blend · (h − N·V)
|
|
11
|
+
* t = saturate(fract(x · tiling))
|
|
12
|
+
* base' = base + (overlay(base, ramp(t)) − base) · x · mask · weight
|
|
13
|
+
*
|
|
14
|
+
* where `overlay` is the usual blend, `a < ½ ? 2ao : 1 − 2(1 − a)(1 − o)` per channel.
|
|
15
|
+
*
|
|
16
|
+
* **The ramp is uniforms, thirty-two colours, and not a texture**, because the lit stage has no
|
|
17
|
+
* texture binding left (`textureUnitBudget.test.ts`): a caller's ramp of any length is resampled to
|
|
18
|
+
* thirty-two on the way in and read back between neighbours. What that gives up is a ramp with
|
|
19
|
+
* detail finer than a thirty-second of its length, which a smooth spectrum does not have. **The
|
|
20
|
+
* mask is a channel of a layer of one of the material's own arrays**, for the same reason, read at
|
|
21
|
+
* the material's coordinates; alpha is linear in every array, and a colour channel of an sRGB array
|
|
22
|
+
* is decoded before it is read.
|
|
23
|
+
*
|
|
24
|
+
* Thirty-six vectors, so WebGL2 refuses it, in words, on a part whose lit stage has no room, as it
|
|
25
|
+
* refuses a material's tiled detail.
|
|
26
|
+
*/
|
|
27
|
+
/** Three numbers, read and never written. */
|
|
28
|
+
type Three = readonly [number, number, number];
|
|
29
|
+
|
|
30
|
+
/** Colours the ramp is resampled to. */
|
|
31
|
+
export const IRIDESCENT_RAMP = 32;
|
|
32
|
+
/** Floats in `uIridescence`: four vectors of numbers, then the ramp a vector a colour. */
|
|
33
|
+
export const IRIDESCENT_FLOATS = (4 + IRIDESCENT_RAMP) * 4;
|
|
34
|
+
|
|
35
|
+
/** Which of a material's arrays holds the mask. */
|
|
36
|
+
export type IridescentMaskMap = 'orm' | 'model' | 'albedo' | 'emissive';
|
|
37
|
+
|
|
38
|
+
/** `SurfaceMaterial.iridescentOverlay`. */
|
|
39
|
+
export interface IridescentOverlay {
|
|
40
|
+
/**
|
|
41
|
+
* The overlay colour along `t`, linear, three floats a colour, at least two colours, the first at
|
|
42
|
+
* `t = 0` and the last at `t = 1`. Resampled to thirty-two.
|
|
43
|
+
*/
|
|
44
|
+
readonly ramp: ArrayLike<number>;
|
|
45
|
+
/** A fixed direction in the world the sheen turns toward, `S`. Need not be unit length. */
|
|
46
|
+
readonly direction: Three;
|
|
47
|
+
/** How far the view's term is turned toward the sheen, 0 to 1. Default 0.6. */
|
|
48
|
+
readonly blend?: number;
|
|
49
|
+
/** The sheen's exponent. Default 1. */
|
|
50
|
+
readonly contrast?: number;
|
|
51
|
+
/** How many times the ramp repeats across `x`. Default 2. */
|
|
52
|
+
readonly tiling?: number;
|
|
53
|
+
/** How much of the overlay is laid on. Default 0.25. */
|
|
54
|
+
readonly weight?: number;
|
|
55
|
+
/** Where it is laid: a channel of a layer of one of the material's arrays. Absent, everywhere. */
|
|
56
|
+
readonly mask?: {
|
|
57
|
+
readonly map: IridescentMaskMap;
|
|
58
|
+
readonly layer: number;
|
|
59
|
+
/** 0 red, 1 green, 2 blue, 3 alpha. */
|
|
60
|
+
readonly channel: 0 | 1 | 2 | 3;
|
|
61
|
+
} | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const MASK_SOURCE: Readonly<Record<IridescentMaskMap, number>> = {
|
|
65
|
+
orm: 1,
|
|
66
|
+
model: 2,
|
|
67
|
+
albedo: 3,
|
|
68
|
+
emissive: 4,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Why an overlay cannot be drawn, or null where it can. */
|
|
72
|
+
export function iridescentRefusal(overlay: IridescentOverlay): string | null {
|
|
73
|
+
const values = overlay.ramp.length;
|
|
74
|
+
if (values < 6 || values % 3 !== 0) {
|
|
75
|
+
return `a ramp of ${values} numbers; it is three a colour, at least two colours`;
|
|
76
|
+
}
|
|
77
|
+
const [x, y, z] = overlay.direction;
|
|
78
|
+
if (
|
|
79
|
+
!(Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) ||
|
|
80
|
+
x * x + y * y + z * z === 0
|
|
81
|
+
) {
|
|
82
|
+
return 'a direction that is not one';
|
|
83
|
+
}
|
|
84
|
+
const mask = overlay.mask ?? null;
|
|
85
|
+
if (mask !== null && !(Number.isInteger(mask.layer) && mask.layer >= 0)) {
|
|
86
|
+
return `a mask layer of ${mask.layer}; a layer is a whole number from 0`;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Pack an overlay into `out`, with the scale and offset that take the material's coordinates back
|
|
93
|
+
* to the mesh's own; all zeros, which the shader reads as none, for null. Refuse what
|
|
94
|
+
* `iridescentRefusal` refuses before calling this.
|
|
95
|
+
*/
|
|
96
|
+
export function packIridescentOverlay(
|
|
97
|
+
overlay: IridescentOverlay | null,
|
|
98
|
+
uv: { uScale?: number; vScale?: number; uOffset?: number; vOffset?: number },
|
|
99
|
+
out: Float32Array,
|
|
100
|
+
): void {
|
|
101
|
+
out.fill(0);
|
|
102
|
+
if (overlay === null) return;
|
|
103
|
+
const [x, y, z] = overlay.direction;
|
|
104
|
+
const length = Math.hypot(x, y, z);
|
|
105
|
+
out[0] = x / length;
|
|
106
|
+
out[1] = y / length;
|
|
107
|
+
out[2] = z / length;
|
|
108
|
+
out[3] = overlay.blend ?? 0.6;
|
|
109
|
+
out[4] = overlay.contrast ?? 1;
|
|
110
|
+
out[5] = overlay.tiling ?? 2;
|
|
111
|
+
out[6] = overlay.weight ?? 0.25;
|
|
112
|
+
/* On: a zero here, which an unwritten block is, is none. */
|
|
113
|
+
out[7] = 1;
|
|
114
|
+
const mask = overlay.mask ?? null;
|
|
115
|
+
out[8] = mask === null ? 0 : MASK_SOURCE[mask.map];
|
|
116
|
+
out[9] = mask === null ? 0 : mask.layer;
|
|
117
|
+
out[10] = mask === null ? 0 : mask.channel;
|
|
118
|
+
const uScale = uv.uScale ?? 1;
|
|
119
|
+
const vScale = uv.vScale ?? 1;
|
|
120
|
+
out[12] = uScale === 0 ? 0 : 1 / uScale;
|
|
121
|
+
out[13] = vScale === 0 ? 0 : 1 / vScale;
|
|
122
|
+
out[14] = uScale === 0 ? 0 : -(uv.uOffset ?? 0) / uScale;
|
|
123
|
+
out[15] = vScale === 0 ? 0 : -(uv.vOffset ?? 0) / vScale;
|
|
124
|
+
/* The ramp, resampled to thirty-two colours between its own neighbours. */
|
|
125
|
+
const colours = overlay.ramp.length / 3;
|
|
126
|
+
for (let i = 0; i < IRIDESCENT_RAMP; i++) {
|
|
127
|
+
const at = (i / (IRIDESCENT_RAMP - 1)) * (colours - 1);
|
|
128
|
+
const below = Math.min(Math.floor(at), colours - 2);
|
|
129
|
+
const share = at - below;
|
|
130
|
+
for (let c = 0; c < 3; c++) {
|
|
131
|
+
const a = overlay.ramp[below * 3 + c] as number;
|
|
132
|
+
const b = overlay.ramp[(below + 1) * 3 + c] as number;
|
|
133
|
+
out[16 + i * 4 + c] = a + (b - a) * share;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The base colour with the overlay laid on, as the lit stage computes it from a packed block: `n`
|
|
140
|
+
* and `toEye` unit length, `uv` the mesh's own coordinates, `mask` the texel's channel. Into `out`.
|
|
141
|
+
*/
|
|
142
|
+
export function iridescentBase(
|
|
143
|
+
packed: ArrayLike<number>,
|
|
144
|
+
base: Three,
|
|
145
|
+
n: Three,
|
|
146
|
+
toEye: Three,
|
|
147
|
+
uv: readonly [number, number],
|
|
148
|
+
mask: number,
|
|
149
|
+
out: [number, number, number],
|
|
150
|
+
): [number, number, number] {
|
|
151
|
+
const p = (i: number): number => packed[i] as number;
|
|
152
|
+
let hx = toEye[0] + p(0);
|
|
153
|
+
let hy = toEye[1] + p(1);
|
|
154
|
+
let hz = toEye[2] + p(2);
|
|
155
|
+
const hl = Math.hypot(hx, hy, hz) || 1;
|
|
156
|
+
hx /= hl;
|
|
157
|
+
hy /= hl;
|
|
158
|
+
hz /= hl;
|
|
159
|
+
const nh = n[0] * hx + n[1] * hy + n[2] * hz;
|
|
160
|
+
const h = nh > 0 ? Math.min(1, Math.max(0, nh ** p(4))) : 0;
|
|
161
|
+
const nv = n[0] * toEye[0] + n[1] * toEye[1] + n[2] * toEye[2];
|
|
162
|
+
const x = nv + uv[0] + p(3) * (h - nv);
|
|
163
|
+
const scaled = x * p(5);
|
|
164
|
+
const t = Math.min(1, Math.max(0, scaled - Math.floor(scaled)));
|
|
165
|
+
const at = t * (IRIDESCENT_RAMP - 1);
|
|
166
|
+
const below = Math.min(Math.floor(at), IRIDESCENT_RAMP - 2);
|
|
167
|
+
const share = at - below;
|
|
168
|
+
const amount = x * mask * p(6);
|
|
169
|
+
for (let c = 0; c < 3; c++) {
|
|
170
|
+
const o0 = p(16 + below * 4 + c);
|
|
171
|
+
const o = o0 + (p(16 + (below + 1) * 4 + c) - o0) * share;
|
|
172
|
+
const a = base[c] as number;
|
|
173
|
+
const blended = a < 0.5 ? 2 * a * o : 1 - 2 * (1 - a) * (1 - o);
|
|
174
|
+
out[c] = a + (blended - a) * amount;
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
package/src/render/mesh.ts
CHANGED
|
@@ -40,6 +40,8 @@ import { validateMeshData } from '@driftengine/drft';
|
|
|
40
40
|
|
|
41
41
|
/** Attribute locations are fixed engine-wide (`layout(location = n)` in GLSL). */
|
|
42
42
|
const ATTR_POSITION = 0;
|
|
43
|
+
/** What the motion pass reads: positions, and a rig's two sets of joints and weights. */
|
|
44
|
+
const MOTION_LOCATIONS: readonly number[] = [ATTR_POSITION, 11, 12, 14, 15];
|
|
43
45
|
const ATTR_NORMAL = 1;
|
|
44
46
|
const ATTR_COLOR = 2;
|
|
45
47
|
const ATTR_EMISSIVE = 3;
|
|
@@ -119,6 +121,16 @@ export class Mesh {
|
|
|
119
121
|
get streams(): { readonly positions: WebGLBuffer; readonly normals: WebGLBuffer } | null {
|
|
120
122
|
return this.deformable;
|
|
121
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* A dynamic mesh's positions as they stood before this frame's first rewrite, for the motion
|
|
126
|
+
* WebGL2's temporal resolve follows (`backend/webgl2/motionTarget.ts`). Made the first time one is
|
|
127
|
+
* kept, so a mesh that never moves under the resolve pays nothing.
|
|
128
|
+
*/
|
|
129
|
+
private previousPositions: WebGLBuffer | null = null;
|
|
130
|
+
/** The frame that rewrite belongs to (`frameOfChange`), or -1 for none yet. */
|
|
131
|
+
changedFrame = -1;
|
|
132
|
+
/** A vertex array for the motion pass, without and with last frame's positions, made on demand. */
|
|
133
|
+
private motionArrays: (WebGLVertexArrayObject | null)[] = [null, null];
|
|
122
134
|
/** The colour buffer, kept so `updateColors` can rewrite it. Null unless `dynamicColors`. */
|
|
123
135
|
private readonly recolourable: WebGLBuffer | null = null;
|
|
124
136
|
/** How many floats the position buffer holds, so an update of the wrong size is refused. */
|
|
@@ -475,6 +487,76 @@ export class Mesh {
|
|
|
475
487
|
this.morph?.dispose(gl);
|
|
476
488
|
for (const buffer of this.buffers) gl.deleteBuffer(buffer);
|
|
477
489
|
gl.deleteVertexArray(this.vao);
|
|
490
|
+
for (const vao of this.motionArrays) if (vao !== null) gl.deleteVertexArray(vao);
|
|
491
|
+
if (this.previousPositions !== null) gl.deleteBuffer(this.previousPositions);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Keep this dynamic mesh's positions as they stand, before a rewrite: one buffer copy on the
|
|
496
|
+
* device. The caller decides whether this rewrite is the frame's first (`keepsPrevious`).
|
|
497
|
+
*/
|
|
498
|
+
keepPrevious(gl: WebGL2RenderingContext): void {
|
|
499
|
+
const deformable = this.deformable;
|
|
500
|
+
if (deformable === null) return;
|
|
501
|
+
const bytes = this.positionFloats * 4;
|
|
502
|
+
if (this.previousPositions === null) {
|
|
503
|
+
const buffer = gl.createBuffer();
|
|
504
|
+
if (buffer === null) throw new Error('createBuffer failed');
|
|
505
|
+
gl.bindBuffer(gl.COPY_WRITE_BUFFER, buffer);
|
|
506
|
+
gl.bufferData(gl.COPY_WRITE_BUFFER, bytes, gl.DYNAMIC_COPY);
|
|
507
|
+
this.previousPositions = buffer;
|
|
508
|
+
}
|
|
509
|
+
gl.bindBuffer(gl.COPY_READ_BUFFER, deformable.positions);
|
|
510
|
+
gl.bindBuffer(gl.COPY_WRITE_BUFFER, this.previousPositions);
|
|
511
|
+
gl.copyBufferSubData(gl.COPY_READ_BUFFER, gl.COPY_WRITE_BUFFER, 0, 0, bytes);
|
|
512
|
+
gl.bindBuffer(gl.COPY_READ_BUFFER, null);
|
|
513
|
+
gl.bindBuffer(gl.COPY_WRITE_BUFFER, null);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** Whether last frame's positions have been kept: a dynamic mesh rewritten under the resolve. */
|
|
517
|
+
get keptPrevious(): boolean {
|
|
518
|
+
return this.previousPositions !== null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Draw this mesh for the motion pass: its positions at 0, last frame's at 1 where `previous` asks
|
|
523
|
+
* and they were kept, and its rig's influences where `flat`'s skinning reads them, 11, 12, 14 and
|
|
524
|
+
* 15. A vertex array of its own, made once a variant, so the one the lit pass draws with is never
|
|
525
|
+
* touched.
|
|
526
|
+
*/
|
|
527
|
+
drawMotion(gl: WebGL2RenderingContext, previous: boolean): void {
|
|
528
|
+
const at = previous && this.previousPositions !== null ? 1 : 0;
|
|
529
|
+
let vao = this.motionArrays[at] ?? null;
|
|
530
|
+
if (vao === null) {
|
|
531
|
+
vao = gl.createVertexArray();
|
|
532
|
+
if (vao === null) throw new Error('createVertexArray failed');
|
|
533
|
+
gl.bindVertexArray(vao);
|
|
534
|
+
for (const attribute of this.layout) {
|
|
535
|
+
if (!MOTION_LOCATIONS.includes(attribute.location)) continue;
|
|
536
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, attribute.buffer);
|
|
537
|
+
gl.enableVertexAttribArray(attribute.location);
|
|
538
|
+
gl.vertexAttribPointer(
|
|
539
|
+
attribute.location,
|
|
540
|
+
attribute.size,
|
|
541
|
+
attribute.type,
|
|
542
|
+
attribute.normalized,
|
|
543
|
+
0,
|
|
544
|
+
0,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
if (at === 1 && this.previousPositions !== null) {
|
|
548
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.previousPositions);
|
|
549
|
+
gl.enableVertexAttribArray(1);
|
|
550
|
+
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
|
|
551
|
+
}
|
|
552
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
|
|
553
|
+
gl.bindVertexArray(null);
|
|
554
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
555
|
+
this.motionArrays[at] = vao;
|
|
556
|
+
}
|
|
557
|
+
gl.bindVertexArray(vao);
|
|
558
|
+
gl.drawElements(gl.TRIANGLES, this.indexCount, this.indexType, 0);
|
|
559
|
+
gl.bindVertexArray(null);
|
|
478
560
|
}
|
|
479
561
|
|
|
480
562
|
/**
|
package/src/render/pointSkin.ts
CHANGED
|
@@ -48,9 +48,27 @@ export interface PointSkinReach {
|
|
|
48
48
|
|
|
49
49
|
/** Refuse a skin whose arrays disagree with a mesh of `vertices`, or that names a joint past the rest. */
|
|
50
50
|
export function validatePointSkin(data: PointSkinData, vertices: number): void {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
checkArrays(data, vertices, 'the mesh has');
|
|
52
|
+
checkJoints(data, 0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Refuse a rewrite of vertices `first` onward, as many as `data` holds, that does not fit a mesh of
|
|
57
|
+
* `vertices`: arrays that disagree with each other, a range that is not whole vertices inside the
|
|
58
|
+
* mesh, or a joint past the rest. Named by the mesh's own vertex numbers.
|
|
59
|
+
*/
|
|
60
|
+
export function validatePointSkinRange(data: PointSkinData, first: number, vertices: number): void {
|
|
61
|
+
const count = data.weights.length / 4;
|
|
62
|
+
checkArrays(data, count, 'the weights hold');
|
|
63
|
+
if (!(Number.isInteger(first) && first >= 0 && first + count <= vertices)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`point skin: a rewrite of ${count} vertices from vertex ${first} for a mesh of ${vertices}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
checkJoints(data, first);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function checkArrays(data: PointSkinData, vertices: number, against: string): void {
|
|
54
72
|
for (const [name, values, width] of [
|
|
55
73
|
['joints', data.joints, 4],
|
|
56
74
|
['weights', data.weights, 4],
|
|
@@ -58,13 +76,22 @@ export function validatePointSkin(data: PointSkinData, vertices: number): void {
|
|
|
58
76
|
['normals', data.normals, 3],
|
|
59
77
|
] as const) {
|
|
60
78
|
if (values.length !== vertices * width) {
|
|
61
|
-
|
|
79
|
+
throw new Error(
|
|
80
|
+
`point skin: ${name} holds ${values.length / width} vertices and ${against} ${vertices}`,
|
|
81
|
+
);
|
|
62
82
|
}
|
|
63
83
|
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Every joint a whole number below `MAX_JOINTS`; a vertex named as `first` plus its place. */
|
|
87
|
+
function checkJoints(data: PointSkinData, first: number): void {
|
|
64
88
|
for (let at = 0; at < data.joints.length; at++) {
|
|
65
89
|
const joint = data.joints[at] as number;
|
|
66
90
|
if (!(Number.isInteger(joint) && joint >= 0 && joint < MAX_JOINTS)) {
|
|
67
|
-
|
|
91
|
+
throw new Error(
|
|
92
|
+
`point skin: vertex ${first + Math.floor(at / 4)} names joint ${joint}; a joint is 0 to ` +
|
|
93
|
+
`${MAX_JOINTS - 1}`,
|
|
94
|
+
);
|
|
68
95
|
}
|
|
69
96
|
}
|
|
70
97
|
}
|
|
@@ -102,6 +129,38 @@ export function reachOf(data: PointSkinData): PointSkinReach {
|
|
|
102
129
|
};
|
|
103
130
|
}
|
|
104
131
|
|
|
132
|
+
/**
|
|
133
|
+
* A skin's reach after part of it is rewritten with `data`: the farther of the old reach and the
|
|
134
|
+
* new at each joint, the joints the two name between them, and the widest span of weight sums.
|
|
135
|
+
*
|
|
136
|
+
* **It widens and never narrows**, because the vertices the rewrite replaced may have been the
|
|
137
|
+
* ones that set a joint's reach, and finding out would mean keeping every vertex on the CPU and
|
|
138
|
+
* reading them all again. A skin that changes shape back and forth settles at the union of its
|
|
139
|
+
* shapes, which is still a box no posed vertex leaves, and culls a little less than it could. What
|
|
140
|
+
* would make that wrong is a skin rewritten into ever larger shapes and never back; making it
|
|
141
|
+
* again with `createPointSkin` measures it afresh.
|
|
142
|
+
*/
|
|
143
|
+
export function widenReach(reach: PointSkinReach, data: PointSkinData): PointSkinReach {
|
|
144
|
+
const added = reachOf(data);
|
|
145
|
+
const joints = Math.max(reach.joints, added.joints);
|
|
146
|
+
const widened = new Float32Array(joints).fill(-1);
|
|
147
|
+
for (let j = 0; j < joints; j++) {
|
|
148
|
+
widened[j] = Math.max(
|
|
149
|
+
j < reach.joints ? (reach.reach[j] as number) : -1,
|
|
150
|
+
j < added.joints ? (added.reach[j] as number) : -1,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const vertices = data.weights.length / 4;
|
|
154
|
+
return {
|
|
155
|
+
joints,
|
|
156
|
+
reach: widened,
|
|
157
|
+
sums:
|
|
158
|
+
vertices === 0
|
|
159
|
+
? reach.sums
|
|
160
|
+
: [Math.min(reach.sums[0], added.sums[0]), Math.max(reach.sums[1], added.sums[1])],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
105
164
|
/**
|
|
106
165
|
* The bounds of a pose, into the mesh's own: `matrices` holds each joint's transform into the
|
|
107
166
|
* mesh's model space, sixteen floats, column-major. Allocates nothing.
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* draw, last frame's palette.
|
|
9
9
|
*
|
|
10
10
|
* **Plain data, advanced by the renderer.** No renderer is needed to make one, so a consumer holds
|
|
11
|
-
* it beside the object it belongs to.
|
|
12
|
-
*
|
|
11
|
+
* it beside the object it belongs to. A reconstructing WebGPU renderer reads and writes it, and so
|
|
12
|
+
* does a WebGL2 one resolving temporally (`backend/webgl2/motionTarget.ts`); it costs nothing
|
|
13
|
+
* anywhere else. The fields past `kind` are the renderer's and a caller leaves them
|
|
13
14
|
* alone.
|
|
14
15
|
*
|
|
15
16
|
* **What would make this wrong** is a consumer whose objects are rebuilt every frame with no state
|
|
@@ -850,6 +850,8 @@ export class SceneTarget {
|
|
|
850
850
|
readonly blend: number;
|
|
851
851
|
readonly periodStart: boolean;
|
|
852
852
|
readonly depthToViewZ: Float32Array;
|
|
853
|
+
/** Where the frame's movers stood last frame, or absent for the camera's motion alone. */
|
|
854
|
+
readonly motion?: WebGLTexture | null;
|
|
853
855
|
},
|
|
854
856
|
): void {
|
|
855
857
|
const { gl } = this;
|
|
@@ -993,6 +995,7 @@ export class SceneTarget {
|
|
|
993
995
|
temporal.blend,
|
|
994
996
|
temporal.periodStart,
|
|
995
997
|
temporal.depthToViewZ,
|
|
998
|
+
temporal.motion ?? null,
|
|
996
999
|
);
|
|
997
1000
|
}
|
|
998
1001
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decals laid into a surface's base colour before it is lit: `setBaseDecals`, packed into
|
|
3
|
+
* `uBaseDecals` by `baseDecals.ts`, where what they are for and what they give up is said, and the
|
|
4
|
+
* material's own refusal in `uDecalReceiver` (`SurfaceMaterial.receivesDecals`).
|
|
5
|
+
*
|
|
6
|
+
* **Each decal is a test and a fetch**: the world point taken into the box by three rows, outside
|
|
7
|
+
* it or turned too far from the projection and nothing, else the image read across the box's face
|
|
8
|
+
* from its square in the fixture atlas, its colour decoded from sRGB and laid over the base colour
|
|
9
|
+
* by its alpha and the decal's opacity. The geometric normal decides the facing, so a normal map's
|
|
10
|
+
* bump does not cut a mark. The loop is over a count in a uniform and the fetch is `textureLod`, as
|
|
11
|
+
* every read of the atlas is, so neither needs uniform control flow: AGENTS.md 2026-08-07.
|
|
12
|
+
*
|
|
13
|
+
* **A lit switch, `BASE_DECALS`**, off until a frame first lays one, declared after
|
|
14
|
+
* `IRIDESCENT_OVERLAY` so each switch before it keeps its id. Compiled out, its thirty-four vectors
|
|
15
|
+
* are not declared and the call is a constant.
|
|
16
|
+
*/
|
|
17
|
+
export function baseDecalsGlsl(on: boolean): string {
|
|
18
|
+
if (!on) {
|
|
19
|
+
return /* glsl */ `
|
|
20
|
+
const bool BASE_DECALS = false; // wgsl:override
|
|
21
|
+
vec3 baseDecals(vec3 base, vec3 world, vec3 geometric) { return base; }
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
24
|
+
return /* glsl */ `
|
|
25
|
+
/* Whether this program lays decals into base colours. Off but where a frame has laid one. */
|
|
26
|
+
const bool BASE_DECALS = true; // wgsl:override
|
|
27
|
+
/*
|
|
28
|
+
* A header, how many decals, the atlas row their band starts at and its squares' side (0); then
|
|
29
|
+
* four vectors a decal, eight decals: the three rows taking a world point into its box, x across,
|
|
30
|
+
* y up the image, z along the projection, and its image, opacity and facing cosine.
|
|
31
|
+
*/
|
|
32
|
+
uniform vec4 uBaseDecals[33]; // wgsl:material
|
|
33
|
+
/* x 1 where the material refuses decals, so an unwritten zero receives them. */
|
|
34
|
+
uniform vec4 uDecalReceiver; // wgsl:material
|
|
35
|
+
|
|
36
|
+
vec3 decalLinear(vec3 c) {
|
|
37
|
+
return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
vec3 baseDecals(vec3 base, vec3 world, vec3 geometric) {
|
|
41
|
+
if (uDecalReceiver.x > 0.5) return base;
|
|
42
|
+
int count = int(uBaseDecals[0].x + 0.5);
|
|
43
|
+
vec2 atlas = vec2(textureSize(uFixtureAtlas, 0));
|
|
44
|
+
float span = uBaseDecals[0].z;
|
|
45
|
+
/* Half a texel of a square, so a bilinear read at its edge cannot reach the next one. */
|
|
46
|
+
float inset = 0.5 / span;
|
|
47
|
+
for (int i = 0; i < 8; i++) {
|
|
48
|
+
if (i >= count) break;
|
|
49
|
+
vec4 across = uBaseDecals[1 + i * 4];
|
|
50
|
+
vec4 up = uBaseDecals[2 + i * 4];
|
|
51
|
+
vec4 along = uBaseDecals[3 + i * 4];
|
|
52
|
+
vec4 laid = uBaseDecals[4 + i * 4];
|
|
53
|
+
vec3 local = vec3(
|
|
54
|
+
dot(across.xyz, world) + across.w,
|
|
55
|
+
dot(up.xyz, world) + up.w,
|
|
56
|
+
dot(along.xyz, world) + along.w
|
|
57
|
+
);
|
|
58
|
+
if (any(greaterThan(abs(local), vec3(1.0)))) continue;
|
|
59
|
+
if (dot(geometric, -normalize(along.xyz)) < laid.z) continue;
|
|
60
|
+
vec2 face = vec2(local.x, -local.y) * 0.5 + 0.5;
|
|
61
|
+
vec2 at = vec2(
|
|
62
|
+
(laid.x + clamp(face.x, inset, 1.0 - inset)) * span / atlas.x,
|
|
63
|
+
(uBaseDecals[0].y + clamp(face.y, inset, 1.0 - inset) * span) / atlas.y
|
|
64
|
+
);
|
|
65
|
+
vec4 texel = textureLod(uFixtureAtlas, at, 0.0);
|
|
66
|
+
base = mix(base, decalLinear(texel.rgb), clamp(texel.a * laid.y, 0.0, 1.0));
|
|
67
|
+
}
|
|
68
|
+
return base;
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A material's glow moved and masked: `SurfaceMaterial.emissiveScroll` and `emissiveMaskLayer`,
|
|
3
|
+
* packed into `uEmissiveFlow` by `emissiveFlow.ts`, where what each gives up is said.
|
|
4
|
+
*
|
|
5
|
+
* **The scroll** is added to wherever the emissive map is read, the mesh's coordinates, a layer's
|
|
6
|
+
* place or a projection's planes, as its own repeats a second times the surface clock, wrapped to
|
|
7
|
+
* one repeat. **The mask** is the alpha of a layer of the emissive map's own array at the mesh's
|
|
8
|
+
* first coordinates as they are: the vertex stage has already laid the material's repeat and offset
|
|
9
|
+
* on `vUv`, so the second vector holds their inverse, which gives the mesh's own back.
|
|
10
|
+
*
|
|
11
|
+
* **A lit switch, `EMISSIVE_FLOW`**, off until a material first asks, declared after
|
|
12
|
+
* `SURFACE_DETAIL` so each switch before it keeps its id. Compiled out, its two vectors are not
|
|
13
|
+
* declared and the shift and the mask are constants.
|
|
14
|
+
*/
|
|
15
|
+
export function emissiveFlowGlsl(on: boolean): string {
|
|
16
|
+
if (!on) {
|
|
17
|
+
return /* glsl */ `
|
|
18
|
+
const bool EMISSIVE_FLOW = false; // wgsl:override
|
|
19
|
+
vec2 emissiveShift() { return vec2(0.0); }
|
|
20
|
+
float emissiveMask() { return 1.0; }
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
return /* glsl */ `
|
|
24
|
+
/* Whether this program moves and masks a material's glow. Off but where one has asked. */
|
|
25
|
+
const bool EMISSIVE_FLOW = true; // wgsl:override
|
|
26
|
+
/*
|
|
27
|
+
* Two vectors, as emissiveFlow.ts packs them: the scroll in repeats a second on u and v (x y) and
|
|
28
|
+
* the mask's layer of the emissive array plus one, 0 for none (z); then what takes the material's
|
|
29
|
+
* coordinates back to the mesh's own, a scale (x y) and an offset (z w).
|
|
30
|
+
*/
|
|
31
|
+
uniform vec4 uEmissiveFlow[2]; // wgsl:material
|
|
32
|
+
|
|
33
|
+
/* The emissive map's coordinates moved by the clock, wrapped to one repeat for precision. */
|
|
34
|
+
vec2 emissiveShift() {
|
|
35
|
+
return fract(uEmissiveFlow[0].xy * uSurfaceScene.x);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* The mask's alpha at the mesh's own coordinates, under the branch on a bound emissive map. */
|
|
39
|
+
float emissiveMask() {
|
|
40
|
+
if (uEmissiveFlow[0].z < 0.5) return 1.0;
|
|
41
|
+
vec2 own = vUv.xy * uEmissiveFlow[1].xy + uEmissiveFlow[1].zw;
|
|
42
|
+
return texture(uEmissiveMap, vec3(own, uEmissiveFlow[0].z - 1.0)).a;
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
@@ -41,7 +41,10 @@ export type LitSwitch =
|
|
|
41
41
|
| 'MOVING_SUN'
|
|
42
42
|
| 'LAYERED'
|
|
43
43
|
| 'LAYER_LOOKS'
|
|
44
|
-
| 'SURFACE_DETAIL'
|
|
44
|
+
| 'SURFACE_DETAIL'
|
|
45
|
+
| 'EMISSIVE_FLOW'
|
|
46
|
+
| 'IRIDESCENT_OVERLAY'
|
|
47
|
+
| 'BASE_DECALS';
|
|
45
48
|
|
|
46
49
|
/** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
|
|
47
50
|
export function noLitSwitches(): Record<LitSwitch, boolean> {
|
|
@@ -59,6 +62,9 @@ export function noLitSwitches(): Record<LitSwitch, boolean> {
|
|
|
59
62
|
LAYERED: false,
|
|
60
63
|
LAYER_LOOKS: false,
|
|
61
64
|
SURFACE_DETAIL: false,
|
|
65
|
+
EMISSIVE_FLOW: false,
|
|
66
|
+
IRIDESCENT_OVERLAY: false,
|
|
67
|
+
BASE_DECALS: false,
|
|
62
68
|
};
|
|
63
69
|
}
|
|
64
70
|
|
|
@@ -25,6 +25,9 @@ import { reflectionSurfaceGlsl } from './reflectionSurface.ts';
|
|
|
25
25
|
import { worldUvGlsl } from './worldUv.ts';
|
|
26
26
|
import { movingSunGlsl } from './movingSun.ts';
|
|
27
27
|
import { detailGlsl } from './detail.ts';
|
|
28
|
+
import { emissiveFlowGlsl } from './emissiveFlow.ts';
|
|
29
|
+
import { iridescentOverlayGlsl } from './iridescentOverlay.ts';
|
|
30
|
+
import { baseDecalsGlsl } from './baseDecals.ts';
|
|
28
31
|
import { layeredGlsl } from './layered.ts';
|
|
29
32
|
import { MAP_READS_GLSL } from './mapReads.ts';
|
|
30
33
|
import { CHANNEL_BEND, LIT_CHANNEL_ATTRIBUTE } from '../vertexChannel.ts';
|
|
@@ -545,6 +548,22 @@ export interface FlatShaderOptions {
|
|
|
545
548
|
* (\`SurfaceMaterial.detail\`). True when absent. See \`detailGlsl\`.
|
|
546
549
|
*/
|
|
547
550
|
readonly surfaceDetail?: boolean;
|
|
551
|
+
/**
|
|
552
|
+
* Whether it moves and masks a material's glow, `uEmissiveFlow` and all
|
|
553
|
+
* (`SurfaceMaterial.emissiveScroll` and `emissiveMaskLayer`). True when absent. See
|
|
554
|
+
* `emissiveFlowGlsl`.
|
|
555
|
+
*/
|
|
556
|
+
readonly emissiveFlow?: boolean;
|
|
557
|
+
/**
|
|
558
|
+
* Whether it lays a material's iridescent overlay on the base colour, `uIridescence` and all
|
|
559
|
+
* (`SurfaceMaterial.iridescentOverlay`). True when absent. See `iridescentOverlayGlsl`.
|
|
560
|
+
*/
|
|
561
|
+
readonly iridescentOverlay?: boolean;
|
|
562
|
+
/**
|
|
563
|
+
* Whether it lays the frame's decals into base colours, `uBaseDecals` and all
|
|
564
|
+
* (`setBaseDecals`). True when absent. See `baseDecalsGlsl`.
|
|
565
|
+
*/
|
|
566
|
+
readonly baseDecals?: boolean;
|
|
548
567
|
/**
|
|
549
568
|
* How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
|
|
550
569
|
*
|
|
@@ -700,6 +719,12 @@ export function flatFrag(options: FlatShaderOptions): string {
|
|
|
700
719
|
layeredGlsl(options.layered ?? true, options.layerLooks ?? true),
|
|
701
720
|
/* After the layers, so its switch takes the next id; main calls it. See detail.ts. */
|
|
702
721
|
detailGlsl(options.surfaceDetail ?? true),
|
|
722
|
+
/* After the detail, so its switch takes the next id; the map reads call it. emissiveFlow.ts. */
|
|
723
|
+
emissiveFlowGlsl(options.emissiveFlow ?? true),
|
|
724
|
+
/* After the glow's flow, so its switch takes the next id; main calls it. iridescentOverlay.ts. */
|
|
725
|
+
iridescentOverlayGlsl(options.iridescentOverlay ?? true),
|
|
726
|
+
/* After the overlay, so its switch takes the next id; main calls it. baseDecals.ts. */
|
|
727
|
+
baseDecalsGlsl(options.baseDecals ?? true),
|
|
703
728
|
/* Where each map is read, after both of the ways it can be read otherwise. See mapReads.ts. */
|
|
704
729
|
MAP_READS_GLSL,
|
|
705
730
|
/* After the frame and the projection it calls, unconditional for the same reason as the
|