@solidrt/3d 0.0.58 → 0.0.59
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/AGENTS.md +734 -264
- package/README.md +4 -2
- package/examples/README.md +50 -4
- package/examples/bloom.tsx +113 -0
- package/examples/cascades.tsx +4 -4
- package/examples/collision.tsx +6 -6
- package/examples/exits.tsx +96 -0
- package/examples/first-person.tsx +5 -5
- package/examples/fleet.tsx +4 -4
- package/examples/fog.tsx +5 -5
- package/examples/instanced.tsx +70 -50
- package/examples/lamps.tsx +4 -4
- package/examples/lod.tsx +125 -0
- package/examples/materials.tsx +9 -9
- package/examples/morph.tsx +174 -0
- package/examples/normals.tsx +118 -0
- package/examples/{lit.tsx → phong.tsx} +9 -9
- package/examples/polyhedra.tsx +61 -0
- package/examples/scene-atlas.tsx +70 -0
- package/examples/scene-views.tsx +5 -5
- package/examples/shadows.tsx +9 -9
- package/examples/sky-lit.tsx +3 -3
- package/examples/skybox.tsx +5 -5
- package/examples/standard.tsx +2 -2
- package/examples/streams.tsx +122 -0
- package/examples/sweep-paths.tsx +5 -5
- package/examples/wireframe.tsx +24 -2
- package/package.json +3 -3
- package/src/clip.ts +18 -5
- package/src/color.ts +11 -2
- package/src/components/context.tsx +11 -2
- package/src/components/directional-light.tsx +2 -2
- package/src/components/first-person-camera.tsx +4 -4
- package/src/components/group.tsx +9 -5
- package/src/components/hemisphere-light.tsx +2 -2
- package/src/components/index.ts +4 -1
- package/src/components/instanced-mesh.tsx +28 -10
- package/src/components/lod.tsx +102 -0
- package/src/components/mesh.tsx +24 -6
- package/src/components/node-props.ts +35 -5
- package/src/components/orbit-camera.tsx +15 -12
- package/src/components/point-light.tsx +2 -2
- package/src/components/record-mesh.tsx +12 -7
- package/src/components/scene.tsx +55 -2
- package/src/components/spot-light.tsx +2 -2
- package/src/components/sprite.tsx +2 -2
- package/src/components/view3d.tsx +18 -1
- package/src/environment.ts +9 -8
- package/src/first-person.ts +125 -38
- package/src/geometry-gpu.ts +172 -39
- package/src/geometry.ts +1573 -239
- package/src/glsl.ts +398 -113
- package/src/gltf.ts +363 -141
- package/src/index.ts +31 -21
- package/src/light.ts +7 -8
- package/src/lod.ts +148 -0
- package/src/material.ts +316 -202
- package/src/mesh.ts +490 -256
- package/src/mixer.ts +27 -14
- package/src/model-file.ts +54 -15
- package/src/model.ts +86 -21
- package/src/motion.ts +17 -0
- package/src/node.ts +421 -22
- package/src/orbit.ts +378 -116
- package/src/profile.ts +4 -25
- package/src/resolve.ts +181 -0
- package/src/scene-shadows.ts +94 -93
- package/src/scene.ts +605 -185
- package/src/sweep.ts +27 -8
- package/tools/model.ts +3 -1
package/src/glsl.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The exported lighting GLSL: string constants an app composes into its
|
|
2
2
|
// own shaderMaterial sources with plain template literals - the same
|
|
3
3
|
// pieces the package's lit materials are built from (sceneSource is the
|
|
4
|
-
// whole scene in one set; the
|
|
4
|
+
// whole scene in one set; the phong, standard and unlit programs are
|
|
5
5
|
// assembled from it), so a custom material never becomes second-class
|
|
6
6
|
// (no preprocessor, no include resolver; the policy is argued in
|
|
7
7
|
// okf/notes/3d-differentiators.md).
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// every function returns its raw term, weighting and color belong to the
|
|
17
17
|
// caller.
|
|
18
18
|
|
|
19
|
-
import type { CullMode,
|
|
19
|
+
import type { CullMode, VertexAttribute } from "@solidrt/core/gpu"
|
|
20
20
|
|
|
21
21
|
// core/gpu's glsl tag, aliased locally (it is String.raw) so this module
|
|
22
22
|
// stays runtime-pure: no flux:gpu import, so the checks/ rigs and any
|
|
@@ -24,7 +24,7 @@ import type { CullMode, InstanceAttribute } from "@solidrt/core/gpu"
|
|
|
24
24
|
let glsl = String.raw
|
|
25
25
|
|
|
26
26
|
/** The cap of the scene's light list (directional, spot and point nodes
|
|
27
|
-
* alike; the hemisphere ambient is not in it) and of the
|
|
27
|
+
* alike; the hemisphere ambient is not in it) and of the stock lit fragments;
|
|
28
28
|
* a custom fragment composes LIGHT_SLOTS and LIGHT_LOOKUP and loops to
|
|
29
29
|
* `uLightCount`. A shader-source constant, so it is fixed for the app
|
|
30
30
|
* (see okf/backlog/app-runtime-config.md). */
|
|
@@ -44,29 +44,31 @@ export const MAX_CASCADES = 4
|
|
|
44
44
|
* every light is a fully cascaded sun. */
|
|
45
45
|
export const MAX_SHADOW_MAPS = 8
|
|
46
46
|
|
|
47
|
-
/** The
|
|
48
|
-
* (`shaderMaterialClass({
|
|
49
|
-
* })`): the four vec4 columns of the
|
|
50
|
-
* writes - the instance's placement
|
|
51
|
-
* addInstance). Read them through
|
|
52
|
-
*
|
|
53
|
-
* record, see INSTANCE_COLOR_ATTRIBUTES). */
|
|
54
|
-
export const INSTANCE_MATRIX_ATTRIBUTES:
|
|
55
|
-
{ name: "iModel0", format: "
|
|
56
|
-
{ name: "iModel1", format: "
|
|
57
|
-
{ name: "iModel2", format: "
|
|
58
|
-
{ name: "iModel3", format: "
|
|
47
|
+
/** The record layout an instanced mesh's material declares as its first
|
|
48
|
+
* instance buffer (`shaderMaterialClass({ instanceBuffers: [{ attributes:
|
|
49
|
+
* INSTANCE_MATRIX_ATTRIBUTES }] })`): the four vec4 columns of the
|
|
50
|
+
* per-instance matrix the spatial core writes - the instance's placement
|
|
51
|
+
* inside the mesh (createInstancedMesh, addInstance). Read them through
|
|
52
|
+
* INSTANCE_MATRIX. An app's own per-instance values go in the second
|
|
53
|
+
* instance buffer (the style record, see INSTANCE_COLOR_ATTRIBUTES). */
|
|
54
|
+
export const INSTANCE_MATRIX_ATTRIBUTES: VertexAttribute[] = [
|
|
55
|
+
{ name: "iModel0", format: "float32x4" },
|
|
56
|
+
{ name: "iModel1", format: "float32x4" },
|
|
57
|
+
{ name: "iModel2", format: "float32x4" },
|
|
58
|
+
{ name: "iModel3", format: "float32x4" },
|
|
59
59
|
]
|
|
60
60
|
|
|
61
61
|
/** The per-instance color the stock materials read with `instanceColors`
|
|
62
62
|
* (`in vec4 iColor`, premultiplied linear like aColor, forwarded as
|
|
63
|
-
* vColor):
|
|
64
|
-
* app-owned, written with setInstanceStyle(instance, [r,
|
|
65
|
-
* the core's matrix in
|
|
66
|
-
* layout of its own)
|
|
67
|
-
* start every instance at white.
|
|
68
|
-
*
|
|
69
|
-
|
|
63
|
+
* vColor): the second instance buffer's layout, so it is the instance's
|
|
64
|
+
* STYLE record - app-owned, written with setInstanceStyle(instance, [r,
|
|
65
|
+
* g, b, a]) - beside the core's matrix in the first. A custom class
|
|
66
|
+
* declares it (or any style layout of its own) as its second instance
|
|
67
|
+
* buffer; the stock materials start every instance at white. Stored as
|
|
68
|
+
* half floats: 8 bytes per instance, and a linear color at 10-bit
|
|
69
|
+
* mantissa precision shows none of the banding an 8-bit unorm would in
|
|
70
|
+
* the darks. Three's instanceColor, Godot's MultiMesh instance color. */
|
|
71
|
+
export const INSTANCE_COLOR_ATTRIBUTES: VertexAttribute[] = [{ name: "iColor", format: "float16x4" }]
|
|
70
72
|
|
|
71
73
|
/**
|
|
72
74
|
* The vertex-stage declarations over INSTANCE_MATRIX_ATTRIBUTES:
|
|
@@ -106,7 +108,9 @@ export const INSTANCE_MATRIX = glsl`
|
|
|
106
108
|
* `normalize(uCamPos - vWorldPos)`.
|
|
107
109
|
*/
|
|
108
110
|
/** The skinned-layout declarations a vertex stage splices in: the
|
|
109
|
-
* aJoints
|
|
111
|
+
* aJoints (integer input, fed by the layout's uint8x4 or uint16x4 joint
|
|
112
|
+
* channel) and aWeights channels, the `uBones` palette and `mat4
|
|
113
|
+
* boneAt(uint)`.
|
|
110
114
|
* The palette is a float texture, not a uniform array, so rig size is
|
|
111
115
|
* bounded only by texture height (>= 2048 everywhere), never by the
|
|
112
116
|
* vertex uniform budget: an rgba32f texture 4 texels wide, one row per
|
|
@@ -115,12 +119,13 @@ export const INSTANCE_MATRIX = glsl`
|
|
|
115
119
|
* skinned mesh). Pair with
|
|
116
120
|
* SKIN_MATRIX in main(). */
|
|
117
121
|
export const SKIN_DECLS = glsl`
|
|
118
|
-
in
|
|
122
|
+
in uvec4 aJoints;
|
|
119
123
|
in vec4 aWeights;
|
|
120
124
|
uniform sampler2D uBones;
|
|
121
|
-
mat4 boneAt(
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
mat4 boneAt(uint j) {
|
|
126
|
+
int row = int(j);
|
|
127
|
+
return mat4(texelFetch(uBones, ivec2(0, row), 0), texelFetch(uBones, ivec2(1, row), 0),
|
|
128
|
+
texelFetch(uBones, ivec2(2, row), 0), texelFetch(uBones, ivec2(3, row), 0));
|
|
124
129
|
}
|
|
125
130
|
`
|
|
126
131
|
/** The linear-blend skin matrix, spliced into main() after SKIN_DECLS:
|
|
@@ -128,25 +133,81 @@ export const SKIN_DECLS = glsl`
|
|
|
128
133
|
* through `mat3(skin)` - exact for rigid bones, the standard
|
|
129
134
|
* approximation under bone scale. */
|
|
130
135
|
export const SKIN_MATRIX = glsl`
|
|
131
|
-
mat4 skin = aWeights.x * boneAt(
|
|
132
|
-
aWeights.z * boneAt(
|
|
136
|
+
mat4 skin = aWeights.x * boneAt(aJoints.x) + aWeights.y * boneAt(aJoints.y) +
|
|
137
|
+
aWeights.z * boneAt(aJoints.z) + aWeights.w * boneAt(aJoints.w);
|
|
133
138
|
`
|
|
134
139
|
|
|
140
|
+
/** The morph-target declarations a vertex stage splices in: `uMorphs`,
|
|
141
|
+
* the geometry's targets packed sparse by vertex (see MorphTargets in
|
|
142
|
+
* geometry.ts: a header texel per vertex, then two-texel entries), laid
|
|
143
|
+
* into rows of `uMorphWidth` texels; `uMorphWeights`, the weights texture
|
|
144
|
+
* (four weights per texel, one row per weights owner - the spatial core
|
|
145
|
+
* writes it from the node's register), read at row `uMorphRow +
|
|
146
|
+
* gl_InstanceID`: a plain mesh's one row, or on an instanced mesh the
|
|
147
|
+
* instance's own row, its record slot (the population's texture has a
|
|
148
|
+
* row per slot), so every copy morphs by its own weights with no
|
|
149
|
+
* per-record data. Pair with MORPH_APPLY in main(). The geometry's GPU
|
|
150
|
+
* buffers bind uMorphs, the mesh's morph owner (the population, when
|
|
151
|
+
* instanced) binds uMorphWeights and the two ints. */
|
|
152
|
+
export const MORPH_DECLS = glsl`
|
|
153
|
+
uniform sampler2D uMorphs;
|
|
154
|
+
uniform sampler2D uMorphWeights;
|
|
155
|
+
uniform int uMorphWidth;
|
|
156
|
+
uniform int uMorphRow;
|
|
157
|
+
vec4 morphTexel(int i) {
|
|
158
|
+
return texelFetch(uMorphs, ivec2(i % uMorphWidth, i / uMorphWidth), 0);
|
|
159
|
+
}
|
|
160
|
+
float morphWeight(int t) {
|
|
161
|
+
vec4 four = texelFetch(uMorphWeights, ivec2(t >> 2, uMorphRow + gl_InstanceID), 0);
|
|
162
|
+
int c = t & 3;
|
|
163
|
+
return c == 0 ? four.x : c == 1 ? four.y : c == 2 ? four.z : four.w;
|
|
164
|
+
}
|
|
165
|
+
`
|
|
166
|
+
/** The morph displacement, spliced into main() after MORPH_DECLS: walks
|
|
167
|
+
* this vertex's entries (gl_VertexID indexes the headers) and leaves
|
|
168
|
+
* `vec3 morphPos` and `vec3 morphNormal`, the base channels plus each
|
|
169
|
+
* target's delta times its weight - apply the skin matrix (when any) and
|
|
170
|
+
* uModel to THESE. Zero-weight targets still cost their fetches; a
|
|
171
|
+
* vertex no target moves costs one. The normal form needs `aNormal`;
|
|
172
|
+
* MORPH_APPLY_POSITION is the same walk for a stage without one. */
|
|
173
|
+
export const MORPH_APPLY = morphApply(true)
|
|
174
|
+
export const MORPH_APPLY_POSITION = morphApply(false)
|
|
175
|
+
|
|
176
|
+
function morphApply(normals: boolean): string {
|
|
177
|
+
return glsl`
|
|
178
|
+
vec3 morphPos = aPos;
|
|
179
|
+
${normals ? "vec3 morphNormal = aNormal;" : ""}
|
|
180
|
+
{
|
|
181
|
+
vec4 morphHead = morphTexel(gl_VertexID);
|
|
182
|
+
int morphAt = int(morphHead.x);
|
|
183
|
+
int morphCount = int(morphHead.y);
|
|
184
|
+
for (int k = 0; k < morphCount; k++) {
|
|
185
|
+
vec4 entry = morphTexel(morphAt + 2 * k);
|
|
186
|
+
float w = morphWeight(int(entry.x));
|
|
187
|
+
morphPos += w * entry.yzw;
|
|
188
|
+
${normals ? "morphNormal += w * morphTexel(morphAt + 2 * k + 1).xyz;" : ""}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
`
|
|
192
|
+
}
|
|
193
|
+
|
|
135
194
|
// The per-instance placement, spliced into a vertex stage's position and
|
|
136
|
-
// normal math for an instanced material: the
|
|
137
|
-
// the instance's matrix inside the mesh,
|
|
138
|
-
// instanced fleet poses every copy by the one
|
|
139
|
-
// each by its own record.
|
|
140
|
-
function instancedPosition(skinned: boolean, instanced: boolean): string {
|
|
141
|
-
let
|
|
195
|
+
// normal math for an instanced material: the morph (when any) and then
|
|
196
|
+
// the skin (when any) first, then the instance's matrix inside the mesh,
|
|
197
|
+
// then uModel - so a skinned instanced fleet poses every copy by the one
|
|
198
|
+
// shared palette and places each by its own record.
|
|
199
|
+
function instancedPosition(skinned: boolean, instanced: boolean, morphed = false): string {
|
|
200
|
+
let base = morphed ? "morphPos" : "aPos"
|
|
201
|
+
let local = skinned ? `(skin * vec4(${base}, 1.0))` : `vec4(${base}, 1.0)`
|
|
142
202
|
return instanced ? `uModel * (instanceMatrix() * ${local})` : `uModel * ${local}`
|
|
143
203
|
}
|
|
144
|
-
function instancedNormal(skinned: boolean, instanced: boolean): string {
|
|
145
|
-
let
|
|
204
|
+
function instancedNormal(skinned: boolean, instanced: boolean, morphed = false): string {
|
|
205
|
+
let base = morphed ? "morphNormal" : "aNormal"
|
|
206
|
+
let local = skinned ? `(mat3(skin) * ${base})` : base
|
|
146
207
|
return instanced ? `mat3(uNormal) * (instanceNormalMatrix() * ${local})` : `mat3(uNormal) * ${local}`
|
|
147
208
|
}
|
|
148
209
|
|
|
149
|
-
// The one lit vertex template: the
|
|
210
|
+
// The one lit vertex template: the base prefix always, aColor and
|
|
150
211
|
// aUV2 only when the fragment reads them (an `in` the source mentions
|
|
151
212
|
// makes the material require that channel, so the blocks must be absent,
|
|
152
213
|
// not inactive), the skin blocks only for a skinned material (they make
|
|
@@ -154,7 +215,7 @@ function instancedNormal(skinned: boolean, instanced: boolean): string {
|
|
|
154
215
|
// and color only for an instanced one. vColor carries the vertex color,
|
|
155
216
|
// the instance color, or their product. LIT_VERTEX / LIT_VERTEX_COLORED
|
|
156
217
|
// are its two named forms; litVertex(o) picks per option set.
|
|
157
|
-
function litVertexSource(colored: boolean, uv2: boolean, skinned: boolean, instanced: boolean, instanceColors: boolean): string {
|
|
218
|
+
function litVertexSource(colored: boolean, uv2: boolean, skinned: boolean, instanced: boolean, instanceColors: boolean, morphed = false): string {
|
|
158
219
|
let color = colored && instanceColors ? "aColor * iColor" : colored ? "aColor" : instanceColors ? "iColor" : ""
|
|
159
220
|
return glsl`
|
|
160
221
|
in vec3 aPos;
|
|
@@ -162,6 +223,7 @@ function litVertexSource(colored: boolean, uv2: boolean, skinned: boolean, insta
|
|
|
162
223
|
in vec2 aUV;
|
|
163
224
|
${colored ? "in vec4 aColor;" : ""}
|
|
164
225
|
${uv2 ? "in vec2 aUV2;" : ""}
|
|
226
|
+
${morphed ? MORPH_DECLS : ""}
|
|
165
227
|
${skinned ? SKIN_DECLS : ""}
|
|
166
228
|
${instanced ? INSTANCE_MATRIX : ""}
|
|
167
229
|
${instanceColors ? "in vec4 iColor;" : ""}
|
|
@@ -175,11 +237,12 @@ function litVertexSource(colored: boolean, uv2: boolean, skinned: boolean, insta
|
|
|
175
237
|
${uv2 ? "out vec2 vUv2;" : ""}
|
|
176
238
|
|
|
177
239
|
void main() {
|
|
240
|
+
${morphed ? MORPH_APPLY : ""}
|
|
178
241
|
${skinned ? SKIN_MATRIX : ""}
|
|
179
|
-
vec4 world = ${instancedPosition(skinned, instanced)};
|
|
242
|
+
vec4 world = ${instancedPosition(skinned, instanced, morphed)};
|
|
180
243
|
gl_Position = uViewProj * world;
|
|
181
244
|
vWorldPos = world.xyz;
|
|
182
|
-
vNormal = ${instancedNormal(skinned, instanced)};
|
|
245
|
+
vNormal = ${instancedNormal(skinned, instanced, morphed)};
|
|
183
246
|
vUv = aUV;
|
|
184
247
|
${color ? `vColor = ${color};` : ""}
|
|
185
248
|
${uv2 ? "vUv2 = aUV2;" : ""}
|
|
@@ -308,7 +371,7 @@ export const PBR = glsl`
|
|
|
308
371
|
* folded in) and `uLightParams[i]` (cosInner, cosOuter, distance, decay
|
|
309
372
|
* - the cone cosines a spot fades between, the falloff cutoff and
|
|
310
373
|
* exponent a spot or point attenuates by; unused for a directional).
|
|
311
|
-
* Compose it before LIGHT_LOOKUP;
|
|
374
|
+
* Compose it before LIGHT_LOOKUP; the stock lit program is the shape.
|
|
312
375
|
*/
|
|
313
376
|
export const LIGHT_SLOTS = glsl`
|
|
314
377
|
uniform int uLightCount;
|
|
@@ -331,7 +394,7 @@ export const LIGHT_SLOTS = glsl`
|
|
|
331
394
|
* zero at `distance` (0 = no cutoff; Three's punctual-light falloff), a
|
|
332
395
|
* spot's additionally faded across its cone from cosInner to cosOuter.
|
|
333
396
|
* Zero means the light cannot reach the fragment - skip its shadow
|
|
334
|
-
* lookup and its terms (
|
|
397
|
+
* lookup and its terms (the stock lit program does exactly that).
|
|
335
398
|
*/
|
|
336
399
|
export const LIGHT_LOOKUP = glsl`
|
|
337
400
|
// Floors the falloff divisors so a fragment at the light's own
|
|
@@ -447,7 +510,7 @@ export const FOG = glsl`
|
|
|
447
510
|
* and `vec3 linearToSrgb(vec3)`, clamped at zero. An "rgba8-srgb" texture
|
|
448
511
|
* decodes in hardware and needs neither; these serve pixels a format
|
|
449
512
|
* cannot tag (a render target sampled as a map) and the output stage.
|
|
450
|
-
*
|
|
513
|
+
* RESOLVE includes this set - never compose both.
|
|
451
514
|
*/
|
|
452
515
|
export const SRGB = glsl`
|
|
453
516
|
vec3 srgbToLinear(vec3 c) {
|
|
@@ -461,25 +524,48 @@ export const SRGB = glsl`
|
|
|
461
524
|
`
|
|
462
525
|
|
|
463
526
|
/**
|
|
464
|
-
* The
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
* alpha);` - a fragment that writes fragColor directly writes final
|
|
473
|
-
* encoded pixels and skips exposure and tone mapping. Includes SRGB.
|
|
527
|
+
* The LOD cross-fade: `uLodFade` is (threshold, side) - the scene writes
|
|
528
|
+
* the band position of the level this entry draws (see createLod's
|
|
529
|
+
* `fade`), `[1, 1]` when solid. Inside a band the nearer level keeps the
|
|
530
|
+
* fragments whose screen hash falls below the threshold (side 1) and the
|
|
531
|
+
* farther level the rest (side -1): a partition, so nothing draws twice
|
|
532
|
+
* and nothing is missed. Call `lodFade()` first thing in main. Every
|
|
533
|
+
* stock fragment composes it; a custom class that declares `uLodFade`
|
|
534
|
+
* opts in (the scene detects the name, like uNormal).
|
|
474
535
|
*/
|
|
475
|
-
export const
|
|
536
|
+
export const LOD_FADE = glsl`
|
|
537
|
+
uniform vec2 uLodFade;
|
|
538
|
+
// Interleaved gradient noise (Jimenez 2014): a screen-position hash
|
|
539
|
+
// whose pattern stays put as the band moves, so the dissolve reads as
|
|
540
|
+
// a steady grain rather than a crawl.
|
|
541
|
+
float lodHash(vec2 p) {
|
|
542
|
+
return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715))));
|
|
543
|
+
}
|
|
544
|
+
void lodFade() {
|
|
545
|
+
if (uLodFade.x < 1.0) {
|
|
546
|
+
if ((lodHash(gl_FragCoord.xy) - uLodFade.x) * uLodFade.y >= 0.0) discard;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
`
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* The resolve set: the display stage of a target, run ONCE per pixel by
|
|
553
|
+
* the resolve pass that turns a scene buffer (premultiplied linear light,
|
|
554
|
+
* half float where the device renders it) into the displayed rgba8
|
|
555
|
+
* pixels. `vec4 resolveColor(vec3 rgb, float alpha)` takes a buffer
|
|
556
|
+
* texel, applies the scene's exposure and tone mapping (`uniform float
|
|
557
|
+
* uExposure`; `uniform float uToneMapping`, 0 none, 1 ACES, 2 AgX, 3
|
|
558
|
+
* Neutral - the shared params scene.setExposure / setToneMapping
|
|
559
|
+
* write), clamps, encodes to
|
|
560
|
+
* sRGB with an ordered dither against 8-bit banding, and returns the
|
|
561
|
+
* premultiplied pixel. It un-premultiplies inside so the encode is exact
|
|
562
|
+
* at partial alpha. A custom resolve (SceneOptions.resolve) gets this set
|
|
563
|
+
* declared and ends with it; a scene fragment never composes it - a
|
|
564
|
+
* fragment writes linear light (see sceneOutput). Includes SRGB.
|
|
565
|
+
*/
|
|
566
|
+
export const RESOLVE = glsl`
|
|
476
567
|
uniform float uExposure;
|
|
477
568
|
uniform float uToneMapping;
|
|
478
|
-
// 1 (the scene's default) clamps and encodes to sRGB for display; 0
|
|
479
|
-
// writes linear light unclamped, what a reflection probe's faces hold
|
|
480
|
-
// so the environment lookups read radiance (the scene sets it per
|
|
481
|
-
// target; an 8-bit target clamps it on write regardless).
|
|
482
|
-
uniform float uOutputEncode;
|
|
483
569
|
${SRGB}
|
|
484
570
|
// Stephen Hill's fit of the ACES RRT and ODT (Three's ACESFilmic, Godot's
|
|
485
571
|
// ACES): the matrices between sRGB and the fit's working space, and the
|
|
@@ -503,18 +589,176 @@ export const OUTPUT = glsl`
|
|
|
503
589
|
c = acesFit(c);
|
|
504
590
|
return clamp(ACES_OUTPUT * c, 0.0, 1.0);
|
|
505
591
|
}
|
|
506
|
-
|
|
592
|
+
// AgX (Blender's, through Filament and Three's AgXToneMapping): rec
|
|
593
|
+
// 2020 primaries in, an inset toward gray, log2 encoding over the
|
|
594
|
+
// scene's dynamic range, the sigmoid, the outset, back to sRGB
|
|
595
|
+
// primaries. The 6th-order fit of the sigmoid is the iolite
|
|
596
|
+
// approximation Three ships.
|
|
597
|
+
const mat3 AGX_SRGB_TO_REC2020 = mat3(
|
|
598
|
+
0.6274, 0.0691, 0.0164,
|
|
599
|
+
0.3293, 0.9195, 0.0880,
|
|
600
|
+
0.0433, 0.0113, 0.8956);
|
|
601
|
+
const mat3 AGX_REC2020_TO_SRGB = mat3(
|
|
602
|
+
1.6605, -0.1246, -0.0182,
|
|
603
|
+
-0.5876, 1.1329, -0.1006,
|
|
604
|
+
-0.0728, -0.0083, 1.1187);
|
|
605
|
+
const mat3 AGX_INSET = mat3(
|
|
606
|
+
0.856627153315983, 0.137318972929847, 0.11189821299995,
|
|
607
|
+
0.0951212405381588, 0.761241990602591, 0.0767994186031903,
|
|
608
|
+
0.0482516061458583, 0.101439036467562, 0.811302368396859);
|
|
609
|
+
const mat3 AGX_OUTSET = mat3(
|
|
610
|
+
1.1271005818144368, -0.1413297634984383, -0.14132976349843826,
|
|
611
|
+
-0.11060664309660323, 1.157823702216272, -0.11060664309660294,
|
|
612
|
+
-0.016493938717834573, -0.016493938717834257, 1.2519364065950405);
|
|
613
|
+
// log2 of middle gray (0.18) at the range's ends, 10 stops below and
|
|
614
|
+
// 6.5 above.
|
|
615
|
+
const float AGX_MIN_EV = -12.47393;
|
|
616
|
+
const float AGX_MAX_EV = 4.026069;
|
|
617
|
+
// The encoded value's display gamma before the primaries go back.
|
|
618
|
+
const float AGX_GAMMA = 2.2;
|
|
619
|
+
vec3 agxSigmoid(vec3 x) {
|
|
620
|
+
vec3 x2 = x * x;
|
|
621
|
+
vec3 x4 = x2 * x2;
|
|
622
|
+
return 15.5 * x4 * x2 - 40.14 * x4 * x + 31.96 * x4 - 6.868 * x2 * x + 0.4298 * x2 + 0.1191 * x - 0.00232;
|
|
623
|
+
}
|
|
624
|
+
vec3 agxToneMap(vec3 c) {
|
|
625
|
+
c = AGX_INSET * (AGX_SRGB_TO_REC2020 * c);
|
|
626
|
+
c = log2(max(c, vec3(1e-10)));
|
|
627
|
+
c = clamp((c - AGX_MIN_EV) / (AGX_MAX_EV - AGX_MIN_EV), 0.0, 1.0);
|
|
628
|
+
c = AGX_OUTSET * agxSigmoid(c);
|
|
629
|
+
c = pow(max(c, vec3(0.0)), vec3(AGX_GAMMA));
|
|
630
|
+
return clamp(AGX_REC2020_TO_SRGB * c, 0.0, 1.0);
|
|
631
|
+
}
|
|
632
|
+
// Khronos PBR Neutral (Three's NeutralToneMapping, Unity's Neutral):
|
|
633
|
+
// hue-preserving, a black-level lift below a knee, then a soft peak
|
|
634
|
+
// compression above START_COMPRESSION that desaturates toward white.
|
|
635
|
+
const float NEUTRAL_START_COMPRESSION = 0.8 - 0.04;
|
|
636
|
+
const float NEUTRAL_DESATURATION = 0.15;
|
|
637
|
+
const float NEUTRAL_KNEE = 0.08;
|
|
638
|
+
const float NEUTRAL_KNEE_CURVE = 6.25;
|
|
639
|
+
const float NEUTRAL_OFFSET = 0.04;
|
|
640
|
+
vec3 neutralToneMap(vec3 c) {
|
|
641
|
+
float x = min(c.r, min(c.g, c.b));
|
|
642
|
+
float offset = x < NEUTRAL_KNEE ? x - NEUTRAL_KNEE_CURVE * x * x : NEUTRAL_OFFSET;
|
|
643
|
+
c -= offset;
|
|
644
|
+
float peak = max(c.r, max(c.g, c.b));
|
|
645
|
+
if (peak < NEUTRAL_START_COMPRESSION) return max(c, vec3(0.0));
|
|
646
|
+
float d = 1.0 - NEUTRAL_START_COMPRESSION;
|
|
647
|
+
float newPeak = 1.0 - d * d / (peak + d - NEUTRAL_START_COMPRESSION);
|
|
648
|
+
c *= newPeak / peak;
|
|
649
|
+
float g = 1.0 - 1.0 / (NEUTRAL_DESATURATION * (peak - newPeak) + 1.0);
|
|
650
|
+
return clamp(mix(c, vec3(newPeak), g), 0.0, 1.0);
|
|
651
|
+
}
|
|
652
|
+
// Debanding: one 8-bit step of interleaved gradient noise (Jimenez
|
|
653
|
+
// 2014) centered on zero, added to the encoded value before the target
|
|
654
|
+
// quantizes it, so a slow gradient dithers instead of stepping.
|
|
655
|
+
const float DITHER_STEP = 1.0 / 255.0;
|
|
656
|
+
float resolveDither(vec2 p) {
|
|
657
|
+
return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715)))) - 0.5;
|
|
658
|
+
}
|
|
659
|
+
vec4 resolveColor(vec3 rgb, float alpha) {
|
|
507
660
|
vec3 c = alpha > 0.0 ? rgb / alpha : rgb;
|
|
508
661
|
c *= uExposure;
|
|
509
|
-
if (uToneMapping >
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
662
|
+
if (uToneMapping > 2.5) c = neutralToneMap(c);
|
|
663
|
+
else if (uToneMapping > 1.5) c = agxToneMap(c);
|
|
664
|
+
else if (uToneMapping > 0.5) c = acesToneMap(c);
|
|
665
|
+
c = linearToSrgb(clamp(c, 0.0, 1.0));
|
|
666
|
+
c += resolveDither(gl_FragCoord.xy) * DITHER_STEP;
|
|
514
667
|
return vec4(c * alpha, alpha);
|
|
515
668
|
}
|
|
516
669
|
`
|
|
517
670
|
|
|
671
|
+
/**
|
|
672
|
+
* The default resolve fragment (SceneOptions.resolve when absent): one
|
|
673
|
+
* sample of the scene buffer through resolveColor. The contract a custom
|
|
674
|
+
* resolve gets: `vUV` (0..1, origin top-left), `iResolution`, `fragColor`,
|
|
675
|
+
* `uniform sampler2D uScene` (the buffer, premultiplied linear) and the
|
|
676
|
+
* RESOLVE set, all declared by the preamble; any name written through
|
|
677
|
+
* scene.setParams (uExposure and uToneMapping among them) is readable.
|
|
678
|
+
* Start a grade from this source and end in resolveColor.
|
|
679
|
+
*/
|
|
680
|
+
export const DEFAULT_RESOLVE = glsl`
|
|
681
|
+
void main() {
|
|
682
|
+
vec4 s = texture(uScene, vUV);
|
|
683
|
+
fragColor = resolveColor(s.rgb, s.a);
|
|
684
|
+
}
|
|
685
|
+
`
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* The default resolve while the stock bloom is on (SceneOptions.bloom):
|
|
689
|
+
* the buffer's texel plus the blurred excess the bloom chain rendered,
|
|
690
|
+
* bound on the resolve target as `uniform sampler2D uBloom` with its
|
|
691
|
+
* weight in `uniform float uBloomIntensity` - both declared by the
|
|
692
|
+
* source, not the preamble. The glow is LIGHT over the pixel whatever it
|
|
693
|
+
* covers: the alpha grows with the glow's brightness (a premultiplied
|
|
694
|
+
* light layer), so it shows over a transparent backdrop - a view with no
|
|
695
|
+
* clearColor composited over UI - and an opaque pixel is exactly the
|
|
696
|
+
* sum. A custom resolve composes the stock bloom by declaring the same
|
|
697
|
+
* two names and adding the term; start from this.
|
|
698
|
+
*/
|
|
699
|
+
export const BLOOM_RESOLVE = glsl`
|
|
700
|
+
uniform sampler2D uBloom;
|
|
701
|
+
uniform float uBloomIntensity;
|
|
702
|
+
void main() {
|
|
703
|
+
vec4 s = texture(uScene, vUV);
|
|
704
|
+
vec3 glow = texture(uBloom, vUV).rgb * uBloomIntensity;
|
|
705
|
+
float coverage = clamp(max(glow.r, max(glow.g, glow.b)), 0.0, 1.0);
|
|
706
|
+
fragColor = resolveColor(s.rgb + glow, max(s.a, coverage));
|
|
707
|
+
}
|
|
708
|
+
`
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* The stock bloom's threshold pass (a shader texture over the buffer):
|
|
712
|
+
* the texel's excess over `uniform float uThreshold` by its brightest
|
|
713
|
+
* channel, so a colored highlight keeps its hue, black below it.
|
|
714
|
+
*/
|
|
715
|
+
export const BLOOM_THRESHOLD = glsl`
|
|
716
|
+
uniform sampler2D uSource;
|
|
717
|
+
uniform float uThreshold;
|
|
718
|
+
void main() {
|
|
719
|
+
vec4 s = texture(uSource, vUV);
|
|
720
|
+
vec3 c = s.a > 0.0 ? s.rgb / s.a : s.rgb;
|
|
721
|
+
float peak = max(max(c.r, c.g), c.b);
|
|
722
|
+
float keep = max(peak - uThreshold, 0.0) / max(peak, 0.0001);
|
|
723
|
+
fragColor = vec4(c * keep, 1.0);
|
|
724
|
+
}
|
|
725
|
+
`
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* The stock bloom's blur pass: a 9-tap gaussian along `uniform vec2
|
|
729
|
+
* uDir` (one texel step on the given axis), run across then down per
|
|
730
|
+
* round of the chain.
|
|
731
|
+
*/
|
|
732
|
+
export const BLOOM_BLUR = glsl`
|
|
733
|
+
uniform sampler2D uSource;
|
|
734
|
+
uniform vec2 uDir;
|
|
735
|
+
const float WEIGHT[5] = float[5](0.2270270270, 0.1945945946, 0.1216216216, 0.0540540541, 0.0162162162);
|
|
736
|
+
void main() {
|
|
737
|
+
vec2 step = uDir / iResolution;
|
|
738
|
+
vec3 sum = texture(uSource, vUV).rgb * WEIGHT[0];
|
|
739
|
+
for (int i = 1; i < 5; i++) {
|
|
740
|
+
vec2 o = step * float(i);
|
|
741
|
+
sum += texture(uSource, vUV + o).rgb * WEIGHT[i];
|
|
742
|
+
sum += texture(uSource, vUV - o).rgb * WEIGHT[i];
|
|
743
|
+
}
|
|
744
|
+
fragColor = vec4(sum, 1.0);
|
|
745
|
+
}
|
|
746
|
+
`
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* A resolve source as a shader-texture fragment of your own: `uniform
|
|
750
|
+
* sampler2D uScene` and the RESOLVE set declared over `source` (default
|
|
751
|
+
* DEFAULT_RESOLVE), the engine preamble supplying vUV, iResolution and
|
|
752
|
+
* fragColor. The way an app resolves a buffer the library does not - the
|
|
753
|
+
* atlas its tiled views (`into`) render linear light into:
|
|
754
|
+
* `createShaderTexture(resolveFragment(), w, h, resolveParams(opts), {
|
|
755
|
+
* textures: { uScene: atlas } })`. A shader target validates its params
|
|
756
|
+
* strictly, so write both uExposure and uToneMapping (resolveParams does).
|
|
757
|
+
*/
|
|
758
|
+
export function resolveFragment(source: string = DEFAULT_RESOLVE): string {
|
|
759
|
+
return "uniform sampler2D uScene;\n" + RESOLVE + "\n" + source
|
|
760
|
+
}
|
|
761
|
+
|
|
518
762
|
/**
|
|
519
763
|
* The scene's environment as a reflecting program declares it: `uEnv`
|
|
520
764
|
* (a samplerCube the scene binds on every receiving target - a 1x1 black
|
|
@@ -539,7 +783,7 @@ export const OUTPUT = glsl`
|
|
|
539
783
|
* camera) at roughness `sqrt(2 / (shininess + 2))`, so a wide sheen
|
|
540
784
|
* reflects a blurred environment and a mirror dot a sharp one. `envWeight(n, v,
|
|
541
785
|
* reflectivity)` is the Schlick fresnel weight: `reflectivity` face-on,
|
|
542
|
-
* 1 at grazing, 0 with no environment. `
|
|
786
|
+
* 1 at grazing, 0 with no environment. `phong` applies them as
|
|
543
787
|
* `rgb = mix(rgb, envReflection(n, v, uShininess), envWeight(n, v,
|
|
544
788
|
* uReflectivity))` - Three's MixOperation with a fresnel weight. Every
|
|
545
789
|
* lookup is a plain `texture(uEnv, dir)` in world space: a cube map here
|
|
@@ -593,7 +837,8 @@ export const ENVIRONMENT = glsl`
|
|
|
593
837
|
* LIGHT_SLOTS, this, SHADOW, then SHADOW_LOOKUP (in that order - the
|
|
594
838
|
* lookup reads the light type and position for a point light's face
|
|
595
839
|
* select) and multiplies
|
|
596
|
-
* light i's term by `lightShadow(i, worldPos, n)` -
|
|
840
|
+
* light i's term by `lightShadow(i, worldPos, n)` - the stock lit program
|
|
841
|
+
* is the shape. A
|
|
597
842
|
* material that does not receive composes none of it, so it declares no
|
|
598
843
|
* sampler for nothing.
|
|
599
844
|
*/
|
|
@@ -731,7 +976,7 @@ export const SHADOW_LOOKUP = glsl`
|
|
|
731
976
|
|
|
732
977
|
/**
|
|
733
978
|
* The material defaults every Surface starts from (`surfaceOf`) and the
|
|
734
|
-
* `
|
|
979
|
+
* `phong`/`standard` options fall back to: one table, so a custom fragment
|
|
735
980
|
* that builds its own Surface and a stock material agree on what
|
|
736
981
|
* "unspecified" means.
|
|
737
982
|
*/
|
|
@@ -765,7 +1010,7 @@ export const SURFACE_DEFAULTS = {
|
|
|
765
1010
|
* `Surface surfaceOf(vec4 base, vec3 normal)` fills the rest with
|
|
766
1011
|
* SURFACE_DEFAULTS (no emissive, no ambient), so a fragment sets only
|
|
767
1012
|
* what it means. One struct for both light models, as Godot and Unity
|
|
768
|
-
* have it, so a `surface` function (
|
|
1013
|
+
* have it, so a `surface` function (phongFragment's slot) has one
|
|
769
1014
|
* signature whatever material it sits in; the model that shades reads
|
|
770
1015
|
* its own fields and ignores the other's. Included by sceneSource - never
|
|
771
1016
|
* compose both.
|
|
@@ -811,19 +1056,21 @@ export type SceneSourceOptions = {
|
|
|
811
1056
|
* diffuse and the split-sum specular. False declares no environment
|
|
812
1057
|
* cube and shades with the lights alone. Needs `lights`. */
|
|
813
1058
|
env?: boolean
|
|
814
|
-
/** Fog inside sceneOutput. False leaves the fog set out
|
|
815
|
-
|
|
1059
|
+
/** Fog inside sceneOutput. False leaves the fog set out; "additive"
|
|
1060
|
+
* composes fogAdditive instead of fog, the form a `blend: "add"` draw
|
|
1061
|
+
* needs (it fades toward black, not the fog color). */
|
|
1062
|
+
fog?: boolean | "additive"
|
|
816
1063
|
}
|
|
817
1064
|
|
|
818
1065
|
/**
|
|
819
1066
|
* The scene as a program declares it, in one set: everything a fragment
|
|
820
1067
|
* needs to be a citizen of the scene - lit by its lights, shadowed by its
|
|
821
|
-
* casters, reflecting its environment
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
1068
|
+
* casters, reflecting its environment and fogged like the stock
|
|
1069
|
+
* materials, because the stock materials are built from this very
|
|
1070
|
+
* source. It declares `uCamPos`, `uHemiSky` / `uHemiGround`, LIGHT_SLOTS
|
|
1071
|
+
* + LIGHT_LOOKUP, the SHADOW trio, the pure light functions (HEMISPHERE,
|
|
1072
|
+
* LAMBERT, BLINN_SPECULAR, PBR), ENVIRONMENT and FOG (per the flags),
|
|
1073
|
+
* SURFACE, and exports:
|
|
827
1074
|
*
|
|
828
1075
|
* struct SceneLight { vec3 dir; vec3 color; };
|
|
829
1076
|
* SceneLight sceneLight(int i, vec3 position, vec3 normal);
|
|
@@ -838,7 +1085,7 @@ export type SceneSourceOptions = {
|
|
|
838
1085
|
* `uLightCount` (URP's GetAdditionalLight, Godot's light() inputs), and
|
|
839
1086
|
* the one that makes a spot light impossible to render as a directional.
|
|
840
1087
|
*
|
|
841
|
-
* The shade functions are the whole light loop of `
|
|
1088
|
+
* The shade functions are the whole light loop of `phong` (Blinn-Phong)
|
|
842
1089
|
* and `standard` (GGX): hemisphere plus `Surface.ambient` plus every
|
|
843
1090
|
* light's diffuse and specular, the environment term, then the emissive,
|
|
844
1091
|
* returning linear PREMULTIPLIED rgb. A term added by hand after them is
|
|
@@ -848,8 +1095,12 @@ export type SceneSourceOptions = {
|
|
|
848
1095
|
* stage carries the world position in.
|
|
849
1096
|
*
|
|
850
1097
|
* `sceneOutput` is the tail every library fragment ends with: fog over
|
|
851
|
-
* the premultiplied color,
|
|
852
|
-
*
|
|
1098
|
+
* the premultiplied color, clamped at zero, and that is the pixel the
|
|
1099
|
+
* scene buffer holds - LINEAR light, unclamped above (a sun disc of 40
|
|
1100
|
+
* stays 40 in a half-float buffer). Exposure, tone mapping and the sRGB
|
|
1101
|
+
* encode happen once per pixel in the target's resolve pass (RESOLVE),
|
|
1102
|
+
* never in a scene fragment: a fragment that writes fragColor directly
|
|
1103
|
+
* writes linear premultiplied light too.
|
|
853
1104
|
*
|
|
854
1105
|
* ${SCENE}
|
|
855
1106
|
* void main() {
|
|
@@ -866,7 +1117,7 @@ export function sceneSource(o: SceneSourceOptions = {}): string {
|
|
|
866
1117
|
let lights = o.lights !== false
|
|
867
1118
|
let receiveShadow = lights && o.receiveShadow !== false
|
|
868
1119
|
let env = lights && o.env !== false
|
|
869
|
-
let fog = o.fog
|
|
1120
|
+
let fog = o.fog ?? true
|
|
870
1121
|
return glsl`
|
|
871
1122
|
${SURFACE}
|
|
872
1123
|
${lights || fog ? "uniform vec3 uCamPos;" : ""}
|
|
@@ -895,12 +1146,11 @@ export function sceneSource(o: SceneSourceOptions = {}): string {
|
|
|
895
1146
|
}
|
|
896
1147
|
${env ? ENVIRONMENT : ""}
|
|
897
1148
|
${fog ? FOG : ""}
|
|
898
|
-
${OUTPUT}
|
|
899
1149
|
${lights ? sceneShadeSource(receiveShadow, env) : ""}
|
|
900
1150
|
|
|
901
1151
|
vec4 sceneOutput(vec3 rgb, float alpha, vec3 position) {
|
|
902
|
-
${fog ? "rgb = fog(rgb, alpha, position, uCamPos);" : ""}
|
|
903
|
-
return
|
|
1152
|
+
${fog === "additive" ? "rgb = fogAdditive(rgb, position, uCamPos);" : fog ? "rgb = fog(rgb, alpha, position, uCamPos);" : ""}
|
|
1153
|
+
return vec4(max(rgb, vec3(0.0)), alpha);
|
|
904
1154
|
}
|
|
905
1155
|
`
|
|
906
1156
|
}
|
|
@@ -910,7 +1160,8 @@ export const SCENE = sceneSource()
|
|
|
910
1160
|
|
|
911
1161
|
// The light accessor and the two shade functions, over the sets
|
|
912
1162
|
// sceneSource composed before them. A light that cannot reach the point
|
|
913
|
-
// is skipped whole (no shadow tap, no terms), the gate
|
|
1163
|
+
// is skipped whole (no shadow tap, no terms), the gate the stock lit
|
|
1164
|
+
// program always had.
|
|
914
1165
|
function sceneShadeSource(receiveShadow: boolean, env: boolean): string {
|
|
915
1166
|
return glsl`
|
|
916
1167
|
struct SceneLight {
|
|
@@ -1021,8 +1272,10 @@ export type LitSourceOptions = {
|
|
|
1021
1272
|
* alpha falls below it (the cutoff is a per-entry uniform, so one
|
|
1022
1273
|
* program serves every value). */
|
|
1023
1274
|
alphaTest?: boolean
|
|
1024
|
-
/** Compose the scene's fog over the result (default true)
|
|
1025
|
-
|
|
1275
|
+
/** Compose the scene's fog over the result (default true); "additive"
|
|
1276
|
+
* for a `blend: "add"` program, which fades toward black instead of the
|
|
1277
|
+
* fog color (see FOG). */
|
|
1278
|
+
fog?: boolean | "additive"
|
|
1026
1279
|
/** Bend the lit normal by a tangent-space normal map (NORMAL_MAP:
|
|
1027
1280
|
* `uniform sampler2D uNormalMap` scaled by `uniform float
|
|
1028
1281
|
* uNormalScale`), sampled at the same uv as uMap. The frame comes from
|
|
@@ -1064,6 +1317,13 @@ export type LitSourceOptions = {
|
|
|
1064
1317
|
* the spatial flush writes it). The vertex stage then requires that layout
|
|
1065
1318
|
* and something must bind `uBones`; the fragment is unchanged. */
|
|
1066
1319
|
skinned?: boolean
|
|
1320
|
+
/** Displace positions and normals by the geometry's morph targets
|
|
1321
|
+
* (MORPH_DECLS and MORPH_APPLY, before the skin): the vertex stage then
|
|
1322
|
+
* declares `uMorphs`, `uMorphWeights`, `uMorphWidth` and `uMorphRow`,
|
|
1323
|
+
* which the scene binds from the geometry's packed targets and the
|
|
1324
|
+
* mesh's morph owner (a mesh whose geometry carries none is rejected
|
|
1325
|
+
* at add()); the fragment is unchanged. */
|
|
1326
|
+
morph?: boolean
|
|
1067
1327
|
/** Place each copy by its instance matrix (INSTANCE_MATRIX under
|
|
1068
1328
|
* uModel, normals through instanceNormalMatrix): the vertex stage of a
|
|
1069
1329
|
* material for createInstancedMesh. The class then declares
|
|
@@ -1104,7 +1364,7 @@ export type StandardSourceOptions = Omit<LitSourceOptions, "specularMap" | "env"
|
|
|
1104
1364
|
}
|
|
1105
1365
|
|
|
1106
1366
|
// The resolved option set: every flag concrete, every slot a string, so
|
|
1107
|
-
// the builders below never re-apply a default and
|
|
1367
|
+
// the builders below never re-apply a default and a stock material's class key and
|
|
1108
1368
|
// its program cannot drift apart.
|
|
1109
1369
|
type LitSource = {
|
|
1110
1370
|
map: boolean
|
|
@@ -1114,7 +1374,7 @@ type LitSource = {
|
|
|
1114
1374
|
receiveShadow: boolean
|
|
1115
1375
|
cull: CullMode
|
|
1116
1376
|
alphaTest: boolean
|
|
1117
|
-
fog: boolean
|
|
1377
|
+
fog: boolean | "additive"
|
|
1118
1378
|
normalMap: boolean
|
|
1119
1379
|
emissive: boolean
|
|
1120
1380
|
emissiveMap: boolean
|
|
@@ -1125,9 +1385,10 @@ type LitSource = {
|
|
|
1125
1385
|
skinned: boolean
|
|
1126
1386
|
instanced: boolean
|
|
1127
1387
|
instanceColors: boolean
|
|
1388
|
+
morph: boolean
|
|
1128
1389
|
prelude: string
|
|
1129
1390
|
surface: string
|
|
1130
|
-
// The light model: Blinn-Phong (`
|
|
1391
|
+
// The light model: Blinn-Phong (`phong`) or GGX metalness/roughness
|
|
1131
1392
|
// (`standard`, which also samples the environment unconditionally).
|
|
1132
1393
|
brdf: "blinn" | "ggx"
|
|
1133
1394
|
metalnessMap: boolean
|
|
@@ -1143,7 +1404,7 @@ function resolveLit(o: LitSourceOptions): LitSource {
|
|
|
1143
1404
|
receiveShadow: o.receiveShadow !== false,
|
|
1144
1405
|
cull: o.cull ?? "back",
|
|
1145
1406
|
alphaTest: o.alphaTest === true,
|
|
1146
|
-
fog: o.fog
|
|
1407
|
+
fog: o.fog ?? true,
|
|
1147
1408
|
normalMap: o.normalMap === true,
|
|
1148
1409
|
emissive: o.emissive === true || o.emissiveMap === true,
|
|
1149
1410
|
emissiveMap: o.emissiveMap === true,
|
|
@@ -1154,6 +1415,7 @@ function resolveLit(o: LitSourceOptions): LitSource {
|
|
|
1154
1415
|
skinned: o.skinned === true,
|
|
1155
1416
|
instanced: o.instanced === true || o.instanceColors === true,
|
|
1156
1417
|
instanceColors: o.instanceColors === true,
|
|
1418
|
+
morph: o.morph === true,
|
|
1157
1419
|
prelude: o.prelude ?? "",
|
|
1158
1420
|
surface: o.surface ?? "",
|
|
1159
1421
|
brdf: "blinn",
|
|
@@ -1221,20 +1483,20 @@ function litBase(c: LitSource, flip: boolean): string {
|
|
|
1221
1483
|
}
|
|
1222
1484
|
|
|
1223
1485
|
/**
|
|
1224
|
-
* The vertex stage
|
|
1486
|
+
* The vertex stage the stock lit materials pair with a given option set: LIT_VERTEX, or
|
|
1225
1487
|
* LIT_VERTEX_COLORED when the fragment reads vColor. The shadow source
|
|
1226
1488
|
* takes this same stage, which is what lets a surface function read the
|
|
1227
1489
|
* same varyings in both passes.
|
|
1228
1490
|
*/
|
|
1229
1491
|
export function litVertex(o: LitSourceOptions = {}): string {
|
|
1230
1492
|
let c = resolveLit(o)
|
|
1231
|
-
return litVertexSource(c.vertexColors, c.lightMap, c.skinned, c.instanced, c.instanceColors)
|
|
1493
|
+
return litVertexSource(c.vertexColors, c.lightMap, c.skinned, c.instanced, c.instanceColors, c.morph)
|
|
1232
1494
|
}
|
|
1233
1495
|
|
|
1234
1496
|
/**
|
|
1235
|
-
* The `
|
|
1497
|
+
* The `phong` fragment source for an option set - the exact program `phong`
|
|
1236
1498
|
* itself builds, composed from the constants above. Pair it with
|
|
1237
|
-
* litVertex(o) in a shaderMaterialClass to get a
|
|
1499
|
+
* litVertex(o) in a shaderMaterialClass to get a Blinn-Phong material with your
|
|
1238
1500
|
* own GLSL in it, and pass litShadowFragment(o) alongside when it can
|
|
1239
1501
|
* discard.
|
|
1240
1502
|
*
|
|
@@ -1246,7 +1508,7 @@ export function litVertex(o: LitSourceOptions = {}): string {
|
|
|
1246
1508
|
* Everything else - the lights, the hemisphere, the camera position, the
|
|
1247
1509
|
* shadow set, the fog - is written by the scene.
|
|
1248
1510
|
*/
|
|
1249
|
-
export function
|
|
1511
|
+
export function phongFragment(o: LitSourceOptions = {}): string {
|
|
1250
1512
|
return lightingFragment(resolveLit(o))
|
|
1251
1513
|
}
|
|
1252
1514
|
|
|
@@ -1257,7 +1519,7 @@ export function litFragment(o: LitSourceOptions = {}): string {
|
|
|
1257
1519
|
* metalness/roughness model (PBR) instead of Blinn-Phong, and the
|
|
1258
1520
|
* scene's ENVIRONMENT sampled unconditionally as the split sum
|
|
1259
1521
|
* (envRadiance times envBrdf). Pair it with litVertex(o), and with
|
|
1260
|
-
* litShadowFragment(o) when it can discard, exactly as
|
|
1522
|
+
* litShadowFragment(o) when it can discard, exactly as phongFragment.
|
|
1261
1523
|
*
|
|
1262
1524
|
* Per-entry uniforms: `uColor` (premultiplied vec4), `uMetalness`,
|
|
1263
1525
|
* `uRoughness`, `uMetalnessMap` (blue channel) and `uRoughnessMap`
|
|
@@ -1298,10 +1560,12 @@ function lightingFragment(c: LitSource): string {
|
|
|
1298
1560
|
${c.mapTransform ? "uniform vec4 uMapTransform;" : ""}
|
|
1299
1561
|
${c.normalMap ? NORMAL_MAP : ""}
|
|
1300
1562
|
${sceneSource({ receiveShadow: c.receiveShadow, env: c.env, fog: c.fog })}
|
|
1563
|
+
${LOD_FADE}
|
|
1301
1564
|
${c.prelude}
|
|
1302
1565
|
${c.surface}
|
|
1303
1566
|
|
|
1304
1567
|
void main() {
|
|
1568
|
+
lodFade();
|
|
1305
1569
|
${litBase(c, c.cull !== "back")}
|
|
1306
1570
|
${c.normalMap ? "n = perturbNormal(n, vWorldPos, uv);" : ""}
|
|
1307
1571
|
Surface s = surfaceOf(base, n);
|
|
@@ -1325,7 +1589,7 @@ function lightingFragment(c: LitSource): string {
|
|
|
1325
1589
|
/**
|
|
1326
1590
|
* The depth-pass source that makes a discarding lit material cast what it
|
|
1327
1591
|
* actually draws: the same base, cutout and surface function as
|
|
1328
|
-
*
|
|
1592
|
+
* phongFragment(o), and nothing after them (no lighting, no fog - a shadow
|
|
1329
1593
|
* has neither). Undefined when the option set cannot discard (no
|
|
1330
1594
|
* alphaTest, no surface), which means the scene's default depth material
|
|
1331
1595
|
* is already right and the material should carry no `shadow` of its own.
|
|
@@ -1383,7 +1647,7 @@ export const UNLIT_VERTEX = glsl`
|
|
|
1383
1647
|
|
|
1384
1648
|
/**
|
|
1385
1649
|
* The option set the unlit builders take: LitSourceOptions minus
|
|
1386
|
-
* everything lighting decides (no
|
|
1650
|
+
* everything lighting decides (no triplanar, receiveShadow)
|
|
1387
1651
|
* and minus cull (the unlit fragment has no facing-dependent code - cull
|
|
1388
1652
|
* is pure pipeline state, passed to shaderMaterialClass directly). The
|
|
1389
1653
|
* slots follow the lit contract; the varyings here are vUv and vWorldPos
|
|
@@ -1392,13 +1656,19 @@ export const UNLIT_VERTEX = glsl`
|
|
|
1392
1656
|
export type UnlitSourceOptions = {
|
|
1393
1657
|
/** The fragment samples a `uniform sampler2D uMap`, tinted by uColor. */
|
|
1394
1658
|
map?: boolean
|
|
1659
|
+
/** Multiply the base by the "colored" layout's per-vertex aColor (see
|
|
1660
|
+
* the lit option): unlitVertex(o) forwards it as vColor, times iColor
|
|
1661
|
+
* under instanceColors. */
|
|
1662
|
+
vertexColors?: boolean
|
|
1395
1663
|
/** Write the base alpha through and blend; opaque (the default) writes
|
|
1396
1664
|
* alpha 1. */
|
|
1397
1665
|
transparent?: boolean
|
|
1398
1666
|
/** Declare `uniform float uAlphaTest` and discard below it. */
|
|
1399
1667
|
alphaTest?: boolean
|
|
1400
|
-
/** Compose the scene's fog over the result (default true)
|
|
1401
|
-
|
|
1668
|
+
/** Compose the scene's fog over the result (default true); "additive"
|
|
1669
|
+
* for a `blend: "add"` program, which fades toward black instead of the
|
|
1670
|
+
* fog color (see FOG). */
|
|
1671
|
+
fog?: boolean | "additive"
|
|
1402
1672
|
/** Declare `uniform vec4 uMapTransform` ([repeatU, repeatV, offsetU,
|
|
1403
1673
|
* offsetV]) and sample the map at `vUv * repeat + offset` (see the lit
|
|
1404
1674
|
* option of the same name). Needs `map`. */
|
|
@@ -1406,6 +1676,9 @@ export type UnlitSourceOptions = {
|
|
|
1406
1676
|
/** Skin positions by the "skinned" layout against uBones (see the lit
|
|
1407
1677
|
* option); unlitVertex(o) reads it, the fragment is unchanged. */
|
|
1408
1678
|
skinned?: boolean
|
|
1679
|
+
/** Displace positions by the geometry's morph targets (see the lit
|
|
1680
|
+
* option); unlitVertex(o) reads it, the fragment is unchanged. */
|
|
1681
|
+
morph?: boolean
|
|
1409
1682
|
/** Place each copy by its instance matrix (see the lit option);
|
|
1410
1683
|
* unlitVertex(o) reads it, the fragment is unchanged. */
|
|
1411
1684
|
instanced?: boolean
|
|
@@ -1423,13 +1696,15 @@ export type UnlitSourceOptions = {
|
|
|
1423
1696
|
|
|
1424
1697
|
type UnlitSource = {
|
|
1425
1698
|
map: boolean
|
|
1699
|
+
vertexColors: boolean
|
|
1426
1700
|
transparent: boolean
|
|
1427
1701
|
alphaTest: boolean
|
|
1428
|
-
fog: boolean
|
|
1702
|
+
fog: boolean | "additive"
|
|
1429
1703
|
mapTransform: boolean
|
|
1430
1704
|
skinned: boolean
|
|
1431
1705
|
instanced: boolean
|
|
1432
1706
|
instanceColors: boolean
|
|
1707
|
+
morph: boolean
|
|
1433
1708
|
prelude: string
|
|
1434
1709
|
surface: string
|
|
1435
1710
|
}
|
|
@@ -1437,13 +1712,15 @@ type UnlitSource = {
|
|
|
1437
1712
|
function resolveUnlit(o: UnlitSourceOptions): UnlitSource {
|
|
1438
1713
|
return {
|
|
1439
1714
|
map: o.map === true,
|
|
1715
|
+
vertexColors: o.vertexColors === true,
|
|
1440
1716
|
transparent: o.transparent === true,
|
|
1441
1717
|
alphaTest: o.alphaTest === true,
|
|
1442
|
-
fog: o.fog
|
|
1718
|
+
fog: o.fog ?? true,
|
|
1443
1719
|
mapTransform: o.mapTransform === true && o.map === true,
|
|
1444
1720
|
skinned: o.skinned === true,
|
|
1445
1721
|
instanced: o.instanced === true || o.instanceColors === true,
|
|
1446
1722
|
instanceColors: o.instanceColors === true,
|
|
1723
|
+
morph: o.morph === true,
|
|
1447
1724
|
prelude: o.prelude ?? "",
|
|
1448
1725
|
surface: o.surface ?? "",
|
|
1449
1726
|
}
|
|
@@ -1451,42 +1728,48 @@ function resolveUnlit(o: UnlitSourceOptions): UnlitSource {
|
|
|
1451
1728
|
|
|
1452
1729
|
/**
|
|
1453
1730
|
* The vertex stage `unlit` pairs with an option set: UNLIT_VERTEX, or its
|
|
1731
|
+
* colored form (aColor forwarded as vColor) when `vertexColors`, its
|
|
1454
1732
|
* skinned form (the skin matrix applied before uModel) when `skinned`,
|
|
1455
1733
|
* its instanced form (the instance matrix under uModel, iColor forwarded
|
|
1456
|
-
* as vColor under `instanceColors`) when `instanced`.
|
|
1734
|
+
* as vColor under `instanceColors`) when `instanced`. vColor carries the
|
|
1735
|
+
* vertex color, the instance color, or their product, as in lit.
|
|
1457
1736
|
*/
|
|
1458
1737
|
export function unlitVertex(o: UnlitSourceOptions = {}): string {
|
|
1459
1738
|
let c = resolveUnlit(o)
|
|
1460
|
-
if (!c.skinned && !c.instanced) return UNLIT_VERTEX
|
|
1739
|
+
if (!c.vertexColors && !c.skinned && !c.instanced && !c.morph) return UNLIT_VERTEX
|
|
1740
|
+
let color = c.vertexColors && c.instanceColors ? "aColor * iColor" : c.vertexColors ? "aColor" : c.instanceColors ? "iColor" : ""
|
|
1461
1741
|
return glsl`
|
|
1462
1742
|
in vec3 aPos;
|
|
1463
1743
|
in vec2 aUV;
|
|
1744
|
+
${c.vertexColors ? "in vec4 aColor;" : ""}
|
|
1745
|
+
${c.morph ? MORPH_DECLS : ""}
|
|
1464
1746
|
${c.skinned ? SKIN_DECLS : ""}
|
|
1465
1747
|
${c.instanced ? INSTANCE_MATRIX : ""}
|
|
1466
1748
|
${c.instanceColors ? "in vec4 iColor;" : ""}
|
|
1467
1749
|
out vec2 vUv;
|
|
1468
1750
|
out vec3 vWorldPos;
|
|
1469
|
-
${
|
|
1751
|
+
${color ? "out vec4 vColor;" : ""}
|
|
1470
1752
|
uniform mat4 uModel;
|
|
1471
1753
|
uniform mat4 uViewProj;
|
|
1472
1754
|
|
|
1473
1755
|
void main() {
|
|
1756
|
+
${c.morph ? MORPH_APPLY_POSITION : ""}
|
|
1474
1757
|
${c.skinned ? SKIN_MATRIX : ""}
|
|
1475
|
-
vec4 world = ${instancedPosition(c.skinned, c.instanced)};
|
|
1758
|
+
vec4 world = ${instancedPosition(c.skinned, c.instanced, c.morph)};
|
|
1476
1759
|
vWorldPos = world.xyz;
|
|
1477
1760
|
gl_Position = uViewProj * world;
|
|
1478
1761
|
vUv = aUV;
|
|
1479
|
-
${
|
|
1762
|
+
${color ? `vColor = ${color};` : ""}
|
|
1480
1763
|
}
|
|
1481
1764
|
`
|
|
1482
1765
|
}
|
|
1483
1766
|
|
|
1484
1767
|
/** The unlit base sample: uColor times the (possibly transformed) map,
|
|
1485
|
-
* times the instance
|
|
1768
|
+
* times the vertex and instance colors when the material carries them. */
|
|
1486
1769
|
function unlitBase(c: UnlitSource): string {
|
|
1487
1770
|
let uv = c.mapTransform ? "vUv * uMapTransform.xy + uMapTransform.zw" : "vUv"
|
|
1488
1771
|
let base = c.map ? `vec4 base = texture(uMap, ${uv}) * uColor;` : "vec4 base = uColor;"
|
|
1489
|
-
return c.instanceColors ? base + "\n base *= vColor;" : base
|
|
1772
|
+
return c.vertexColors || c.instanceColors ? base + "\n base *= vColor;" : base
|
|
1490
1773
|
}
|
|
1491
1774
|
|
|
1492
1775
|
/**
|
|
@@ -1509,15 +1792,17 @@ export function unlitFragment(o: UnlitSourceOptions = {}): string {
|
|
|
1509
1792
|
return glsl`
|
|
1510
1793
|
${c.map ? "in vec2 vUv;" : ""}
|
|
1511
1794
|
in vec3 vWorldPos;
|
|
1512
|
-
${c.instanceColors ? "in vec4 vColor;" : ""}
|
|
1795
|
+
${c.vertexColors || c.instanceColors ? "in vec4 vColor;" : ""}
|
|
1513
1796
|
${c.map ? "uniform sampler2D uMap;" : ""}
|
|
1514
1797
|
uniform vec4 uColor;
|
|
1515
1798
|
${c.alphaTest ? "uniform float uAlphaTest;" : ""}
|
|
1516
1799
|
${c.mapTransform ? "uniform vec4 uMapTransform;" : ""}
|
|
1517
1800
|
${sceneSource({ lights: false, fog: c.fog })}
|
|
1801
|
+
${LOD_FADE}
|
|
1518
1802
|
${c.prelude}
|
|
1519
1803
|
${c.surface}
|
|
1520
1804
|
void main() {
|
|
1805
|
+
lodFade();
|
|
1521
1806
|
${unlitBase(c)}
|
|
1522
1807
|
${c.alphaTest ? "if (base.a < uAlphaTest) discard;" : ""}
|
|
1523
1808
|
${c.surface ? "Surface s = surfaceOf(base, vec3(0.0));\n surface(s);\n base = s.base;" : ""}
|
|
@@ -1537,7 +1822,7 @@ export function unlitShadowFragment(o: UnlitSourceOptions = {}): string | undefi
|
|
|
1537
1822
|
if (!c.alphaTest && !c.surface) return undefined
|
|
1538
1823
|
return glsl`
|
|
1539
1824
|
${c.map ? "in vec2 vUv;" : ""}
|
|
1540
|
-
${c.instanceColors ? "in vec4 vColor;" : ""}
|
|
1825
|
+
${c.vertexColors || c.instanceColors ? "in vec4 vColor;" : ""}
|
|
1541
1826
|
uniform vec4 uColor;
|
|
1542
1827
|
${c.map ? "uniform sampler2D uMap;" : ""}
|
|
1543
1828
|
${c.alphaTest ? "uniform float uAlphaTest;" : ""}
|