@solidrt/3d 0.0.55 → 0.0.57
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 +1206 -224
- package/README.md +16 -7
- package/examples/README.md +88 -9
- package/examples/aim.tsx +2 -3
- package/examples/cascades.tsx +1 -4
- package/examples/collision.tsx +263 -0
- package/examples/first-person.tsx +129 -0
- package/examples/fleet.tsx +200 -0
- package/examples/fog.tsx +2 -3
- package/examples/instanced.tsx +72 -32
- package/examples/lamps.tsx +92 -0
- package/examples/lit.tsx +3 -5
- package/examples/materials.tsx +150 -0
- package/examples/model-load.tsx +95 -0
- package/examples/model.glb +0 -0
- package/examples/model.tsx +35 -9
- package/examples/pick.tsx +145 -37
- package/examples/probe.tsx +107 -0
- package/examples/scene-background.tsx +2 -4
- package/examples/scene-basic.tsx +2 -4
- package/examples/scene-views.tsx +68 -48
- package/examples/shadows.tsx +2 -4
- package/examples/sky-lit.tsx +92 -0
- package/examples/skybox.tsx +134 -0
- package/examples/sprites.tsx +4 -6
- package/examples/standard.tsx +138 -0
- package/examples/sweep-paths.tsx +2 -4
- package/package.json +3 -3
- package/src/camera.ts +148 -0
- package/src/clip.ts +96 -0
- package/src/collision.ts +20 -0
- package/src/color.ts +32 -0
- package/src/components/context.tsx +35 -0
- package/src/components/directional-light.tsx +48 -0
- package/src/components/first-person-camera.tsx +72 -0
- package/src/components/group.tsx +18 -0
- package/src/components/hemisphere-light.tsx +24 -0
- package/src/components/index.ts +46 -0
- package/src/components/instanced-mesh.tsx +105 -0
- package/src/components/mesh.tsx +92 -0
- package/src/components/node-props.ts +90 -0
- package/src/components/orbit-camera.tsx +75 -0
- package/src/components/perspective-camera.tsx +32 -0
- package/src/components/point-light.tsx +52 -0
- package/src/components/record-mesh.tsx +68 -0
- package/src/components/scene.tsx +281 -0
- package/src/components/spot-light.tsx +65 -0
- package/src/components/sprite.tsx +38 -0
- package/src/components/view3d.tsx +152 -0
- package/src/environment-bake.ts +492 -0
- package/src/environment.ts +325 -0
- package/src/first-person.ts +258 -0
- package/src/geometry.ts +6 -2
- package/src/glsl.ts +1343 -82
- package/src/gltf.ts +432 -112
- package/src/index.ts +61 -9
- package/src/input.ts +70 -0
- package/src/light.ts +383 -0
- package/src/material.ts +667 -232
- package/src/math.ts +110 -0
- package/src/mesh.ts +728 -0
- package/src/mixer.ts +476 -0
- package/src/model-file.ts +80 -15
- package/src/model.ts +204 -35
- package/src/node.ts +579 -0
- package/src/orbit.ts +292 -212
- package/src/scene-pointer.ts +294 -0
- package/src/scene-shadows.ts +471 -0
- package/src/scene.ts +1435 -1576
- package/tools/environment.ts +69 -0
- package/tools/model.ts +4 -3
- package/src/components.tsx +0 -420
package/src/glsl.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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
|
-
// pieces the package's
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// pieces the package's lit materials are built from (sceneSource is the
|
|
4
|
+
// whole scene in one set; the lit, standard and unlit programs are
|
|
5
|
+
// assembled from it), so a custom material never becomes second-class
|
|
6
|
+
// (no preprocessor, no include resolver; the policy is argued in
|
|
7
|
+
// okf/notes/3d-differentiators.md).
|
|
6
8
|
//
|
|
7
9
|
// The light-model functions are PURE: normals, view vectors, light
|
|
8
10
|
// directions, colors and exponents all arrive as arguments, so these
|
|
@@ -14,23 +16,84 @@
|
|
|
14
16
|
// every function returns its raw term, weighting and color belong to the
|
|
15
17
|
// caller.
|
|
16
18
|
|
|
17
|
-
import {
|
|
19
|
+
import type { CullMode, InstanceAttribute } from "@solidrt/core/gpu"
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
// core/gpu's glsl tag, aliased locally (it is String.raw) so this module
|
|
22
|
+
// stays runtime-pure: no flux:gpu import, so the checks/ rigs and any
|
|
23
|
+
// bake-time tool can run it headless on bare flux.
|
|
24
|
+
let glsl = String.raw
|
|
25
|
+
|
|
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 `lit` fragment;
|
|
28
|
+
* a custom fragment composes LIGHT_SLOTS and LIGHT_LOOKUP and loops to
|
|
29
|
+
* `uLightCount`. A shader-source constant, so it is fixed for the app
|
|
30
|
+
* (see okf/backlog/app-runtime-config.md). */
|
|
31
|
+
export const MAX_LIGHTS = 8
|
|
25
32
|
|
|
26
33
|
/** The most cascades one casting light splits its shadow into
|
|
27
34
|
* (`shadow.cascades`, 1..MAX_CASCADES). */
|
|
28
35
|
export const MAX_CASCADES = 4
|
|
29
36
|
|
|
30
|
-
/** The shadow-map slot
|
|
31
|
-
* light
|
|
32
|
-
*
|
|
33
|
-
|
|
37
|
+
/** The shadow-map slot budget of the scene's shadow set: every casting
|
|
38
|
+
* light claims consecutive slots, one per map (a directional light
|
|
39
|
+
* `shadow.cascades`, a point light its six faces, a spot one), dealt in
|
|
40
|
+
* light order, and a caster past the
|
|
41
|
+
* budget throws at attach. Bounds `uShadowRect`/`uShadowMatrix` - its
|
|
42
|
+
* own constant, NOT MAX_LIGHTS * MAX_CASCADES, so raising the light cap
|
|
43
|
+
* does not size the fragment uniform budget for the worst case where
|
|
44
|
+
* every light is a fully cascaded sun. */
|
|
45
|
+
export const MAX_SHADOW_MAPS = 8
|
|
46
|
+
|
|
47
|
+
/** The instance attributes an instanced mesh's material declares
|
|
48
|
+
* (`shaderMaterialClass({ instanceAttributes: INSTANCE_MATRIX_ATTRIBUTES
|
|
49
|
+
* })`): the four vec4 columns of the per-instance matrix the spatial core
|
|
50
|
+
* writes - the instance's placement inside the mesh (createInstancedMesh,
|
|
51
|
+
* addInstance). Read them through INSTANCE_MATRIX. Slot 0, the core's
|
|
52
|
+
* record; an app's own per-instance floats go in slot 1 (the style
|
|
53
|
+
* record, see INSTANCE_COLOR_ATTRIBUTES). */
|
|
54
|
+
export const INSTANCE_MATRIX_ATTRIBUTES: InstanceAttribute[] = [
|
|
55
|
+
{ name: "iModel0", format: "vec4" },
|
|
56
|
+
{ name: "iModel1", format: "vec4" },
|
|
57
|
+
{ name: "iModel2", format: "vec4" },
|
|
58
|
+
{ name: "iModel3", format: "vec4" },
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
/** The per-instance color the stock materials read with `instanceColors`
|
|
62
|
+
* (`in vec4 iColor`, premultiplied linear like aColor, forwarded as
|
|
63
|
+
* vColor): a slot-1 attribute, so it is the instance's STYLE record -
|
|
64
|
+
* app-owned, written with setInstanceStyle(instance, [r, g, b, a]) - beside
|
|
65
|
+
* the core's matrix in slot 0. A custom class appends it (or any slot-1
|
|
66
|
+
* layout of its own) to INSTANCE_MATRIX_ATTRIBUTES; the stock materials
|
|
67
|
+
* start every instance at white. Three's instanceColor, Godot's
|
|
68
|
+
* MultiMesh instance color. */
|
|
69
|
+
export const INSTANCE_COLOR_ATTRIBUTES: InstanceAttribute[] = [{ name: "iColor", format: "vec4", slot: 1 }]
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The vertex-stage declarations over INSTANCE_MATRIX_ATTRIBUTES:
|
|
73
|
+
* `instanceMatrix()` is the instance's placement inside the mesh, applied
|
|
74
|
+
* under the mesh's own uModel (`uModel * instanceMatrix() * vec4(aPos,
|
|
75
|
+
* 1.0)`, and `mat3(uNormal) * instanceNormalMatrix() * aNormal` for the
|
|
76
|
+
* normal). `instanceNormalMatrix()` is Three's derivation: each column
|
|
77
|
+
* divided by its squared length, which is the inverse-transpose of any
|
|
78
|
+
* rotation-times-scale matrix, so it is exact for every instance and for
|
|
79
|
+
* uniformly scaled groups between mesh and instance at no per-instance
|
|
80
|
+
* data. The one case it misses is a sheared hierarchy - a non-uniformly
|
|
81
|
+
* scaled group above a rotated instance; a stage that builds one writes
|
|
82
|
+
* `transpose(inverse(mat3(instanceMatrix())))` instead (Godot's form).
|
|
83
|
+
*/
|
|
84
|
+
export const INSTANCE_MATRIX = glsl`
|
|
85
|
+
in vec4 iModel0;
|
|
86
|
+
in vec4 iModel1;
|
|
87
|
+
in vec4 iModel2;
|
|
88
|
+
in vec4 iModel3;
|
|
89
|
+
mat4 instanceMatrix() {
|
|
90
|
+
return mat4(iModel0, iModel1, iModel2, iModel3);
|
|
91
|
+
}
|
|
92
|
+
mat3 instanceNormalMatrix() {
|
|
93
|
+
mat3 m = mat3(iModel0.xyz, iModel1.xyz, iModel2.xyz);
|
|
94
|
+
return mat3(m[0] / dot(m[0], m[0]), m[1] / dot(m[1], m[1]), m[2] / dot(m[2], m[2]));
|
|
95
|
+
}
|
|
96
|
+
`
|
|
34
97
|
|
|
35
98
|
/**
|
|
36
99
|
* The standard lit vertex stage: clip position via uViewProj * uModel,
|
|
@@ -42,25 +105,89 @@ export const MAX_SHADOW_MAPS = MAX_LIGHTS * MAX_CASCADES
|
|
|
42
105
|
* Pair it with your own fragment; the view vector there is
|
|
43
106
|
* `normalize(uCamPos - vWorldPos)`.
|
|
44
107
|
*/
|
|
45
|
-
|
|
108
|
+
/** The skinned-layout declarations a vertex stage splices in: the
|
|
109
|
+
* aJoints/aWeights channels, the `uBones` palette and `mat4 boneAt(int)`.
|
|
110
|
+
* The palette is a float texture, not a uniform array, so rig size is
|
|
111
|
+
* bounded only by texture height (>= 2048 everywhere), never by the
|
|
112
|
+
* vertex uniform budget: an rgba32f texture 4 texels wide, one row per
|
|
113
|
+
* joint, each row the four columns of that joint's mat4 (the spatial
|
|
114
|
+
* flush writes it from the bound joint nodes; createModel binds it per
|
|
115
|
+
* skinned mesh). Pair with
|
|
116
|
+
* SKIN_MATRIX in main(). */
|
|
117
|
+
export const SKIN_DECLS = glsl`
|
|
118
|
+
in vec4 aJoints;
|
|
119
|
+
in vec4 aWeights;
|
|
120
|
+
uniform sampler2D uBones;
|
|
121
|
+
mat4 boneAt(int j) {
|
|
122
|
+
return mat4(texelFetch(uBones, ivec2(0, j), 0), texelFetch(uBones, ivec2(1, j), 0),
|
|
123
|
+
texelFetch(uBones, ivec2(2, j), 0), texelFetch(uBones, ivec2(3, j), 0));
|
|
124
|
+
}
|
|
125
|
+
`
|
|
126
|
+
/** The linear-blend skin matrix, spliced into main() after SKIN_DECLS:
|
|
127
|
+
* apply as `skin * vec4(aPos, 1.0)` before uModel. Bone-space normals go
|
|
128
|
+
* through `mat3(skin)` - exact for rigid bones, the standard
|
|
129
|
+
* approximation under bone scale. */
|
|
130
|
+
export const SKIN_MATRIX = glsl`
|
|
131
|
+
mat4 skin = aWeights.x * boneAt(int(aJoints.x)) + aWeights.y * boneAt(int(aJoints.y)) +
|
|
132
|
+
aWeights.z * boneAt(int(aJoints.z)) + aWeights.w * boneAt(int(aJoints.w));
|
|
133
|
+
`
|
|
134
|
+
|
|
135
|
+
// The per-instance placement, spliced into a vertex stage's position and
|
|
136
|
+
// normal math for an instanced material: the skin (when any) first, then
|
|
137
|
+
// the instance's matrix inside the mesh, then uModel - so a skinned
|
|
138
|
+
// instanced fleet poses every copy by the one shared palette and places
|
|
139
|
+
// each by its own record.
|
|
140
|
+
function instancedPosition(skinned: boolean, instanced: boolean): string {
|
|
141
|
+
let local = skinned ? "(skin * vec4(aPos, 1.0))" : "vec4(aPos, 1.0)"
|
|
142
|
+
return instanced ? `uModel * (instanceMatrix() * ${local})` : `uModel * ${local}`
|
|
143
|
+
}
|
|
144
|
+
function instancedNormal(skinned: boolean, instanced: boolean): string {
|
|
145
|
+
let local = skinned ? "(mat3(skin) * aNormal)" : "aNormal"
|
|
146
|
+
return instanced ? `mat3(uNormal) * (instanceNormalMatrix() * ${local})` : `mat3(uNormal) * ${local}`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The one lit vertex template: the standard prefix always, aColor and
|
|
150
|
+
// aUV2 only when the fragment reads them (an `in` the source mentions
|
|
151
|
+
// makes the material require that channel, so the blocks must be absent,
|
|
152
|
+
// not inactive), the skin blocks only for a skinned material (they make
|
|
153
|
+
// it require the "skinned" layout the same way), the instance matrix
|
|
154
|
+
// and color only for an instanced one. vColor carries the vertex color,
|
|
155
|
+
// the instance color, or their product. LIT_VERTEX / LIT_VERTEX_COLORED
|
|
156
|
+
// 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 {
|
|
158
|
+
let color = colored && instanceColors ? "aColor * iColor" : colored ? "aColor" : instanceColors ? "iColor" : ""
|
|
159
|
+
return glsl`
|
|
46
160
|
in vec3 aPos;
|
|
47
161
|
in vec3 aNormal;
|
|
48
162
|
in vec2 aUV;
|
|
163
|
+
${colored ? "in vec4 aColor;" : ""}
|
|
164
|
+
${uv2 ? "in vec2 aUV2;" : ""}
|
|
165
|
+
${skinned ? SKIN_DECLS : ""}
|
|
166
|
+
${instanced ? INSTANCE_MATRIX : ""}
|
|
167
|
+
${instanceColors ? "in vec4 iColor;" : ""}
|
|
49
168
|
uniform mat4 uModel;
|
|
50
169
|
uniform mat4 uViewProj;
|
|
51
170
|
uniform mat4 uNormal;
|
|
52
171
|
out vec3 vWorldPos;
|
|
53
172
|
out vec3 vNormal;
|
|
54
173
|
out vec2 vUv;
|
|
174
|
+
${color ? "out vec4 vColor;" : ""}
|
|
175
|
+
${uv2 ? "out vec2 vUv2;" : ""}
|
|
55
176
|
|
|
56
177
|
void main() {
|
|
57
|
-
|
|
178
|
+
${skinned ? SKIN_MATRIX : ""}
|
|
179
|
+
vec4 world = ${instancedPosition(skinned, instanced)};
|
|
58
180
|
gl_Position = uViewProj * world;
|
|
59
181
|
vWorldPos = world.xyz;
|
|
60
|
-
vNormal =
|
|
182
|
+
vNormal = ${instancedNormal(skinned, instanced)};
|
|
61
183
|
vUv = aUV;
|
|
184
|
+
${color ? `vColor = ${color};` : ""}
|
|
185
|
+
${uv2 ? "vUv2 = aUV2;" : ""}
|
|
62
186
|
}
|
|
63
187
|
`
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const LIT_VERTEX = litVertexSource(false, false, false, false, false)
|
|
64
191
|
|
|
65
192
|
/**
|
|
66
193
|
* LIT_VERTEX for "colored"-layout geometry: the same interface plus the
|
|
@@ -70,28 +197,7 @@ export const LIT_VERTEX = glsl`
|
|
|
70
197
|
* collects the vertex stage's `in` declarations), so its meshes need
|
|
71
198
|
* geometry carrying that channel - withColors() - or add() throws.
|
|
72
199
|
*/
|
|
73
|
-
export const LIT_VERTEX_COLORED =
|
|
74
|
-
in vec3 aPos;
|
|
75
|
-
in vec3 aNormal;
|
|
76
|
-
in vec2 aUV;
|
|
77
|
-
in vec4 aColor;
|
|
78
|
-
uniform mat4 uModel;
|
|
79
|
-
uniform mat4 uViewProj;
|
|
80
|
-
uniform mat4 uNormal;
|
|
81
|
-
out vec3 vWorldPos;
|
|
82
|
-
out vec3 vNormal;
|
|
83
|
-
out vec2 vUv;
|
|
84
|
-
out vec4 vColor;
|
|
85
|
-
|
|
86
|
-
void main() {
|
|
87
|
-
vec4 world = uModel * vec4(aPos, 1.0);
|
|
88
|
-
gl_Position = uViewProj * world;
|
|
89
|
-
vWorldPos = world.xyz;
|
|
90
|
-
vNormal = mat3(uNormal) * aNormal;
|
|
91
|
-
vUv = aUV;
|
|
92
|
-
vColor = aColor;
|
|
93
|
-
}
|
|
94
|
-
`
|
|
200
|
+
export const LIT_VERTEX_COLORED = litVertexSource(true, false, false, false, false)
|
|
95
201
|
|
|
96
202
|
/** `vec3 hemisphere(vec3 n, vec3 sky, vec3 ground)` - ambient from a
|
|
97
203
|
* sky/ground gradient by the normal's vertical tilt: sky straight up,
|
|
@@ -127,6 +233,168 @@ export const FRESNEL = glsl`
|
|
|
127
233
|
}
|
|
128
234
|
`
|
|
129
235
|
|
|
236
|
+
/**
|
|
237
|
+
* The metalness/roughness light model `standard` shades with (Three's
|
|
238
|
+
* MeshStandardMaterial, Godot's StandardMaterial3D, Unity's Standard).
|
|
239
|
+
* `vec3 ggxSpecular(n, v, l, f0, roughness)` is one light's specular
|
|
240
|
+
* lobe - GGX distribution x height-correlated Smith visibility x Schlick
|
|
241
|
+
* fresnel, times pi - for the caller to weight by `lambert(n, l)` and
|
|
242
|
+
* the light color, the same convention the diffuse uses (a light of
|
|
243
|
+
* intensity 1 lights a white diffuse surface to 1 face-on; Three's
|
|
244
|
+
* lights are the same lobe with intensities a factor pi larger).
|
|
245
|
+
* `roughness` is perceptual: alpha is its square, floored at
|
|
246
|
+
* MIN_ROUGHNESS so a mirror keeps a lobe of finite width. `vec2
|
|
247
|
+
* envBrdf(nv, roughness)` is the split-sum environment BRDF - the scale
|
|
248
|
+
* and bias on f0 (`f0 * ab.x + ab.y`) that turns prefiltered radiance
|
|
249
|
+
* into the specular image term - as Lazarov's analytic fit (Three's
|
|
250
|
+
* DFGApprox, Godot's brdf_approx; no lookup texture). `DIELECTRIC_F0` is
|
|
251
|
+
* a non-metal's face-on reflectance (0.04 in all three engines); a
|
|
252
|
+
* metal's f0 is its base color. The pieces (`ggxDistribution(nh, a2)`,
|
|
253
|
+
* `smithVisibility(nl, nv, a2)`, `schlickFresnel(f0, vh)`) are there for
|
|
254
|
+
* a custom lobe. Defines PBR_PI, not PI, so a prelude's own PI stands.
|
|
255
|
+
*/
|
|
256
|
+
export const PBR = glsl`
|
|
257
|
+
const float PBR_PI = 3.14159265;
|
|
258
|
+
// Face-on reflectance of a dielectric (index of refraction about 1.5).
|
|
259
|
+
const float DIELECTRIC_F0 = 0.04;
|
|
260
|
+
// Floor on the perceptual roughness: alpha never reaches zero, so a
|
|
261
|
+
// perfect mirror keeps a highlight of finite width (Three's clamp).
|
|
262
|
+
const float MIN_ROUGHNESS = 0.0525;
|
|
263
|
+
// Floor on the visibility denominator at grazing angles.
|
|
264
|
+
const float VISIBILITY_MIN = 1e-5;
|
|
265
|
+
// Schlick's approximation exponent.
|
|
266
|
+
const float SCHLICK_POWER = 5.0;
|
|
267
|
+
float ggxDistribution(float nh, float a2) {
|
|
268
|
+
float d = nh * nh * (a2 - 1.0) + 1.0;
|
|
269
|
+
return a2 / (PBR_PI * d * d);
|
|
270
|
+
}
|
|
271
|
+
float smithVisibility(float nl, float nv, float a2) {
|
|
272
|
+
float gv = nl * sqrt(nv * nv * (1.0 - a2) + a2);
|
|
273
|
+
float gl = nv * sqrt(nl * nl * (1.0 - a2) + a2);
|
|
274
|
+
return 0.5 / max(gv + gl, VISIBILITY_MIN);
|
|
275
|
+
}
|
|
276
|
+
vec3 schlickFresnel(vec3 f0, float vh) {
|
|
277
|
+
return f0 + (1.0 - f0) * pow(1.0 - vh, SCHLICK_POWER);
|
|
278
|
+
}
|
|
279
|
+
vec3 ggxSpecular(vec3 n, vec3 v, vec3 l, vec3 f0, float roughness) {
|
|
280
|
+
vec3 h = normalize(l + v);
|
|
281
|
+
float nl = max(dot(n, l), 0.0);
|
|
282
|
+
float nv = max(dot(n, v), 0.0);
|
|
283
|
+
float nh = max(dot(n, h), 0.0);
|
|
284
|
+
float vh = max(dot(v, h), 0.0);
|
|
285
|
+
float r = max(roughness, MIN_ROUGHNESS);
|
|
286
|
+
float a2 = r * r * r * r;
|
|
287
|
+
return schlickFresnel(f0, vh) * (ggxDistribution(nh, a2) * smithVisibility(nl, nv, a2) * PBR_PI);
|
|
288
|
+
}
|
|
289
|
+
// Lazarov's fit of the environment BRDF integral: the coefficients of
|
|
290
|
+
// the two polynomials in roughness, and the grazing-angle exponent.
|
|
291
|
+
vec2 envBrdf(float nv, float roughness) {
|
|
292
|
+
const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
|
|
293
|
+
const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
|
|
294
|
+
vec4 r = roughness * c0 + c1;
|
|
295
|
+
float a004 = min(r.x * r.x, exp2(-9.28 * nv)) * r.x + r.y;
|
|
296
|
+
return vec2(-1.04, 1.04) * a004 + r.zw;
|
|
297
|
+
}
|
|
298
|
+
`
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The scene's light list as a receiving program declares it, written by
|
|
302
|
+
* the scene (`uLightDir` and `uLightPos` core-driven, so a moving light
|
|
303
|
+
* costs no JS): per light i to `uLightCount`, its `uLightType[i]`
|
|
304
|
+
* (LIGHT_DIRECTIONAL | LIGHT_SPOT | LIGHT_POINT), `uLightDir[i]` (world
|
|
305
|
+
* unit vector TOWARD a directional light; a spot's axis toward the
|
|
306
|
+
* light; unused for a point), `uLightPos[i]` (a spot's or point's world
|
|
307
|
+
* position; unused for a directional), `uLightColor[i]` (rgb, intensity
|
|
308
|
+
* folded in) and `uLightParams[i]` (cosInner, cosOuter, distance, decay
|
|
309
|
+
* - the cone cosines a spot fades between, the falloff cutoff and
|
|
310
|
+
* exponent a spot or point attenuates by; unused for a directional).
|
|
311
|
+
* Compose it before LIGHT_LOOKUP; `lit` is the shape.
|
|
312
|
+
*/
|
|
313
|
+
export const LIGHT_SLOTS = glsl`
|
|
314
|
+
uniform int uLightCount;
|
|
315
|
+
uniform int uLightType[${MAX_LIGHTS}];
|
|
316
|
+
uniform vec3 uLightDir[${MAX_LIGHTS}];
|
|
317
|
+
uniform vec3 uLightPos[${MAX_LIGHTS}];
|
|
318
|
+
uniform vec3 uLightColor[${MAX_LIGHTS}];
|
|
319
|
+
uniform vec4 uLightParams[${MAX_LIGHTS}];
|
|
320
|
+
const int LIGHT_DIRECTIONAL = 0;
|
|
321
|
+
const int LIGHT_SPOT = 1;
|
|
322
|
+
const int LIGHT_POINT = 2;
|
|
323
|
+
`
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The step from a light index to its incoming vector and strength, over
|
|
327
|
+
* LIGHT_SLOTS (compose it first). `float lightVector(int i, vec3
|
|
328
|
+
* worldPos, out vec3 l)` writes the unit vector from `worldPos` TOWARD
|
|
329
|
+
* light i and returns its attenuation: 1 for a directional light; for a
|
|
330
|
+
* spot or point, the windowed inverse falloff `1 / d^decay` faded to
|
|
331
|
+
* zero at `distance` (0 = no cutoff; Three's punctual-light falloff), a
|
|
332
|
+
* spot's additionally faded across its cone from cosInner to cosOuter.
|
|
333
|
+
* Zero means the light cannot reach the fragment - skip its shadow
|
|
334
|
+
* lookup and its terms (`lit` does exactly that).
|
|
335
|
+
*/
|
|
336
|
+
export const LIGHT_LOOKUP = glsl`
|
|
337
|
+
// Floors the falloff divisors so a fragment at the light's own
|
|
338
|
+
// position stays finite (Three's punctual-light rule).
|
|
339
|
+
const float FALLOFF_MIN = 0.01;
|
|
340
|
+
|
|
341
|
+
float lightVector(int i, vec3 worldPos, out vec3 l) {
|
|
342
|
+
if (uLightType[i] == LIGHT_DIRECTIONAL) {
|
|
343
|
+
l = uLightDir[i];
|
|
344
|
+
return 1.0;
|
|
345
|
+
}
|
|
346
|
+
vec3 dv = uLightPos[i] - worldPos;
|
|
347
|
+
float d = length(dv);
|
|
348
|
+
l = dv / max(d, FALLOFF_MIN);
|
|
349
|
+
vec4 p = uLightParams[i];
|
|
350
|
+
float atten = 1.0 / max(pow(d, p.w), FALLOFF_MIN);
|
|
351
|
+
if (p.z > 0.0) {
|
|
352
|
+
float win = clamp(1.0 - pow(d / p.z, 4.0), 0.0, 1.0);
|
|
353
|
+
atten *= win * win;
|
|
354
|
+
}
|
|
355
|
+
if (uLightType[i] == LIGHT_SPOT) {
|
|
356
|
+
atten *= smoothstep(p.y, p.x, dot(l, uLightDir[i]));
|
|
357
|
+
}
|
|
358
|
+
return atten;
|
|
359
|
+
}
|
|
360
|
+
`
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* `vec3 perturbNormal(vec3 n, vec3 worldPos, vec2 uv)` - the surface
|
|
364
|
+
* normal bent by a tangent-space normal map (`uniform sampler2D
|
|
365
|
+
* uNormalMap`, OpenGL-style +Y as glTF mandates, `uniform float
|
|
366
|
+
* uNormalScale` weighting the bend). The tangent frame is built per
|
|
367
|
+
* fragment from screen-space derivatives of worldPos and uv (Three's
|
|
368
|
+
* untangented path, Schuler's cotangent frame), so it works on ANY
|
|
369
|
+
* UV-mapped geometry with no tangent attribute; the trade is mild seams
|
|
370
|
+
* on mirrored UVs, the case a real aTangent layout would fix. Needs no
|
|
371
|
+
* varying of its own - pass the same worldPos and uv the caller lights
|
|
372
|
+
* and samples with. `n` must be normalized and already facing the viewer
|
|
373
|
+
* (flip back faces before calling).
|
|
374
|
+
*/
|
|
375
|
+
export const NORMAL_MAP = glsl`
|
|
376
|
+
uniform sampler2D uNormalMap;
|
|
377
|
+
uniform float uNormalScale;
|
|
378
|
+
// NORMAL_MAP_EPS floors the tangent frame's magnitude so a face with
|
|
379
|
+
// no UV variation (degenerate derivatives) yields the geometric normal
|
|
380
|
+
// instead of a NaN.
|
|
381
|
+
const float NORMAL_MAP_EPS = 1e-20;
|
|
382
|
+
vec3 perturbNormal(vec3 n, vec3 worldPos, vec2 uv) {
|
|
383
|
+
vec3 mapN = texture(uNormalMap, uv).xyz * 2.0 - 1.0;
|
|
384
|
+
mapN.xy *= uNormalScale;
|
|
385
|
+
vec3 dp1 = dFdx(worldPos);
|
|
386
|
+
vec3 dp2 = dFdy(worldPos);
|
|
387
|
+
vec2 duv1 = dFdx(uv);
|
|
388
|
+
vec2 duv2 = dFdy(uv);
|
|
389
|
+
vec3 dp2perp = cross(dp2, n);
|
|
390
|
+
vec3 dp1perp = cross(n, dp1);
|
|
391
|
+
vec3 t = dp2perp * duv1.x + dp1perp * duv2.x;
|
|
392
|
+
vec3 b = dp2perp * duv1.y + dp1perp * duv2.y;
|
|
393
|
+
float invMax = inversesqrt(max(max(dot(t, t), dot(b, b)), NORMAL_MAP_EPS));
|
|
394
|
+
return normalize(mat3(t * invMax, b * invMax, n) * mapN);
|
|
395
|
+
}
|
|
396
|
+
`
|
|
397
|
+
|
|
130
398
|
/**
|
|
131
399
|
* The scene's fog (`scene.setFog`): the uniform set it writes - `uFogColor`,
|
|
132
400
|
* the linear band `uFogNear` / `uFogInv` (1 / (far - near)), the exp2
|
|
@@ -174,31 +442,170 @@ export const FOG = glsl`
|
|
|
174
442
|
}
|
|
175
443
|
`
|
|
176
444
|
|
|
445
|
+
/**
|
|
446
|
+
* The sRGB transfer functions (IEC 61966-2-1): `vec3 srgbToLinear(vec3)`
|
|
447
|
+
* and `vec3 linearToSrgb(vec3)`, clamped at zero. An "rgba8-srgb" texture
|
|
448
|
+
* decodes in hardware and needs neither; these serve pixels a format
|
|
449
|
+
* cannot tag (a render target sampled as a map) and the output stage.
|
|
450
|
+
* OUTPUT includes this set - never compose both.
|
|
451
|
+
*/
|
|
452
|
+
export const SRGB = glsl`
|
|
453
|
+
vec3 srgbToLinear(vec3 c) {
|
|
454
|
+
c = max(c, vec3(0.0));
|
|
455
|
+
return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));
|
|
456
|
+
}
|
|
457
|
+
vec3 linearToSrgb(vec3 c) {
|
|
458
|
+
c = max(c, vec3(0.0));
|
|
459
|
+
return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));
|
|
460
|
+
}
|
|
461
|
+
`
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* The output stage every library fragment ends with: `vec4
|
|
465
|
+
* outputColor(vec3 rgb, float alpha)` takes the PREMULTIPLIED linear
|
|
466
|
+
* result and its alpha, applies the scene's exposure and tone mapping
|
|
467
|
+
* (`uniform float uExposure`; `uniform float uToneMapping`, 0 none, 1
|
|
468
|
+
* ACES - the shared params scene.setExposure / setToneMapping write),
|
|
469
|
+
* encodes to sRGB and returns the premultiplied pixel the target holds.
|
|
470
|
+
* It un-premultiplies inside so the encode is exact at partial alpha.
|
|
471
|
+
* Compose it last in a custom fragment: `fragColor = outputColor(rgb,
|
|
472
|
+
* alpha);` - a fragment that writes fragColor directly writes final
|
|
473
|
+
* encoded pixels and skips exposure and tone mapping. Includes SRGB.
|
|
474
|
+
*/
|
|
475
|
+
export const OUTPUT = glsl`
|
|
476
|
+
uniform float uExposure;
|
|
477
|
+
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
|
+
${SRGB}
|
|
484
|
+
// Stephen Hill's fit of the ACES RRT and ODT (Three's ACESFilmic, Godot's
|
|
485
|
+
// ACES): the matrices between sRGB and the fit's working space, and the
|
|
486
|
+
// exposure scale that puts middle gray where the reference does.
|
|
487
|
+
const float ACES_EXPOSURE_SCALE = 1.0 / 0.6;
|
|
488
|
+
const mat3 ACES_INPUT = mat3(
|
|
489
|
+
0.59719, 0.07600, 0.02840,
|
|
490
|
+
0.35458, 0.90834, 0.13383,
|
|
491
|
+
0.04823, 0.01566, 0.83777);
|
|
492
|
+
const mat3 ACES_OUTPUT = mat3(
|
|
493
|
+
1.60475, -0.10208, -0.00327,
|
|
494
|
+
-0.53108, 1.10813, -0.07276,
|
|
495
|
+
-0.07367, -0.00605, 1.07602);
|
|
496
|
+
vec3 acesFit(vec3 v) {
|
|
497
|
+
vec3 a = v * (v + 0.0245786) - 0.000090537;
|
|
498
|
+
vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;
|
|
499
|
+
return a / b;
|
|
500
|
+
}
|
|
501
|
+
vec3 acesToneMap(vec3 c) {
|
|
502
|
+
c = ACES_INPUT * (c * ACES_EXPOSURE_SCALE);
|
|
503
|
+
c = acesFit(c);
|
|
504
|
+
return clamp(ACES_OUTPUT * c, 0.0, 1.0);
|
|
505
|
+
}
|
|
506
|
+
vec4 outputColor(vec3 rgb, float alpha) {
|
|
507
|
+
vec3 c = alpha > 0.0 ? rgb / alpha : rgb;
|
|
508
|
+
c *= uExposure;
|
|
509
|
+
if (uToneMapping > 0.5) c = acesToneMap(c);
|
|
510
|
+
// Linear output keeps its range (a half-float probe holds a sun's
|
|
511
|
+
// radiance); the display encode is 8-bit and clamps first.
|
|
512
|
+
if (uOutputEncode > 0.5) c = linearToSrgb(clamp(c, 0.0, 1.0));
|
|
513
|
+
else c = max(c, vec3(0.0));
|
|
514
|
+
return vec4(c * alpha, alpha);
|
|
515
|
+
}
|
|
516
|
+
`
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* The scene's environment as a reflecting program declares it: `uEnv`
|
|
520
|
+
* (a samplerCube the scene binds on every receiving target - a 1x1 black
|
|
521
|
+
* cube while no environment is set), `uEnvIntensity`, `uEnvRotation`
|
|
522
|
+
* (the inverse of the environment's y turn, the skybox's convention) and
|
|
523
|
+
* `uEnvOn` (0 with no environment, so the term contributes nothing
|
|
524
|
+
* instead of reflecting black). `envRadiance(r, roughness)` samples the
|
|
525
|
+
* environment along world direction `r` at a mip level from a perceptual
|
|
526
|
+
* roughness - 0 the sharp base level, 1 the level whose faces are
|
|
527
|
+
* ENV_ROUGH_FACE (4) texels wide, linear between (Godot's
|
|
528
|
+
* `roughness * MAX_ROUGHNESS_LOD`; Unity's six LOD steps; Three maps
|
|
529
|
+
* onto a 16-texel floor): the level a baked chain (`srt tool
|
|
530
|
+
* 3d/environment`, loadEnvironment) convolves at that roughness, and a
|
|
531
|
+
* generated chain merely box-filters - scaled by the intensity: the
|
|
532
|
+
* prefiltered radiance of the split sum (`standard` multiplies it by PBR's
|
|
533
|
+
* envBrdf); a cube uploaded without mipmaps stays sharp.
|
|
534
|
+
* `envIrradiance(n)` is the diffuse face of it - the fully rough sample
|
|
535
|
+
* along the normal, what Three (`getIBLIrradiance`) and Godot read for
|
|
536
|
+
* the image-lit diffuse term; `standard` adds it to the hemisphere
|
|
537
|
+
* ambient. `envReflection(n, v, shininess)`
|
|
538
|
+
* is the Blinn-Phong face of it: the mirror direction (v toward the
|
|
539
|
+
* camera) at roughness `sqrt(2 / (shininess + 2))`, so a wide sheen
|
|
540
|
+
* reflects a blurred environment and a mirror dot a sharp one. `envWeight(n, v,
|
|
541
|
+
* reflectivity)` is the Schlick fresnel weight: `reflectivity` face-on,
|
|
542
|
+
* 1 at grazing, 0 with no environment. `lit` applies them as
|
|
543
|
+
* `rgb = mix(rgb, envReflection(n, v, uShininess), envWeight(n, v,
|
|
544
|
+
* uReflectivity))` - Three's MixOperation with a fresnel weight. Every
|
|
545
|
+
* lookup is a plain `texture(uEnv, dir)` in world space: a cube map here
|
|
546
|
+
* holds what a lookup returns (GL's own convention, as Godot and Unity
|
|
547
|
+
* sample theirs), so there is no handedness flip and a Three shader
|
|
548
|
+
* ported with `flipEnvMap` drops it.
|
|
549
|
+
*/
|
|
550
|
+
export const ENVIRONMENT = glsl`
|
|
551
|
+
uniform samplerCube uEnv;
|
|
552
|
+
uniform float uEnvIntensity;
|
|
553
|
+
uniform mat4 uEnvRotation;
|
|
554
|
+
uniform float uEnvOn;
|
|
555
|
+
// Schlick's approximation exponent.
|
|
556
|
+
const float ENV_SCHLICK_POWER = 5.0;
|
|
557
|
+
// log2 of the face edge (4 texels) a roughness of 1 samples: the chain
|
|
558
|
+
// is convolved down to it (environment-bake.ts ENV_ROUGH_FACE), and the
|
|
559
|
+
// levels below keep the sampler complete.
|
|
560
|
+
const float ENV_ROUGH_FACE_LOG2 = 2.0;
|
|
561
|
+
vec3 envRadiance(vec3 r, float roughness) {
|
|
562
|
+
float roughLevel = max(log2(float(textureSize(uEnv, 0).x)) - ENV_ROUGH_FACE_LOG2, 0.0);
|
|
563
|
+
return textureLod(uEnv, mat3(uEnvRotation) * r, roughness * roughLevel).rgb * uEnvIntensity;
|
|
564
|
+
}
|
|
565
|
+
vec3 envIrradiance(vec3 n) {
|
|
566
|
+
return envRadiance(n, 1.0);
|
|
567
|
+
}
|
|
568
|
+
vec3 envReflection(vec3 n, vec3 v, float shininess) {
|
|
569
|
+
return envRadiance(reflect(-v, n), sqrt(2.0 / (shininess + 2.0)));
|
|
570
|
+
}
|
|
571
|
+
float envWeight(vec3 n, vec3 v, float reflectivity) {
|
|
572
|
+
float f = pow(1.0 - max(dot(n, v), 0.0), ENV_SCHLICK_POWER);
|
|
573
|
+
return (reflectivity + (1.0 - reflectivity) * f) * uEnvOn;
|
|
574
|
+
}
|
|
575
|
+
`
|
|
576
|
+
|
|
177
577
|
/**
|
|
178
578
|
* The scene's shadow set as a receiving program declares it: ONE
|
|
179
|
-
* `uShadowAtlas` (every casting light's depth map is
|
|
180
|
-
* maps render as one pass; a
|
|
579
|
+
* `uShadowAtlas` (a `sampler2DShadow` - every casting light's depth map is
|
|
580
|
+
* a tile of it, so N maps render as one pass; a cleared one-texel depth
|
|
581
|
+
* map when nothing casts), a MAP slot
|
|
181
582
|
* set of `MAX_SHADOW_MAPS` - `uShadowRect[M]` (map slot j's tile as x, y,
|
|
182
583
|
* width, height in atlas 0..1 UV) and `uShadowMatrix[M]` (its light-space
|
|
183
|
-
* viewProj) - and, per
|
|
584
|
+
* viewProj) - and, per light index (the light list's, hemisphere
|
|
585
|
+
* excluded), `uShadowFirst[N]` /
|
|
184
586
|
* `uShadowCount[N]` (light i's maps are slots `first .. first + count - 1`;
|
|
185
587
|
* count 0 = it does not cast; a box light has one map, a cascaded light
|
|
186
|
-
* `shadow.cascades` of them
|
|
187
|
-
*
|
|
588
|
+
* `shadow.cascades` of them tightest first, a point light six face maps
|
|
589
|
+
* in +X, -X, +Y, -Y, +Z, -Z order), `uShadowBias[N]`,
|
|
590
|
+
* `uShadowNormalBias[N]` and `uShadowRadius[N]` (the filter radius in
|
|
591
|
+
* map texels, 1 = one tap). The scene binds and writes all of it on every
|
|
188
592
|
* target a receiving material can draw into; a custom material composes
|
|
189
|
-
* this,
|
|
593
|
+
* LIGHT_SLOTS, this, SHADOW, then SHADOW_LOOKUP (in that order - the
|
|
594
|
+
* lookup reads the light type and position for a point light's face
|
|
595
|
+
* select) and multiplies
|
|
190
596
|
* light i's term by `lightShadow(i, worldPos, n)` - `lit` is the shape. A
|
|
191
597
|
* material that does not receive composes none of it, so it declares no
|
|
192
598
|
* sampler for nothing.
|
|
193
599
|
*/
|
|
194
600
|
export const SHADOW_SLOTS = glsl`
|
|
195
|
-
uniform
|
|
601
|
+
uniform sampler2DShadow uShadowAtlas;
|
|
196
602
|
uniform vec4 uShadowRect[${MAX_SHADOW_MAPS}];
|
|
197
603
|
uniform mat4 uShadowMatrix[${MAX_SHADOW_MAPS}];
|
|
198
604
|
uniform int uShadowFirst[${MAX_LIGHTS}];
|
|
199
605
|
uniform int uShadowCount[${MAX_LIGHTS}];
|
|
200
606
|
uniform float uShadowBias[${MAX_LIGHTS}];
|
|
201
607
|
uniform float uShadowNormalBias[${MAX_LIGHTS}];
|
|
608
|
+
uniform float uShadowRadius[${MAX_LIGHTS}];
|
|
202
609
|
`
|
|
203
610
|
|
|
204
611
|
/**
|
|
@@ -208,18 +615,25 @@ export const SHADOW_SLOTS = glsl`
|
|
|
208
615
|
* point: xy in 0..1 across the map, z the depth to compare. `bool
|
|
209
616
|
* shadowInside(vec3 p)` is whether the map has it at all (xy in 0..1, z
|
|
210
617
|
* not past the far plane) - the cascade select. `float shadowSample(
|
|
211
|
-
*
|
|
212
|
-
* shadowed) of a point the map has
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
618
|
+
* sampler2DShadow map, vec4 rect, vec3 p, float bias, float radius)` is
|
|
619
|
+
* the factor (1 lit, 0 shadowed) of a point the map has. At `radius` 1
|
|
620
|
+
* it is ONE comparison tap - the sampler compares `p.z - bias` against
|
|
621
|
+
* the map in hardware (LEQUAL) and LINEAR-weights the four neighbours'
|
|
622
|
+
* results, the 2x2 PCF a shader loop cannot match (it weights the
|
|
623
|
+
* compare, not the depth). Above 1 it averages a 3x3 grid of those taps
|
|
624
|
+
* `radius` texels apart (Three's PCF kernel: the edge softens over about
|
|
625
|
+
* 2 * radius texels; past ~3 the taps separate and the edge bands). A tap
|
|
626
|
+
* lands in the map's tile `rect` (x, y, width, height in the atlas's 0..1
|
|
627
|
+
* UV; `vec4(0, 0, 1, 1)` is a whole map), clamped to the tile inset by
|
|
628
|
+
* half a texel so the footprint never reads a neighbouring map's tile.
|
|
629
|
+
* `float shadow(sampler2DShadow map, vec4 rect, vec4 coord, float bias,
|
|
630
|
+
* float radius)` composes the three: 1 (lit) outside the map, else the
|
|
631
|
+
* sample - `shadow(uShadowAtlas, uShadowRect[0], uShadowMatrix[0] *
|
|
632
|
+
* vec4(vWorldPos, 1.0), uShadowBias[0], uShadowRadius[0])`.
|
|
633
|
+
* SHADOW_LOOKUP uses the steps, so it projects each map once. The engine
|
|
634
|
+
* binds the comparison sampler wherever a program declares the uniform
|
|
635
|
+
* as sampler2DShadow; a program declaring plain `sampler2D uShadowAtlas`
|
|
636
|
+
* (hand-rolled old GLSL) still reads raw depth values at nearest.
|
|
223
637
|
*/
|
|
224
638
|
export const SHADOW = glsl`
|
|
225
639
|
vec3 shadowPoint(vec4 coord) {
|
|
@@ -230,43 +644,50 @@ export const SHADOW = glsl`
|
|
|
230
644
|
return all(greaterThanEqual(p.xy, vec2(0.0))) && all(lessThanEqual(p, vec3(1.0)));
|
|
231
645
|
}
|
|
232
646
|
|
|
233
|
-
float shadowSample(
|
|
647
|
+
float shadowSample(sampler2DShadow map, vec4 rect, vec3 p, float bias, float radius) {
|
|
234
648
|
vec2 texel = 1.0 / vec2(textureSize(map, 0));
|
|
235
649
|
vec2 lo = rect.xy + 0.5 * texel;
|
|
236
650
|
vec2 hi = rect.xy + rect.zw - 0.5 * texel;
|
|
237
651
|
vec2 base = rect.xy + p.xy * rect.zw;
|
|
238
|
-
float
|
|
652
|
+
float z = p.z - bias;
|
|
653
|
+
if (radius <= 1.0) return texture(map, vec3(clamp(base, lo, hi), z));
|
|
654
|
+
vec2 step = texel * radius;
|
|
655
|
+
float sum = 0.0;
|
|
239
656
|
for (int y = -1; y <= 1; y++) {
|
|
240
657
|
for (int x = -1; x <= 1; x++) {
|
|
241
|
-
|
|
242
|
-
lit += p.z - bias <= d ? 1.0 : 0.0;
|
|
658
|
+
sum += texture(map, vec3(clamp(base + vec2(float(x), float(y)) * step, lo, hi), z));
|
|
243
659
|
}
|
|
244
660
|
}
|
|
245
|
-
return
|
|
661
|
+
return sum / 9.0;
|
|
246
662
|
}
|
|
247
663
|
|
|
248
|
-
float shadow(
|
|
664
|
+
float shadow(sampler2DShadow map, vec4 rect, vec4 coord, float bias, float radius) {
|
|
249
665
|
vec3 p = shadowPoint(coord);
|
|
250
|
-
return shadowInside(p) ? shadowSample(map, rect, p, bias) : 1.0;
|
|
666
|
+
return shadowInside(p) ? shadowSample(map, rect, p, bias, radius) : 1.0;
|
|
251
667
|
}
|
|
252
668
|
`
|
|
253
669
|
|
|
254
670
|
/**
|
|
255
|
-
* The step from a light index to its shadow factor, over
|
|
256
|
-
* SHADOW (compose
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
671
|
+
* The step from a light index to its shadow factor, over LIGHT_SLOTS,
|
|
672
|
+
* SHADOW_SLOTS and SHADOW (compose all three first - the light type and
|
|
673
|
+
* position pick a point light's face map). `float lightShadow(int i,
|
|
674
|
+
* vec3 worldPos, vec3 n)` is the one to call per light: 1 for a light
|
|
675
|
+
* that does not cast, else the factor for `worldPos` pushed along its
|
|
676
|
+
* normal `n` by `uShadowNormalBias[i]` (the acne knob to reach for
|
|
677
|
+
* first). A point light's six face maps are selected directly: the
|
|
678
|
+
* dominant axis of the light-to-point vector names the face (the slot
|
|
679
|
+
* order +X, -X, +Y, -Y, +Z, -Z), one projection, one tap. Every other
|
|
680
|
+
* light is looked up in the FIRST of its maps that has the point (a box
|
|
681
|
+
* light has one; a cascaded light's maps come tightest first, so the
|
|
682
|
+
* sharpest cascade that has the point wins and a point past the last is
|
|
683
|
+
* lit) with that map's `uShadowMatrix[j]`, its tile, `uShadowBias[i]`
|
|
684
|
+
* and `uShadowRadius[i]`. Inside the outer SHADOW_BLEND of a map (in map 0..1
|
|
685
|
+
* units, so 0.1 is its outer 10% on each side) the factor fades into
|
|
686
|
+
* the next cascade's, so the hand-over is a band and not a seam; the
|
|
687
|
+
* last map, a box light's only one, and any rim the next cascade does
|
|
688
|
+
* not reach (the near side, at the camera's feet) have no band.
|
|
689
|
+
* Position and normal are arguments, so no varying name is pinned and a
|
|
690
|
+
* custom vertex stage composes freely.
|
|
270
691
|
*/
|
|
271
692
|
export const SHADOW_LOOKUP = glsl`
|
|
272
693
|
const float SHADOW_BLEND = 0.1;
|
|
@@ -276,19 +697,859 @@ export const SHADOW_LOOKUP = glsl`
|
|
|
276
697
|
if (count == 0) return 1.0;
|
|
277
698
|
vec4 w = vec4(worldPos + n * uShadowNormalBias[i], 1.0);
|
|
278
699
|
float bias = uShadowBias[i];
|
|
700
|
+
float radius = uShadowRadius[i];
|
|
279
701
|
int first = uShadowFirst[i];
|
|
702
|
+
if (uLightType[i] == LIGHT_POINT) {
|
|
703
|
+
// Six 90-degree face frusta partition every direction: the
|
|
704
|
+
// dominant axis IS the face, so no scan and no behind-the-camera
|
|
705
|
+
// projection (a perspective map's w flips sign there and xy can
|
|
706
|
+
// land inside 0..1 spuriously).
|
|
707
|
+
vec3 dv = worldPos - uLightPos[i];
|
|
708
|
+
vec3 ad = abs(dv);
|
|
709
|
+
int face;
|
|
710
|
+
if (ad.x >= ad.y && ad.x >= ad.z) face = dv.x > 0.0 ? 0 : 1;
|
|
711
|
+
else if (ad.y >= ad.z) face = dv.y > 0.0 ? 2 : 3;
|
|
712
|
+
else face = dv.z > 0.0 ? 4 : 5;
|
|
713
|
+
int j = first + face;
|
|
714
|
+
return shadow(uShadowAtlas, uShadowRect[j], uShadowMatrix[j] * w, bias, radius);
|
|
715
|
+
}
|
|
280
716
|
int last = first + count - 1;
|
|
281
717
|
for (int j = first; j <= last; j++) {
|
|
282
718
|
vec3 p = shadowPoint(uShadowMatrix[j] * w);
|
|
283
719
|
if (!shadowInside(p)) continue;
|
|
284
|
-
float s = shadowSample(uShadowAtlas, uShadowRect[j], p, bias);
|
|
720
|
+
float s = shadowSample(uShadowAtlas, uShadowRect[j], p, bias, radius);
|
|
285
721
|
if (j == last) return s;
|
|
286
722
|
float edge = min(min(p.x, 1.0 - p.x), min(p.y, 1.0 - p.y));
|
|
287
723
|
if (edge >= SHADOW_BLEND) return s;
|
|
288
724
|
vec3 q = shadowPoint(uShadowMatrix[j + 1] * w);
|
|
289
725
|
if (!shadowInside(q)) return s;
|
|
290
|
-
return mix(shadowSample(uShadowAtlas, uShadowRect[j + 1], q, bias), s, edge / SHADOW_BLEND);
|
|
726
|
+
return mix(shadowSample(uShadowAtlas, uShadowRect[j + 1], q, bias, radius), s, edge / SHADOW_BLEND);
|
|
291
727
|
}
|
|
292
728
|
return 1.0;
|
|
293
729
|
}
|
|
294
730
|
`
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* The material defaults every Surface starts from (`surfaceOf`) and the
|
|
734
|
+
* `lit`/`standard` options fall back to: one table, so a custom fragment
|
|
735
|
+
* that builds its own Surface and a stock material agree on what
|
|
736
|
+
* "unspecified" means.
|
|
737
|
+
*/
|
|
738
|
+
export const SURFACE_DEFAULTS = {
|
|
739
|
+
/** Blinn-Phong highlight strength: none, pure diffuse. */
|
|
740
|
+
specular: 0,
|
|
741
|
+
/** Blinn-Phong exponent: a medium sheen. */
|
|
742
|
+
shininess: 30,
|
|
743
|
+
/** Environment mix weight face-on: none. */
|
|
744
|
+
reflectivity: 0,
|
|
745
|
+
/** A dielectric. */
|
|
746
|
+
metalness: 0,
|
|
747
|
+
/** Fully matte. */
|
|
748
|
+
roughness: 1,
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* What a material knows about a fragment, as one struct - the contract
|
|
753
|
+
* between a surface description and the scene's shading (Godot's
|
|
754
|
+
* fragment() outputs, URP's SurfaceData, Filament's MaterialInputs):
|
|
755
|
+
*
|
|
756
|
+
* struct Surface {
|
|
757
|
+
* vec4 base; // linear light, PREMULTIPLIED; alpha = coverage
|
|
758
|
+
* vec3 normal; // unit, facing the viewer
|
|
759
|
+
* vec3 emissive; // added after lighting, unlit by design, fogged
|
|
760
|
+
* vec3 ambient; // extra irradiance (a light map), added to the diffuse sum
|
|
761
|
+
* float specular, shininess, reflectivity; // the Blinn-Phong model (shadeBlinn)
|
|
762
|
+
* float metalness, roughness; // the metalness/roughness model (shadePbr)
|
|
763
|
+
* };
|
|
764
|
+
*
|
|
765
|
+
* `Surface surfaceOf(vec4 base, vec3 normal)` fills the rest with
|
|
766
|
+
* SURFACE_DEFAULTS (no emissive, no ambient), so a fragment sets only
|
|
767
|
+
* what it means. One struct for both light models, as Godot and Unity
|
|
768
|
+
* have it, so a `surface` function (litFragment's slot) has one
|
|
769
|
+
* signature whatever material it sits in; the model that shades reads
|
|
770
|
+
* its own fields and ignores the other's. Included by sceneSource - never
|
|
771
|
+
* compose both.
|
|
772
|
+
*/
|
|
773
|
+
export const SURFACE = glsl`
|
|
774
|
+
struct Surface {
|
|
775
|
+
vec4 base;
|
|
776
|
+
vec3 normal;
|
|
777
|
+
vec3 emissive;
|
|
778
|
+
vec3 ambient;
|
|
779
|
+
float specular;
|
|
780
|
+
float shininess;
|
|
781
|
+
float reflectivity;
|
|
782
|
+
float metalness;
|
|
783
|
+
float roughness;
|
|
784
|
+
};
|
|
785
|
+
const float SURFACE_SPECULAR = ${SURFACE_DEFAULTS.specular.toFixed(1)};
|
|
786
|
+
const float SURFACE_SHININESS = ${SURFACE_DEFAULTS.shininess.toFixed(1)};
|
|
787
|
+
const float SURFACE_REFLECTIVITY = ${SURFACE_DEFAULTS.reflectivity.toFixed(1)};
|
|
788
|
+
const float SURFACE_METALNESS = ${SURFACE_DEFAULTS.metalness.toFixed(1)};
|
|
789
|
+
const float SURFACE_ROUGHNESS = ${SURFACE_DEFAULTS.roughness.toFixed(1)};
|
|
790
|
+
Surface surfaceOf(vec4 base, vec3 normal) {
|
|
791
|
+
return Surface(base, normal, vec3(0.0), vec3(0.0), SURFACE_SPECULAR, SURFACE_SHININESS, SURFACE_REFLECTIVITY, SURFACE_METALNESS, SURFACE_ROUGHNESS);
|
|
792
|
+
}
|
|
793
|
+
`
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* What the scene set composes: every flag defaults to true, and each one
|
|
797
|
+
* exists only because it changes a DECLARATION (a sampler the program
|
|
798
|
+
* would otherwise bind for nothing, a uniform block it would otherwise
|
|
799
|
+
* carry) - the light list itself is dynamic (uLightCount, uShadowCount),
|
|
800
|
+
* so there is no per-light or per-effect variant to pick.
|
|
801
|
+
*/
|
|
802
|
+
export type SceneSourceOptions = {
|
|
803
|
+
/** The light list, the hemisphere, the shade functions and sceneLight.
|
|
804
|
+
* False is an unlit program: only sceneOutput remains (and Surface). */
|
|
805
|
+
lights?: boolean
|
|
806
|
+
/** Compose the shadow set and factor each light by it. False declares
|
|
807
|
+
* no shadow sampler; every light is unshadowed. Needs `lights`. */
|
|
808
|
+
receiveShadow?: boolean
|
|
809
|
+
/** Compose ENVIRONMENT: shadeBlinn mixes the fresnel-weighted mirror
|
|
810
|
+
* reflection in by `Surface.reflectivity`, shadePbr adds the image-lit
|
|
811
|
+
* diffuse and the split-sum specular. False declares no environment
|
|
812
|
+
* cube and shades with the lights alone. Needs `lights`. */
|
|
813
|
+
env?: boolean
|
|
814
|
+
/** Fog inside sceneOutput. False leaves the fog set out. */
|
|
815
|
+
fog?: boolean
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* The scene as a program declares it, in one set: everything a fragment
|
|
820
|
+
* needs to be a citizen of the scene - lit by its lights, shadowed by its
|
|
821
|
+
* casters, reflecting its environment, fogged, exposed, tone mapped and
|
|
822
|
+
* encoded like the stock materials, because the stock materials are
|
|
823
|
+
* built from this very source. It declares `uCamPos`, `uHemiSky` /
|
|
824
|
+
* `uHemiGround`, LIGHT_SLOTS + LIGHT_LOOKUP, the SHADOW trio, the pure
|
|
825
|
+
* light functions (HEMISPHERE, LAMBERT, BLINN_SPECULAR, PBR),
|
|
826
|
+
* ENVIRONMENT, FOG and OUTPUT (per the flags), SURFACE, and exports:
|
|
827
|
+
*
|
|
828
|
+
* struct SceneLight { vec3 dir; vec3 color; };
|
|
829
|
+
* SceneLight sceneLight(int i, vec3 position, vec3 normal);
|
|
830
|
+
* vec3 shadeBlinn(Surface s, vec3 position);
|
|
831
|
+
* vec3 shadePbr(Surface s, vec3 position);
|
|
832
|
+
* vec4 sceneOutput(vec3 rgb, float alpha, vec3 position);
|
|
833
|
+
*
|
|
834
|
+
* `sceneLight` is light i seen from a world point: `dir` the unit vector
|
|
835
|
+
* TOWARD it, `color` its rgb already attenuated (falloff, cone) and
|
|
836
|
+
* shadowed - zero when it cannot reach the fragment, and then no shadow
|
|
837
|
+
* tap was spent. It is the accessor a custom light model loops over to
|
|
838
|
+
* `uLightCount` (URP's GetAdditionalLight, Godot's light() inputs), and
|
|
839
|
+
* the one that makes a spot light impossible to render as a directional.
|
|
840
|
+
*
|
|
841
|
+
* The shade functions are the whole light loop of `lit` (Blinn-Phong)
|
|
842
|
+
* and `standard` (GGX): hemisphere plus `Surface.ambient` plus every
|
|
843
|
+
* light's diffuse and specular, the environment term, then the emissive,
|
|
844
|
+
* returning linear PREMULTIPLIED rgb. A term added by hand after them is
|
|
845
|
+
* multiplied by the surface's alpha, the premultiplied rule the functions
|
|
846
|
+
* follow inside. The view vector is derived from `uCamPos` and
|
|
847
|
+
* `position`, so no varying name is pinned: pass whatever your vertex
|
|
848
|
+
* stage carries the world position in.
|
|
849
|
+
*
|
|
850
|
+
* `sceneOutput` is the tail every library fragment ends with: fog over
|
|
851
|
+
* the premultiplied color, then OUTPUT's exposure, tone mapping and
|
|
852
|
+
* encode - the one place a post effect or a scene buffer change lands.
|
|
853
|
+
*
|
|
854
|
+
* ${SCENE}
|
|
855
|
+
* void main() {
|
|
856
|
+
* Surface s = surfaceOf(vec4(albedo, 1.0), normalize(vNormal));
|
|
857
|
+
* s.specular = 0.4;
|
|
858
|
+
* fragColor = sceneOutput(shadeBlinn(s, vWorldPos), 1.0, vWorldPos);
|
|
859
|
+
* }
|
|
860
|
+
*
|
|
861
|
+
* `SCENE` is `sceneSource()` with every flag on; `sceneSource(flags)`
|
|
862
|
+
* leaves the samplers a program does not need undeclared. A fragment that
|
|
863
|
+
* composes this declares none of the sets it includes by itself.
|
|
864
|
+
*/
|
|
865
|
+
export function sceneSource(o: SceneSourceOptions = {}): string {
|
|
866
|
+
let lights = o.lights !== false
|
|
867
|
+
let receiveShadow = lights && o.receiveShadow !== false
|
|
868
|
+
let env = lights && o.env !== false
|
|
869
|
+
let fog = o.fog !== false
|
|
870
|
+
return glsl`
|
|
871
|
+
${SURFACE}
|
|
872
|
+
${lights || fog ? "uniform vec3 uCamPos;" : ""}
|
|
873
|
+
${
|
|
874
|
+
lights
|
|
875
|
+
? `uniform vec3 uHemiSky;
|
|
876
|
+
uniform vec3 uHemiGround;
|
|
877
|
+
${LIGHT_SLOTS}
|
|
878
|
+
${LIGHT_LOOKUP}`
|
|
879
|
+
: ""
|
|
880
|
+
}
|
|
881
|
+
${
|
|
882
|
+
receiveShadow
|
|
883
|
+
? `${SHADOW_SLOTS}
|
|
884
|
+
${SHADOW}
|
|
885
|
+
${SHADOW_LOOKUP}`
|
|
886
|
+
: ""
|
|
887
|
+
}
|
|
888
|
+
${
|
|
889
|
+
lights
|
|
890
|
+
? `${HEMISPHERE}
|
|
891
|
+
${LAMBERT}
|
|
892
|
+
${BLINN_SPECULAR}
|
|
893
|
+
${PBR}`
|
|
894
|
+
: ""
|
|
895
|
+
}
|
|
896
|
+
${env ? ENVIRONMENT : ""}
|
|
897
|
+
${fog ? FOG : ""}
|
|
898
|
+
${OUTPUT}
|
|
899
|
+
${lights ? sceneShadeSource(receiveShadow, env) : ""}
|
|
900
|
+
|
|
901
|
+
vec4 sceneOutput(vec3 rgb, float alpha, vec3 position) {
|
|
902
|
+
${fog ? "rgb = fog(rgb, alpha, position, uCamPos);" : ""}
|
|
903
|
+
return outputColor(rgb, alpha);
|
|
904
|
+
}
|
|
905
|
+
`
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** `sceneSource()` with every flag on. */
|
|
909
|
+
export const SCENE = sceneSource()
|
|
910
|
+
|
|
911
|
+
// The light accessor and the two shade functions, over the sets
|
|
912
|
+
// sceneSource composed before them. A light that cannot reach the point
|
|
913
|
+
// is skipped whole (no shadow tap, no terms), the gate `lit` always had.
|
|
914
|
+
function sceneShadeSource(receiveShadow: boolean, env: boolean): string {
|
|
915
|
+
return glsl`
|
|
916
|
+
struct SceneLight {
|
|
917
|
+
vec3 dir;
|
|
918
|
+
vec3 color;
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
SceneLight sceneLight(int i, vec3 position, vec3 normal) {
|
|
922
|
+
SceneLight light;
|
|
923
|
+
float a = lightVector(i, position, light.dir);
|
|
924
|
+
${receiveShadow ? "if (a > 0.0) a *= lightShadow(i, position, normal);" : ""}
|
|
925
|
+
light.color = uLightColor[i] * a;
|
|
926
|
+
return light;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
vec3 shadeBlinn(Surface s, vec3 position) {
|
|
930
|
+
vec3 n = s.normal;
|
|
931
|
+
vec3 v = normalize(uCamPos - position);
|
|
932
|
+
vec3 light = hemisphere(n, uHemiSky, uHemiGround);
|
|
933
|
+
light += s.ambient;
|
|
934
|
+
vec3 spec = vec3(0.0);
|
|
935
|
+
for (int i = 0; i < ${MAX_LIGHTS}; i++) {
|
|
936
|
+
if (i >= uLightCount) break;
|
|
937
|
+
SceneLight l = sceneLight(i, position, n);
|
|
938
|
+
if (all(equal(l.color, vec3(0.0)))) continue;
|
|
939
|
+
light += l.color * lambert(n, l.dir);
|
|
940
|
+
spec += l.color * blinnSpecular(n, v, l.dir, s.shininess);
|
|
941
|
+
}
|
|
942
|
+
vec3 rgb = s.base.rgb * light + spec * s.specular * s.base.a;
|
|
943
|
+
${env ? "rgb = mix(rgb, envReflection(n, v, s.shininess) * s.base.a, envWeight(n, v, s.reflectivity));" : ""}
|
|
944
|
+
rgb += s.emissive * s.base.a;
|
|
945
|
+
return rgb;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
vec3 shadePbr(Surface s, vec3 position) {
|
|
949
|
+
vec3 n = s.normal;
|
|
950
|
+
vec3 v = normalize(uCamPos - position);
|
|
951
|
+
vec3 diffuseColor = s.base.rgb * (1.0 - s.metalness);
|
|
952
|
+
vec3 f0 = mix(vec3(DIELECTRIC_F0) * s.base.a, s.base.rgb, s.metalness);
|
|
953
|
+
vec3 light = hemisphere(n, uHemiSky, uHemiGround);
|
|
954
|
+
${env ? "light += envIrradiance(n);" : ""}
|
|
955
|
+
light += s.ambient;
|
|
956
|
+
vec3 spec = vec3(0.0);
|
|
957
|
+
for (int i = 0; i < ${MAX_LIGHTS}; i++) {
|
|
958
|
+
if (i >= uLightCount) break;
|
|
959
|
+
SceneLight l = sceneLight(i, position, n);
|
|
960
|
+
if (all(equal(l.color, vec3(0.0)))) continue;
|
|
961
|
+
float nl = lambert(n, l.dir);
|
|
962
|
+
light += l.color * nl;
|
|
963
|
+
spec += l.color * (nl * ggxSpecular(n, v, l.dir, f0, s.roughness));
|
|
964
|
+
}
|
|
965
|
+
vec3 rgb = diffuseColor * light + spec;
|
|
966
|
+
${
|
|
967
|
+
env
|
|
968
|
+
? `vec2 ab = envBrdf(max(dot(n, v), 0.0), s.roughness);
|
|
969
|
+
rgb += envRadiance(reflect(-v, n), s.roughness) * (f0 * ab.x + ab.y * s.base.a);`
|
|
970
|
+
: ""
|
|
971
|
+
}
|
|
972
|
+
rgb += s.emissive * s.base.a;
|
|
973
|
+
return rgb;
|
|
974
|
+
}
|
|
975
|
+
`
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* The option set the lit program builders take: the flags that pick the
|
|
980
|
+
* program - the same names and defaults as LitOptions, minus the values
|
|
981
|
+
* (`map: true` says the fragment samples uMap, not which texture) - plus
|
|
982
|
+
* two slots an app splices its own GLSL into.
|
|
983
|
+
*
|
|
984
|
+
* The slots are a typed contract, not named locals: `prelude` adds
|
|
985
|
+
* declarations, `surface` is a function over the Surface struct (SURFACE)
|
|
986
|
+
* the program fills before shading, so no local of the generated program
|
|
987
|
+
* is part of the contract and restructuring it stays an internal change.
|
|
988
|
+
* A slot can read the varyings (vWorldPos, vNormal, vUv, and vColor with
|
|
989
|
+
* vertexColors), the uniforms the flags declare, and whatever `prelude`
|
|
990
|
+
* declares; a uniform `prelude` declares is an ordinary per-entry param,
|
|
991
|
+
* passed to instance() like uColor. Colors are linear light,
|
|
992
|
+
* PREMULTIPLIED (the engine's pixel contract), `Surface.base` included.
|
|
993
|
+
*
|
|
994
|
+
* Reach past the slots - a different light model, a term added after the
|
|
995
|
+
* shade - by composing a fragment from litVertex and the scene set
|
|
996
|
+
* (sceneSource) the program itself is built from. Same import list, not a
|
|
997
|
+
* lower tier.
|
|
998
|
+
*/
|
|
999
|
+
export type LitSourceOptions = {
|
|
1000
|
+
/** The fragment samples a `uniform sampler2D uMap`, tinted by uColor. */
|
|
1001
|
+
map?: boolean
|
|
1002
|
+
/** Multiply the base by the colored layout's per-vertex vColor; pairs
|
|
1003
|
+
* with LIT_VERTEX_COLORED (litVertex picks it). */
|
|
1004
|
+
vertexColors?: boolean
|
|
1005
|
+
/** Sample uMap by world position blended across the three axis planes,
|
|
1006
|
+
* at `uniform float uTriplanar` repeats per world unit, instead of by
|
|
1007
|
+
* UV. Needs `map`. */
|
|
1008
|
+
triplanar?: boolean
|
|
1009
|
+
/** Write the base alpha through and blend; opaque (the default) writes
|
|
1010
|
+
* alpha 1, so a leaked texel alpha cannot punch through it. */
|
|
1011
|
+
transparent?: boolean
|
|
1012
|
+
/** Compose the scene's shadow set and multiply each light's term by its
|
|
1013
|
+
* factor (default true). False declares no shadow sampler at all. */
|
|
1014
|
+
receiveShadow?: boolean
|
|
1015
|
+
/** Which faces the material's own pipeline drops (default "back"). The
|
|
1016
|
+
* fragment's business because a program that shows back faces lights
|
|
1017
|
+
* them with the normal flipped, else a double-sided leaf's back is
|
|
1018
|
+
* black. */
|
|
1019
|
+
cull?: CullMode
|
|
1020
|
+
/** Declare `uniform float uAlphaTest` and discard a fragment whose base
|
|
1021
|
+
* alpha falls below it (the cutoff is a per-entry uniform, so one
|
|
1022
|
+
* program serves every value). */
|
|
1023
|
+
alphaTest?: boolean
|
|
1024
|
+
/** Compose the scene's fog over the result (default true). */
|
|
1025
|
+
fog?: boolean
|
|
1026
|
+
/** Bend the lit normal by a tangent-space normal map (NORMAL_MAP:
|
|
1027
|
+
* `uniform sampler2D uNormalMap` scaled by `uniform float
|
|
1028
|
+
* uNormalScale`), sampled at the same uv as uMap. The frame comes from
|
|
1029
|
+
* screen-space derivatives - no tangent attribute. The cutout still
|
|
1030
|
+
* sees the geometric normal; the surface slot and the lighting see the
|
|
1031
|
+
* bent one. Not with `triplanar` (which samples by world position, not
|
|
1032
|
+
* uv). */
|
|
1033
|
+
normalMap?: boolean
|
|
1034
|
+
/** Add `uniform vec3 uEmissive` after the lighting terms - unlit by
|
|
1035
|
+
* design, shadow-proof, fogged like everything else. */
|
|
1036
|
+
emissive?: boolean
|
|
1037
|
+
/** `uniform sampler2D uEmissiveMap` multiplying uEmissive; implies
|
|
1038
|
+
* `emissive`. */
|
|
1039
|
+
emissiveMap?: boolean
|
|
1040
|
+
/** `uniform sampler2D uSpecularMap`: its RED channel scales uSpecular
|
|
1041
|
+
* per fragment (Three's specularMap - chrome and rubber on one mesh). */
|
|
1042
|
+
specularMap?: boolean
|
|
1043
|
+
/** Compose the scene's ENVIRONMENT and mix its mirror reflection in by
|
|
1044
|
+
* `uniform float uReflectivity` (the Schlick face-on weight), scaled
|
|
1045
|
+
* per fragment by uSpecularMap's red when `specularMap`. */
|
|
1046
|
+
env?: boolean
|
|
1047
|
+
/** Add a baked-light term: `uniform sampler2D uLightMap` times
|
|
1048
|
+
* `uniform float uLightMapIntensity`, sampled by the aUV2 channel and
|
|
1049
|
+
* ADDED to the light sum like the hemisphere term (a fully baked scene
|
|
1050
|
+
* runs with no lights at all). litVertex(o) then reads aUV2, so the
|
|
1051
|
+
* geometry must carry that channel. */
|
|
1052
|
+
lightMap?: boolean
|
|
1053
|
+
/** Declare `uniform vec4 uMapTransform` ([repeatU, repeatV, offsetU,
|
|
1054
|
+
* offsetV]) and sample every uv-driven map at `vUv * repeat + offset` -
|
|
1055
|
+
* ONE transform for the material's uv maps (Godot's uv1_offset/scale,
|
|
1056
|
+
* Unity's Tiling/Offset), deliberately not Three's per-texture
|
|
1057
|
+
* transform: a TextureId is a shared value whose sampling is
|
|
1058
|
+
* creation-time state. The shadow source transforms its cutout the
|
|
1059
|
+
* same way; lightMap's aUV2 is not transformed. Not with `triplanar`. */
|
|
1060
|
+
mapTransform?: boolean
|
|
1061
|
+
/** Skin positions and normals by the "skinned" layout's aJoints/
|
|
1062
|
+
* aWeights against the `uBones` palette texture (rgba32f, 4 texels
|
|
1063
|
+
* wide, one row per joint: model-local jointWorld x inverseBind -
|
|
1064
|
+
* the spatial flush writes it). The vertex stage then requires that layout
|
|
1065
|
+
* and something must bind `uBones`; the fragment is unchanged. */
|
|
1066
|
+
skinned?: boolean
|
|
1067
|
+
/** Place each copy by its instance matrix (INSTANCE_MATRIX under
|
|
1068
|
+
* uModel, normals through instanceNormalMatrix): the vertex stage of a
|
|
1069
|
+
* material for createInstancedMesh. The class then declares
|
|
1070
|
+
* INSTANCE_MATRIX_ATTRIBUTES; the fragment is unchanged. */
|
|
1071
|
+
instanced?: boolean
|
|
1072
|
+
/** Multiply the base by the per-instance `iColor` (implies `instanced`;
|
|
1073
|
+
* the class declares INSTANCE_COLOR_ATTRIBUTES too): vColor carries it,
|
|
1074
|
+
* times aColor under `vertexColors`. */
|
|
1075
|
+
instanceColors?: boolean
|
|
1076
|
+
/** GLSL spliced at file scope, before main: the uniforms, constants and
|
|
1077
|
+
* helper functions `surface` calls. */
|
|
1078
|
+
prelude?: string
|
|
1079
|
+
/** GLSL declaring `void surface(inout Surface s)`: the material's own
|
|
1080
|
+
* say over the fragment, called once the program has filled `s` from
|
|
1081
|
+
* its options (base from uColor, the map and the vertex color; the
|
|
1082
|
+
* normal, bent by the normal map; emissive, ambient, and the light
|
|
1083
|
+
* model's fields from the per-entry uniforms) and before it shades.
|
|
1084
|
+
* Rewrite any field, or `discard`. Runs in the SHADOW source too with
|
|
1085
|
+
* the same base and the geometric normal, so what it discards casts no
|
|
1086
|
+
* shadow. Reads the varyings and prelude's names. */
|
|
1087
|
+
surface?: string
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* The option set `standardFragment` builds from: every lit option but
|
|
1092
|
+
* the Blinn-Phong ones (`specularMap`; `env`, since the environment is
|
|
1093
|
+
* always composed), plus the two packed data maps.
|
|
1094
|
+
*/
|
|
1095
|
+
export type StandardSourceOptions = Omit<LitSourceOptions, "specularMap" | "env"> & {
|
|
1096
|
+
/** `uniform sampler2D uMetalnessMap`: its BLUE channel scales
|
|
1097
|
+
* uMetalness per fragment (Three's metalnessMap, glTF's packed
|
|
1098
|
+
* channel). */
|
|
1099
|
+
metalnessMap?: boolean
|
|
1100
|
+
/** `uniform sampler2D uRoughnessMap`: its GREEN channel scales
|
|
1101
|
+
* uRoughness (Three's roughnessMap, glTF's packed channel - bind the
|
|
1102
|
+
* same texture to both). */
|
|
1103
|
+
roughnessMap?: boolean
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// The resolved option set: every flag concrete, every slot a string, so
|
|
1107
|
+
// the builders below never re-apply a default and `lit`'s class key and
|
|
1108
|
+
// its program cannot drift apart.
|
|
1109
|
+
type LitSource = {
|
|
1110
|
+
map: boolean
|
|
1111
|
+
vertexColors: boolean
|
|
1112
|
+
triplanar: boolean
|
|
1113
|
+
transparent: boolean
|
|
1114
|
+
receiveShadow: boolean
|
|
1115
|
+
cull: CullMode
|
|
1116
|
+
alphaTest: boolean
|
|
1117
|
+
fog: boolean
|
|
1118
|
+
normalMap: boolean
|
|
1119
|
+
emissive: boolean
|
|
1120
|
+
emissiveMap: boolean
|
|
1121
|
+
specularMap: boolean
|
|
1122
|
+
env: boolean
|
|
1123
|
+
lightMap: boolean
|
|
1124
|
+
mapTransform: boolean
|
|
1125
|
+
skinned: boolean
|
|
1126
|
+
instanced: boolean
|
|
1127
|
+
instanceColors: boolean
|
|
1128
|
+
prelude: string
|
|
1129
|
+
surface: string
|
|
1130
|
+
// The light model: Blinn-Phong (`lit`) or GGX metalness/roughness
|
|
1131
|
+
// (`standard`, which also samples the environment unconditionally).
|
|
1132
|
+
brdf: "blinn" | "ggx"
|
|
1133
|
+
metalnessMap: boolean
|
|
1134
|
+
roughnessMap: boolean
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function resolveLit(o: LitSourceOptions): LitSource {
|
|
1138
|
+
return {
|
|
1139
|
+
map: o.map === true,
|
|
1140
|
+
vertexColors: o.vertexColors === true,
|
|
1141
|
+
triplanar: o.triplanar === true && o.map === true,
|
|
1142
|
+
transparent: o.transparent === true,
|
|
1143
|
+
receiveShadow: o.receiveShadow !== false,
|
|
1144
|
+
cull: o.cull ?? "back",
|
|
1145
|
+
alphaTest: o.alphaTest === true,
|
|
1146
|
+
fog: o.fog !== false,
|
|
1147
|
+
normalMap: o.normalMap === true,
|
|
1148
|
+
emissive: o.emissive === true || o.emissiveMap === true,
|
|
1149
|
+
emissiveMap: o.emissiveMap === true,
|
|
1150
|
+
specularMap: o.specularMap === true,
|
|
1151
|
+
env: o.env === true,
|
|
1152
|
+
lightMap: o.lightMap === true,
|
|
1153
|
+
mapTransform: o.mapTransform === true,
|
|
1154
|
+
skinned: o.skinned === true,
|
|
1155
|
+
instanced: o.instanced === true || o.instanceColors === true,
|
|
1156
|
+
instanceColors: o.instanceColors === true,
|
|
1157
|
+
prelude: o.prelude ?? "",
|
|
1158
|
+
surface: o.surface ?? "",
|
|
1159
|
+
brdf: "blinn",
|
|
1160
|
+
metalnessMap: false,
|
|
1161
|
+
roughnessMap: false,
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function resolveStandard(o: StandardSourceOptions): LitSource {
|
|
1166
|
+
return {
|
|
1167
|
+
...resolveLit(o),
|
|
1168
|
+
specularMap: false,
|
|
1169
|
+
env: true,
|
|
1170
|
+
brdf: "ggx",
|
|
1171
|
+
metalnessMap: o.metalnessMap === true,
|
|
1172
|
+
roughnessMap: o.roughnessMap === true,
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/** Whether the source samples anything by uv - then litBase resolves the
|
|
1177
|
+
* `uv` local (transformed when mapTransform) both passes sample by. */
|
|
1178
|
+
function litUv(c: LitSource): boolean {
|
|
1179
|
+
return (c.map && !c.triplanar) || c.normalMap || c.emissiveMap || c.specularMap || c.metalnessMap || c.roughnessMap
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// The varyings both lit sources read. The shadow source declares the same
|
|
1183
|
+
// set even where it uses none of them, so a surface function written
|
|
1184
|
+
// against the main pass compiles unchanged there.
|
|
1185
|
+
function litVaryings(c: LitSource): string {
|
|
1186
|
+
return glsl`
|
|
1187
|
+
in vec3 vWorldPos;
|
|
1188
|
+
in vec3 vNormal;
|
|
1189
|
+
in vec2 vUv;
|
|
1190
|
+
${c.vertexColors || c.instanceColors ? "in vec4 vColor;" : ""}
|
|
1191
|
+
${c.lightMap ? "in vec2 vUv2;" : ""}
|
|
1192
|
+
`
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Base color to cutout, shared by the main and shadow sources: uColor,
|
|
1196
|
+
// the map (by UV or triplanar), the vertex color, then the alphaTest
|
|
1197
|
+
// discard. `n` is the surface normal the sampling and the surface slot
|
|
1198
|
+
// see: flipped toward the viewer in the main pass of a program that shows
|
|
1199
|
+
// back faces, the plain geometric normal in the shadow pass (which draws
|
|
1200
|
+
// the other side of the surface; triplanar weights are abs(n), so the
|
|
1201
|
+
// sample is the same either way).
|
|
1202
|
+
function litBase(c: LitSource, flip: boolean): string {
|
|
1203
|
+
return glsl`
|
|
1204
|
+
vec3 n = normalize(vNormal);
|
|
1205
|
+
${flip ? "if (!gl_FrontFacing) n = -n;" : ""}
|
|
1206
|
+
${litUv(c) ? `vec2 uv = ${c.mapTransform ? "vUv * uMapTransform.xy + uMapTransform.zw" : "vUv"};` : ""}
|
|
1207
|
+
vec4 base = uColor;
|
|
1208
|
+
${
|
|
1209
|
+
c.map
|
|
1210
|
+
? c.triplanar
|
|
1211
|
+
? `vec3 w = pow(abs(n), vec3(4.0));
|
|
1212
|
+
w /= w.x + w.y + w.z;
|
|
1213
|
+
vec3 p = vWorldPos * uTriplanar;
|
|
1214
|
+
base *= texture(uMap, p.yz) * w.x + texture(uMap, p.xz) * w.y + texture(uMap, p.xy) * w.z;`
|
|
1215
|
+
: "base *= texture(uMap, uv);"
|
|
1216
|
+
: ""
|
|
1217
|
+
}
|
|
1218
|
+
${c.vertexColors || c.instanceColors ? "base *= vColor;" : ""}
|
|
1219
|
+
${c.alphaTest ? "if (base.a < uAlphaTest) discard;" : ""}
|
|
1220
|
+
`
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* The vertex stage `lit` pairs with a given option set: LIT_VERTEX, or
|
|
1225
|
+
* LIT_VERTEX_COLORED when the fragment reads vColor. The shadow source
|
|
1226
|
+
* takes this same stage, which is what lets a surface function read the
|
|
1227
|
+
* same varyings in both passes.
|
|
1228
|
+
*/
|
|
1229
|
+
export function litVertex(o: LitSourceOptions = {}): string {
|
|
1230
|
+
let c = resolveLit(o)
|
|
1231
|
+
return litVertexSource(c.vertexColors, c.lightMap, c.skinned, c.instanced, c.instanceColors)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* The `lit` fragment source for an option set - the exact program `lit`
|
|
1236
|
+
* itself builds, composed from the constants above. Pair it with
|
|
1237
|
+
* litVertex(o) in a shaderMaterialClass to get a lit material with your
|
|
1238
|
+
* own GLSL in it, and pass litShadowFragment(o) alongside when it can
|
|
1239
|
+
* discard.
|
|
1240
|
+
*
|
|
1241
|
+
* Per-entry uniforms to supply on instance(): `uColor` (premultiplied
|
|
1242
|
+
* vec4), `uSpecular`, `uShininess`, plus `uMap`/`uTriplanar`/`uAlphaTest`/
|
|
1243
|
+
* `uNormalMap`+`uNormalScale`/`uEmissive`/`uEmissiveMap`/`uSpecularMap`/
|
|
1244
|
+
* `uLightMap`+`uLightMapIntensity`/`uMapTransform`
|
|
1245
|
+
* for the options that declare them, plus whatever `prelude` declares.
|
|
1246
|
+
* Everything else - the lights, the hemisphere, the camera position, the
|
|
1247
|
+
* shadow set, the fog - is written by the scene.
|
|
1248
|
+
*/
|
|
1249
|
+
export function litFragment(o: LitSourceOptions = {}): string {
|
|
1250
|
+
return lightingFragment(resolveLit(o))
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* The `standard` fragment source for an option set - the exact program
|
|
1255
|
+
* `standard` builds: the lit program (base, cutout, normal map, the
|
|
1256
|
+
* light and shadow loop, emissive, fog, output) shaded with the GGX
|
|
1257
|
+
* metalness/roughness model (PBR) instead of Blinn-Phong, and the
|
|
1258
|
+
* scene's ENVIRONMENT sampled unconditionally as the split sum
|
|
1259
|
+
* (envRadiance times envBrdf). Pair it with litVertex(o), and with
|
|
1260
|
+
* litShadowFragment(o) when it can discard, exactly as litFragment.
|
|
1261
|
+
*
|
|
1262
|
+
* Per-entry uniforms: `uColor` (premultiplied vec4), `uMetalness`,
|
|
1263
|
+
* `uRoughness`, `uMetalnessMap` (blue channel) and `uRoughnessMap`
|
|
1264
|
+
* (green) when declared, plus the lit slots the options declare
|
|
1265
|
+
* (`uMap`/`uTriplanar`/`uAlphaTest`/`uNormalMap`+`uNormalScale`/
|
|
1266
|
+
* `uEmissive`/`uEmissiveMap`/`uLightMap`+`uLightMapIntensity`/
|
|
1267
|
+
* `uMapTransform`) and whatever `prelude` declares.
|
|
1268
|
+
*/
|
|
1269
|
+
export function standardFragment(o: StandardSourceOptions = {}): string {
|
|
1270
|
+
return lightingFragment(resolveStandard(o))
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// The one lit program, both light models: the options fill a Surface,
|
|
1274
|
+
// the scene set shades it. What the brdf changes: the material uniforms
|
|
1275
|
+
// and the shade function (shadeBlinn mixes a fresnel-weighted mirror in
|
|
1276
|
+
// when asked; shadePbr adds the split-sum term always). The light
|
|
1277
|
+
// convention is Godot's and Unity's for both: a light of intensity 1
|
|
1278
|
+
// lights a white diffuse surface to 1 face-on, and the GGX lobe carries
|
|
1279
|
+
// the pi its normalisation assumes (PBR's doc).
|
|
1280
|
+
function lightingFragment(c: LitSource): string {
|
|
1281
|
+
let alpha = c.transparent ? "s.base.a" : "1.0"
|
|
1282
|
+
let ggx = c.brdf === "ggx"
|
|
1283
|
+
let specularMap = c.specularMap ? " * texture(uSpecularMap, uv).r" : ""
|
|
1284
|
+
return glsl`
|
|
1285
|
+
${litVaryings(c)}
|
|
1286
|
+
uniform vec4 uColor;
|
|
1287
|
+
${c.map ? "uniform sampler2D uMap;" : ""}
|
|
1288
|
+
${ggx ? "uniform float uMetalness;\n uniform float uRoughness;" : "uniform float uSpecular;\n uniform float uShininess;"}
|
|
1289
|
+
${c.metalnessMap ? "uniform sampler2D uMetalnessMap;" : ""}
|
|
1290
|
+
${c.roughnessMap ? "uniform sampler2D uRoughnessMap;" : ""}
|
|
1291
|
+
${c.triplanar ? "uniform float uTriplanar;" : ""}
|
|
1292
|
+
${c.alphaTest ? "uniform float uAlphaTest;" : ""}
|
|
1293
|
+
${c.emissive ? "uniform vec3 uEmissive;" : ""}
|
|
1294
|
+
${c.emissiveMap ? "uniform sampler2D uEmissiveMap;" : ""}
|
|
1295
|
+
${c.specularMap ? "uniform sampler2D uSpecularMap;" : ""}
|
|
1296
|
+
${c.env && !ggx ? "uniform float uReflectivity;" : ""}
|
|
1297
|
+
${c.lightMap ? "uniform sampler2D uLightMap;\n uniform float uLightMapIntensity;" : ""}
|
|
1298
|
+
${c.mapTransform ? "uniform vec4 uMapTransform;" : ""}
|
|
1299
|
+
${c.normalMap ? NORMAL_MAP : ""}
|
|
1300
|
+
${sceneSource({ receiveShadow: c.receiveShadow, env: c.env, fog: c.fog })}
|
|
1301
|
+
${c.prelude}
|
|
1302
|
+
${c.surface}
|
|
1303
|
+
|
|
1304
|
+
void main() {
|
|
1305
|
+
${litBase(c, c.cull !== "back")}
|
|
1306
|
+
${c.normalMap ? "n = perturbNormal(n, vWorldPos, uv);" : ""}
|
|
1307
|
+
Surface s = surfaceOf(base, n);
|
|
1308
|
+
${
|
|
1309
|
+
ggx
|
|
1310
|
+
? `s.metalness = uMetalness${c.metalnessMap ? " * texture(uMetalnessMap, uv).b" : ""};
|
|
1311
|
+
s.roughness = uRoughness${c.roughnessMap ? " * texture(uRoughnessMap, uv).g" : ""};`
|
|
1312
|
+
: `s.specular = uSpecular${specularMap};
|
|
1313
|
+
s.shininess = uShininess;
|
|
1314
|
+
${c.env ? `s.reflectivity = uReflectivity${specularMap};` : ""}`
|
|
1315
|
+
}
|
|
1316
|
+
${c.emissive ? `s.emissive = uEmissive${c.emissiveMap ? " * texture(uEmissiveMap, uv).rgb" : ""};` : ""}
|
|
1317
|
+
${c.lightMap ? "s.ambient = texture(uLightMap, vUv2).rgb * uLightMapIntensity;" : ""}
|
|
1318
|
+
${c.surface ? "surface(s);" : ""}
|
|
1319
|
+
vec3 rgb = ${ggx ? "shadePbr" : "shadeBlinn"}(s, vWorldPos);
|
|
1320
|
+
fragColor = sceneOutput(rgb, ${alpha}, vWorldPos);
|
|
1321
|
+
}
|
|
1322
|
+
`
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* The depth-pass source that makes a discarding lit material cast what it
|
|
1327
|
+
* actually draws: the same base, cutout and surface function as
|
|
1328
|
+
* litFragment(o), and nothing after them (no lighting, no fog - a shadow
|
|
1329
|
+
* has neither). Undefined when the option set cannot discard (no
|
|
1330
|
+
* alphaTest, no surface), which means the scene's default depth material
|
|
1331
|
+
* is already right and the material should carry no `shadow` of its own.
|
|
1332
|
+
*
|
|
1333
|
+
* Use it as a second shaderMaterialClass on litVertex(o) with the
|
|
1334
|
+
* OPPOSITE cull (Three's shadowSide rule: `cull: "back"` casts from
|
|
1335
|
+
* "front"), instanced with the uniforms this source declares - uColor,
|
|
1336
|
+
* plus uMap/uTriplanar/uAlphaTest/prelude's as opted into - and passed as
|
|
1337
|
+
* the main instance's `shadow`.
|
|
1338
|
+
*/
|
|
1339
|
+
export function litShadowFragment(o: LitSourceOptions = {}): string | undefined {
|
|
1340
|
+
let c = resolveLit(o)
|
|
1341
|
+
if (!c.alphaTest && !c.surface) return undefined
|
|
1342
|
+
return glsl`
|
|
1343
|
+
${litVaryings(c)}
|
|
1344
|
+
uniform vec4 uColor;
|
|
1345
|
+
${c.map ? "uniform sampler2D uMap;" : ""}
|
|
1346
|
+
${c.triplanar ? "uniform float uTriplanar;" : ""}
|
|
1347
|
+
${c.alphaTest ? "uniform float uAlphaTest;" : ""}
|
|
1348
|
+
${c.mapTransform ? "uniform vec4 uMapTransform;" : ""}
|
|
1349
|
+
${SURFACE}
|
|
1350
|
+
${c.prelude}
|
|
1351
|
+
${c.surface}
|
|
1352
|
+
|
|
1353
|
+
void main() {
|
|
1354
|
+
${litBase(c, false)}
|
|
1355
|
+
${c.surface ? "Surface s = surfaceOf(base, n);\n surface(s);" : ""}
|
|
1356
|
+
fragColor = vec4(1.0);
|
|
1357
|
+
}
|
|
1358
|
+
`
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* The unlit vertex stage: model then view-projection, with UV and world
|
|
1363
|
+
* position as varyings (vWorldPos is the fog distance input; a fragment
|
|
1364
|
+
* that reads neither leaves the outs unmatched, which links fine).
|
|
1365
|
+
* aNormal from the shared layout is deliberately not declared - inactive
|
|
1366
|
+
* attributes are skipped and only the stride accounts for them.
|
|
1367
|
+
*/
|
|
1368
|
+
export const UNLIT_VERTEX = glsl`
|
|
1369
|
+
in vec3 aPos;
|
|
1370
|
+
in vec2 aUV;
|
|
1371
|
+
out vec2 vUv;
|
|
1372
|
+
out vec3 vWorldPos;
|
|
1373
|
+
uniform mat4 uModel;
|
|
1374
|
+
uniform mat4 uViewProj;
|
|
1375
|
+
|
|
1376
|
+
void main() {
|
|
1377
|
+
vec4 world = uModel * vec4(aPos, 1.0);
|
|
1378
|
+
vWorldPos = world.xyz;
|
|
1379
|
+
gl_Position = uViewProj * world;
|
|
1380
|
+
vUv = aUV;
|
|
1381
|
+
}
|
|
1382
|
+
`
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* The option set the unlit builders take: LitSourceOptions minus
|
|
1386
|
+
* everything lighting decides (no vertexColors, triplanar, receiveShadow)
|
|
1387
|
+
* and minus cull (the unlit fragment has no facing-dependent code - cull
|
|
1388
|
+
* is pure pipeline state, passed to shaderMaterialClass directly). The
|
|
1389
|
+
* slots follow the lit contract; the varyings here are vUv and vWorldPos
|
|
1390
|
+
* only.
|
|
1391
|
+
*/
|
|
1392
|
+
export type UnlitSourceOptions = {
|
|
1393
|
+
/** The fragment samples a `uniform sampler2D uMap`, tinted by uColor. */
|
|
1394
|
+
map?: boolean
|
|
1395
|
+
/** Write the base alpha through and blend; opaque (the default) writes
|
|
1396
|
+
* alpha 1. */
|
|
1397
|
+
transparent?: boolean
|
|
1398
|
+
/** Declare `uniform float uAlphaTest` and discard below it. */
|
|
1399
|
+
alphaTest?: boolean
|
|
1400
|
+
/** Compose the scene's fog over the result (default true). */
|
|
1401
|
+
fog?: boolean
|
|
1402
|
+
/** Declare `uniform vec4 uMapTransform` ([repeatU, repeatV, offsetU,
|
|
1403
|
+
* offsetV]) and sample the map at `vUv * repeat + offset` (see the lit
|
|
1404
|
+
* option of the same name). Needs `map`. */
|
|
1405
|
+
mapTransform?: boolean
|
|
1406
|
+
/** Skin positions by the "skinned" layout against uBones (see the lit
|
|
1407
|
+
* option); unlitVertex(o) reads it, the fragment is unchanged. */
|
|
1408
|
+
skinned?: boolean
|
|
1409
|
+
/** Place each copy by its instance matrix (see the lit option);
|
|
1410
|
+
* unlitVertex(o) reads it, the fragment is unchanged. */
|
|
1411
|
+
instanced?: boolean
|
|
1412
|
+
/** Multiply the base by the per-instance `iColor` (implies `instanced`;
|
|
1413
|
+
* see the lit option): the one per-copy value an unlit fleet wants. */
|
|
1414
|
+
instanceColors?: boolean
|
|
1415
|
+
/** GLSL spliced at file scope, before main. */
|
|
1416
|
+
prelude?: string
|
|
1417
|
+
/** GLSL declaring `void surface(inout Surface s)`, called with the base
|
|
1418
|
+
* resolved (see the lit slot): the unlit program shades nothing, so only
|
|
1419
|
+
* `s.base` is read back and `s.normal` is zero (no normal varying). A
|
|
1420
|
+
* `discard` inside it runs in the shadow source too. */
|
|
1421
|
+
surface?: string
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
type UnlitSource = {
|
|
1425
|
+
map: boolean
|
|
1426
|
+
transparent: boolean
|
|
1427
|
+
alphaTest: boolean
|
|
1428
|
+
fog: boolean
|
|
1429
|
+
mapTransform: boolean
|
|
1430
|
+
skinned: boolean
|
|
1431
|
+
instanced: boolean
|
|
1432
|
+
instanceColors: boolean
|
|
1433
|
+
prelude: string
|
|
1434
|
+
surface: string
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function resolveUnlit(o: UnlitSourceOptions): UnlitSource {
|
|
1438
|
+
return {
|
|
1439
|
+
map: o.map === true,
|
|
1440
|
+
transparent: o.transparent === true,
|
|
1441
|
+
alphaTest: o.alphaTest === true,
|
|
1442
|
+
fog: o.fog !== false,
|
|
1443
|
+
mapTransform: o.mapTransform === true && o.map === true,
|
|
1444
|
+
skinned: o.skinned === true,
|
|
1445
|
+
instanced: o.instanced === true || o.instanceColors === true,
|
|
1446
|
+
instanceColors: o.instanceColors === true,
|
|
1447
|
+
prelude: o.prelude ?? "",
|
|
1448
|
+
surface: o.surface ?? "",
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* The vertex stage `unlit` pairs with an option set: UNLIT_VERTEX, or its
|
|
1454
|
+
* skinned form (the skin matrix applied before uModel) when `skinned`,
|
|
1455
|
+
* its instanced form (the instance matrix under uModel, iColor forwarded
|
|
1456
|
+
* as vColor under `instanceColors`) when `instanced`.
|
|
1457
|
+
*/
|
|
1458
|
+
export function unlitVertex(o: UnlitSourceOptions = {}): string {
|
|
1459
|
+
let c = resolveUnlit(o)
|
|
1460
|
+
if (!c.skinned && !c.instanced) return UNLIT_VERTEX
|
|
1461
|
+
return glsl`
|
|
1462
|
+
in vec3 aPos;
|
|
1463
|
+
in vec2 aUV;
|
|
1464
|
+
${c.skinned ? SKIN_DECLS : ""}
|
|
1465
|
+
${c.instanced ? INSTANCE_MATRIX : ""}
|
|
1466
|
+
${c.instanceColors ? "in vec4 iColor;" : ""}
|
|
1467
|
+
out vec2 vUv;
|
|
1468
|
+
out vec3 vWorldPos;
|
|
1469
|
+
${c.instanceColors ? "out vec4 vColor;" : ""}
|
|
1470
|
+
uniform mat4 uModel;
|
|
1471
|
+
uniform mat4 uViewProj;
|
|
1472
|
+
|
|
1473
|
+
void main() {
|
|
1474
|
+
${c.skinned ? SKIN_MATRIX : ""}
|
|
1475
|
+
vec4 world = ${instancedPosition(c.skinned, c.instanced)};
|
|
1476
|
+
vWorldPos = world.xyz;
|
|
1477
|
+
gl_Position = uViewProj * world;
|
|
1478
|
+
vUv = aUV;
|
|
1479
|
+
${c.instanceColors ? "vColor = iColor;" : ""}
|
|
1480
|
+
}
|
|
1481
|
+
`
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/** The unlit base sample: uColor times the (possibly transformed) map,
|
|
1485
|
+
* times the instance color when the material carries one. */
|
|
1486
|
+
function unlitBase(c: UnlitSource): string {
|
|
1487
|
+
let uv = c.mapTransform ? "vUv * uMapTransform.xy + uMapTransform.zw" : "vUv"
|
|
1488
|
+
let base = c.map ? `vec4 base = texture(uMap, ${uv}) * uColor;` : "vec4 base = uColor;"
|
|
1489
|
+
return c.instanceColors ? base + "\n base *= vColor;" : base
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* The `unlit` fragment source for an option set (sprite shares it): the
|
|
1494
|
+
* color, times the map when there is one, the cutout, the surface slot,
|
|
1495
|
+
* then the scene tail (sceneOutput: fog unless opted out, exposure, tone
|
|
1496
|
+
* mapping, encode) like every library fragment. An opaque program writes
|
|
1497
|
+
* alpha 1: the scene target is composited premultiplied, so a leaked
|
|
1498
|
+
* texel alpha would punch a hole through an opaque draw. Pair with
|
|
1499
|
+
* UNLIT_VERTEX (or a custom vertex stage writing vUv/vWorldPos, as sprite
|
|
1500
|
+
* does - vWorldPos is read whether or not the program fogs).
|
|
1501
|
+
*
|
|
1502
|
+
* Per-entry uniforms on instance(): `uColor` (linear light, premultiplied
|
|
1503
|
+
* vec4), plus `uMap`/`uAlphaTest` for the options that declare them, plus
|
|
1504
|
+
* whatever `prelude` declares.
|
|
1505
|
+
*/
|
|
1506
|
+
export function unlitFragment(o: UnlitSourceOptions = {}): string {
|
|
1507
|
+
let c = resolveUnlit(o)
|
|
1508
|
+
let alpha = c.transparent ? "base.a" : "1.0"
|
|
1509
|
+
return glsl`
|
|
1510
|
+
${c.map ? "in vec2 vUv;" : ""}
|
|
1511
|
+
in vec3 vWorldPos;
|
|
1512
|
+
${c.instanceColors ? "in vec4 vColor;" : ""}
|
|
1513
|
+
${c.map ? "uniform sampler2D uMap;" : ""}
|
|
1514
|
+
uniform vec4 uColor;
|
|
1515
|
+
${c.alphaTest ? "uniform float uAlphaTest;" : ""}
|
|
1516
|
+
${c.mapTransform ? "uniform vec4 uMapTransform;" : ""}
|
|
1517
|
+
${sceneSource({ lights: false, fog: c.fog })}
|
|
1518
|
+
${c.prelude}
|
|
1519
|
+
${c.surface}
|
|
1520
|
+
void main() {
|
|
1521
|
+
${unlitBase(c)}
|
|
1522
|
+
${c.alphaTest ? "if (base.a < uAlphaTest) discard;" : ""}
|
|
1523
|
+
${c.surface ? "Surface s = surfaceOf(base, vec3(0.0));\n surface(s);\n base = s.base;" : ""}
|
|
1524
|
+
fragColor = sceneOutput(base.rgb, ${alpha}, vWorldPos);
|
|
1525
|
+
}
|
|
1526
|
+
`
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* The depth-pass source for a discarding unlit material, litShadowFragment's
|
|
1531
|
+
* unlit twin: the same base and discards, nothing else. Undefined when
|
|
1532
|
+
* the option set cannot discard. Pair with UNLIT_VERTEX and the opposite
|
|
1533
|
+
* cull, instanced with the uniforms it declares.
|
|
1534
|
+
*/
|
|
1535
|
+
export function unlitShadowFragment(o: UnlitSourceOptions = {}): string | undefined {
|
|
1536
|
+
let c = resolveUnlit(o)
|
|
1537
|
+
if (!c.alphaTest && !c.surface) return undefined
|
|
1538
|
+
return glsl`
|
|
1539
|
+
${c.map ? "in vec2 vUv;" : ""}
|
|
1540
|
+
${c.instanceColors ? "in vec4 vColor;" : ""}
|
|
1541
|
+
uniform vec4 uColor;
|
|
1542
|
+
${c.map ? "uniform sampler2D uMap;" : ""}
|
|
1543
|
+
${c.alphaTest ? "uniform float uAlphaTest;" : ""}
|
|
1544
|
+
${c.mapTransform ? "uniform vec4 uMapTransform;" : ""}
|
|
1545
|
+
${SURFACE}
|
|
1546
|
+
${c.prelude}
|
|
1547
|
+
${c.surface}
|
|
1548
|
+
void main() {
|
|
1549
|
+
${unlitBase(c)}
|
|
1550
|
+
${c.alphaTest ? "if (base.a < uAlphaTest) discard;" : ""}
|
|
1551
|
+
${c.surface ? "Surface s = surfaceOf(base, vec3(0.0));\n surface(s);" : ""}
|
|
1552
|
+
fragColor = vec4(1.0);
|
|
1553
|
+
}
|
|
1554
|
+
`
|
|
1555
|
+
}
|