@zephyr3d/scene 0.9.26 → 0.9.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animation/animationset.js +92 -30
- package/dist/animation/animationset.js.map +1 -1
- package/dist/animation/joint_dynamics/convex_collider.js +320 -0
- package/dist/animation/joint_dynamics/convex_collider.js.map +1 -0
- package/dist/animation/skeleton.js +21 -1
- package/dist/animation/skeleton.js.map +1 -1
- package/dist/asset/model.js +6 -2
- package/dist/asset/model.js.map +1 -1
- package/dist/camera/camera.js +75 -23
- package/dist/camera/camera.js.map +1 -1
- package/dist/index.d.ts +4764 -3600
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/material/hairstrand.js +235 -235
- package/dist/material/hairstrand.js.map +1 -1
- package/dist/material/material.js +296 -296
- package/dist/material/material.js.map +1 -1
- package/dist/material/mixins/lit.js +33 -1
- package/dist/material/mixins/lit.js.map +1 -1
- package/dist/material/shader/helper.js +404 -3
- package/dist/material/shader/helper.js.map +1 -1
- package/dist/material/water.js +2057 -72
- package/dist/material/water.js.map +1 -1
- package/dist/posteffect/motionblur.js +663 -58
- package/dist/posteffect/motionblur.js.map +1 -1
- package/dist/posteffect/posteffect.js +12 -1
- package/dist/posteffect/posteffect.js.map +1 -1
- package/dist/posteffect/sao.js +46 -44
- package/dist/posteffect/sao.js.map +1 -1
- package/dist/posteffect/sss.js +30 -2
- package/dist/posteffect/sss.js.map +1 -1
- package/dist/render/abuffer_oit.js +37 -37
- package/dist/render/abuffer_oit.js.map +1 -1
- package/dist/render/clipmap.js +156 -5
- package/dist/render/clipmap.js.map +1 -1
- package/dist/render/cull_visitor.js +1 -0
- package/dist/render/cull_visitor.js.map +1 -1
- package/dist/render/dualdepthpeeling_oit.js +49 -49
- package/dist/render/dualdepthpeeling_oit.js.map +1 -1
- package/dist/render/fbm_wavegenerator.js +3 -3
- package/dist/render/fbm_wavegenerator.js.map +1 -1
- package/dist/render/fft_wavegenerator.js +57 -15
- package/dist/render/fft_wavegenerator.js.map +1 -1
- package/dist/render/gerstner_wavegenerator.js +127 -20
- package/dist/render/gerstner_wavegenerator.js.map +1 -1
- package/dist/render/hzb.js +80 -22
- package/dist/render/hzb.js.map +1 -1
- package/dist/render/lightpass.js +6 -3
- package/dist/render/lightpass.js.map +1 -1
- package/dist/render/oit.js +11 -11
- package/dist/render/oit.js.map +1 -1
- package/dist/render/render_queue.js +36 -0
- package/dist/render/render_queue.js.map +1 -1
- package/dist/render/renderer.js +4 -0
- package/dist/render/renderer.js.map +1 -1
- package/dist/render/rendergraph/blackboard.js +1 -0
- package/dist/render/rendergraph/blackboard.js.map +1 -1
- package/dist/render/rendergraph/forward_plus_builder.js +318 -9
- package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
- package/dist/render/rendergraph/frame_resource_requirements.js +1 -0
- package/dist/render/rendergraph/frame_resource_requirements.js.map +1 -1
- package/dist/render/rendergraph/history_resources.js +8 -1
- package/dist/render/rendergraph/history_resources.js.map +1 -1
- package/dist/render/underwater.js +624 -0
- package/dist/render/underwater.js.map +1 -0
- package/dist/render/water_caustics.js +1380 -0
- package/dist/render/water_caustics.js.map +1 -0
- package/dist/render/weightedblended_oit.js +33 -33
- package/dist/render/weightedblended_oit.js.map +1 -1
- package/dist/scene/water.js +540 -4
- package/dist/scene/water.js.map +1 -1
- package/dist/shaders/water.js +22 -19
- package/dist/shaders/water.js.map +1 -1
- package/dist/shaders/water_medium.js +56 -0
- package/dist/shaders/water_medium.js.map +1 -0
- package/dist/shadow/shadow_region.js +49 -49
- package/dist/shadow/shadow_region.js.map +1 -1
- package/dist/utility/serialization/manager.js +3 -1
- package/dist/utility/serialization/manager.js.map +1 -1
- package/dist/utility/serialization/scene/animation.js +31 -2
- package/dist/utility/serialization/scene/animation.js.map +1 -1
- package/dist/utility/serialization/scene/camera.js +53 -25
- package/dist/utility/serialization/scene/camera.js.map +1 -1
- package/dist/utility/serialization/scene/light.js.map +1 -1
- package/dist/utility/serialization/scene/water.js +1101 -21
- package/dist/utility/serialization/scene/water.js.map +1 -1
- package/dist/utility/shprojector.js +87 -87
- package/dist/utility/shprojector.js.map +1 -1
- package/package.json +1 -1
- package/dist/render/rendergraph/frame_graph_context.js +0 -41
- package/dist/render/rendergraph/frame_graph_context.js.map +0 -1
- package/dist/shaders/ssgi.js +0 -63
- package/dist/shaders/ssgi.js.map +0 -1
- package/dist/shaders/ssgi_reprojection.js +0 -118
- package/dist/shaders/ssgi_reprojection.js.map +0 -1
|
@@ -17,6 +17,28 @@ const UNIFORM_NAME_SKYDISTANTLIGHT_LUT = 'Z_UniformSkyDistantLightLUT';
|
|
|
17
17
|
const UNIFORM_NAME_SHADOW_MAP = 'Z_UniformShadowMap';
|
|
18
18
|
const UNIFORM_NAME_SHADOW_MASK = 'Z_UniformShadowMask';
|
|
19
19
|
const UNIFORM_NAME_SHADOW_MASK_MODE = 'Z_UniformShadowMaskMode';
|
|
20
|
+
const UNIFORM_NAME_CAUSTIC_MAP = 'Z_UniformCausticMap';
|
|
21
|
+
const UNIFORM_NAME_CAUSTIC_HEIGHT_MAP = 'Z_UniformCausticHeightMap';
|
|
22
|
+
/**
|
|
23
|
+
* Offset added to a surface height before it is stored in the caustic height
|
|
24
|
+
* map, so that a cleared texel - no surface on that ray - is distinguishable
|
|
25
|
+
* from a surface at the rest level. A ray with no water on it must read as
|
|
26
|
+
* dry, not as submerged under a flat surface: on the down-sun side of a
|
|
27
|
+
* footprint edge, a receiver below the rest level but above a trough would
|
|
28
|
+
* otherwise be lit through water that is not there. Eight keeps troughs down
|
|
29
|
+
* to seven metres positive and leaves half floats millimetre precision.
|
|
30
|
+
* @internal
|
|
31
|
+
*/ const CAUSTIC_HEIGHT_BIAS = 8;
|
|
32
|
+
/**
|
|
33
|
+
* Water bodies that can cast caustics into one map.
|
|
34
|
+
*
|
|
35
|
+
* They share the map and the slice, so this is not a limit on how much water a
|
|
36
|
+
* scene may hold - only on how many separate surfaces the receiver will test
|
|
37
|
+
* itself against, which it does per fragment. Four covers a lake with a couple
|
|
38
|
+
* of pools beside it; beyond that the renderer keeps the largest four, which
|
|
39
|
+
* are the ones a viewer is most likely to be standing over.
|
|
40
|
+
*/ const MAX_CAUSTIC_WATERS = 4;
|
|
41
|
+
const UNIFORM_NAME_CAUSTIC_PARAMS = 'Z_UniformCausticParams';
|
|
20
42
|
const UNIFORM_NAME_LINEAR_DEPTH_MAP = 'Z_UniformLinearDepth';
|
|
21
43
|
const UNIFORM_NAME_LINEAR_DEPTH_MAP_SIZE = 'Z_UniformLinearDepthSize';
|
|
22
44
|
const UNIFORM_NAME_SCENE_COLOR_MAP = 'Z_UniformSceneColor';
|
|
@@ -294,6 +316,9 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
|
|
|
294
316
|
}
|
|
295
317
|
scope[UNIFORM_NAME_SHADOW_MAP] = tex.uniform(0);
|
|
296
318
|
}
|
|
319
|
+
if (this.usesWaterCaustics(ctx)) {
|
|
320
|
+
this.declareWaterCausticUniforms(pb);
|
|
321
|
+
}
|
|
297
322
|
if (ctx.drawEnvLight) {
|
|
298
323
|
ctx.env.light.envLight.initShaderBindings(pb, ctx);
|
|
299
324
|
}
|
|
@@ -304,8 +329,18 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
|
|
|
304
329
|
scope[UNIFORM_NAME_LINEAR_DEPTH_MAP_SIZE] = pb.vec2().uniform(0);
|
|
305
330
|
}
|
|
306
331
|
if (ctx.sceneColorTexture) {
|
|
307
|
-
//
|
|
308
|
-
|
|
332
|
+
// Mip-filtered, because the refraction background is read at a LOD and
|
|
333
|
+
// not only at mip 0: the water picks one from how deep and how turbid
|
|
334
|
+
// the column it is looking through is. A `nomip` sampler pins mip 0
|
|
335
|
+
// whatever LOD the shader passes, so this and the copy's level count
|
|
336
|
+
// have to change together - and each backend only honours one of them.
|
|
337
|
+
// WebGPU binds the sampler declared here as a static binding the runtime
|
|
338
|
+
// cannot overwrite; WebGL falls back to the texture's own sampler, whose
|
|
339
|
+
// mip filter follows its level count.
|
|
340
|
+
//
|
|
341
|
+
// Every other reader of this texture passes an explicit LOD 0 (SSR hits,
|
|
342
|
+
// the sky blend), so none of them changes behaviour.
|
|
343
|
+
scope[UNIFORM_NAME_SCENE_COLOR_MAP] = pb.tex2D().uniform(0).withSampler(getSamplerOptions('clamp_linear'));
|
|
309
344
|
scope[UNIFORM_NAME_SCENE_COLOR_MAP_SIZE] = pb.vec2().uniform(0);
|
|
310
345
|
}
|
|
311
346
|
if (ctx.HiZTexture) {
|
|
@@ -1091,10 +1126,329 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
|
|
|
1091
1126
|
bindGroup.setTexture(UNIFORM_NAME_SHADOW_MASK, ctx.shadowMaskTexture ?? this.getDummyShadowMask(ctx.device), fetchSampler('clamp_nearest'));
|
|
1092
1127
|
}
|
|
1093
1128
|
bindGroup.setTexture(UNIFORM_NAME_BAKED_SKY_MAP, ctx.scene.env.sky.getBakedSkyTexture(ctx));
|
|
1129
|
+
if (this.usesWaterCaustics(ctx)) {
|
|
1130
|
+
this.setWaterCausticUniforms(bindGroup, ctx);
|
|
1131
|
+
}
|
|
1094
1132
|
if (ctx.drawEnvLight) {
|
|
1095
1133
|
ctx.env.light.envLight.updateBindGroup(bindGroup, ctx);
|
|
1096
1134
|
}
|
|
1097
1135
|
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Whether the pass being built modulates its light with the water caustic map.
|
|
1138
|
+
*
|
|
1139
|
+
* True for every light pass of a frame that produced a map; which light the
|
|
1140
|
+
* caustics actually attach to is decided per-fragment by
|
|
1141
|
+
* {@link ShaderHelper.calculateWaterCaustic}. Both the shader variant hash and
|
|
1142
|
+
* the global bind group hash fold this in, so a program that declares the
|
|
1143
|
+
* caustic uniforms is always paired with a bind group that supplies them.
|
|
1144
|
+
*
|
|
1145
|
+
* Deliberately a function of the draw context alone, with no material input.
|
|
1146
|
+
* The caustic uniforms sit in the global bind group, whose layout is built
|
|
1147
|
+
* once per {@link DrawContext.renderPassHash} and shared by every material in
|
|
1148
|
+
* the pass, so the declarations have to be uniform across the pass. A material
|
|
1149
|
+
* that must not receive caustics - water itself - opts out of the sampling
|
|
1150
|
+
* instead, through `receivesWaterCaustics()`; the uniforms stay declared and
|
|
1151
|
+
* simply go unread.
|
|
1152
|
+
*
|
|
1153
|
+
* @param ctx - Current draw context.
|
|
1154
|
+
* @returns True when the caustic uniforms take part in this pass.
|
|
1155
|
+
* @internal
|
|
1156
|
+
*/ static usesWaterCaustics(ctx) {
|
|
1157
|
+
return !!ctx.waterCaustics && !!ctx.waterCausticLight && ctx.renderPass.type === RENDER_PASS_TYPE_LIGHT;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Declares the global uniforms {@link ShaderHelper.calculateWaterCaustic} reads.
|
|
1161
|
+
*
|
|
1162
|
+
* Split out of the global uniform setup so a test can build a program that
|
|
1163
|
+
* exercises the sampling code without standing up a whole light pass.
|
|
1164
|
+
*
|
|
1165
|
+
* @param pb - Program builder whose global scope receives the uniforms.
|
|
1166
|
+
* @internal
|
|
1167
|
+
*/ /**
|
|
1168
|
+
* Warps a linear caustic-map coordinate so texels concentrate near the centre.
|
|
1169
|
+
*
|
|
1170
|
+
* The map covers a fixed footprint, so its world texel size is the range
|
|
1171
|
+
* divided by the resolution and a range large enough to light the scene is
|
|
1172
|
+
* already too coarse to resolve a caustic cell. This trades the edge of the
|
|
1173
|
+
* map, which is far from the camera, for the middle, which is not:
|
|
1174
|
+
*
|
|
1175
|
+
* ```
|
|
1176
|
+
* warp(u) = u (1 + k) / (1 + k |u|)
|
|
1177
|
+
* unwarp(w) = w / ((1 + k) - k |w|)
|
|
1178
|
+
* ```
|
|
1179
|
+
*
|
|
1180
|
+
* Texel density is `1 + k` times uniform at the centre and `1 / (1 + k)` times
|
|
1181
|
+
* it at the border, and `k = 0` is exactly the identity, so the pass can apply
|
|
1182
|
+
* this unconditionally. Both ends of `[-1, 1]` are fixed points, which is what
|
|
1183
|
+
* lets the warp sit inside the existing parameterisation without moving the
|
|
1184
|
+
* footprint the receiver gates on. The first derivative is continuous at the
|
|
1185
|
+
* centre; the second flips sign there, which is not visible in a pattern this
|
|
1186
|
+
* noisy.
|
|
1187
|
+
*
|
|
1188
|
+
* Applied per axis rather than radially: the photon grid, the map and the blur
|
|
1189
|
+
* are all axis-aligned, so a separable warp keeps photon `(i,j)` on texel
|
|
1190
|
+
* `(i,j)` for calm water and leaves the map's normalisation to 1.0 intact.
|
|
1191
|
+
*
|
|
1192
|
+
* @param scope - Current shader scope.
|
|
1193
|
+
* @param ndc - Linear map coordinate in `[-1, 1]`.
|
|
1194
|
+
* @param strength - Warp strength `k`; 0 is the identity.
|
|
1195
|
+
* @returns The warped coordinate, also in `[-1, 1]`.
|
|
1196
|
+
* @internal
|
|
1197
|
+
*/ static warpCausticNDC(scope, ndc, strength) {
|
|
1198
|
+
const pb = scope.$builder;
|
|
1199
|
+
const funcName = 'Z_warpCausticNDC';
|
|
1200
|
+
pb.func(funcName, [
|
|
1201
|
+
pb.vec2('ndc'),
|
|
1202
|
+
pb.float('k')
|
|
1203
|
+
], function() {
|
|
1204
|
+
this.$return(pb.div(pb.mul(this.ndc, pb.add(this.k, 1)), pb.add(pb.vec2(1), pb.mul(pb.abs(this.ndc), this.k))));
|
|
1205
|
+
});
|
|
1206
|
+
return pb.getGlobalScope()[funcName](ndc, strength);
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Inverse of {@link ShaderHelper.warpCausticNDC}.
|
|
1210
|
+
*
|
|
1211
|
+
* The denominator is `1` at the border and `1 + k` at the centre, so it stays
|
|
1212
|
+
* positive for any input the passes feed it - all of which come from `[-1, 1]`.
|
|
1213
|
+
*
|
|
1214
|
+
* @param scope - Current shader scope.
|
|
1215
|
+
* @param warped - Warped map coordinate in `[-1, 1]`.
|
|
1216
|
+
* @param strength - Warp strength `k`; 0 is the identity.
|
|
1217
|
+
* @returns The linear coordinate the warp came from.
|
|
1218
|
+
* @internal
|
|
1219
|
+
*/ static unwarpCausticNDC(scope, warped, strength) {
|
|
1220
|
+
const pb = scope.$builder;
|
|
1221
|
+
const funcName = 'Z_unwarpCausticNDC';
|
|
1222
|
+
pb.func(funcName, [
|
|
1223
|
+
pb.vec2('warped'),
|
|
1224
|
+
pb.float('k')
|
|
1225
|
+
], function() {
|
|
1226
|
+
this.$return(pb.div(this.warped, pb.sub(pb.vec2(pb.add(this.k, 1)), pb.mul(pb.abs(this.warped), this.k))));
|
|
1227
|
+
});
|
|
1228
|
+
return pb.getGlobalScope()[funcName](warped, strength);
|
|
1229
|
+
}
|
|
1230
|
+
/** @internal */ static declareWaterCausticUniforms(pb) {
|
|
1231
|
+
const scope = pb.getGlobalScope();
|
|
1232
|
+
const causticStruct = pb.defineStruct([
|
|
1233
|
+
pb.vec4('frameX'),
|
|
1234
|
+
pb.vec4('frameY'),
|
|
1235
|
+
pb.vec4('center'),
|
|
1236
|
+
pb.vec4('lightDir'),
|
|
1237
|
+
pb.vec4('params'),
|
|
1238
|
+
pb.vec4('extinction'),
|
|
1239
|
+
// Per water body, because the slice is shared but the water is not: each
|
|
1240
|
+
// has its own surface height, footprint and medium, and none of the three
|
|
1241
|
+
// means anything until a receiver has resolved which water is above it.
|
|
1242
|
+
pb.int('waterCount'),
|
|
1243
|
+
pb.vec4[MAX_CAUSTIC_WATERS]('waterRegions'),
|
|
1244
|
+
// (sigma_t.xyz, surface height)
|
|
1245
|
+
pb.vec4[MAX_CAUSTIC_WATERS]('waterMedia')
|
|
1246
|
+
]);
|
|
1247
|
+
scope[UNIFORM_NAME_CAUSTIC_PARAMS] = causticStruct().uniform(0);
|
|
1248
|
+
// The map is single mip and read bilinearly; clamping keeps receivers
|
|
1249
|
+
// outside the covered area reading the border rather than wrapping.
|
|
1250
|
+
scope[UNIFORM_NAME_CAUSTIC_MAP] = pb.tex2D().uniform(0).withSampler(getSamplerOptions('clamp_linear_nomip'));
|
|
1251
|
+
// Displaced surface height per water slot, on the same slice as the map.
|
|
1252
|
+
// Read with the map's uv, so it shares the map's sampling.
|
|
1253
|
+
scope[UNIFORM_NAME_CAUSTIC_HEIGHT_MAP] = pb.tex2D().uniform(0).withSampler(getSamplerOptions('clamp_linear_nomip'));
|
|
1254
|
+
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Attenuation the water caustics apply to one light at a world position.
|
|
1257
|
+
*
|
|
1258
|
+
* Returns the product of two effects: the caustic pattern focused by the waves,
|
|
1259
|
+
* and the Beer-Lambert transmittance of the water column the light travelled
|
|
1260
|
+
* through to reach the point. The result is 1 for any light that is not the
|
|
1261
|
+
* caustic light, above the surface, and outside the water region, so the caller
|
|
1262
|
+
* can multiply unconditionally.
|
|
1263
|
+
*
|
|
1264
|
+
* The caustic light is identified at runtime by comparing directions rather
|
|
1265
|
+
* than by which pass is running. Shadow-casting lights reach the opaque queue
|
|
1266
|
+
* through the clustered path when the screen-space shadow mask is on (the
|
|
1267
|
+
* default) and through a per-light additive pass when it is off; only a runtime
|
|
1268
|
+
* test covers both. Two directional lights pointing the same way would both
|
|
1269
|
+
* pick up the caustics, but they are physically the same light.
|
|
1270
|
+
*
|
|
1271
|
+
* @param scope - Current shader scope.
|
|
1272
|
+
* @param worldPos - World position of the shaded fragment.
|
|
1273
|
+
* @param lightType - The light's type constant.
|
|
1274
|
+
* @param lightDirection - Direction the light travels, i.e. `directionAndCutoff.xyz`.
|
|
1275
|
+
* @param ctx - Current draw context.
|
|
1276
|
+
* @returns A `vec3` multiplier, or null when this pass has no caustics.
|
|
1277
|
+
*/ static calculateWaterCaustic(scope, worldPos, lightType, lightDirection, ctx) {
|
|
1278
|
+
if (!this.usesWaterCaustics(ctx)) {
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
return this.sampleWaterCaustic(scope, worldPos, lightType, lightDirection);
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* {@link ShaderHelper.calculateWaterCaustic} without the pass gate.
|
|
1285
|
+
*
|
|
1286
|
+
* The gate asks whether the *light pass* has caustics, which is the right
|
|
1287
|
+
* question for a material and the wrong one for a standalone full-screen pass
|
|
1288
|
+
* that declared the same uniforms itself. Such a pass has already decided it
|
|
1289
|
+
* has a map; this is the sampling half on its own.
|
|
1290
|
+
*
|
|
1291
|
+
* @param scope - Current shader scope.
|
|
1292
|
+
* @param worldPos - World position of the shaded fragment.
|
|
1293
|
+
* @param lightType - The light's type constant.
|
|
1294
|
+
* @param lightDirection - Direction the light travels, i.e. `directionAndCutoff.xyz`.
|
|
1295
|
+
* @param defocus - Whether to fade the pattern's contrast by how far the point
|
|
1296
|
+
* is from the map's focal depth. True for a receiver surface, which is what
|
|
1297
|
+
* that fade describes. False for a point inside the water column: see the
|
|
1298
|
+
* comment on the term itself.
|
|
1299
|
+
* @returns A `vec3` multiplier.
|
|
1300
|
+
* @internal
|
|
1301
|
+
*/ static sampleWaterCaustic(scope, worldPos, lightType, lightDirection, defocus = true) {
|
|
1302
|
+
const pb = scope.$builder;
|
|
1303
|
+
const funcName = defocus ? 'Z_calculateWaterCaustic' : 'Z_calculateWaterCausticVolume';
|
|
1304
|
+
pb.func(funcName, [
|
|
1305
|
+
pb.vec3('worldPos'),
|
|
1306
|
+
pb.int('lightType'),
|
|
1307
|
+
pb.vec3('lightDirection')
|
|
1308
|
+
], function() {
|
|
1309
|
+
this.$l.cu = this[UNIFORM_NAME_CAUSTIC_PARAMS];
|
|
1310
|
+
// Only the one light the map was built for casts caustics.
|
|
1311
|
+
this.$if(pb.or(pb.notEqual(this.lightType, LIGHT_TYPE_DIRECTIONAL), pb.lessThan(pb.dot(this.lightDirection, this.cu.lightDir.xyz), 0.999)), function() {
|
|
1312
|
+
this.$return(pb.vec3(1));
|
|
1313
|
+
});
|
|
1314
|
+
// Project into the map: frameX/frameY are orthonormal and perpendicular to
|
|
1315
|
+
// the light, so the same two dot products the splat pass used invert it.
|
|
1316
|
+
this.$l.rel = pb.sub(this.worldPos, this.cu.center.xyz);
|
|
1317
|
+
// Per-axis inverse extents: the slice is fitted to the water, so it is
|
|
1318
|
+
// only square in texels, not in meters.
|
|
1319
|
+
this.$l.mapNDC = pb.mul(pb.vec2(pb.dot(this.rel, this.cu.frameX.xyz), pb.dot(this.rel, this.cu.frameY.xyz)), pb.vec2(this.cu.frameX.w, this.cu.frameY.w));
|
|
1320
|
+
// Texels are concentrated towards the middle of the map, so the lookup
|
|
1321
|
+
// goes through the same warp the splat laid the photons out under.
|
|
1322
|
+
this.$l.warpedNDC = ShaderHelper.warpCausticNDC(this, this.mapNDC, this.cu.extinction.w);
|
|
1323
|
+
this.$l.uv = pb.add(pb.mul(this.warpedNDC, 0.5), pb.vec2(0.5));
|
|
1324
|
+
// The displaced surface height of each water slot on this fragment's own
|
|
1325
|
+
// sun ray, biased by CAUSTIC_HEIGHT_BIAS. The height map rasterises the
|
|
1326
|
+
// displaced surface into the same light-space slice as the pattern, so
|
|
1327
|
+
// the texel under this uv holds the surface the ray through this fragment
|
|
1328
|
+
// passed through - no entry point has to be estimated. Beyond the map the
|
|
1329
|
+
// sampler would repeat its border, which is some other ray's wave; the
|
|
1330
|
+
// surface is taken as flat there.
|
|
1331
|
+
this.$l.insideMap = pb.all(pb.lessThanEqual(pb.abs(this.mapNDC), pb.vec2(1)));
|
|
1332
|
+
// How far this fragment is towards the map border, and how much the map is
|
|
1333
|
+
// therefore trusted. Hoisted above the water loop because the wave height
|
|
1334
|
+
// is faded by it too, not just the pattern.
|
|
1335
|
+
//
|
|
1336
|
+
// Measured on the linear coordinate, not the warped one. The warp fixes
|
|
1337
|
+
// both ends of [-1, 1], so the border is in the same world place either
|
|
1338
|
+
// way, but the band between them is not: warping it would make its world
|
|
1339
|
+
// width depend on the warp strength, and the CPU sized it in meters.
|
|
1340
|
+
//
|
|
1341
|
+
// Distance is measured under an L4 norm rather than per-axis. A per-axis
|
|
1342
|
+
// fade is a rectangle in light space, and the sun tilts that rectangle, so
|
|
1343
|
+
// its border lands on the sea bed as straight lines - which the eye picks
|
|
1344
|
+
// out of a caustic pattern however wide the gradient is. An L2 circle has
|
|
1345
|
+
// no straight part anywhere but discards the 21% of the map outside its
|
|
1346
|
+
// inscribed disc; L4 gives up 7% instead and still curves everywhere.
|
|
1347
|
+
this.$l.ndcSq = pb.mul(this.mapNDC, this.mapNDC);
|
|
1348
|
+
this.$l.edgeDist = pb.sqrt(pb.sqrt(pb.dot(this.ndcSq, this.ndcSq)));
|
|
1349
|
+
this.$l.edge = pb.sub(1, pb.smoothStep(this.cu.params.w, 1, this.edgeDist));
|
|
1350
|
+
this.$l.waveHeights = pb.textureSampleLevel(this[UNIFORM_NAME_CAUSTIC_HEIGHT_MAP], this.uv, 0);
|
|
1351
|
+
// Find the water above this point. Several can qualify - a pool inside a
|
|
1352
|
+
// lake, water on terraces - and the one that matters is the lowest of
|
|
1353
|
+
// them, because that is the last surface the sun crossed on its way here.
|
|
1354
|
+
this.$l.depth = pb.float(-1);
|
|
1355
|
+
this.$l.medium = pb.vec3(0);
|
|
1356
|
+
this.$for(pb.int('i'), 0, MAX_CAUSTIC_WATERS, function() {
|
|
1357
|
+
this.$if(pb.greaterThanEqual(this.i, this.cu.waterCount), function() {
|
|
1358
|
+
this.$break();
|
|
1359
|
+
});
|
|
1360
|
+
this.$l.slotMedium = this.cu.waterMedia.at(this.i);
|
|
1361
|
+
// Where the sun ray that lights this point crossed that water's rest
|
|
1362
|
+
// plane, for the footprint test. The splat pass clips photons by that
|
|
1363
|
+
// same crossing, so the receiver must ask the same question: a point
|
|
1364
|
+
// under the footprint that the sun reaches from outside it saw no water
|
|
1365
|
+
// at all, and a point beyond the footprint that the sun reaches through
|
|
1366
|
+
// it did. Testing the point's own xz gets both wrong by the offset
|
|
1367
|
+
// depth / tan(elevation). Only the footprint is decided here; the depth
|
|
1368
|
+
// itself comes from the height map, which needs no crossing point.
|
|
1369
|
+
this.$l.slotFlat = pb.sub(this.slotMedium.w, this.worldPos.y);
|
|
1370
|
+
this.$l.slotPath = pb.mul(this.slotFlat, this.cu.lightDir.w);
|
|
1371
|
+
this.$l.slotEntry = pb.sub(this.worldPos.xz, pb.mul(this.cu.lightDir.xz, this.slotPath));
|
|
1372
|
+
this.$l.slotRegion = this.cu.waterRegions.at(this.i);
|
|
1373
|
+
this.$if(pb.and(pb.all(pb.greaterThanEqual(this.slotEntry, this.slotRegion.xy)), pb.all(pb.lessThanEqual(this.slotEntry, this.slotRegion.zw))), function() {
|
|
1374
|
+
// Depth under the *displaced* surface on this ray, which is what
|
|
1375
|
+
// the splat refracted the photons through. Gating on the rest plane
|
|
1376
|
+
// instead cut the caustics off at a flat line across any receiver
|
|
1377
|
+
// that pokes above it under a crest. Slot i lives in channel i.
|
|
1378
|
+
this.$l.slotMask = pb.vec4(pb.float(pb.equal(this.i, 0)), pb.float(pb.equal(this.i, 1)), pb.float(pb.equal(this.i, 2)), pb.float(pb.equal(this.i, 3)));
|
|
1379
|
+
this.$l.slotSample = pb.dot(this.waveHeights, this.slotMask);
|
|
1380
|
+
// A cleared texel inside the map means this ray never meets that
|
|
1381
|
+
// body's surface - it crosses the rest plane beyond the footprint,
|
|
1382
|
+
// or the surface is on the far side of the receiver - and the
|
|
1383
|
+
// receiver is dry as far as this body is concerned. Outside the map
|
|
1384
|
+
// nothing is known and the surface is taken as flat.
|
|
1385
|
+
this.$l.slotCovered = pb.or(pb.not(this.insideMap), pb.greaterThan(this.slotSample, 0.5));
|
|
1386
|
+
this.$if(this.slotCovered, function() {
|
|
1387
|
+
// Faded into the rest plane over the same band the pattern fades
|
|
1388
|
+
// on, rather than switched off at the border. Beyond the map the
|
|
1389
|
+
// surface is taken as flat, and exp(-sigma * depth) is convex, so
|
|
1390
|
+
// a hard switch makes the wavy side systematically brighter than
|
|
1391
|
+
// the flat side - E[exp(-sigma*w)] > 1 - and the step lands as a
|
|
1392
|
+
// mean brightness seam that grows with both the swell and the
|
|
1393
|
+
// turbidity. Ramping the wave to zero puts the two sides on the
|
|
1394
|
+
// same value at the border.
|
|
1395
|
+
this.$l.slotWave = pb.mul(pb.sub(this.slotSample, CAUSTIC_HEIGHT_BIAS), pb.float(this.insideMap), this.edge);
|
|
1396
|
+
this.$l.slotDepth = pb.sub(pb.add(this.slotMedium.w, this.slotWave), this.worldPos.y);
|
|
1397
|
+
this.$if(pb.greaterThan(this.slotDepth, 0), function() {
|
|
1398
|
+
this.$if(pb.or(pb.lessThan(this.depth, 0), pb.lessThan(this.slotDepth, this.depth)), function() {
|
|
1399
|
+
this.depth = this.slotDepth;
|
|
1400
|
+
this.medium = this.slotMedium.xyz;
|
|
1401
|
+
});
|
|
1402
|
+
});
|
|
1403
|
+
});
|
|
1404
|
+
});
|
|
1405
|
+
});
|
|
1406
|
+
this.$if(pb.lessThanEqual(this.depth, 0), function() {
|
|
1407
|
+
this.$return(pb.vec3(1));
|
|
1408
|
+
});
|
|
1409
|
+
this.$l.pathLength = pb.mul(this.depth, this.cu.lightDir.w);
|
|
1410
|
+
this.$l.transmittance = pb.exp(pb.neg(pb.mul(this.medium, this.pathLength)));
|
|
1411
|
+
this.$l.pattern = pb.textureSampleLevel(this[UNIFORM_NAME_CAUSTIC_MAP], this.uv, 0).x;
|
|
1412
|
+
if (defocus) {
|
|
1413
|
+
// Receivers away from the focal plane see a defocused, lower-contrast
|
|
1414
|
+
// pattern rather than a displaced one.
|
|
1415
|
+
this.$l.defocus = pb.div(1, pb.add(1, pb.mul(pb.abs(pb.sub(this.depth, this.cu.params.y)), this.cu.params.z)));
|
|
1416
|
+
} else {
|
|
1417
|
+
// A point inside the water column is not a receiver, and has no depth of
|
|
1418
|
+
// its own to be out of focus at - the volumetric march visits every
|
|
1419
|
+
// depth along the ray. Charging it the receiver's fade makes the light
|
|
1420
|
+
// shafts fade according to where the *sea bed* was told to be sharpest:
|
|
1421
|
+
// the open-ocean preset focuses the map at 26m for its bed, which at the
|
|
1422
|
+
// default defocus leaves a point 2m down at 26% contrast, and the shafts
|
|
1423
|
+
// dissolve into grain. The shafts instead take the pattern as the map
|
|
1424
|
+
// holds it and fade with the edge and the transmittance like everything
|
|
1425
|
+
// else in the column.
|
|
1426
|
+
this.$l.defocus = pb.float(1);
|
|
1427
|
+
}
|
|
1428
|
+
this.$l.weight = pb.mul(this.cu.params.x, this.defocus, this.edge);
|
|
1429
|
+
this.$l.caustic = pb.add(1, pb.mul(pb.sub(this.pattern, 1), this.weight));
|
|
1430
|
+
// Soften the waterline so the transmittance does not switch on abruptly.
|
|
1431
|
+
this.$l.submerged = pb.smoothStep(0, 0.05, this.depth);
|
|
1432
|
+
this.$return(pb.mix(pb.vec3(1), pb.mul(pb.vec3(this.caustic), this.transmittance), this.submerged));
|
|
1433
|
+
});
|
|
1434
|
+
return pb.getGlobalScope()[funcName](worldPos, lightType, lightDirection);
|
|
1435
|
+
}
|
|
1436
|
+
/** @internal */ static setWaterCausticUniforms(bindGroup, ctx) {
|
|
1437
|
+
const uniforms = ctx.waterCausticUniforms;
|
|
1438
|
+
bindGroup.setValue(UNIFORM_NAME_CAUSTIC_PARAMS, {
|
|
1439
|
+
frameX: uniforms.frameX,
|
|
1440
|
+
frameY: uniforms.frameY,
|
|
1441
|
+
center: uniforms.center,
|
|
1442
|
+
lightDir: uniforms.lightDir,
|
|
1443
|
+
params: uniforms.params,
|
|
1444
|
+
extinction: uniforms.extinction,
|
|
1445
|
+
waterCount: uniforms.waterCount,
|
|
1446
|
+
waterRegions: uniforms.waterRegions,
|
|
1447
|
+
waterMedia: uniforms.waterMedia
|
|
1448
|
+
});
|
|
1449
|
+
bindGroup.setTexture(UNIFORM_NAME_CAUSTIC_MAP, ctx.waterCausticTexture, fetchSampler('clamp_linear_nomip'));
|
|
1450
|
+
bindGroup.setTexture(UNIFORM_NAME_CAUSTIC_HEIGHT_MAP, ctx.waterCausticHeightTexture, fetchSampler('clamp_linear_nomip'));
|
|
1451
|
+
}
|
|
1098
1452
|
/** @internal */ static setLightUniformsShadow(bindGroup, ctx, light) {
|
|
1099
1453
|
const shadowMapParams = ctx.shadowMapInfo.get(light);
|
|
1100
1454
|
this._lightUniformShadow.sunDir = ctx.sunLight ? ctx.sunLight.directionAndCutoff.xyz().scaleBy(-1) : this.defaultSunDir;
|
|
@@ -1114,6 +1468,9 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
|
|
|
1114
1468
|
this._lightUniformShadow.envLightSpecularStrength = ctx.env?.light.specularStrength ?? 1;
|
|
1115
1469
|
bindGroup.setValue('light', this._lightUniformShadow);
|
|
1116
1470
|
bindGroup.setTexture(UNIFORM_NAME_SHADOW_MAP, shadowMapParams.shadowMap, shadowMapParams.shadowMapSampler);
|
|
1471
|
+
if (this.usesWaterCaustics(ctx)) {
|
|
1472
|
+
this.setWaterCausticUniforms(bindGroup, ctx);
|
|
1473
|
+
}
|
|
1117
1474
|
bindGroup.setTexture(UNIFORM_NAME_BAKED_SKY_MAP, ctx.scene.env.sky.getBakedSkyTexture(ctx));
|
|
1118
1475
|
if (ctx.drawEnvLight) {
|
|
1119
1476
|
ctx.env.light.envLight.updateBindGroup(bindGroup, ctx);
|
|
@@ -1235,6 +1592,50 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
|
|
|
1235
1592
|
return scope.camera.elapsedTime;
|
|
1236
1593
|
}
|
|
1237
1594
|
/**
|
|
1595
|
+
* Declares the one camera field {@link ShaderHelper.getElapsedTime} reads.
|
|
1596
|
+
*
|
|
1597
|
+
* Wave generators animate off `camera.elapsedTime`, which the light and depth
|
|
1598
|
+
* passes provide as part of the full camera struct. A standalone program that
|
|
1599
|
+
* drives a wave generator - the caustics splat pass - has no such struct, so it
|
|
1600
|
+
* declares that single field under the same name. Any generator that starts
|
|
1601
|
+
* reading a second camera field will need it added here.
|
|
1602
|
+
*
|
|
1603
|
+
* The three padding floats round the struct up to 16 bytes. std140 aligns a
|
|
1604
|
+
* struct to a vec4 boundary, so a lone float leaves the GLSL block larger than
|
|
1605
|
+
* the buffer the engine sizes from the struct's own members - WebGL2 then
|
|
1606
|
+
* rejects every draw with "uniform buffer that is too small" and rasterises
|
|
1607
|
+
* nothing, while WebGPU is unaffected.
|
|
1608
|
+
*
|
|
1609
|
+
* @param scope - Global scope of the standalone program.
|
|
1610
|
+
* @param uniformGroup - Bind group the uniform belongs to.
|
|
1611
|
+
* @internal
|
|
1612
|
+
*/ static declareStandaloneCameraTime(scope, uniformGroup) {
|
|
1613
|
+
const pb = scope.$builder;
|
|
1614
|
+
scope.camera = pb.defineStruct([
|
|
1615
|
+
pb.float('elapsedTime'),
|
|
1616
|
+
pb.float('_pad0'),
|
|
1617
|
+
pb.float('_pad1'),
|
|
1618
|
+
pb.float('_pad2')
|
|
1619
|
+
])().uniform(uniformGroup);
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Uploads the value declared by {@link ShaderHelper.declareStandaloneCameraTime}.
|
|
1623
|
+
*
|
|
1624
|
+
* Must be the same clock the light pass uses, or a wave generator's caustics
|
|
1625
|
+
* would animate out of step with the surface they are refracted through.
|
|
1626
|
+
*
|
|
1627
|
+
* @param bindGroup - Bind group of the standalone program.
|
|
1628
|
+
* @param ctx - Current draw context.
|
|
1629
|
+
* @internal
|
|
1630
|
+
*/ static setStandaloneCameraTime(bindGroup, ctx) {
|
|
1631
|
+
bindGroup.setValue('camera', {
|
|
1632
|
+
elapsedTime: ctx.device.frameInfo.elapsedOverall * 0.001,
|
|
1633
|
+
_pad0: 0,
|
|
1634
|
+
_pad1: 0,
|
|
1635
|
+
_pad2: 0
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1238
1639
|
* Gets the elapsed time since last frame in seconds
|
|
1239
1640
|
* @param scope - Current shader scope
|
|
1240
1641
|
* @returns The elapsed time since last frame in seconds
|
|
@@ -1865,5 +2266,5 @@ const UNIFORM_NAME_MORPH_INFO = 'Z_UniformMorphInfo';
|
|
|
1865
2266
|
}
|
|
1866
2267
|
}
|
|
1867
2268
|
|
|
1868
|
-
export { ShaderHelper };
|
|
2269
|
+
export { CAUSTIC_HEIGHT_BIAS, MAX_CAUSTIC_WATERS, ShaderHelper };
|
|
1869
2270
|
//# sourceMappingURL=helper.js.map
|