@driftengine/core 4.5.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +51 -0
- package/README.md +1 -1
- package/dist/geometry/boxCsg.d.ts +28 -0
- package/dist/geometry/boxCsg.js +139 -0
- package/dist/geometry/csg.d.ts +36 -0
- package/dist/geometry/csg.js +329 -0
- package/dist/geometry/meshBuilder.js +13 -0
- package/dist/geometry/meshConcat.js +1 -0
- package/dist/geometry/pixelFont.js +2 -0
- package/dist/geometry/solid.d.ts +61 -0
- package/dist/geometry/solid.js +152 -0
- package/dist/geometry/solidFlat.d.ts +35 -0
- package/dist/geometry/solidFlat.js +428 -0
- package/dist/geometry/solidHarness.d.ts +19 -0
- package/dist/geometry/solidHarness.js +66 -0
- package/dist/geometry/solidNormals.d.ts +17 -0
- package/dist/geometry/solidNormals.js +86 -0
- package/dist/geometry/solidRound.d.ts +41 -0
- package/dist/geometry/solidRound.js +323 -0
- package/dist/geometry/solidWriter.d.ts +16 -0
- package/dist/geometry/solidWriter.js +42 -0
- package/dist/index.d.ts +23 -2
- package/dist/index.js +13 -1
- package/dist/render/backend/api.d.ts +17 -3
- package/dist/render/backend/drawFog.d.ts +26 -0
- package/dist/render/backend/drawFog.js +30 -0
- package/dist/render/backend/materialChanges.d.ts +2 -1
- package/dist/render/backend/materialChanges.js +6 -1
- package/dist/render/backend/webgl2/instanced.d.ts +24 -5
- package/dist/render/backend/webgl2/instanced.js +72 -15
- package/dist/render/backend/webgl2/renderer.d.ts +122 -8
- package/dist/render/backend/webgl2/renderer.js +238 -121
- package/dist/render/backend/webgpu/buffers.d.ts +10 -0
- package/dist/render/backend/webgpu/buffers.js +34 -3
- package/dist/render/backend/webgpu/depthPass.js +5 -1
- package/dist/render/backend/webgpu/drawCommand.d.ts +5 -0
- package/dist/render/backend/webgpu/drawCommand.js +1 -0
- package/dist/render/backend/webgpu/flatPass.d.ts +4 -1
- package/dist/render/backend/webgpu/flatPass.js +38 -9
- package/dist/render/backend/webgpu/glassTintPass.js +2 -1
- package/dist/render/backend/webgpu/instanceCullPass.d.ts +34 -0
- package/dist/render/backend/webgpu/instanceCullPass.js +144 -0
- package/dist/render/backend/webgpu/instanced.d.ts +11 -1
- package/dist/render/backend/webgpu/instanced.js +21 -3
- package/dist/render/backend/webgpu/latePass.d.ts +6 -2
- package/dist/render/backend/webgpu/latePass.js +11 -4
- package/dist/render/backend/webgpu/postPass.d.ts +1 -1
- package/dist/render/backend/webgpu/renderer.d.ts +100 -10
- package/dist/render/backend/webgpu/renderer.js +462 -201
- package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
- package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
- package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +17 -3
- package/dist/render/backend/webgpu/surfaceTexturePass.js +77 -38
- package/dist/render/driftLight/bake.d.ts +7 -0
- package/dist/render/driftLight/bake.js +61 -56
- package/dist/render/driftLight/denseField.d.ts +46 -0
- package/dist/render/driftLight/denseField.js +109 -0
- package/dist/render/driftLight/lightField.d.ts +10 -5
- package/dist/render/driftLight/lightField.js +25 -19
- package/dist/render/driftLight/presence.d.ts +50 -0
- package/dist/render/driftLight/presence.js +43 -0
- package/dist/render/driftLight/uniforms.d.ts +3 -3
- package/dist/render/driftLight/uniforms.js +5 -6
- package/dist/render/driftLight/worldLightField.d.ts +41 -0
- package/dist/render/driftLight/worldLightField.js +66 -0
- package/dist/render/fixtureAtlas.d.ts +20 -0
- package/dist/render/fixtureAtlas.js +49 -0
- package/dist/render/instanceCull.d.ts +63 -0
- package/dist/render/instanceCull.js +160 -0
- package/dist/render/instances.d.ts +15 -0
- package/dist/render/lightBudget.d.ts +22 -33
- package/dist/render/lightBudget.js +24 -35
- package/dist/render/lightGrid.d.ts +42 -0
- package/dist/render/lightGrid.js +87 -0
- package/dist/render/mesh.d.ts +18 -24
- package/dist/render/mesh.js +51 -68
- package/dist/render/occlusion.d.ts +10 -0
- package/dist/render/occlusion.js +36 -7
- package/dist/render/pointLightSelection.d.ts +7 -0
- package/dist/render/pointLightSelection.js +29 -4
- package/dist/render/rainField.d.ts +13 -0
- package/dist/render/rainField.js +27 -5
- package/dist/render/recon/lateDepth.d.ts +11 -0
- package/dist/render/recon/lateDepth.js +70 -0
- package/dist/render/recon/resolve.d.ts +34 -4
- package/dist/render/recon/resolve.js +161 -20
- package/dist/render/rendererHarness.js +4 -0
- package/dist/render/sceneTarget.d.ts +2 -0
- package/dist/render/sceneTarget.js +2 -0
- package/dist/render/shaders/depth.js +12 -9
- package/dist/render/shaders/flat/driftLight.d.ts +1 -1
- package/dist/render/shaders/flat/driftLight.js +32 -7
- package/dist/render/shaders/flat/index.js +5 -3
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +75 -23
- package/dist/render/shaders/flat/preamble.js +75 -20
- package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
- package/dist/render/shaders/flat/surfaceEffects.js +152 -0
- package/dist/render/shaders/generated/depth.wgsl.d.ts +8 -8
- package/dist/render/shaders/generated/depth.wgsl.js +8 -8
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1320 -1320
- package/dist/render/shaders/generated/flat.wgsl.js +1359 -1345
- package/dist/render/shaders/generated/lightVolume.wgsl.js +18 -4
- package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
- package/dist/render/shaders/generated/rush.wgsl.js +21 -16
- package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
- package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
- package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/text.wgsl.js +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
- package/dist/render/shaders/recon/lateDepth.wgsl.js +38 -13
- package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
- package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
- package/dist/render/shaders/rush.js +11 -1
- package/dist/render/shaders/sdfText.d.ts +1 -1
- package/dist/render/shaders/sdfText.js +3 -2
- package/dist/render/shaders/text.d.ts +1 -1
- package/dist/render/shaders/text.js +18 -9
- package/dist/render/surfaceEffects.d.ts +83 -0
- package/dist/render/surfaceEffects.js +75 -0
- package/dist/render/surfaceScene.d.ts +8 -0
- package/dist/render/surfaceScene.js +8 -0
- package/dist/render/surfaceTexture.d.ts +18 -2
- package/dist/render/surfaceTexture.js +68 -15
- package/dist/render/temporalAa.d.ts +25 -6
- package/dist/render/temporalAa.js +45 -15
- package/dist/render/textureSource.d.ts +30 -0
- package/dist/render/textureSource.js +53 -0
- package/dist/render/vertexDefaults.js +2 -0
- package/dist/world/hlod.d.ts +104 -0
- package/dist/world/hlod.js +214 -0
- package/package.json +6 -6
- package/src/geometry/boxCsg.ts +186 -0
- package/src/geometry/csg.ts +384 -0
- package/src/geometry/meshBuilder.ts +15 -0
- package/src/geometry/meshConcat.ts +1 -0
- package/src/geometry/pixelFont.ts +2 -0
- package/src/geometry/solid.ts +189 -0
- package/src/geometry/solidFlat.ts +522 -0
- package/src/geometry/solidHarness.ts +83 -0
- package/src/geometry/solidNormals.ts +103 -0
- package/src/geometry/solidRound.ts +443 -0
- package/src/geometry/solidWriter.ts +58 -0
- package/src/index.ts +51 -1
- package/src/render/backend/api.ts +21 -2
- package/src/render/backend/drawFog.ts +31 -0
- package/src/render/backend/materialChanges.ts +9 -2
- package/src/render/backend/webgl2/instanced.ts +80 -14
- package/src/render/backend/webgl2/renderer.ts +300 -143
- package/src/render/backend/webgpu/buffers.ts +42 -3
- package/src/render/backend/webgpu/depthPass.ts +5 -1
- package/src/render/backend/webgpu/drawCommand.ts +6 -0
- package/src/render/backend/webgpu/flatPass.ts +41 -11
- package/src/render/backend/webgpu/glassTintPass.ts +2 -1
- package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
- package/src/render/backend/webgpu/instanced.ts +21 -3
- package/src/render/backend/webgpu/latePass.ts +12 -3
- package/src/render/backend/webgpu/renderer.ts +540 -226
- package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
- package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
- package/src/render/backend/webgpu/surfaceTexturePass.ts +90 -50
- package/src/render/driftLight/bake.ts +85 -62
- package/src/render/driftLight/denseField.ts +149 -0
- package/src/render/driftLight/lightField.ts +33 -18
- package/src/render/driftLight/presence.ts +69 -0
- package/src/render/driftLight/uniforms.ts +8 -9
- package/src/render/driftLight/worldLightField.ts +83 -0
- package/src/render/fixtureAtlas.ts +63 -0
- package/src/render/instanceCull.ts +205 -0
- package/src/render/instances.ts +16 -0
- package/src/render/lightBudget.ts +25 -36
- package/src/render/lightGrid.ts +130 -0
- package/src/render/mesh.ts +59 -72
- package/src/render/occlusion.ts +51 -7
- package/src/render/pointLightSelection.ts +72 -4
- package/src/render/rainField.ts +39 -5
- package/src/render/recon/lateDepth.ts +83 -0
- package/src/render/recon/resolve.ts +206 -20
- package/src/render/rendererHarness.ts +4 -0
- package/src/render/sceneTarget.ts +4 -0
- package/src/render/shaders/depth.ts +12 -9
- package/src/render/shaders/flat/driftLight.ts +32 -7
- package/src/render/shaders/flat/index.ts +5 -3
- package/src/render/shaders/flat/main.ts +75 -23
- package/src/render/shaders/flat/preamble.ts +75 -20
- package/src/render/shaders/flat/surfaceEffects.ts +152 -0
- package/src/render/shaders/generated/depth.wgsl.ts +8 -8
- package/src/render/shaders/generated/flat.wgsl.ts +1361 -1345
- package/src/render/shaders/generated/lightVolume.wgsl.ts +20 -4
- package/src/render/shaders/generated/rush.wgsl.ts +21 -16
- package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
- package/src/render/shaders/generated/text.wgsl.ts +1 -1
- package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
- package/src/render/shaders/recon/lateDepth.wgsl.ts +38 -13
- package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
- package/src/render/shaders/rush.ts +11 -1
- package/src/render/shaders/sdfText.ts +3 -2
- package/src/render/shaders/text.ts +18 -9
- package/src/render/surfaceEffects.ts +132 -0
- package/src/render/surfaceScene.ts +16 -0
- package/src/render/surfaceTexture.ts +110 -19
- package/src/render/temporalAa.ts +78 -14
- package/src/render/textureSource.ts +72 -0
- package/src/render/vertexDefaults.ts +2 -0
- package/src/world/hlod.ts +290 -0
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
/** The fragment entry point, which assembles everything above into a colour. */
|
|
2
2
|
export const MAIN_GLSL = `void main() {
|
|
3
|
-
|
|
3
|
+
/*
|
|
4
|
+
* Where every material map is read: the texture coordinate, and the layer of the array the face
|
|
5
|
+
* wears, rounded here so an interpolated 2.9999 lands on layer 3 however the generated WGSL
|
|
6
|
+
* turns a float into an array index. A plain texture is an array of one, so layer 0.
|
|
7
|
+
*/
|
|
8
|
+
vec3 surfaceAt = vec3(vUv.xy, floor(vUv.z + 0.5));
|
|
9
|
+
if (dot(vec4(vWorldPos, 1.0), uClipPlane) < 0.0) discard;
|
|
10
|
+
/*
|
|
11
|
+
* The layer's surface effects, zeros where the albedo array carries no table — see
|
|
12
|
+
* surfaceEffects.ts. Scroll and flipbook move surfaceAt before any map reads it, so every map of
|
|
13
|
+
* the material moves together. The gradients are taken here, outside every branch, for the room
|
|
14
|
+
* a window samples through textureGrad further down.
|
|
15
|
+
*/
|
|
16
|
+
fxLoad(int(surfaceAt.z));
|
|
17
|
+
surfaceAt = fxAnimate(surfaceAt);
|
|
18
|
+
vec2 fxDx = dFdx(surfaceAt.xy);
|
|
19
|
+
vec2 fxDy = dFdy(surfaceAt.xy);
|
|
20
|
+
if (uWriteMode.y != 0.0 && !ditherKeeps(uWriteMode.y)) discard;
|
|
4
21
|
|
|
5
22
|
/*
|
|
6
23
|
* Surface colour and its coverage, resolved before anything else — including before it is
|
|
@@ -34,13 +51,13 @@ export const MAIN_GLSL = `void main() {
|
|
|
34
51
|
*/
|
|
35
52
|
float coverage = 1.0;
|
|
36
53
|
if (uAlbedoEnabled != 0) {
|
|
37
|
-
vec4 texel = texture(uAlbedo,
|
|
54
|
+
vec4 texel = texture(uAlbedo, surfaceAt);
|
|
38
55
|
// Discarded before anything else is computed: a thrown-away fragment should not pay
|
|
39
56
|
// for the lighting it will never contribute to. The test credits alpha for the mip level
|
|
40
57
|
// it samples, so a needle averaged thin down the chain is not lost; see cutoutCoverage.ts.
|
|
41
58
|
// Behind a branch on a uniform, so a material with no cutoff pays nothing for it.
|
|
42
59
|
if (uAlbedoCutout > 0.0) {
|
|
43
|
-
float tested = cutoutAlpha(texel.a, vUv * vec2(textureSize(uAlbedo, 0)));
|
|
60
|
+
float tested = cutoutAlpha(texel.a, vUv.xy * vec2(textureSize(uAlbedo, 0).xy));
|
|
44
61
|
if (tested < uAlbedoCutout) discard;
|
|
45
62
|
}
|
|
46
63
|
albedo *= texel.rgb;
|
|
@@ -69,7 +86,7 @@ export const MAIN_GLSL = `void main() {
|
|
|
69
86
|
float ormRoughness = vRoughness;
|
|
70
87
|
float metal = 0.0;
|
|
71
88
|
if (uOrmEnabled != 0) {
|
|
72
|
-
vec3 t = texture(uOrmMap,
|
|
89
|
+
vec3 t = texture(uOrmMap, surfaceAt).rgb;
|
|
73
90
|
ormOcclusion = mix(1.0, t.r, uOrmScale.r);
|
|
74
91
|
ormRoughness = clamp(t.g * uOrmScale.g, 0.0, 1.0);
|
|
75
92
|
metal = clamp(t.b * uOrmScale.b, 0.0, 1.0);
|
|
@@ -81,6 +98,22 @@ export const MAIN_GLSL = `void main() {
|
|
|
81
98
|
* \`uLightingEnabled\` at 0 keeps to the end, and the value every lit draw starts from before
|
|
82
99
|
* the branch below builds a shaded result on top of it.
|
|
83
100
|
*/
|
|
101
|
+
/*
|
|
102
|
+
* Wear, rain and windows, on the colour and roughness everything below shades. Each is exactly
|
|
103
|
+
* nothing where its numbers are zero, which is every surface of a scene that names no effect.
|
|
104
|
+
*/
|
|
105
|
+
fxNormal = normalize(vNormal);
|
|
106
|
+
fxDistance = distance(vWorldPos, uCameraPos);
|
|
107
|
+
vec4 weathered = fxWeather(vWorldPos, albedo, ormRoughness);
|
|
108
|
+
albedo = weathered.rgb;
|
|
109
|
+
ormRoughness = weathered.w;
|
|
110
|
+
vec3 windowGlow = vec3(0.0);
|
|
111
|
+
if (fx0.x > 0.0) {
|
|
112
|
+
vec4 window = fxWindows(surfaceAt, fxDx, fxDy, coverage);
|
|
113
|
+
albedo = mix(albedo, fxRoom * 0.35, window.w);
|
|
114
|
+
windowGlow = window.rgb;
|
|
115
|
+
}
|
|
116
|
+
|
|
84
117
|
vec3 lit = albedo;
|
|
85
118
|
/* The light arriving at a glass pane from behind it, gathered in each light loop below and added
|
|
86
119
|
over the see-through at the end. Stays zero for anything that is not glass. See glass.ts. */
|
|
@@ -122,8 +155,8 @@ export const MAIN_GLSL = `void main() {
|
|
|
122
155
|
* below. The rule is about non-uniform branches, and this branch is on a uniform.
|
|
123
156
|
*/
|
|
124
157
|
if (uNormalStrength > 0.0) {
|
|
125
|
-
vec3 mapped = texture(uNormalMap,
|
|
126
|
-
mat3 tbn = tangentFrame(n, vWorldPos, vUv, vTangent, vHasTangents);
|
|
158
|
+
vec3 mapped = texture(uNormalMap, surfaceAt).xyz * 2.0 - 1.0;
|
|
159
|
+
mat3 tbn = tangentFrame(n, vWorldPos, vUv.xy, vTangent, vHasTangents);
|
|
127
160
|
n = normalize(mix(n, normalize(tbn * mapped), uNormalStrength));
|
|
128
161
|
}
|
|
129
162
|
|
|
@@ -243,14 +276,14 @@ export const MAIN_GLSL = `void main() {
|
|
|
243
276
|
* antisymmetric, the cancellation holds, and it is the more accurate estimate besides: a
|
|
244
277
|
* forward difference reports the slope half a step away from the point it is used at.
|
|
245
278
|
*/
|
|
246
|
-
vec2 duvdx = dFdx(vUv);
|
|
247
|
-
vec2 duvdy = dFdy(vUv);
|
|
279
|
+
vec2 duvdx = dFdx(vUv.xy);
|
|
280
|
+
vec2 duvdy = dFdy(vUv.xy);
|
|
248
281
|
/* Half, because a centred difference spans two steps and three.js's number means one. */
|
|
249
282
|
float reliefPerStep = uTextureRelief * 0.5;
|
|
250
|
-
float dHdx = (surfaceHeight(texture(uAlbedo, vUv + duvdx).rgb)
|
|
251
|
-
- surfaceHeight(texture(uAlbedo, vUv - duvdx).rgb)) * reliefPerStep;
|
|
252
|
-
float dHdy = (surfaceHeight(texture(uAlbedo, vUv + duvdy).rgb)
|
|
253
|
-
- surfaceHeight(texture(uAlbedo, vUv - duvdy).rgb)) * reliefPerStep;
|
|
283
|
+
float dHdx = (surfaceHeight(texture(uAlbedo, vec3(vUv.xy + duvdx, surfaceAt.z)).rgb)
|
|
284
|
+
- surfaceHeight(texture(uAlbedo, vec3(vUv.xy - duvdx, surfaceAt.z)).rgb)) * reliefPerStep;
|
|
285
|
+
float dHdy = (surfaceHeight(texture(uAlbedo, vec3(vUv.xy + duvdy, surfaceAt.z)).rgb)
|
|
286
|
+
- surfaceHeight(texture(uAlbedo, vec3(vUv.xy - duvdy, surfaceAt.z)).rgb)) * reliefPerStep;
|
|
254
287
|
|
|
255
288
|
vec3 dpdx = dFdx(vWorldPos);
|
|
256
289
|
vec3 dpdy = dFdy(vWorldPos);
|
|
@@ -907,6 +940,9 @@ export const MAIN_GLSL = `void main() {
|
|
|
907
940
|
* CLUSTER_TEXELS;
|
|
908
941
|
int clusterLights = uClustered == 1 ? int(clusterTexel(clusterBaseTexel).x) : 0;
|
|
909
942
|
|
|
943
|
+
/* The fixture atlas's size, asked once: its cookie band and its profile rows are found from it. */
|
|
944
|
+
vec2 fixtureSize = vec2(textureSize(uFixtureAtlas, 0));
|
|
945
|
+
|
|
910
946
|
// Point lights. Inverse-square-ish falloff clipped to a finite radius, so a
|
|
911
947
|
// light can be culled without a visible seam.
|
|
912
948
|
for (int slot = 0; slot < LIGHT_LOOP_MAX; slot++) {
|
|
@@ -1201,7 +1237,9 @@ export const MAIN_GLSL = `void main() {
|
|
|
1201
1237
|
vec3 iesRay = -toLight / max(dist, 1e-4);
|
|
1202
1238
|
/* The angle from the fixture's axis, as a fraction of the 0-to-180 arc every row spans. */
|
|
1203
1239
|
float iesAngle = acos(clamp(dot(iesRay, lightDir), -1.0, 1.0));
|
|
1204
|
-
|
|
1240
|
+
/* Clamped to the row's first and last texel centres, which is what the edge clamp of a
|
|
1241
|
+
texture exactly one row wide gave; the atlas can be wider now, with cookies beside it. */
|
|
1242
|
+
float iesU = clamp(iesAngle / PI_IES * IES_WIDTH, 0.5, IES_WIDTH - 0.5) / fixtureSize.x;
|
|
1205
1243
|
/*
|
|
1206
1244
|
* **The azimuth about the fixture's own axis**, behind a branch on a uniform so a scene whose
|
|
1207
1245
|
* profiles are all axially symmetric pays none of this arithmetic. A \`uniform float\` is
|
|
@@ -1255,13 +1293,14 @@ export const MAIN_GLSL = `void main() {
|
|
|
1255
1293
|
*/
|
|
1256
1294
|
float cookieTile = max(cookie, 0.0);
|
|
1257
1295
|
vec2 cookieAt = vec2(
|
|
1258
|
-
(cookieTile + clamp(cookieUv.x, COOKIE_INSET, 1.0 - COOKIE_INSET))
|
|
1259
|
-
|
|
1296
|
+
(cookieTile + clamp(cookieUv.x, COOKIE_INSET, 1.0 - COOKIE_INSET)) * COOKIE_SPAN
|
|
1297
|
+
/ fixtureSize.x,
|
|
1298
|
+
clamp(cookieUv.y, COOKIE_INSET, 1.0 - COOKIE_INSET) * COOKIE_SPAN / fixtureSize.y);
|
|
1260
1299
|
/* Unconditional and \`textureLod\`, and selected with a \`mix\` afterwards: the light
|
|
1261
1300
|
loop is not provably uniform control flow — the clustered arm reads its index from a
|
|
1262
1301
|
texture — so a branch on the *light's* cookie index around a sample is what the
|
|
1263
1302
|
2026-08-07 rule forbids. */
|
|
1264
|
-
vec3 cookieSample = textureLod(
|
|
1303
|
+
vec3 cookieSample = textureLod(uFixtureAtlas, cookieAt, 0.0).rgb;
|
|
1265
1304
|
cookieTint = mix(vec3(1.0), cookieSample, step(0.0, cookie));
|
|
1266
1305
|
}
|
|
1267
1306
|
}
|
|
@@ -1276,10 +1315,12 @@ export const MAIN_GLSL = `void main() {
|
|
|
1276
1315
|
* bytes on \`core-only\`** across the sixteen fragment permutations.
|
|
1277
1316
|
*/
|
|
1278
1317
|
float iesBase = iesRow * (uIesPlaneCount + 1.0);
|
|
1318
|
+
/* The profiles are the atlas's bottom rows, below any cookies. */
|
|
1319
|
+
float iesTop = fixtureSize.y - uIesAtlasRows;
|
|
1279
1320
|
float iesV = uIesAtlasRows <= 1.0
|
|
1280
|
-
? 0.5
|
|
1281
|
-
: (iesBase + iesTurns * uIesPlaneCount + 0.5) /
|
|
1282
|
-
float iesGain = textureLod(
|
|
1321
|
+
? (iesTop + 0.5) / fixtureSize.y
|
|
1322
|
+
: (iesTop + iesBase + iesTurns * uIesPlaneCount + 0.5) / fixtureSize.y;
|
|
1323
|
+
float iesGain = textureLod(uFixtureAtlas, vec2(iesU, iesV), 0.0).r;
|
|
1283
1324
|
float photometric = mix(1.0, iesGain, step(0.0, iesProfile));
|
|
1284
1325
|
|
|
1285
1326
|
float shape = (uLightFalloff == 1 ? falloff : falloff * falloff) * coneFalloff * photometric;
|
|
@@ -1696,10 +1737,14 @@ export const MAIN_GLSL = `void main() {
|
|
|
1696
1737
|
*/
|
|
1697
1738
|
vec3 emissiveMapped = vec3(1.0);
|
|
1698
1739
|
if (uEmissiveMapEnabled != 0) {
|
|
1699
|
-
emissiveMapped = texture(uEmissiveMap,
|
|
1740
|
+
emissiveMapped = texture(uEmissiveMap, surfaceAt).rgb * uEmissiveScale;
|
|
1700
1741
|
}
|
|
1742
|
+
/* Pulse, flicker and fade from the layer's effects; exactly 1 where it names none. */
|
|
1743
|
+
float fxGlow = fx4.z + fx5.x + fx5.w > 0.0 ? fxEmission(surfaceAt.z) : 1.0;
|
|
1701
1744
|
lit += emissiveTint * emissiveMapped * uEmissiveGain * vEmissive * uNightFactor
|
|
1702
|
-
* mix(1.0, min(lightShade, sunShade), EMISSIVE_SHADOW_SHARE);
|
|
1745
|
+
* mix(1.0, min(lightShade, sunShade), EMISSIVE_SHADOW_SHARE) * fxGlow;
|
|
1746
|
+
/* And a lit window's glow, which the scene's lit share already times: not gated on night. */
|
|
1747
|
+
lit += windowGlow * uEmissiveGain;
|
|
1703
1748
|
|
|
1704
1749
|
#if NIGHT_EMISSIVE
|
|
1705
1750
|
/*
|
|
@@ -1773,7 +1818,14 @@ export const MAIN_GLSL = `void main() {
|
|
|
1773
1818
|
fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);
|
|
1774
1819
|
}
|
|
1775
1820
|
|
|
1776
|
-
|
|
1821
|
+
/*
|
|
1822
|
+
* A surface recedes into the medium; light added to the frame fades in it (\`uFogEnabled\` at 2,
|
|
1823
|
+
* \`drawFog.ts\`). What is behind an additive draw is already fogged, so mixing the draw toward
|
|
1824
|
+
* the medium's colour and adding it puts the haze into the frame twice — a glow by day drew as
|
|
1825
|
+
* a grey shape of itself, greyer the thicker the air.
|
|
1826
|
+
*/
|
|
1827
|
+
vec3 shaded = applyOutputTransform(
|
|
1828
|
+
uFogEnabled == 2 ? lit * (1.0 - fog) : mix(lit, mediumColor(), fog));
|
|
1777
1829
|
|
|
1778
1830
|
/*
|
|
1779
1831
|
* **Refraction: the scene behind this surface, bent and absorbed.**
|
|
@@ -1870,7 +1922,7 @@ export const MAIN_GLSL = `void main() {
|
|
|
1870
1922
|
* up holding the weighted sum of colour and the weighted sum of alpha. The revealage pass draws
|
|
1871
1923
|
* the same geometry with this at 0, where the branch below is not taken at all.
|
|
1872
1924
|
*/
|
|
1873
|
-
if (
|
|
1925
|
+
if (uWriteMode.x != 0.0) {
|
|
1874
1926
|
float z = distance(vWorldPos, uCameraPos) / 200.0;
|
|
1875
1927
|
float falloff = 0.03 / (1e-5 + z * z * z * z);
|
|
1876
1928
|
float w = alpha * clamp(falloff, 1e-2, 3e3);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Version, inputs, uniforms and the defines the rest of the fragment shader is written against. */
|
|
2
2
|
import { COOKIE_TILE } from '../../lightBudget.js';
|
|
3
|
+
import { IES_ATLAS_WIDTH } from '../../iesProfile.js';
|
|
3
4
|
import { CLUSTER_TEXELS, CLUSTER_X, CLUSTER_Y, CLUSTER_Z, LIGHT_REGION_TEXELS, LIGHT_TEXELS, MAX_LIGHTS_PER_CLUSTER, TABLE_WIDTH, } from '../../clusteredLights.js';
|
|
4
5
|
/**
|
|
5
6
|
* How much roughness a relieved or normal-mapped surface gains per unit of relief.
|
|
@@ -39,7 +40,7 @@ in vec3 vWorldPos;
|
|
|
39
40
|
in float vEmissive;
|
|
40
41
|
in float vSpecular;
|
|
41
42
|
in vec4 vLightPos;
|
|
42
|
-
in
|
|
43
|
+
in vec3 vUv;
|
|
43
44
|
in vec3 vEmissiveColor;
|
|
44
45
|
in float vRoughness;
|
|
45
46
|
/*
|
|
@@ -124,7 +125,7 @@ flat in int vHasTangents;
|
|
|
124
125
|
* biome recolours a whole world without a second texture, and a mesh whose texture is
|
|
125
126
|
* white shades identically to one with no texture at all.
|
|
126
127
|
*/
|
|
127
|
-
uniform
|
|
128
|
+
uniform mediump sampler2DArray uAlbedo;
|
|
128
129
|
/** 0 leaves every textured path unevaluated, for the majority of geometry with no image. */
|
|
129
130
|
uniform int uAlbedoEnabled;
|
|
130
131
|
/**
|
|
@@ -140,6 +141,12 @@ uniform int uAlbedoEnabled;
|
|
|
140
141
|
* else changes.
|
|
141
142
|
*/
|
|
142
143
|
uniform float uAlbedoCutout;
|
|
144
|
+
/**
|
|
145
|
+
* The albedo array's per-layer effects table: six texels a layer, read with \`texelFetch\` at the
|
|
146
|
+
* layer a vertex carries. A 1×1 stand-in where the array has none, which the size test below reads
|
|
147
|
+
* as no effects at all. See \`surfaceEffects.ts\` for the layout.
|
|
148
|
+
*/
|
|
149
|
+
uniform mediump sampler2D uSurfaceEffects;
|
|
143
150
|
|
|
144
151
|
/**
|
|
145
152
|
* 1 for a surface seen from both faces: glTF's doubleSided, a curtain or a leaf card. Its draw culls
|
|
@@ -157,7 +164,7 @@ uniform int uDoubleSided;
|
|
|
157
164
|
* display values bends every normal toward the surface and reads as a green tint over everything.
|
|
158
165
|
* SurfaceTextureOptions.colorSpace already defaults to linear and says why.
|
|
159
166
|
*/
|
|
160
|
-
uniform highp
|
|
167
|
+
uniform highp sampler2DArray uNormalMap;
|
|
161
168
|
/**
|
|
162
169
|
* How hard the map turns the shading normal. 0 is no map at all, and gates the whole block.
|
|
163
170
|
*
|
|
@@ -179,7 +186,7 @@ uniform float uNormalStrength;
|
|
|
179
186
|
* Sampled linear and never sRGB. These three numbers *are* the data, not a picture of it, and
|
|
180
187
|
* decoding them as display values would bend every one of them toward its floor.
|
|
181
188
|
*/
|
|
182
|
-
uniform highp
|
|
189
|
+
uniform highp sampler2DArray uOrmMap;
|
|
183
190
|
/**
|
|
184
191
|
* 1 when a map is bound. Gates the whole block, following uAlbedoEnabled.
|
|
185
192
|
*
|
|
@@ -225,7 +232,7 @@ uniform float uEnvironmentPrefiltered;
|
|
|
225
232
|
* above, and now with a number behind them: a fifth flag was built and measured at 49% of the
|
|
226
233
|
* bundle. See \`ARCHITECTURE.md\` §1.
|
|
227
234
|
*/
|
|
228
|
-
uniform highp
|
|
235
|
+
uniform highp sampler2DArray uEmissiveMap;
|
|
229
236
|
/** 1 when a map is bound. Gates the sample, following uOrmEnabled. */
|
|
230
237
|
uniform int uEmissiveMapEnabled;
|
|
231
238
|
/**
|
|
@@ -301,9 +308,21 @@ uniform vec3 uAmbient;
|
|
|
301
308
|
uniform vec3 uAmbientGround;
|
|
302
309
|
${FOG_GLSL}
|
|
303
310
|
uniform vec3 uCameraPos;
|
|
304
|
-
/**
|
|
311
|
+
/**
|
|
312
|
+
* Optional world plane; reflection passes retain only the viewer's side.
|
|
313
|
+
*
|
|
314
|
+
* **Zero is off, and there is no flag beside it.** A zero plane gives \`dot(p, 0) = 0\`, which is
|
|
315
|
+
* never below zero, so it discards nothing — the \`uClipEnabled\` int that stood here said nothing
|
|
316
|
+
* the plane could not, and a scalar is a whole uniform row on WebGL2. Its row paid for
|
|
317
|
+
* \`uSurfaceScene\`. Other programs keep their own flags; this is the lit stage's alone.
|
|
318
|
+
*/
|
|
305
319
|
uniform vec4 uClipPlane;
|
|
306
|
-
|
|
320
|
+
/**
|
|
321
|
+
* The scene's surface clock and weather, from the environment: x seconds on the caller's clock, y
|
|
322
|
+
* how wet the world is, z the share of facade windows lit, w the share that stays lit however late.
|
|
323
|
+
* One row for all four, paid for by the flag the clip plane no longer needs.
|
|
324
|
+
*/
|
|
325
|
+
uniform vec4 uSurfaceScene;
|
|
307
326
|
uniform float uEmissiveGain;
|
|
308
327
|
uniform float uNightFactor;
|
|
309
328
|
#if NIGHT_EMISSIVE
|
|
@@ -333,22 +352,47 @@ uniform float uNightEmissive;
|
|
|
333
352
|
uniform float uOpacity;
|
|
334
353
|
|
|
335
354
|
/**
|
|
336
|
-
*
|
|
337
|
-
*
|
|
355
|
+
* Two per-draw switches on how a fragment is *written* rather than what colour it is, sharing one
|
|
356
|
+
* row: on WebGL2 every scalar uniform takes a row of its own, and the eight-light rung stands at 255
|
|
357
|
+
* of the 256 an Adreno 740 offers (\`uniformVectorBudget.test.ts\`), so a second scalar would have
|
|
358
|
+
* spent the last row and the next uniform anybody added would drop that part to four lights.
|
|
359
|
+
*
|
|
360
|
+
* **\`.x\` — whether this draw is accumulating into the order-independent transparency buffers**,
|
|
361
|
+
* and therefore weights its own output. 0 is every draw that is not, which is every draw on every
|
|
338
362
|
* published scene.
|
|
339
363
|
*
|
|
340
|
-
*
|
|
364
|
+
* A uniform rather than a permutation, and the cost is why. Writing the accumulation and the
|
|
341
365
|
* revealage from one pass needs a second fragment output, which is another \`flatFrag\` axis —
|
|
342
366
|
* measured in \`scripts/wgsl.ts\` at about 247 KB gzipped, doubling the sixteen fragment variants,
|
|
343
367
|
* and paid by every consumer whether or not they ever switch this on. A branch on a uniform costs
|
|
344
368
|
* a compare, and the geometry is submitted twice instead. See \`orderIndependent.ts\`.
|
|
345
369
|
*
|
|
346
|
-
*
|
|
370
|
+
* The revealage pass does not read it. It draws with it at 0 and a blend of
|
|
347
371
|
* \`(ZERO, ONE_MINUS_SRC_ALPHA)\`, so the colour it computes is multiplied away and only its alpha
|
|
348
372
|
* reaches the target — which is exactly the point: both passes run the same code to decide what
|
|
349
373
|
* alpha this fragment has, so they cannot disagree about it.
|
|
374
|
+
*
|
|
375
|
+
* **\`.y\` — how much of this draw survives a screen-door dither**, for a crossfade between two
|
|
376
|
+
* levels of detail of one thing. 0 is off, and every draw that never asked for it. Positive keeps
|
|
377
|
+
* that share of an 8×8 Bayer pattern's cells; negative keeps the complement — the cells a draw at
|
|
378
|
+
* the positive amount leaves out — so two draws at \`t\` and \`-t\` cover every pixel exactly once,
|
|
379
|
+
* whatever \`t\` is. Keyed on the framebuffer's own pixel, whose y runs the other way on WebGPU, so
|
|
380
|
+
* the grain is mirrored between backends and nothing else about it differs. What it gives up: a
|
|
381
|
+
* crossfade reads as grain for as long as it lasts, which is why it should be short; and a blended
|
|
382
|
+
* draw that asks for it is dithered too, since the test is a discard.
|
|
350
383
|
*/
|
|
351
|
-
uniform
|
|
384
|
+
uniform vec2 uWriteMode;
|
|
385
|
+
|
|
386
|
+
/** Whether this fragment is one \`uWriteMode.y\` keeps. Integer arithmetic, per the device rule. */
|
|
387
|
+
bool ditherKeeps(float amount) {
|
|
388
|
+
uvec2 p = uvec2(gl_FragCoord.xy) & 7u;
|
|
389
|
+
uint a = p.x ^ p.y;
|
|
390
|
+
uint b = p.y;
|
|
391
|
+
uint cell = ((a & 1u) << 5) | ((b & 1u) << 4) | ((a & 2u) << 2) | ((b & 2u) << 1) |
|
|
392
|
+
((a & 4u) >> 1) | ((b & 4u) >> 2);
|
|
393
|
+
float threshold = (float(cell) + 0.5) / 64.0;
|
|
394
|
+
return amount > 0.0 ? threshold < amount : threshold >= -amount;
|
|
395
|
+
}
|
|
352
396
|
|
|
353
397
|
/**
|
|
354
398
|
* Whether the fragment stage runs any lighting: ambient, the sun, point lights, specular,
|
|
@@ -372,10 +416,11 @@ uniform float uOitWeighted;
|
|
|
372
416
|
*/
|
|
373
417
|
uniform int uLightingEnabled;
|
|
374
418
|
/**
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
419
|
+
* How this draw meets the medium: the atmospheric haze and the underwater tint both, folded
|
|
420
|
+
* together because both are the *camera's* medium rather than a property of the surface — see
|
|
421
|
+
* \`mediumFog\`/\`mediumColor\` in \`fog.ts\`. 1 recedes into it, matching every draw before this
|
|
422
|
+
* existed; 2 fades in it without taking its colour, which is what an additive draw is given; 0
|
|
423
|
+
* keeps out of it. The rule is \`fogModeOf\` in \`backend/drawFog.ts\`.
|
|
379
424
|
*
|
|
380
425
|
* **Independent of \`uLightingEnabled\`, and that is the point of it being a second uniform
|
|
381
426
|
* rather than the same one.** "Unlit but still fading into the haze" is a real thing to
|
|
@@ -725,15 +770,15 @@ uniform vec3 uLightIesAxis[MAX_LIGHTS];
|
|
|
725
770
|
*/
|
|
726
771
|
uniform float uLightCookie[MAX_LIGHTS];
|
|
727
772
|
/**
|
|
728
|
-
* Photometric profiles: one row per fixture, a fixture's intensity by vertical angle across it
|
|
773
|
+
* Photometric profiles: one row per fixture, a fixture's intensity by vertical angle across it, in
|
|
774
|
+
* the bottom band of the fixture atlas below.
|
|
729
775
|
*
|
|
730
776
|
* **Every row spans the same 0 to 180 degree arc**, whatever the file measured, which is what lets
|
|
731
777
|
* a light carry only a row index — see \`iesProfile.ts\`. With no profile loaded this holds a
|
|
732
778
|
* single row of ones, the multiplicative identity, because a declared sampler needs a complete
|
|
733
779
|
* texture whether or not the branch reads it and a row of zeros would switch off every light.
|
|
734
780
|
*/
|
|
735
|
-
|
|
736
|
-
/** Rows in the atlas, so a row index becomes a coordinate. At least 1. */
|
|
781
|
+
/** Rows of profile in the fixture atlas, below its cookies. At least 1. */
|
|
737
782
|
uniform float uIesAtlasRows;
|
|
738
783
|
/**
|
|
739
784
|
* Horizontal planes each profile occupies, so a row is \`profile * planes + plane\`.
|
|
@@ -753,8 +798,14 @@ uniform float uIesPlaneCount;
|
|
|
753
798
|
* identity, because a declared sampler needs a complete texture whether or not the branch reads it
|
|
754
799
|
* and a black stand-in would switch off every light that reached it.
|
|
755
800
|
*/
|
|
756
|
-
uniform sampler2D uCookieAtlas;
|
|
757
801
|
uniform float uCookieTiles;
|
|
802
|
+
/**
|
|
803
|
+
* Both tables in one texture: the cookies' row of tiles in the top band, the profiles' rows below.
|
|
804
|
+
* One sampler for the two since 2026-09-30, because the lit stage bound fifteen of the sixteen
|
|
805
|
+
* WebGL2 guarantees; see \`fixtureAtlas.ts\`. The bands are found from \`textureSize\` and
|
|
806
|
+
* \`uIesAtlasRows\`, so the fold cost no uniform.
|
|
807
|
+
*/
|
|
808
|
+
uniform sampler2D uFixtureAtlas;
|
|
758
809
|
/**
|
|
759
810
|
* Half a texel of a tile, so a cookie's edge cannot reach its neighbour in the atlas.
|
|
760
811
|
*
|
|
@@ -763,6 +814,10 @@ uniform float uCookieTiles;
|
|
|
763
814
|
* cookie. Derived from \`COOKIE_TILE\` rather than written twice.
|
|
764
815
|
*/
|
|
765
816
|
#define COOKIE_INSET (0.5 / ${COOKIE_TILE}.0)
|
|
817
|
+
/** Texels across a profile's row, where a vertical angle of 0 to 180 degrees is laid. */
|
|
818
|
+
#define IES_WIDTH ${IES_ATLAS_WIDTH}.0
|
|
819
|
+
/** Texels a cookie's tile spans, on both axes. */
|
|
820
|
+
#define COOKIE_SPAN ${COOKIE_TILE}.0
|
|
766
821
|
/**
|
|
767
822
|
* Rectangular area lights, and how many of them are live.
|
|
768
823
|
*
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface effects in the lit shader: what a layer does beyond its picture, read from the albedo
|
|
3
|
+
* array's effects table at the layer a vertex carries (`surfaceEffects.ts` has the layout).
|
|
4
|
+
*
|
|
5
|
+
* **Nothing here changes a surface that asks for nothing.** With no table the fetch never runs and
|
|
6
|
+
* every value is zero; each effect is then `mix(x, y, 0)` or a multiply by exactly 1, so a scene
|
|
7
|
+
* that never names an effect draws bit for bit what it drew before. Rain is the one effect that
|
|
8
|
+
* needs no table — it is the scene's (`Environment.wetness`) — and at zero it is the same no-op.
|
|
9
|
+
*
|
|
10
|
+
* **Windows and rooms.** A layer's windows are a grid of cells over one repeat. Each window lights
|
|
11
|
+
* by a hash of its cell against the scene's lit share, quantised to tenths, so a rising share lights
|
|
12
|
+
* more windows rather than brightening the lit ones; a share of them stays lit in the small hours.
|
|
13
|
+
* A room behind a window is **interior mapping from one image**: a one-point perspective picture of
|
|
14
|
+
* a room, back wall in its centre half, is sampled where the view ray meets the room's box. A point
|
|
15
|
+
* on any wall at depth s maps into that picture at `0.5 + (xy - 0.5) / (1 + s)` — the back wall at
|
|
16
|
+
* s = 1 lands in the centre square, and a side wall runs from the picture's edge in to it — so one
|
|
17
|
+
* formula serves every wall and the picture carries its own shading.
|
|
18
|
+
*
|
|
19
|
+
* **Wear** is procedural: dust settling on what faces up, grime rising from the ground and in
|
|
20
|
+
* blotches, streaks running down walls, all fading out with distance where a table says to.
|
|
21
|
+
*
|
|
22
|
+
* Every sample inside these branches is `textureGrad` with gradients taken outside them, or
|
|
23
|
+
* `texelFetch` (the 2026-08-07 rule). Hashes are integer arithmetic (the 2026-09-20 rule).
|
|
24
|
+
*
|
|
25
|
+
* What it gives up: the room picture is one per layer, so every room of a facade is the same room
|
|
26
|
+
* lit or dark; and a room's box assumes a window as wide as it is tall, so a very tall cell
|
|
27
|
+
* stretches its room.
|
|
28
|
+
*/
|
|
29
|
+
export declare const SURFACE_EFFECTS_GLSL = "\n/*\n * The layer's six rows, and what every effect reads beside them, filled once at the top of main.\n * Globals rather than arguments, because naga passes every argument through a pointer and a copy,\n * and in sixteen permutations that traffic was most of what the effects cost in bytes.\n */\nvec4 fx0;\nvec4 fx1;\nvec4 fx2;\nvec4 fx3;\nvec4 fx4;\nvec4 fx5;\nvec3 fxNormal;\nfloat fxDistance;\n\n/** Fill the rows for this layer, or leave them zero where the array carries no table. */\nvoid fxLoad(int layer) {\n fx0 = vec4(0.0);\n fx1 = vec4(0.0);\n fx2 = vec4(0.0);\n fx3 = vec4(0.0);\n fx4 = vec4(0.0);\n fx5 = vec4(0.0);\n ivec2 size = textureSize(uSurfaceEffects, 0);\n if (size.x < 6 || layer < 0 || layer >= size.y) return;\n fx0 = texelFetch(uSurfaceEffects, ivec2(0, layer), 0);\n fx1 = texelFetch(uSurfaceEffects, ivec2(1, layer), 0);\n fx2 = texelFetch(uSurfaceEffects, ivec2(2, layer), 0);\n fx3 = texelFetch(uSurfaceEffects, ivec2(3, layer), 0);\n fx4 = texelFetch(uSurfaceEffects, ivec2(4, layer), 0);\n fx5 = texelFetch(uSurfaceEffects, ivec2(5, layer), 0);\n}\n\n/** A cell and a seed to 0..1, the same on every device. */\nfloat fxHash(vec2 cell, float seed) {\n uvec2 q = uvec2(ivec2(cell)) + uvec2(uint(seed) * 7919u, uint(seed) * 104729u);\n uint h = (q.x * 1597334677u) ^ (q.y * 3812015801u);\n h = (h ^ (h >> 16)) * 2246822519u;\n h ^= h >> 13;\n return float(h & 16777215u) / 16777216.0;\n}\n\nfloat fxNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 u = smoothstep(0.0, 1.0, fract(p));\n return mix(\n mix(fxHash(i, 0.0), fxHash(i + vec2(1.0, 0.0), 0.0), u.x),\n mix(fxHash(i + vec2(0.0, 1.0), 0.0), fxHash(i + vec2(1.0, 1.0), 0.0), u.x),\n u.y);\n}\n\n/** Scroll and flipbook, on the caller's clock. `fract` keeps a long clock from eating precision. */\nvec3 fxAnimate(vec3 at) {\n float time = uSurfaceScene.x;\n at.xy += fract(fx4.xy * time);\n if (fx5.y > 0.0) at.z += mod(floor(time * fx5.z), fx5.y);\n return at;\n}\n\n/** Pulse, flicker and fade with distance, as a factor on the emission. */\nfloat fxEmission(float layer) {\n float time = uSurfaceScene.x;\n float e = 1.0 - fx4.w * (0.5 + 0.5 * sin(6.2831853 * fract(fx4.z * time)));\n e *= 1.0 - fx5.x * step(0.7, fxHash(vec2(floor(time * 14.0), layer), 3.0));\n if (fx5.w > 0.0) e *= 1.0 - smoothstep(fx5.w * 0.7, fx5.w, fxDistance);\n return e;\n}\n\n/** Dust, grime and streaks, then the scene's rain: the colour in xyz, the roughness in w. */\nvec4 fxWeather(vec3 world, vec3 albedo, float roughness) {\n float fade = fx3.w > 0.0 ? 1.0 - smoothstep(fx3.w * 0.5, fx3.w, fxDistance) : 1.0;\n if (fx3.x + fx3.y + fx3.z > 0.0) {\n float blotch = fxNoise(world.xz * 0.35 + world.y * 0.2);\n float dust = fx3.x * smoothstep(0.35, 0.9, fxNormal.y) * (0.5 + 0.5 * blotch) * fade;\n float grime = fx3.y * (0.55 - 0.55 * smoothstep(0.0, 2.5, world.y) + 0.45 * blotch) * fade;\n float streak = fx3.z * (1.0 - abs(fxNormal.y))\n * fxNoise(vec2((world.x + world.z) * 2.5, world.y * 0.15)) * fade;\n albedo = mix(albedo, vec3(0.56, 0.53, 0.48), dust * 0.6) * (1.0 - 0.5 * grime - 0.35 * streak);\n roughness = mix(roughness, 1.0, clamp(0.5 * (dust + grime), 0.0, 1.0));\n }\n float wet = uSurfaceScene.y * (1.0 - fx2.w) * mix(0.35, 1.0, smoothstep(0.2, 0.8, fxNormal.y));\n return vec4(albedo * (1.0 - 0.45 * wet), mix(roughness, 0.08, wet));\n}\n\n/**\n * Windows and the rooms behind them: the glow a lit window adds to the emission in xyz, and in w\n * how much of the surface is glass, which the caller mixes toward the room's dim colour. The room\n * itself is returned through `fxRoom`. `mask` is the albedo's alpha, and a window is a hole in the\n * wall's picture: glass where it is 0. The other way round would lose the wall's colour, which does\n * not survive an upload at alpha 0.\n *\n * **A building's own light, where its vertices name one.** The table is per layer, and every\n * facade of one style shares a layer, so a light that differs from building to building \u2014 its\n * colour and its strength \u2014 cannot live there. A window layer's vertex emissive is otherwise unused\n * (its windows glow here, not through the emissive term), so the vertex's emissive colour, strength\n * folded in, is that light: the window's colour where there is no room, a tint on the room where\n * there is. An absent colour reads negative and leaves both exactly as the table says.\n */\nvec3 fxRoom;\nvec4 fxWindows(vec3 at, vec2 dx, vec2 dy, float mask) {\n float glass = fx0.z > 0.0 ? 1.0 - mask : 1.0;\n vec2 g = at.xy * fx0.xy;\n vec2 local = fract(g);\n float share = floor(max(uSurfaceScene.z, uSurfaceScene.w) * 10.0 + 0.5) / 10.0;\n float lit = fxHash(floor(g), fx0.w) < share * (fx1.x > 0.0 ? fx1.z : 1.0) ? 1.0 : 0.0;\n fxRoom = fx2.rgb;\n bool own = vEmissiveColor.r >= 0.0;\n vec3 glow = own ? vEmissiveColor : fx2.rgb;\n float tangentLength = length(vTangent.xyz);\n if (fx1.x > 0.0 && tangentLength > 1e-4) {\n vec3 t = vTangent.xyz / tangentLength;\n vec3 v = normalize(vWorldPos - uCameraPos);\n vec3 d = vec3(dot(v, t), dot(v, cross(fxNormal, t)) * sign(vTangent.w + 0.5),\n min(dot(v, fxNormal), -1e-3) / max(fx1.y, 0.05));\n vec2 across = (step(0.0, d.xy) - local) / (sign(d.xy + 1e-9) * max(abs(d.xy), 1e-5));\n float travel = min(min(across.x, across.y), -1.0 / d.z);\n float depth = min(-d.z * travel, 1.0);\n vec2 picture = 0.5 + (local + d.xy * travel - 0.5) / (1.0 + depth);\n fxRoom = textureGrad(uAlbedo, vec3(picture, fx1.x - 1.0), dx * fx0.xy * 0.5, dy * fx0.xy * 0.5).rgb;\n glow = own ? fxRoom * vEmissiveColor : fxRoom;\n }\n return vec4(glow * (lit * fx1.w * glass), glass);\n}\n";
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surface effects in the lit shader: what a layer does beyond its picture, read from the albedo
|
|
3
|
+
* array's effects table at the layer a vertex carries (`surfaceEffects.ts` has the layout).
|
|
4
|
+
*
|
|
5
|
+
* **Nothing here changes a surface that asks for nothing.** With no table the fetch never runs and
|
|
6
|
+
* every value is zero; each effect is then `mix(x, y, 0)` or a multiply by exactly 1, so a scene
|
|
7
|
+
* that never names an effect draws bit for bit what it drew before. Rain is the one effect that
|
|
8
|
+
* needs no table — it is the scene's (`Environment.wetness`) — and at zero it is the same no-op.
|
|
9
|
+
*
|
|
10
|
+
* **Windows and rooms.** A layer's windows are a grid of cells over one repeat. Each window lights
|
|
11
|
+
* by a hash of its cell against the scene's lit share, quantised to tenths, so a rising share lights
|
|
12
|
+
* more windows rather than brightening the lit ones; a share of them stays lit in the small hours.
|
|
13
|
+
* A room behind a window is **interior mapping from one image**: a one-point perspective picture of
|
|
14
|
+
* a room, back wall in its centre half, is sampled where the view ray meets the room's box. A point
|
|
15
|
+
* on any wall at depth s maps into that picture at `0.5 + (xy - 0.5) / (1 + s)` — the back wall at
|
|
16
|
+
* s = 1 lands in the centre square, and a side wall runs from the picture's edge in to it — so one
|
|
17
|
+
* formula serves every wall and the picture carries its own shading.
|
|
18
|
+
*
|
|
19
|
+
* **Wear** is procedural: dust settling on what faces up, grime rising from the ground and in
|
|
20
|
+
* blotches, streaks running down walls, all fading out with distance where a table says to.
|
|
21
|
+
*
|
|
22
|
+
* Every sample inside these branches is `textureGrad` with gradients taken outside them, or
|
|
23
|
+
* `texelFetch` (the 2026-08-07 rule). Hashes are integer arithmetic (the 2026-09-20 rule).
|
|
24
|
+
*
|
|
25
|
+
* What it gives up: the room picture is one per layer, so every room of a facade is the same room
|
|
26
|
+
* lit or dark; and a room's box assumes a window as wide as it is tall, so a very tall cell
|
|
27
|
+
* stretches its room.
|
|
28
|
+
*/
|
|
29
|
+
export const SURFACE_EFFECTS_GLSL = /* glsl */ `
|
|
30
|
+
/*
|
|
31
|
+
* The layer's six rows, and what every effect reads beside them, filled once at the top of main.
|
|
32
|
+
* Globals rather than arguments, because naga passes every argument through a pointer and a copy,
|
|
33
|
+
* and in sixteen permutations that traffic was most of what the effects cost in bytes.
|
|
34
|
+
*/
|
|
35
|
+
vec4 fx0;
|
|
36
|
+
vec4 fx1;
|
|
37
|
+
vec4 fx2;
|
|
38
|
+
vec4 fx3;
|
|
39
|
+
vec4 fx4;
|
|
40
|
+
vec4 fx5;
|
|
41
|
+
vec3 fxNormal;
|
|
42
|
+
float fxDistance;
|
|
43
|
+
|
|
44
|
+
/** Fill the rows for this layer, or leave them zero where the array carries no table. */
|
|
45
|
+
void fxLoad(int layer) {
|
|
46
|
+
fx0 = vec4(0.0);
|
|
47
|
+
fx1 = vec4(0.0);
|
|
48
|
+
fx2 = vec4(0.0);
|
|
49
|
+
fx3 = vec4(0.0);
|
|
50
|
+
fx4 = vec4(0.0);
|
|
51
|
+
fx5 = vec4(0.0);
|
|
52
|
+
ivec2 size = textureSize(uSurfaceEffects, 0);
|
|
53
|
+
if (size.x < 6 || layer < 0 || layer >= size.y) return;
|
|
54
|
+
fx0 = texelFetch(uSurfaceEffects, ivec2(0, layer), 0);
|
|
55
|
+
fx1 = texelFetch(uSurfaceEffects, ivec2(1, layer), 0);
|
|
56
|
+
fx2 = texelFetch(uSurfaceEffects, ivec2(2, layer), 0);
|
|
57
|
+
fx3 = texelFetch(uSurfaceEffects, ivec2(3, layer), 0);
|
|
58
|
+
fx4 = texelFetch(uSurfaceEffects, ivec2(4, layer), 0);
|
|
59
|
+
fx5 = texelFetch(uSurfaceEffects, ivec2(5, layer), 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A cell and a seed to 0..1, the same on every device. */
|
|
63
|
+
float fxHash(vec2 cell, float seed) {
|
|
64
|
+
uvec2 q = uvec2(ivec2(cell)) + uvec2(uint(seed) * 7919u, uint(seed) * 104729u);
|
|
65
|
+
uint h = (q.x * 1597334677u) ^ (q.y * 3812015801u);
|
|
66
|
+
h = (h ^ (h >> 16)) * 2246822519u;
|
|
67
|
+
h ^= h >> 13;
|
|
68
|
+
return float(h & 16777215u) / 16777216.0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
float fxNoise(vec2 p) {
|
|
72
|
+
vec2 i = floor(p);
|
|
73
|
+
vec2 u = smoothstep(0.0, 1.0, fract(p));
|
|
74
|
+
return mix(
|
|
75
|
+
mix(fxHash(i, 0.0), fxHash(i + vec2(1.0, 0.0), 0.0), u.x),
|
|
76
|
+
mix(fxHash(i + vec2(0.0, 1.0), 0.0), fxHash(i + vec2(1.0, 1.0), 0.0), u.x),
|
|
77
|
+
u.y);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Scroll and flipbook, on the caller's clock. \`fract\` keeps a long clock from eating precision. */
|
|
81
|
+
vec3 fxAnimate(vec3 at) {
|
|
82
|
+
float time = uSurfaceScene.x;
|
|
83
|
+
at.xy += fract(fx4.xy * time);
|
|
84
|
+
if (fx5.y > 0.0) at.z += mod(floor(time * fx5.z), fx5.y);
|
|
85
|
+
return at;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Pulse, flicker and fade with distance, as a factor on the emission. */
|
|
89
|
+
float fxEmission(float layer) {
|
|
90
|
+
float time = uSurfaceScene.x;
|
|
91
|
+
float e = 1.0 - fx4.w * (0.5 + 0.5 * sin(6.2831853 * fract(fx4.z * time)));
|
|
92
|
+
e *= 1.0 - fx5.x * step(0.7, fxHash(vec2(floor(time * 14.0), layer), 3.0));
|
|
93
|
+
if (fx5.w > 0.0) e *= 1.0 - smoothstep(fx5.w * 0.7, fx5.w, fxDistance);
|
|
94
|
+
return e;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Dust, grime and streaks, then the scene's rain: the colour in xyz, the roughness in w. */
|
|
98
|
+
vec4 fxWeather(vec3 world, vec3 albedo, float roughness) {
|
|
99
|
+
float fade = fx3.w > 0.0 ? 1.0 - smoothstep(fx3.w * 0.5, fx3.w, fxDistance) : 1.0;
|
|
100
|
+
if (fx3.x + fx3.y + fx3.z > 0.0) {
|
|
101
|
+
float blotch = fxNoise(world.xz * 0.35 + world.y * 0.2);
|
|
102
|
+
float dust = fx3.x * smoothstep(0.35, 0.9, fxNormal.y) * (0.5 + 0.5 * blotch) * fade;
|
|
103
|
+
float grime = fx3.y * (0.55 - 0.55 * smoothstep(0.0, 2.5, world.y) + 0.45 * blotch) * fade;
|
|
104
|
+
float streak = fx3.z * (1.0 - abs(fxNormal.y))
|
|
105
|
+
* fxNoise(vec2((world.x + world.z) * 2.5, world.y * 0.15)) * fade;
|
|
106
|
+
albedo = mix(albedo, vec3(0.56, 0.53, 0.48), dust * 0.6) * (1.0 - 0.5 * grime - 0.35 * streak);
|
|
107
|
+
roughness = mix(roughness, 1.0, clamp(0.5 * (dust + grime), 0.0, 1.0));
|
|
108
|
+
}
|
|
109
|
+
float wet = uSurfaceScene.y * (1.0 - fx2.w) * mix(0.35, 1.0, smoothstep(0.2, 0.8, fxNormal.y));
|
|
110
|
+
return vec4(albedo * (1.0 - 0.45 * wet), mix(roughness, 0.08, wet));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Windows and the rooms behind them: the glow a lit window adds to the emission in xyz, and in w
|
|
115
|
+
* how much of the surface is glass, which the caller mixes toward the room's dim colour. The room
|
|
116
|
+
* itself is returned through \`fxRoom\`. \`mask\` is the albedo's alpha, and a window is a hole in the
|
|
117
|
+
* wall's picture: glass where it is 0. The other way round would lose the wall's colour, which does
|
|
118
|
+
* not survive an upload at alpha 0.
|
|
119
|
+
*
|
|
120
|
+
* **A building's own light, where its vertices name one.** The table is per layer, and every
|
|
121
|
+
* facade of one style shares a layer, so a light that differs from building to building — its
|
|
122
|
+
* colour and its strength — cannot live there. A window layer's vertex emissive is otherwise unused
|
|
123
|
+
* (its windows glow here, not through the emissive term), so the vertex's emissive colour, strength
|
|
124
|
+
* folded in, is that light: the window's colour where there is no room, a tint on the room where
|
|
125
|
+
* there is. An absent colour reads negative and leaves both exactly as the table says.
|
|
126
|
+
*/
|
|
127
|
+
vec3 fxRoom;
|
|
128
|
+
vec4 fxWindows(vec3 at, vec2 dx, vec2 dy, float mask) {
|
|
129
|
+
float glass = fx0.z > 0.0 ? 1.0 - mask : 1.0;
|
|
130
|
+
vec2 g = at.xy * fx0.xy;
|
|
131
|
+
vec2 local = fract(g);
|
|
132
|
+
float share = floor(max(uSurfaceScene.z, uSurfaceScene.w) * 10.0 + 0.5) / 10.0;
|
|
133
|
+
float lit = fxHash(floor(g), fx0.w) < share * (fx1.x > 0.0 ? fx1.z : 1.0) ? 1.0 : 0.0;
|
|
134
|
+
fxRoom = fx2.rgb;
|
|
135
|
+
bool own = vEmissiveColor.r >= 0.0;
|
|
136
|
+
vec3 glow = own ? vEmissiveColor : fx2.rgb;
|
|
137
|
+
float tangentLength = length(vTangent.xyz);
|
|
138
|
+
if (fx1.x > 0.0 && tangentLength > 1e-4) {
|
|
139
|
+
vec3 t = vTangent.xyz / tangentLength;
|
|
140
|
+
vec3 v = normalize(vWorldPos - uCameraPos);
|
|
141
|
+
vec3 d = vec3(dot(v, t), dot(v, cross(fxNormal, t)) * sign(vTangent.w + 0.5),
|
|
142
|
+
min(dot(v, fxNormal), -1e-3) / max(fx1.y, 0.05));
|
|
143
|
+
vec2 across = (step(0.0, d.xy) - local) / (sign(d.xy + 1e-9) * max(abs(d.xy), 1e-5));
|
|
144
|
+
float travel = min(min(across.x, across.y), -1.0 / d.z);
|
|
145
|
+
float depth = min(-d.z * travel, 1.0);
|
|
146
|
+
vec2 picture = 0.5 + (local + d.xy * travel - 0.5) / (1.0 + depth);
|
|
147
|
+
fxRoom = textureGrad(uAlbedo, vec3(picture, fx1.x - 1.0), dx * fx0.xy * 0.5, dy * fx0.xy * 0.5).rgb;
|
|
148
|
+
glow = own ? fxRoom * vEmissiveColor : fxRoom;
|
|
149
|
+
}
|
|
150
|
+
return vec4(glow * (lit * fx1.w * glass), glass);
|
|
151
|
+
}
|
|
152
|
+
`;
|