@driftengine/core 4.8.6 → 4.8.7
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 +71 -0
- package/README.md +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/render/anisotropicLobe.d.ts +10 -0
- package/dist/render/anisotropicLobe.js +19 -0
- package/dist/render/areaLights.d.ts +35 -0
- package/dist/render/areaLights.js +35 -0
- package/dist/render/backend/api.d.ts +33 -3
- package/dist/render/backend/webgl2/renderer.d.ts +50 -14
- package/dist/render/backend/webgl2/renderer.js +141 -30
- package/dist/render/backend/webgl2/sceneCaptureTarget.d.ts +29 -0
- package/dist/render/backend/webgl2/sceneCaptureTarget.js +96 -0
- package/dist/render/backend/webgpu/particlePass.js +2 -1
- package/dist/render/backend/webgpu/renderer.d.ts +35 -1
- package/dist/render/backend/webgpu/renderer.js +155 -16
- package/dist/render/backend/webgpu/sceneCaptureTarget.d.ts +24 -0
- package/dist/render/backend/webgpu/sceneCaptureTarget.js +59 -0
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
- package/dist/render/backend/webgpu/surfaceTexturePass.js +34 -8
- package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
- package/dist/render/clusteredLights.d.ts +23 -5
- package/dist/render/clusteredLights.js +35 -8
- package/dist/render/driftLight/bake.d.ts +2 -0
- package/dist/render/driftLight/bake.js +1 -1
- package/dist/render/driftLight/falloff.d.ts +4 -2
- package/dist/render/driftLight/falloff.js +8 -2
- package/dist/render/gi/bounceLights.d.ts +3 -2
- package/dist/render/gi/bounceLights.js +8 -3
- package/dist/render/instanceCull.js +5 -0
- package/dist/render/instances.d.ts +19 -5
- package/dist/render/instances.js +9 -3
- package/dist/render/lightBudget.d.ts +12 -1
- package/dist/render/lightBudget.js +23 -3
- package/dist/render/lightmap.d.ts +23 -9
- package/dist/render/lightmap.js +40 -18
- package/dist/render/particlePool.d.ts +21 -0
- package/dist/render/particlePool.js +7 -0
- package/dist/render/pointLightSelection.d.ts +13 -0
- package/dist/render/pointLightSelection.js +3 -1
- package/dist/render/renderQuality.d.ts +8 -7
- package/dist/render/rendererHarness.js +3 -0
- package/dist/render/rgb9e5.d.ts +22 -0
- package/dist/render/rgb9e5.js +47 -0
- package/dist/render/sceneCapture.d.ts +29 -0
- package/dist/render/sceneCapture.js +36 -0
- package/dist/render/screenSpaceReflection.d.ts +27 -1
- package/dist/render/screenSpaceReflection.js +9 -0
- package/dist/render/shaders/bolt.d.ts +1 -1
- package/dist/render/shaders/bolt.js +7 -1
- package/dist/render/shaders/depth.js +11 -0
- package/dist/render/shaders/flat/anisotropic.d.ts +1 -1
- package/dist/render/shaders/flat/anisotropic.js +37 -3
- package/dist/render/shaders/flat/areaLight.d.ts +1 -1
- package/dist/render/shaders/flat/areaLight.js +46 -0
- package/dist/render/shaders/flat/glassTint.d.ts +3 -2
- package/dist/render/shaders/flat/glassTint.js +3 -1
- package/dist/render/shaders/flat/index.d.ts +5 -0
- package/dist/render/shaders/flat/index.js +16 -2
- package/dist/render/shaders/flat/lobes.d.ts +6 -0
- package/dist/render/shaders/flat/lobes.js +49 -43
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +57 -22
- package/dist/render/shaders/flat/models.d.ts +5 -2
- package/dist/render/shaders/flat/models.js +100 -16
- package/dist/render/shaders/flat/preamble.js +10 -3
- package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
- package/dist/render/shaders/generated/depth.wgsl.d.ts +4 -4
- package/dist/render/shaders/generated/depth.wgsl.js +4 -4
- package/dist/render/shaders/generated/flat.wgsl.d.ts +16 -0
- package/dist/render/shaders/generated/flat.wgsl.js +54 -38
- package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +11 -6
- package/dist/render/shaders/generated/ssrTrace.wgsl.js +11 -6
- package/dist/render/shaders/generated/water.wgsl.d.ts +23 -16
- package/dist/render/shaders/generated/water.wgsl.js +23 -16
- package/dist/render/shaders/gi/probeBake.wgsl.js +9 -5
- package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
- package/dist/render/shaders/recon/motion.wgsl.js +13 -2
- package/dist/render/shaders/ssrTrace.js +20 -1
- package/dist/render/shaders/water.d.ts +1 -1
- package/dist/render/shaders/water.js +12 -3
- package/dist/render/skinScatter.d.ts +3 -1
- package/dist/render/skinScatter.js +3 -1
- package/dist/render/ssrPass.js +1 -0
- package/dist/render/surfaceModel.d.ts +29 -8
- package/dist/render/surfaceModel.js +33 -9
- package/dist/render/surfaceTexture.d.ts +25 -1
- package/dist/render/surfaceTexture.js +27 -5
- package/package.json +6 -6
- package/src/index.ts +6 -2
- package/src/render/anisotropicLobe.ts +29 -0
- package/src/render/areaLights.ts +58 -0
- package/src/render/backend/api.ts +49 -2
- package/src/render/backend/webgl2/renderer.ts +192 -38
- package/src/render/backend/webgl2/sceneCaptureTarget.ts +130 -0
- package/src/render/backend/webgpu/particlePass.ts +2 -1
- package/src/render/backend/webgpu/renderer.ts +194 -19
- package/src/render/backend/webgpu/sceneCaptureTarget.ts +63 -0
- package/src/render/backend/webgpu/surfaceTexturePass.ts +44 -14
- package/src/render/clusteredLights.ts +41 -8
- package/src/render/driftLight/bake.ts +3 -1
- package/src/render/driftLight/falloff.ts +13 -2
- package/src/render/gi/bounceLights.ts +12 -3
- package/src/render/instanceCull.ts +4 -0
- package/src/render/instances.ts +26 -6
- package/src/render/lightBudget.ts +40 -2
- package/src/render/lightmap.ts +64 -23
- package/src/render/particlePool.ts +23 -0
- package/src/render/pointLightSelection.ts +20 -1
- package/src/render/renderQuality.ts +8 -7
- package/src/render/rendererHarness.ts +3 -0
- package/src/render/rgb9e5.ts +52 -0
- package/src/render/sceneCapture.ts +56 -0
- package/src/render/screenSpaceReflection.ts +33 -1
- package/src/render/shaders/bolt.ts +7 -1
- package/src/render/shaders/depth.ts +11 -0
- package/src/render/shaders/flat/anisotropic.ts +37 -3
- package/src/render/shaders/flat/areaLight.ts +46 -0
- package/src/render/shaders/flat/glassTint.ts +9 -2
- package/src/render/shaders/flat/index.ts +21 -2
- package/src/render/shaders/flat/lobes.ts +50 -43
- package/src/render/shaders/flat/main.ts +57 -22
- package/src/render/shaders/flat/models.ts +100 -16
- package/src/render/shaders/flat/preamble.ts +10 -3
- package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
- package/src/render/shaders/generated/depth.wgsl.ts +4 -4
- package/src/render/shaders/generated/flat.wgsl.ts +54 -38
- package/src/render/shaders/generated/ssrTrace.wgsl.ts +11 -6
- package/src/render/shaders/generated/water.wgsl.ts +23 -16
- package/src/render/shaders/gi/probeBake.wgsl.ts +9 -5
- package/src/render/shaders/recon/motion.wgsl.ts +13 -2
- package/src/render/shaders/ssrTrace.ts +20 -1
- package/src/render/shaders/water.ts +12 -3
- package/src/render/skinScatter.ts +3 -1
- package/src/render/ssrPass.ts +1 -0
- package/src/render/surfaceModel.ts +47 -10
- package/src/render/surfaceTexture.ts +69 -9
|
@@ -672,15 +672,19 @@ fn probeLampsAt(p: vec3<f32>, n: vec3<f32>) -> vec3<f32> {
|
|
|
672
672
|
var sum = vec3<f32>(0.0);
|
|
673
673
|
let count = u32(bake.lightCount);
|
|
674
674
|
for (var i = 0u; i < count; i = i + 1u) {
|
|
675
|
-
let at = bounceLights[i *
|
|
676
|
-
let colour = bounceLights[i *
|
|
677
|
-
let aim = bounceLights[i *
|
|
675
|
+
let at = bounceLights[i * 4u];
|
|
676
|
+
let colour = bounceLights[i * 4u + 1u];
|
|
677
|
+
let aim = bounceLights[i * 4u + 2u];
|
|
678
|
+
let lightExponent = bounceLights[i * 4u + 3u].x;
|
|
678
679
|
let lightRadius = at.w;
|
|
679
680
|
let toLight = at.xyz - p;
|
|
680
681
|
let dist = length(toLight);
|
|
681
682
|
let coneFalloff = smoothstep(aim.w, colour.w, dot(-toLight / max(dist, 1e-4), aim.xyz));
|
|
682
683
|
var falloff: f32;
|
|
683
|
-
if (
|
|
684
|
+
if (lightExponent > 0.0) {
|
|
685
|
+
let reach = dist / max(lightRadius, 1e-4);
|
|
686
|
+
falloff = pow(clamp(1.0 - reach * reach, 0.0, 1.0), lightExponent);
|
|
687
|
+
} else if (bake.falloff == 1.0) {
|
|
684
688
|
let window = clamp(1.0 - pow(dist / max(lightRadius, 1e-4), 4.0), 0.0, 1.0);
|
|
685
689
|
falloff = window * window / max(dist * dist, 0.01);
|
|
686
690
|
} else {
|
|
@@ -689,7 +693,7 @@ fn probeLampsAt(p: vec3<f32>, n: vec3<f32>) -> vec3<f32> {
|
|
|
689
693
|
if (falloff <= 0.0) { continue; }
|
|
690
694
|
let ndl = max(dot(n, toLight / max(dist, 1e-4)), 0.0);
|
|
691
695
|
if (ndl <= 0.0) { continue; }
|
|
692
|
-
let shape = select(falloff * falloff, falloff, bake.falloff == 1.0) * coneFalloff;
|
|
696
|
+
let shape = select(falloff * falloff, falloff, lightExponent > 0.0 || bake.falloff == 1.0) * coneFalloff;
|
|
693
697
|
if (shape <= 0.0) { continue; }
|
|
694
698
|
let fixture = max(0.3, 2.0 * march.finestStep);
|
|
695
699
|
let seen = probeLampVisible(p, n, toLight / max(dist, 1e-4), dist - fixture);
|
|
@@ -41,4 +41,4 @@ export declare const MOTION_DRAW_STRIDE = 256;
|
|
|
41
41
|
export declare const MOTION_DRAW_FLOATS = 32;
|
|
42
42
|
/** The motion target's format. Half float holds a uv to a hundredth of a pixel and a view depth. */
|
|
43
43
|
export declare const MOTION_TARGET_FORMAT: GPUTextureFormat;
|
|
44
|
-
export declare const RECON_MOTION_WGSL = "\nstruct MotionFrame {\n /* The scene's own clip transform, with the generated shaders' y negation already folded in. */\n raster: mat4x4<f32>,\n /* This frame's and last frame's view-projections, unjittered, in the matrices' own convention. */\n viewProj: mat4x4<f32>,\n previousViewProj: mat4x4<f32>,\n}\n\nstruct MotionDraw {\n model: mat4x4<f32>,\n previousModel: mat4x4<f32>,\n}\n\n@group(0) @binding(0) var<uniform> frame: MotionFrame;\n@group(0) @binding(1) var<uniform> draw: MotionDraw;\n\nstruct Varyings {\n @builtin(position) clip: vec4<f32>,\n /* Clip positions rather than uv: an interpolator divides by w for us, so dividing these by their\n own interpolated w in the fragment stage is the perspective-correct answer at every pixel. */\n @location(0) now: vec4<f32>,\n @location(1) was: vec4<f32>,\n}\n\n@vertex\nfn motionVert(@location(0) position: vec3<f32>) -> Varyings {\n let world = draw.model * vec4<f32>(position, 1.0);\n let wasWorld = draw.previousModel * vec4<f32>(position, 1.0);\n var out: Varyings;\n out.clip = frame.raster * world;\n out.now = frame.viewProj * world;\n out.was = frame.previousViewProj * wasWorld;\n return out;\n}\n\n/* Normalised device coordinates to the uv the resolve measures in: v increases upward, as the\n references' images do, and the accessor that reads this target is what flips the texture's rows. */\nfn motionUv(clip: vec4<f32>) -> vec2<f32> {\n return clip.xy / clip.w * 0.5 + vec2<f32>(0.5);\n}\n\n/*\n * The skinned stage: the joints this frame and last frame, read from two palette textures of the\n * frame's ring. The same four weighted products in the same order as the scene's own skinning, so\n * the depth this stage produces is the depth the scene wrote.\n */\n@group(1) @binding(0) var palette: texture_2d<f32>;\n@group(1) @binding(1) var previousPalette: texture_2d<f32>;\n\nfn jointOf(source: texture_2d<f32>, index: i32) -> mat4x4<f32> {\n let x = index * 4;\n return mat4x4<f32>(\n textureLoad(source, vec2<i32>(x, 0), 0),\n textureLoad(source, vec2<i32>(x + 1, 0), 0),\n textureLoad(source, vec2<i32>(x + 2, 0), 0),\n textureLoad(source, vec2<i32>(x + 3, 0), 0));\n}\n\nfn skinOf(source: texture_2d<f32>, joints: vec4<f32>, weights: vec4<f32>) -> mat4x4<f32> {\n return jointOf(source, i32(joints.x)) * weights.x\n + jointOf(source, i32(joints.y)) * weights.y\n + jointOf(source, i32(joints.z)) * weights.z\n + jointOf(source, i32(joints.w)) * weights.w;\n}\n\n@vertex\nfn motionSkinnedVert(\n @location(0) position: vec3<f32>,\n @location(11) joints: vec4<f32>,\n @location(12) weights: vec4<f32>,\n) -> Varyings {\n let world = draw.model * (skinOf(palette, joints, weights) * vec4<f32>(position, 1.0));\n let wasWorld =\n draw.previousModel * (skinOf(previousPalette, joints, weights) * vec4<f32>(position, 1.0));\n var out: Varyings;\n out.clip = frame.raster * world;\n out.now = frame.viewProj * world;\n out.was = frame.previousViewProj * wasWorld;\n return out;\n}\n\n/*\n * The same for a rig with eight influences a vertex, the second four at the scene's own locations\n * 14 and 15 and summed after the first, as `skinMatrix()` sums them with `SKIN_EIGHT` on \u2014 or the\n * motion of a face or a shoulder would be the four heaviest joints' alone.\n */\n@vertex\nfn motionSkinnedEightVert(\n @location(0) position: vec3<f32>,\n @location(11) joints: vec4<f32>,\n @location(12) weights: vec4<f32>,\n @location(14) joints2: vec4<f32>,\n @location(15) weights2: vec4<f32>,\n) -> Varyings {\n let skin = skinOf(palette, joints, weights) + skinOf(palette, joints2, weights2);\n let wasSkin =\n skinOf(previousPalette, joints, weights) + skinOf(previousPalette, joints2, weights2);\n let world = draw.model * (skin * vec4<f32>(position, 1.0));\n let wasWorld = draw.previousModel * (wasSkin * vec4<f32>(position, 1.0));\n var out: Varyings;\n out.clip = frame.raster * world;\n out.now = frame.viewProj * world;\n out.was = frame.previousViewProj * wasWorld;\n return out;\n}\n\n/*\n * The cloth-bound stage: a skinned vertex moved toward its simulation triangle by its painted\n * weight, as shaders/clothBinding.ts moves it in the scene, once with this frame's particles and\n * once with last frame's. **The same arithmetic in the same order**, or the depth this stage writes\n * is not the scene's and the equal test admits none of the garment. The binding's turn is not\n * needed: a motion is where a point went, not which way it faces.\n */\n@group(2) @binding(0) var clothBinding: texture_2d<f32>;\n@group(2) @binding(1) var clothParticles: texture_2d<f32>;\n@group(2) @binding(2) var previousClothParticles: texture_2d<f32>;\n\nfn clothTexel(i: i32, width: i32) -> vec2<i32> {\n return vec2<i32>(i % width, i / width);\n}\n\nfn clothPoint(particles: texture_2d<f32>, indices: vec4<f32>, at: vec4<f32>) -> vec3<f32> {\n let width = i32(textureDimensions(particles, 0).x);\n let a = i32(indices.x);\n let b = i32(indices.y);\n let c = i32(indices.z);\n let pa = textureLoad(particles, clothTexel(a, width), 0).xyz;\n if (a == b && b == c) {\n return pa;\n }\n let pb = textureLoad(particles, clothTexel(b, width), 0).xyz;\n let pc = textureLoad(particles, clothTexel(c, width), 0).xyz;\n let n = normalize(cross(pb - pa, pc - pa));\n return pa * (1.0 - at.x - at.y) + pb * at.x + pc * at.y + n * at.z;\n}\n\n/* A skinned point now and last frame, carried to the cloth by the vertex's own weight. */\nfn clothVaryings(vertex: u32, world: vec4<f32>, wasWorld: vec4<f32>) -> Varyings {\n let width = i32(textureDimensions(clothBinding, 0).x);\n let indices = textureLoad(clothBinding, clothTexel(i32(vertex) * 2, width), 0);\n let at = textureLoad(clothBinding, clothTexel(i32(vertex) * 2 + 1, width), 0);\n let follow = indices.w;\n let placed = vec4<f32>(mix(world.xyz, clothPoint(clothParticles, indices, at), follow), world.w);\n let wasPlaced = vec4<f32>(\n mix(wasWorld.xyz, clothPoint(previousClothParticles, indices, at), follow),\n wasWorld.w);\n var out: Varyings;\n out.clip = frame.raster * placed;\n out.now = frame.viewProj * placed;\n out.was = frame.previousViewProj * wasPlaced;\n return out;\n}\n\n@vertex\nfn motionClothVert(\n @builtin(vertex_index) vertex: u32,\n @location(0) position: vec3<f32>,\n @location(11) joints: vec4<f32>,\n @location(12) weights: vec4<f32>,\n) -> Varyings {\n let world = draw.model * (skinOf(palette, joints, weights) * vec4<f32>(position, 1.0));\n let wasWorld =\n draw.previousModel * (skinOf(previousPalette, joints, weights) * vec4<f32>(position, 1.0));\n return clothVaryings(vertex, world, wasWorld);\n}\n\n@vertex\nfn motionClothEightVert(\n @builtin(vertex_index) vertex: u32,\n @location(0) position: vec3<f32>,\n @location(11) joints: vec4<f32>,\n @location(12) weights: vec4<f32>,\n @location(14) joints2: vec4<f32>,\n @location(15) weights2: vec4<f32>,\n) -> Varyings {\n let skin = skinOf(palette, joints, weights) + skinOf(palette, joints2, weights2);\n let wasSkin =\n skinOf(previousPalette, joints, weights) + skinOf(previousPalette, joints2, weights2);\n let world = draw.model * (skin * vec4<f32>(position, 1.0));\n let wasWorld = draw.previousModel * (wasSkin * vec4<f32>(position, 1.0));\n return clothVaryings(vertex, world, wasWorld);\n}\n\n/*\n * The rewritten stage: a mesh whose vertices were rewritten this frame reads last frame's rows from\n * a second buffer of the same stride, so each vertex carries both of its positions.\n */\n@vertex\nfn motionDynamicVert(\n @location(0) position: vec3<f32>,\n @location(1) previousPosition: vec3<f32>,\n) -> Varyings {\n let world = draw.model * vec4<f32>(position, 1.0);\n let wasWorld = draw.previousModel * vec4<f32>(previousPosition, 1.0);\n var out: Varyings;\n out.clip = frame.raster * world;\n out.now = frame.viewProj * world;\n out.was = frame.previousViewProj * wasWorld;\n return out;\n}\n\n/*\n * The instanced stage: each instance's matrix this frame and last frame, read from the batch's\n * placement and the copy of last frame's it kept. Slot for slot, so an instance is paired with\n * itself; the per-draw matrices are not read.\n */\n@vertex\nfn motionInstancedVert(\n @location(0) position: vec3<f32>,\n @location(2) m0: vec4<f32>,\n @location(3) m1: vec4<f32>,\n @location(4) m2: vec4<f32>,\n @location(5) m3: vec4<f32>,\n @location(6) p0: vec4<f32>,\n @location(7) p1: vec4<f32>,\n @location(8) p2: vec4<f32>,\n @location(9) p3: vec4<f32>,\n) -> Varyings {\n let world =
|
|
44
|
+
export declare const RECON_MOTION_WGSL = "\nstruct MotionFrame {\n /* The scene's own clip transform, with the generated shaders' y negation already folded in. */\n raster: mat4x4<f32>,\n /* This frame's and last frame's view-projections, unjittered, in the matrices' own convention. */\n viewProj: mat4x4<f32>,\n previousViewProj: mat4x4<f32>,\n}\n\nstruct MotionDraw {\n model: mat4x4<f32>,\n previousModel: mat4x4<f32>,\n}\n\n@group(0) @binding(0) var<uniform> frame: MotionFrame;\n@group(0) @binding(1) var<uniform> draw: MotionDraw;\n\nstruct Varyings {\n @builtin(position) clip: vec4<f32>,\n /* Clip positions rather than uv: an interpolator divides by w for us, so dividing these by their\n own interpolated w in the fragment stage is the perspective-correct answer at every pixel. */\n @location(0) now: vec4<f32>,\n @location(1) was: vec4<f32>,\n}\n\n@vertex\nfn motionVert(@location(0) position: vec3<f32>) -> Varyings {\n let world = draw.model * vec4<f32>(position, 1.0);\n let wasWorld = draw.previousModel * vec4<f32>(position, 1.0);\n var out: Varyings;\n out.clip = frame.raster * world;\n out.now = frame.viewProj * world;\n out.was = frame.previousViewProj * wasWorld;\n return out;\n}\n\n/* Normalised device coordinates to the uv the resolve measures in: v increases upward, as the\n references' images do, and the accessor that reads this target is what flips the texture's rows. */\nfn motionUv(clip: vec4<f32>) -> vec2<f32> {\n return clip.xy / clip.w * 0.5 + vec2<f32>(0.5);\n}\n\n/*\n * The skinned stage: the joints this frame and last frame, read from two palette textures of the\n * frame's ring. The same four weighted products in the same order as the scene's own skinning, so\n * the depth this stage produces is the depth the scene wrote.\n */\n@group(1) @binding(0) var palette: texture_2d<f32>;\n@group(1) @binding(1) var previousPalette: texture_2d<f32>;\n\nfn jointOf(source: texture_2d<f32>, index: i32) -> mat4x4<f32> {\n let x = index * 4;\n return mat4x4<f32>(\n textureLoad(source, vec2<i32>(x, 0), 0),\n textureLoad(source, vec2<i32>(x + 1, 0), 0),\n textureLoad(source, vec2<i32>(x + 2, 0), 0),\n textureLoad(source, vec2<i32>(x + 3, 0), 0));\n}\n\nfn skinOf(source: texture_2d<f32>, joints: vec4<f32>, weights: vec4<f32>) -> mat4x4<f32> {\n return jointOf(source, i32(joints.x)) * weights.x\n + jointOf(source, i32(joints.y)) * weights.y\n + jointOf(source, i32(joints.z)) * weights.z\n + jointOf(source, i32(joints.w)) * weights.w;\n}\n\n@vertex\nfn motionSkinnedVert(\n @location(0) position: vec3<f32>,\n @location(11) joints: vec4<f32>,\n @location(12) weights: vec4<f32>,\n) -> Varyings {\n let world = draw.model * (skinOf(palette, joints, weights) * vec4<f32>(position, 1.0));\n let wasWorld =\n draw.previousModel * (skinOf(previousPalette, joints, weights) * vec4<f32>(position, 1.0));\n var out: Varyings;\n out.clip = frame.raster * world;\n out.now = frame.viewProj * world;\n out.was = frame.previousViewProj * wasWorld;\n return out;\n}\n\n/*\n * The same for a rig with eight influences a vertex, the second four at the scene's own locations\n * 14 and 15 and summed after the first, as `skinMatrix()` sums them with `SKIN_EIGHT` on \u2014 or the\n * motion of a face or a shoulder would be the four heaviest joints' alone.\n */\n@vertex\nfn motionSkinnedEightVert(\n @location(0) position: vec3<f32>,\n @location(11) joints: vec4<f32>,\n @location(12) weights: vec4<f32>,\n @location(14) joints2: vec4<f32>,\n @location(15) weights2: vec4<f32>,\n) -> Varyings {\n let skin = skinOf(palette, joints, weights) + skinOf(palette, joints2, weights2);\n let wasSkin =\n skinOf(previousPalette, joints, weights) + skinOf(previousPalette, joints2, weights2);\n let world = draw.model * (skin * vec4<f32>(position, 1.0));\n let wasWorld = draw.previousModel * (wasSkin * vec4<f32>(position, 1.0));\n var out: Varyings;\n out.clip = frame.raster * world;\n out.now = frame.viewProj * world;\n out.was = frame.previousViewProj * wasWorld;\n return out;\n}\n\n/*\n * The cloth-bound stage: a skinned vertex moved toward its simulation triangle by its painted\n * weight, as shaders/clothBinding.ts moves it in the scene, once with this frame's particles and\n * once with last frame's. **The same arithmetic in the same order**, or the depth this stage writes\n * is not the scene's and the equal test admits none of the garment. The binding's turn is not\n * needed: a motion is where a point went, not which way it faces.\n */\n@group(2) @binding(0) var clothBinding: texture_2d<f32>;\n@group(2) @binding(1) var clothParticles: texture_2d<f32>;\n@group(2) @binding(2) var previousClothParticles: texture_2d<f32>;\n\nfn clothTexel(i: i32, width: i32) -> vec2<i32> {\n return vec2<i32>(i % width, i / width);\n}\n\nfn clothPoint(particles: texture_2d<f32>, indices: vec4<f32>, at: vec4<f32>) -> vec3<f32> {\n let width = i32(textureDimensions(particles, 0).x);\n let a = i32(indices.x);\n let b = i32(indices.y);\n let c = i32(indices.z);\n let pa = textureLoad(particles, clothTexel(a, width), 0).xyz;\n if (a == b && b == c) {\n return pa;\n }\n let pb = textureLoad(particles, clothTexel(b, width), 0).xyz;\n let pc = textureLoad(particles, clothTexel(c, width), 0).xyz;\n let n = normalize(cross(pb - pa, pc - pa));\n return pa * (1.0 - at.x - at.y) + pb * at.x + pc * at.y + n * at.z;\n}\n\n/* A skinned point now and last frame, carried to the cloth by the vertex's own weight. */\nfn clothVaryings(vertex: u32, world: vec4<f32>, wasWorld: vec4<f32>) -> Varyings {\n let width = i32(textureDimensions(clothBinding, 0).x);\n let indices = textureLoad(clothBinding, clothTexel(i32(vertex) * 2, width), 0);\n let at = textureLoad(clothBinding, clothTexel(i32(vertex) * 2 + 1, width), 0);\n let follow = indices.w;\n let placed = vec4<f32>(mix(world.xyz, clothPoint(clothParticles, indices, at), follow), world.w);\n let wasPlaced = vec4<f32>(\n mix(wasWorld.xyz, clothPoint(previousClothParticles, indices, at), follow),\n wasWorld.w);\n var out: Varyings;\n out.clip = frame.raster * placed;\n out.now = frame.viewProj * placed;\n out.was = frame.previousViewProj * wasPlaced;\n return out;\n}\n\n@vertex\nfn motionClothVert(\n @builtin(vertex_index) vertex: u32,\n @location(0) position: vec3<f32>,\n @location(11) joints: vec4<f32>,\n @location(12) weights: vec4<f32>,\n) -> Varyings {\n let world = draw.model * (skinOf(palette, joints, weights) * vec4<f32>(position, 1.0));\n let wasWorld =\n draw.previousModel * (skinOf(previousPalette, joints, weights) * vec4<f32>(position, 1.0));\n return clothVaryings(vertex, world, wasWorld);\n}\n\n@vertex\nfn motionClothEightVert(\n @builtin(vertex_index) vertex: u32,\n @location(0) position: vec3<f32>,\n @location(11) joints: vec4<f32>,\n @location(12) weights: vec4<f32>,\n @location(14) joints2: vec4<f32>,\n @location(15) weights2: vec4<f32>,\n) -> Varyings {\n let skin = skinOf(palette, joints, weights) + skinOf(palette, joints2, weights2);\n let wasSkin =\n skinOf(previousPalette, joints, weights) + skinOf(previousPalette, joints2, weights2);\n let world = draw.model * (skin * vec4<f32>(position, 1.0));\n let wasWorld = draw.previousModel * (wasSkin * vec4<f32>(position, 1.0));\n return clothVaryings(vertex, world, wasWorld);\n}\n\n/*\n * The rewritten stage: a mesh whose vertices were rewritten this frame reads last frame's rows from\n * a second buffer of the same stride, so each vertex carries both of its positions.\n */\n@vertex\nfn motionDynamicVert(\n @location(0) position: vec3<f32>,\n @location(1) previousPosition: vec3<f32>,\n) -> Varyings {\n let world = draw.model * vec4<f32>(position, 1.0);\n let wasWorld = draw.previousModel * vec4<f32>(previousPosition, 1.0);\n var out: Varyings;\n out.clip = frame.raster * world;\n out.now = frame.viewProj * world;\n out.was = frame.previousViewProj * wasWorld;\n return out;\n}\n\n/* An instance's placement with its bottom row, which carries its texture cell, rebuilt as the\n [0, 0, 0, 1] an affine placement has: `MeshInstances.uvRegions`. */\nfn placementOf(c0: vec4<f32>, c1: vec4<f32>, c2: vec4<f32>, c3: vec4<f32>) -> mat4x4<f32> {\n return mat4x4<f32>(\n vec4<f32>(c0.xyz, 0.0),\n vec4<f32>(c1.xyz, 0.0),\n vec4<f32>(c2.xyz, 0.0),\n vec4<f32>(c3.xyz, 1.0),\n );\n}\n\n/*\n * The instanced stage: each instance's matrix this frame and last frame, read from the batch's\n * placement and the copy of last frame's it kept. Slot for slot, so an instance is paired with\n * itself; the per-draw matrices are not read.\n */\n@vertex\nfn motionInstancedVert(\n @location(0) position: vec3<f32>,\n @location(2) m0: vec4<f32>,\n @location(3) m1: vec4<f32>,\n @location(4) m2: vec4<f32>,\n @location(5) m3: vec4<f32>,\n @location(6) p0: vec4<f32>,\n @location(7) p1: vec4<f32>,\n @location(8) p2: vec4<f32>,\n @location(9) p3: vec4<f32>,\n) -> Varyings {\n let world = placementOf(m0, m1, m2, m3) * vec4<f32>(position, 1.0);\n let wasWorld = placementOf(p0, p1, p2, p3) * vec4<f32>(position, 1.0);\n var out: Varyings;\n out.clip = frame.raster * world;\n out.now = frame.viewProj * world;\n out.was = frame.previousViewProj * wasWorld;\n return out;\n}\n\n@fragment\nfn motionFrag(in: Varyings) -> @location(0) vec4<f32> {\n /*\n * A surface behind last frame's eye has a w that is not positive, and there is no uv for it. The\n * flag stays set: the disocclusion refuses a depth that is not positive, so the resolve drops the\n * history rather than reading it from wherever a divide by a negative w would have pointed.\n */\n let offset = select(vec2<f32>(0.0), motionUv(in.was) - motionUv(in.now), in.was.w > 0.0);\n return vec4<f32>(offset, in.was.w, 1.0);\n}\n";
|
|
@@ -243,6 +243,17 @@ fn motionDynamicVert(
|
|
|
243
243
|
return out;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/* An instance's placement with its bottom row, which carries its texture cell, rebuilt as the
|
|
247
|
+
[0, 0, 0, 1] an affine placement has: \`MeshInstances.uvRegions\`. */
|
|
248
|
+
fn placementOf(c0: vec4<f32>, c1: vec4<f32>, c2: vec4<f32>, c3: vec4<f32>) -> mat4x4<f32> {
|
|
249
|
+
return mat4x4<f32>(
|
|
250
|
+
vec4<f32>(c0.xyz, 0.0),
|
|
251
|
+
vec4<f32>(c1.xyz, 0.0),
|
|
252
|
+
vec4<f32>(c2.xyz, 0.0),
|
|
253
|
+
vec4<f32>(c3.xyz, 1.0),
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
246
257
|
/*
|
|
247
258
|
* The instanced stage: each instance's matrix this frame and last frame, read from the batch's
|
|
248
259
|
* placement and the copy of last frame's it kept. Slot for slot, so an instance is paired with
|
|
@@ -260,8 +271,8 @@ fn motionInstancedVert(
|
|
|
260
271
|
@location(8) p2: vec4<f32>,
|
|
261
272
|
@location(9) p3: vec4<f32>,
|
|
262
273
|
) -> Varyings {
|
|
263
|
-
let world =
|
|
264
|
-
let wasWorld =
|
|
274
|
+
let world = placementOf(m0, m1, m2, m3) * vec4<f32>(position, 1.0);
|
|
275
|
+
let wasWorld = placementOf(p0, p1, p2, p3) * vec4<f32>(position, 1.0);
|
|
265
276
|
var out: Varyings;
|
|
266
277
|
out.clip = frame.raster * world;
|
|
267
278
|
out.now = frame.viewProj * world;
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* for the same rule read the other way.
|
|
21
21
|
*/
|
|
22
22
|
import { glslIsFarDepth } from '../depthConvention.js';
|
|
23
|
+
import { ENV_BRDF_GLSL } from './flat/lobes.js';
|
|
23
24
|
/**
|
|
24
25
|
* The most samples the march may take. `uSsrSteps` breaks out early.
|
|
25
26
|
*
|
|
@@ -61,6 +62,11 @@ uniform vec3 uSsrEye;
|
|
|
61
62
|
uniform vec3 uSsrAxis;
|
|
62
63
|
uniform vec3 uSsrTint;
|
|
63
64
|
uniform float uSsrStrength;
|
|
65
|
+
/**
|
|
66
|
+
* Whether \`uSsrStrength\` is the surface's reflectance head-on, carried per pixel through the
|
|
67
|
+
* split-sum BRDF (x, 1 or 0), and the roughness it is carried at (y). \`ReflectiveSurface.fresnel\`.
|
|
68
|
+
*/
|
|
69
|
+
uniform vec2 uSsrFresnel;
|
|
64
70
|
uniform float uSsrFacingCos;
|
|
65
71
|
uniform float uSsrReach;
|
|
66
72
|
uniform float uSsrThickness;
|
|
@@ -70,6 +76,8 @@ uniform float uSsrEdgeFade;
|
|
|
70
76
|
|
|
71
77
|
out vec4 fragColor;
|
|
72
78
|
|
|
79
|
+
${ENV_BRDF_GLSL}
|
|
80
|
+
|
|
73
81
|
/** What the frame drew at a screen position, as a world point. */
|
|
74
82
|
vec3 worldAt(vec2 uv, float stored) {
|
|
75
83
|
vec4 world = uSsrDepthToWorld * vec4(uv * 2.0 - 1.0, stored, 1.0);
|
|
@@ -130,7 +138,18 @@ void main() {
|
|
|
130
138
|
mask *= smoothstep(uSsrFacingCos, min(1.0, uSsrFacingCos + 0.25), facing);
|
|
131
139
|
/* Nothing was drawn here, so there is no surface to reflect in. */
|
|
132
140
|
mask *= ${glslIsFarDepth('stored')} ? 0.0 : 1.0;
|
|
133
|
-
|
|
141
|
+
/*
|
|
142
|
+
* **How much the surface returns**: one number over the box, or, where the caller asks, its
|
|
143
|
+
* reflectance head-on carried through the split-sum BRDF at this pixel's own view and the
|
|
144
|
+
* surface's roughness — the lit stage's fit, so the two agree on what a surface reflects. A floor
|
|
145
|
+
* then returns more toward the horizon than underfoot, and the share follows the camera.
|
|
146
|
+
*/
|
|
147
|
+
float returned = uSsrStrength;
|
|
148
|
+
if (uSsrFresnel.x > 0.5) {
|
|
149
|
+
vec2 dfg = envBrdfApprox(clamp(dot(normal, normalize(toEye)), 0.0, 1.0), uSsrFresnel.y);
|
|
150
|
+
returned = clamp(uSsrStrength * dfg.x + dfg.y, 0.0, 1.0);
|
|
151
|
+
}
|
|
152
|
+
mask *= returned;
|
|
134
153
|
|
|
135
154
|
vec3 found = vec3(0.0);
|
|
136
155
|
float weight = 0.0;
|
|
@@ -15,4 +15,4 @@
|
|
|
15
15
|
* ocean from a fixed vertex budget without the surface sliding underfoot.
|
|
16
16
|
*/
|
|
17
17
|
export declare const WATER_VERT = "#version 300 es\nlayout(location = 0) in vec2 aGrid;\n\nuniform mat4 uViewProj;\nuniform mat4 uReflectionViewProj;\nuniform vec3 uCameraPos;\nuniform float uTime;\n/**\n * Where the sheet is laid, in world XZ.\n *\n * The ocean passes its own camera position snapped to whole cells, which is what\n * gives an endless sea from a fixed vertex budget without the surface sliding\n * underfoot. A *bounded* sheet \u2014 a pool in a courtyard \u2014 passes the middle of the\n * thing it is filling instead, and is world-fixed by construction.\n *\n * Snapped on the CPU rather than here so that one number is the only difference\n * between an ocean and a basin, and the shader has no idea which it is drawing.\n */\nuniform vec2 uGridOrigin;\n/**\n * Metres per unit of the sheet's own grid, along each of the body's own axes.\n *\n * One and one for the ocean, whose grid is authored in metres and is two meshes wide.\n * A bounded body \u2014 a pool, a tank, a puddle, a channel \u2014 is drawn from a unit sheet\n * and scaled by its own half-extents, which is what lets one mesh and one material\n * serve every body of water in the world.\n *\n * **Two numbers and not one since 2026-08-28.** A single span makes every bounded\n * body square, and a square that contains a channel floods the ground either side\n * of it. The ocean passes the same number twice and is bit-identical to what it was.\n */\nuniform vec2 uGridSpan;\nuniform vec2 uGridHalf;\nuniform vec2 uNearHalf;\n/**\n * The body's own +z, in world XZ, unit length. (0, 1) leaves the sheet unturned.\n *\n * A direction rather than an angle, so nothing here needs a trig call and a consumer\n * bounding a channel can hand over the direction its run already takes. The across\n * axis is world up crossed with this, `(fz, -fx)`, which is the basis\n * `MeshBuilder.addOrientedBox` builds from a forward vector \u2014 one convention for an\n * oriented thing, so a reader who has met one has met both.\n */\nuniform vec2 uGridForward;\n/** Prevailing wind bearing, normalised. Waves are steered toward it. */\nuniform vec2 uWindDir;\n/** Multiplier on authored steepness \u2014 how built-up the sea is. */\nuniform float uWaveGain;\nuniform float uWaterLevel;\n\nout vec3 vWorldPos;\nout vec3 vNormal;\nout float vCrest;\nout vec4 vReflectionClip;\n/** Grid-local offset, so the sheet can fade out its own boundary. */\nout vec2 vGrid;\n\n\nconst int GERSTNER_COUNT = 4;\n/*\n * direction.xy, steepness, wavelength. Amplitude is steepness/k, so these sum to\n * roughly a 1m swell \u2014 enough to read as a living surface, low enough that crests\n * never wash over an island whose deck sits ~1.8m above sea level.\n */\nconst vec4 GERSTNER_WAVES[4] = vec4[4](\n vec4( 1.0, 0.0, 0.115, 34.0),\n vec4( 0.6, 0.8, 0.100, 18.0),\n vec4(-0.7, 0.7, 0.080, 9.0),\n vec4( 0.2, -0.98, 0.060, 5.0)\n);\n\n/*\n * Steer each wave toward the wind, but only partway. A real sea is a dominant\n * swell plus cross-chop; rotating all four onto one bearing gives corduroy,\n * which reads as a material rather than as water.\n */\nvec2 gerstnerDirection(int i, vec2 windDir) {\n return normalize(mix(normalize(GERSTNER_WAVES[i].xy), windDir, 0.62));\n}\n\nfloat gerstnerWavenumber(int i) {\n return 6.28318530718 / GERSTNER_WAVES[i].w;\n}\n\n/** Deep-water phase in radians at a point on the resting plane. */\nfloat gerstnerPhase(float k, vec2 dir, vec2 p, float time) {\n float c = sqrt(9.81 / k); // deep-water phase speed\n return k * (dot(dir, p) - c * time);\n}\n\nstruct GerstnerSurface {\n /** Displacement from the resting plane \u2014 horizontal as well as vertical. */\n vec3 offset;\n vec3 normal;\n /** Height in units of steepness; positive near a crest. */\n float crest;\n};\n\nGerstnerSurface gerstnerSurface(vec2 p, float time, vec2 windDir, float gain) {\n GerstnerSurface s;\n s.offset = vec3(0.0);\n s.normal = vec3(0.0, 1.0, 0.0);\n s.crest = 0.0;\n for (int i = 0; i < GERSTNER_COUNT; i++) {\n vec2 dir = gerstnerDirection(i, windDir);\n float steepness = GERSTNER_WAVES[i].z * gain;\n float k = gerstnerWavenumber(i);\n float f = gerstnerPhase(k, dir, p, time);\n float a = steepness / k; // amplitude from steepness\n\n s.offset.x += dir.x * a * cos(f);\n s.offset.y += a * sin(f);\n s.offset.z += dir.y * a * cos(f);\n\n // Analytic normal: the partial derivatives of the displacement above.\n s.normal.x -= dir.x * steepness * cos(f);\n s.normal.z -= dir.y * steepness * cos(f);\n s.normal.y -= steepness * sin(f);\n\n s.crest += sin(f) * steepness;\n }\n return s;\n}\n\n/**\n * Second derivatives of the height field: (d2h/dx2, d2h/dz2, d2h/dxdz).\n *\n * How the surface *bends*, which is what decides where reflected light\n * converges. Analytic from the same sum, so it cannot disagree with the shape\n * the vertex stage displaced.\n */\nvec3 gerstnerCurvature(vec2 p, float time, vec2 windDir, float gain) {\n vec3 h = vec3(0.0);\n for (int i = 0; i < GERSTNER_COUNT; i++) {\n vec2 dir = gerstnerDirection(i, windDir);\n float steepness = GERSTNER_WAVES[i].z * gain;\n float k = gerstnerWavenumber(i);\n float f = gerstnerPhase(k, dir, p, time);\n // Second derivative of (steepness/k)*sin(f) along the wave's own bearing.\n float second = -steepness * k * sin(f);\n h.x += second * dir.x * dir.x;\n h.y += second * dir.y * dir.y;\n h.z += second * dir.x * dir.y;\n }\n return h;\n}\n\n\nvoid main() {\n /*\n * Waves must reach nothing by the near sheet's edge, because beyond it lies\n * the flat skirt. A displaced rim meeting an undisplaced skirt would tear the\n * surface open along a visible seam; fading first makes the two meshes agree\n * exactly where they meet. Chebyshev distance, so the fade band follows the\n * rectangular boundary rather than cutting an ellipse inside it.\n *\n * **Measured as a fraction of each axis' own extent**, which is what lets a\n * channel three metres across and twenty-one long fade evenly at both its ends\n * and both its sides. Against one shared extent the narrow axis would be inside\n * the fade band along its whole length and the water would never reach its own\n * bank. For the ocean, whose two extents are equal, this is the same arithmetic\n * with the division moved one step earlier.\n *\n * It is also the honest thing to draw. At a few hundred metres a 1 m swell is\n * well under a pixel, and out on the skirt a float32 phase argument has lost\n * most of its meaningful bits \u2014 a calm horizon is what an ocean actually looks\n * like from that far away.\n */\n vec2 grid = aGrid * uGridSpan;\n /* The same offset the fragment stage fades the rim on, so the two cannot disagree. */\n vGrid = grid;\n vec2 rimFraction = abs(grid) / max(uNearHalf, vec2(1e-4));\n float rimDistance = max(rimFraction.x, rimFraction.y);\n float waveFade = 1.0 - smoothstep(0.35, 0.97, rimDistance);\n\n /*\n * Into the world: across the body, then along it. `right = up x forward` is\n * `(fz, -fx)`, and getting that pair the wrong way round is a sheet mirrored\n * about its own axis, which on a square is invisible and on a channel is a\n * channel across the road instead of along it.\n */\n vec2 laid = vec2(uGridForward.y, -uGridForward.x) * grid.x + uGridForward * grid.y;\n\n vec3 base = vec3(uGridOrigin.x + laid.x, uWaterLevel, uGridOrigin.y + laid.y);\n\n GerstnerSurface sea = gerstnerSurface(base.xz, uTime, uWindDir, waveFade * uWaveGain);\n vec3 pos = base + sea.offset;\n\n vWorldPos = pos;\n vNormal = normalize(sea.normal);\n vCrest = sea.crest;\n vReflectionClip = uReflectionViewProj * vec4(pos, 1.0);\n gl_Position = uViewProj * vec4(pos, 1.0);\n}\n";
|
|
18
|
-
export declare const WATER_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vWorldPos;\nin vec3 vNormal;\nin float vCrest;\nin vec4 vReflectionClip;\nin vec2 vGrid;\n\nuniform vec2 uGridHalf;\nuniform float uFoamGain;\n/**\n * How much of what is beneath it a body of water hides, looked straight down.\n *\n * The *density* of the water rather than a rendering trick: a sea hides its own\n * floor, a fountain shows the tiles at the bottom of it, and a puddle is nearly\n * clear. Only the nadir end is configurable \u2014 at a grazing angle every water\n * surface goes reflective, which is Fresnel and not a property of the liquid.\n */\nuniform float uNadirOpacity;\n/** The whole surface's visibility, 0 to 1. Zero draws nothing at all. */\nuniform float uVisibility;\n/**\n * How much this body mirrors regardless of the angle it is seen at, 0 to 1.\n *\n * Zero is physics: reflection is Fresnel alone, which is right for an ocean seen\n * across its own surface and which means *almost nothing* when you stand over a\n * basin and look down into it \u2014 the angle is steep, the Fresnel term is a few\n * percent, and a reflection nobody can see is the same as no reflection: the\n * character's reflection is technically present and far too faint to perceive.\n *\n * So a body may declare itself more mirror than water. It is a lie about optics\n * and an honest one about what a small pool is *for*: you look into it to see\n * something. The sea keeps zero and behaves exactly as it always has.\n */\nuniform float uMirror;\nuniform vec3 uDeepColor;\nuniform vec3 uShallowColor;\nuniform vec3 uDirectionalDir;\nuniform vec3 uDirectionalColor;\nuniform vec3 uAmbient;\n/**\n * The scene's point lights, which water is entitled to as much as stone is.\n *\n * The surface used to answer to the sun alone, on the reasonable assumption that water\n * is a thing that sits under the sky. It is not: a flooded corridor, a cistern, an\n * indoor pool and a harbour at night are all water lit entirely by lamps, and under\n * those this shaded to flat ambient \u2014 a coloured plane, with the specular glitter that\n * is the single strongest cue that a surface *is* water switched off because no\n * directional source existed to make it. A lantern on a jetty threw nothing.\n *\n * Same slots, same weights and the same two falloff shapes as the surface shader, so a\n * lamp behaves the same way over water as it does over the ground beside it.\n */\n#define MAX_LIGHTS 16\nuniform int uLightCount;\nuniform vec3 uLightPos[MAX_LIGHTS];\nuniform vec3 uLightColor[MAX_LIGHTS];\nuniform float uLightRadius[MAX_LIGHTS];\nuniform float uLightWeight[MAX_LIGHTS];\nuniform int uLightFalloff;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(t) < 1e-4 ? 1e-4 : t;\n float air = 1.0 - exp(-uFogDensity * dist * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\nuniform vec3 uCameraPos;\nuniform sampler2D uReflectionMap;\nuniform int uReflectionEnabled;\nuniform vec2 uReflectionTexelSize;\nuniform int uReflectionFilterTaps;\n\nconst vec2 REFLECTION_FILTER[9] = vec2[9](\n vec2( 0.00, 0.00), vec2(-0.62, -0.31), vec2( 0.54, 0.43),\n vec2(-0.34, 0.76), vec2( 0.78, -0.57), vec2(-0.88, 0.28),\n vec2( 0.22, -0.91), vec2( 0.91, 0.08), vec2(-0.18, 0.94)\n);\n// Preserve small moving silhouettes such as a character while still spreading\n// the surrounding taps enough to make the target read as rough water.\nconst float REFLECTION_FILTER_WEIGHT[9] = float[9](\n 0.28, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09\n);\n\nout vec4 outColor;\n\nvoid main() {\n vec3 n = normalize(vNormal);\n vec3 view = normalize(uCameraPos - vWorldPos);\n\n // Fresnel: water is nearly a mirror at grazing angles and clear straight down.\n // abs keeps the underside physically continuous with the top instead of\n // turning the entire surface into an opaque mirror below the waterline.\n float facing = clamp(abs(dot(n, view)), 0.0, 1.0);\n float fresnel = 0.02 + 0.98 * pow(1.0 - facing, 5.0);\n\n vec3 atmosphereColor = mediumColor();\n\n vec3 col = mix(uDeepColor, uShallowColor, clamp(vCrest * 1.4 + 0.4, 0.0, 1.0));\n vec3 incident = uAmbient +\n uDirectionalColor * max(dot(n, uDirectionalDir), 0.0) * 0.6;\n\n // Specular glitter along the crests \u2014 the strongest single cue that a surface\n // is water rather than a coloured plane.\n vec3 halfVec = normalize(uDirectionalDir + view);\n float spec = pow(max(dot(n, halfVec), 0.0), 220.0);\n vec3 highlight = uDirectionalColor * spec * 1.6;\n\n /*\n * And the same from every lamp in range: a light over water lights it, and glitters\n * on it. Indoors this is the whole of the surface's lighting.\n *\n * The exponent is the sun's, so a crest picks a lamp out as tightly as it picks out\n * the sky \u2014 which is what makes a strip light read as a *streak* across a pool rather\n * than a broad sheen. Weighted by presence like every other consumer of these slots,\n * so a light entering the set fades in instead of appearing.\n */\n for (int i = 0; i < MAX_LIGHTS; i++) {\n if (i >= uLightCount) break;\n vec3 toLight = uLightPos[i] - vWorldPos;\n float lightDist = length(toLight);\n float falloff;\n if (uLightFalloff == 1) {\n float window = clamp(1.0 - pow(lightDist / max(uLightRadius[i], 1e-4), 4.0), 0.0, 1.0);\n falloff = window * window / max(lightDist * lightDist, 0.01);\n } else {\n falloff = clamp(1.0 - lightDist / max(uLightRadius[i], 1e-4), 0.0, 1.0);\n }\n if (falloff <= 0.0) continue;\n vec3 lightDir = toLight / max(lightDist, 1e-4);\n float shaped = falloff * uLightWeight[i];\n incident += uLightColor[i] * max(dot(n, lightDir), 0.0) * shaped * 0.6;\n vec3 lampHalf = normalize(lightDir + view);\n highlight += uLightColor[i] * pow(max(dot(n, lampHalf), 0.0), 220.0) * shaped * 1.6;\n }\n\n col *= incident;\n col += highlight;\n\n // Foam where crests peak \u2014 and only when the wind is actually breaking them.\n float foam = smoothstep(0.55, 0.85, vCrest) * uFoamGain;\n col = mix(col, vec3(0.92, 0.96, 1.0), foam * 0.5);\n\n float dist = distance(vWorldPos, uCameraPos);\n float fog = mediumFog(dist, vWorldPos.y);\n col = mix(col, atmosphereColor, fog);\n\n // Transparency is angle-dependent, like real water: looking down you see\n // straight through to the seabed, at grazing angles it turns reflective.\n // This is computed before reflection because straight-alpha compositing must\n // not attenuate reflected radiance a second time below.\n float airOpacity = mix(uNadirOpacity, 0.96, fresnel);\n float underwaterOpacity = mix(uNadirOpacity * 0.55, 0.88, fresnel);\n float opacity = mix(airOpacity, underwaterOpacity, uUnderwaterFactor);\n opacity = mix(opacity, 0.96, foam * 0.7);\n\n // Project the displaced surface into the mirrored camera. The Gerstner\n // displacement supplies broad distortion; the analytic normal adds the\n // smaller ripples that make reflected silhouettes move with each crest.\n vec3 reflected = atmosphereColor;\n float reflectionValid = 0.0;\n if (uReflectionEnabled != 0 && vReflectionClip.w > 0.0) {\n vec2 reflectionUv = vReflectionClip.xy / vReflectionClip.w * 0.5 + 0.5;\n reflectionUv += n.xz * mix(0.008, 0.022, fresnel);\n vec2 border = min(reflectionUv, 1.0 - reflectionUv);\n reflectionValid = smoothstep(0.0, 0.025, min(border.x, border.y));\n // A planar target is optically sharp, which is correct for a mirror but\n // wrong for a moving water surface. Broaden it over a Poisson footprint\n // that grows gently with viewing distance; normal displacement above\n // still makes every crest steer that softened image independently.\n float blurPixels = 1.35 + min(dist * 0.022, 3.5);\n vec3 sceneReflection = vec3(0.0);\n float filterWeight = 0.0;\n for (int i = 0; i < 9; i++) {\n if (i >= uReflectionFilterTaps) break;\n vec2 sampleUv = reflectionUv +\n REFLECTION_FILTER[i] * uReflectionTexelSize * blurPixels;\n float weight = REFLECTION_FILTER_WEIGHT[i];\n /*\n * `textureLod` rather than `texture`, by the rule `AGENTS.md` records on 2026-08-07:\n * this loop is reached through a branch on `vReflectionClip`, which is not uniform, and\n * an implicit derivative there is undefined. **WGSL does not merely leave it undefined,\n * it refuses to compile the module**: *\"'textureSample' must only be called from uniform\n * control flow\"*, with the note that reading `vReflectionClip` may be non-uniform.\n *\n * Bit-identical on WebGL2: `planarReflection.ts` allocates one level and filters\n * `LINEAR`, so there is no mip to select and level zero is the only thing `texture`\n * could have read.\n */\n sceneReflection +=\n textureLod(uReflectionMap, clamp(sampleUv, 0.0, 1.0), 0.0).rgb * weight;\n filterWeight += weight;\n }\n sceneReflection /= max(filterWeight, 0.001);\n reflected = mix(atmosphereColor, sceneReflection, reflectionValid);\n }\n // The atmosphere-colour fallback preserves the old sky reflection when a\n // quality profile disables the extra scene pass. Foam scatters rather than\n // mirroring, so it masks the reflection at breaking crests.\n float sceneWeight = float(uReflectionEnabled) * reflectionValid;\n float reflectionGain = mix(0.75, 0.94, sceneWeight);\n // Angle-honest by default, and a mirror where a body says so. See uMirror above.\n float reflectivity = mix(fresnel, 1.0, uMirror);\n float reflectedRadiance = min(\n reflectivity * reflectionGain * (1.0 - foam * 0.8),\n opacity\n );\n // With SRC_ALPHA blending, mixing by Fresnel directly would multiply the\n // reflection by opacity again. Divide it out here so the framebuffer gets\n // the intended reflected fraction while the remainder stays transmissive.\n float reflectionWeight = reflectedRadiance / max(opacity, 0.001);\n col = mix(col, reflected, reflectionWeight);\n\n /*\n * Dissolve the sheet at its own boundary.\n *\n * Fading on fog alone made the horizon a function of the daily palette: a\n * clear day never reached the threshold, so the grid's rim \u2014 corner and all \u2014\n * became the skyline. Fading on the grid coordinate cannot fail that way,\n * because alpha reaches zero at the last row of vertices by construction.\n * Chebyshev distance because the sheet is a rectangle, so the fade band\n * follows its four edges instead of cutting an ellipse out of it \u2014 and it is\n * measured per axis, or a channel's narrow sides would be one continuous fade\n * and it would have no visible edge at all.\n */\n vec2 rimFraction = abs(vGrid) / max(uGridHalf, vec2(0.001));\n float rimT = max(rimFraction.x, rimFraction.y);\n float rim = 1.0 - smoothstep(0.9, 1.0, rimT);\n // Fog still helps where it is thick, it just is no longer load-bearing.\n float edge = 1.0 - smoothstep(0.82, 1.0, fog);\n outColor = vec4(col, opacity * edge * rim * uVisibility);\n}\n";
|
|
18
|
+
export declare const WATER_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vWorldPos;\nin vec3 vNormal;\nin float vCrest;\nin vec4 vReflectionClip;\nin vec2 vGrid;\n\nuniform vec2 uGridHalf;\nuniform float uFoamGain;\n/**\n * How much of what is beneath it a body of water hides, looked straight down.\n *\n * The *density* of the water rather than a rendering trick: a sea hides its own\n * floor, a fountain shows the tiles at the bottom of it, and a puddle is nearly\n * clear. Only the nadir end is configurable \u2014 at a grazing angle every water\n * surface goes reflective, which is Fresnel and not a property of the liquid.\n */\nuniform float uNadirOpacity;\n/** The whole surface's visibility, 0 to 1. Zero draws nothing at all. */\nuniform float uVisibility;\n/**\n * How much this body mirrors regardless of the angle it is seen at, 0 to 1.\n *\n * Zero is physics: reflection is Fresnel alone, which is right for an ocean seen\n * across its own surface and which means *almost nothing* when you stand over a\n * basin and look down into it \u2014 the angle is steep, the Fresnel term is a few\n * percent, and a reflection nobody can see is the same as no reflection: the\n * character's reflection is technically present and far too faint to perceive.\n *\n * So a body may declare itself more mirror than water. It is a lie about optics\n * and an honest one about what a small pool is *for*: you look into it to see\n * something. The sea keeps zero and behaves exactly as it always has.\n */\nuniform float uMirror;\nuniform vec3 uDeepColor;\nuniform vec3 uShallowColor;\nuniform vec3 uDirectionalDir;\nuniform vec3 uDirectionalColor;\nuniform vec3 uAmbient;\n/**\n * The scene's point lights, which water is entitled to as much as stone is.\n *\n * The surface used to answer to the sun alone, on the reasonable assumption that water\n * is a thing that sits under the sky. It is not: a flooded corridor, a cistern, an\n * indoor pool and a harbour at night are all water lit entirely by lamps, and under\n * those this shaded to flat ambient \u2014 a coloured plane, with the specular glitter that\n * is the single strongest cue that a surface *is* water switched off because no\n * directional source existed to make it. A lantern on a jetty threw nothing.\n *\n * Same slots, same weights and the surface shader's falloffs, a light's own exponent among\n * them, so a lamp reaches as far over water as over the ground beside it. **What it gives up**:\n * a spot's cone, which water does not read, so a spot lights the water around it as a lamp\n * would; and the smooth falloff's square, which the surface applies and this has never, so under\n * that falloff a lamp's pool on water is the wider of the two.\n */\n#define MAX_LIGHTS 16\nuniform int uLightCount;\nuniform vec3 uLightPos[MAX_LIGHTS];\nuniform vec3 uLightColor[MAX_LIGHTS];\nuniform float uLightRadius[MAX_LIGHTS];\nuniform float uLightWeight[MAX_LIGHTS];\nuniform int uLightFalloff;\n/* The surface shader's cone record, of which water reads z alone: a light's own falloff exponent. */\nuniform vec4 uLightCone[MAX_LIGHTS];\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(t) < 1e-4 ? 1e-4 : t;\n float air = 1.0 - exp(-uFogDensity * dist * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\nuniform vec3 uCameraPos;\nuniform sampler2D uReflectionMap;\nuniform int uReflectionEnabled;\nuniform vec2 uReflectionTexelSize;\nuniform int uReflectionFilterTaps;\n\nconst vec2 REFLECTION_FILTER[9] = vec2[9](\n vec2( 0.00, 0.00), vec2(-0.62, -0.31), vec2( 0.54, 0.43),\n vec2(-0.34, 0.76), vec2( 0.78, -0.57), vec2(-0.88, 0.28),\n vec2( 0.22, -0.91), vec2( 0.91, 0.08), vec2(-0.18, 0.94)\n);\n// Preserve small moving silhouettes such as a character while still spreading\n// the surrounding taps enough to make the target read as rough water.\nconst float REFLECTION_FILTER_WEIGHT[9] = float[9](\n 0.28, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09\n);\n\nout vec4 outColor;\n\nvoid main() {\n vec3 n = normalize(vNormal);\n vec3 view = normalize(uCameraPos - vWorldPos);\n\n // Fresnel: water is nearly a mirror at grazing angles and clear straight down.\n // abs keeps the underside physically continuous with the top instead of\n // turning the entire surface into an opaque mirror below the waterline.\n float facing = clamp(abs(dot(n, view)), 0.0, 1.0);\n float fresnel = 0.02 + 0.98 * pow(1.0 - facing, 5.0);\n\n vec3 atmosphereColor = mediumColor();\n\n vec3 col = mix(uDeepColor, uShallowColor, clamp(vCrest * 1.4 + 0.4, 0.0, 1.0));\n vec3 incident = uAmbient +\n uDirectionalColor * max(dot(n, uDirectionalDir), 0.0) * 0.6;\n\n // Specular glitter along the crests \u2014 the strongest single cue that a surface\n // is water rather than a coloured plane.\n vec3 halfVec = normalize(uDirectionalDir + view);\n float spec = pow(max(dot(n, halfVec), 0.0), 220.0);\n vec3 highlight = uDirectionalColor * spec * 1.6;\n\n /*\n * And the same from every lamp in range: a light over water lights it, and glitters\n * on it. Indoors this is the whole of the surface's lighting.\n *\n * The exponent is the sun's, so a crest picks a lamp out as tightly as it picks out\n * the sky \u2014 which is what makes a strip light read as a *streak* across a pool rather\n * than a broad sheen. Weighted by presence like every other consumer of these slots,\n * so a light entering the set fades in instead of appearing.\n */\n for (int i = 0; i < MAX_LIGHTS; i++) {\n if (i >= uLightCount) break;\n vec3 toLight = uLightPos[i] - vWorldPos;\n float lightDist = length(toLight);\n float falloff;\n float exponent = uLightCone[i].z;\n if (exponent > 0.0) {\n float reach = lightDist / max(uLightRadius[i], 1e-4);\n falloff = pow(clamp(1.0 - reach * reach, 0.0, 1.0), exponent);\n } else if (uLightFalloff == 1) {\n float window = clamp(1.0 - pow(lightDist / max(uLightRadius[i], 1e-4), 4.0), 0.0, 1.0);\n falloff = window * window / max(lightDist * lightDist, 0.01);\n } else {\n falloff = clamp(1.0 - lightDist / max(uLightRadius[i], 1e-4), 0.0, 1.0);\n }\n if (falloff <= 0.0) continue;\n vec3 lightDir = toLight / max(lightDist, 1e-4);\n float shaped = falloff * uLightWeight[i];\n incident += uLightColor[i] * max(dot(n, lightDir), 0.0) * shaped * 0.6;\n vec3 lampHalf = normalize(lightDir + view);\n highlight += uLightColor[i] * pow(max(dot(n, lampHalf), 0.0), 220.0) * shaped * 1.6;\n }\n\n col *= incident;\n col += highlight;\n\n // Foam where crests peak \u2014 and only when the wind is actually breaking them.\n float foam = smoothstep(0.55, 0.85, vCrest) * uFoamGain;\n col = mix(col, vec3(0.92, 0.96, 1.0), foam * 0.5);\n\n float dist = distance(vWorldPos, uCameraPos);\n float fog = mediumFog(dist, vWorldPos.y);\n col = mix(col, atmosphereColor, fog);\n\n // Transparency is angle-dependent, like real water: looking down you see\n // straight through to the seabed, at grazing angles it turns reflective.\n // This is computed before reflection because straight-alpha compositing must\n // not attenuate reflected radiance a second time below.\n float airOpacity = mix(uNadirOpacity, 0.96, fresnel);\n float underwaterOpacity = mix(uNadirOpacity * 0.55, 0.88, fresnel);\n float opacity = mix(airOpacity, underwaterOpacity, uUnderwaterFactor);\n opacity = mix(opacity, 0.96, foam * 0.7);\n\n // Project the displaced surface into the mirrored camera. The Gerstner\n // displacement supplies broad distortion; the analytic normal adds the\n // smaller ripples that make reflected silhouettes move with each crest.\n vec3 reflected = atmosphereColor;\n float reflectionValid = 0.0;\n if (uReflectionEnabled != 0 && vReflectionClip.w > 0.0) {\n vec2 reflectionUv = vReflectionClip.xy / vReflectionClip.w * 0.5 + 0.5;\n reflectionUv += n.xz * mix(0.008, 0.022, fresnel);\n vec2 border = min(reflectionUv, 1.0 - reflectionUv);\n reflectionValid = smoothstep(0.0, 0.025, min(border.x, border.y));\n // A planar target is optically sharp, which is correct for a mirror but\n // wrong for a moving water surface. Broaden it over a Poisson footprint\n // that grows gently with viewing distance; normal displacement above\n // still makes every crest steer that softened image independently.\n float blurPixels = 1.35 + min(dist * 0.022, 3.5);\n vec3 sceneReflection = vec3(0.0);\n float filterWeight = 0.0;\n for (int i = 0; i < 9; i++) {\n if (i >= uReflectionFilterTaps) break;\n vec2 sampleUv = reflectionUv +\n REFLECTION_FILTER[i] * uReflectionTexelSize * blurPixels;\n float weight = REFLECTION_FILTER_WEIGHT[i];\n /*\n * `textureLod` rather than `texture`, by the rule `AGENTS.md` records on 2026-08-07:\n * this loop is reached through a branch on `vReflectionClip`, which is not uniform, and\n * an implicit derivative there is undefined. **WGSL does not merely leave it undefined,\n * it refuses to compile the module**: *\"'textureSample' must only be called from uniform\n * control flow\"*, with the note that reading `vReflectionClip` may be non-uniform.\n *\n * Bit-identical on WebGL2: `planarReflection.ts` allocates one level and filters\n * `LINEAR`, so there is no mip to select and level zero is the only thing `texture`\n * could have read.\n */\n sceneReflection +=\n textureLod(uReflectionMap, clamp(sampleUv, 0.0, 1.0), 0.0).rgb * weight;\n filterWeight += weight;\n }\n sceneReflection /= max(filterWeight, 0.001);\n reflected = mix(atmosphereColor, sceneReflection, reflectionValid);\n }\n // The atmosphere-colour fallback preserves the old sky reflection when a\n // quality profile disables the extra scene pass. Foam scatters rather than\n // mirroring, so it masks the reflection at breaking crests.\n float sceneWeight = float(uReflectionEnabled) * reflectionValid;\n float reflectionGain = mix(0.75, 0.94, sceneWeight);\n // Angle-honest by default, and a mirror where a body says so. See uMirror above.\n float reflectivity = mix(fresnel, 1.0, uMirror);\n float reflectedRadiance = min(\n reflectivity * reflectionGain * (1.0 - foam * 0.8),\n opacity\n );\n // With SRC_ALPHA blending, mixing by Fresnel directly would multiply the\n // reflection by opacity again. Divide it out here so the framebuffer gets\n // the intended reflected fraction while the remainder stays transmissive.\n float reflectionWeight = reflectedRadiance / max(opacity, 0.001);\n col = mix(col, reflected, reflectionWeight);\n\n /*\n * Dissolve the sheet at its own boundary.\n *\n * Fading on fog alone made the horizon a function of the daily palette: a\n * clear day never reached the threshold, so the grid's rim \u2014 corner and all \u2014\n * became the skyline. Fading on the grid coordinate cannot fail that way,\n * because alpha reaches zero at the last row of vertices by construction.\n * Chebyshev distance because the sheet is a rectangle, so the fade band\n * follows its four edges instead of cutting an ellipse out of it \u2014 and it is\n * measured per axis, or a channel's narrow sides would be one continuous fade\n * and it would have no visible edge at all.\n */\n vec2 rimFraction = abs(vGrid) / max(uGridHalf, vec2(0.001));\n float rimT = max(rimFraction.x, rimFraction.y);\n float rim = 1.0 - smoothstep(0.9, 1.0, rimT);\n // Fog still helps where it is thick, it just is no longer load-bearing.\n float edge = 1.0 - smoothstep(0.82, 1.0, fog);\n outColor = vec4(col, opacity * edge * rim * uVisibility);\n}\n";
|
|
@@ -175,8 +175,11 @@ uniform vec3 uAmbient;
|
|
|
175
175
|
* is the single strongest cue that a surface *is* water switched off because no
|
|
176
176
|
* directional source existed to make it. A lantern on a jetty threw nothing.
|
|
177
177
|
*
|
|
178
|
-
* Same slots, same weights and the
|
|
179
|
-
* lamp
|
|
178
|
+
* Same slots, same weights and the surface shader's falloffs, a light's own exponent among
|
|
179
|
+
* them, so a lamp reaches as far over water as over the ground beside it. **What it gives up**:
|
|
180
|
+
* a spot's cone, which water does not read, so a spot lights the water around it as a lamp
|
|
181
|
+
* would; and the smooth falloff's square, which the surface applies and this has never, so under
|
|
182
|
+
* that falloff a lamp's pool on water is the wider of the two.
|
|
180
183
|
*/
|
|
181
184
|
#define MAX_LIGHTS ${MAX_POINT_LIGHTS}
|
|
182
185
|
uniform int uLightCount;
|
|
@@ -185,6 +188,8 @@ uniform vec3 uLightColor[MAX_LIGHTS];
|
|
|
185
188
|
uniform float uLightRadius[MAX_LIGHTS];
|
|
186
189
|
uniform float uLightWeight[MAX_LIGHTS];
|
|
187
190
|
uniform int uLightFalloff;
|
|
191
|
+
/* The surface shader's cone record, of which water reads z alone: a light's own falloff exponent. */
|
|
192
|
+
uniform vec4 uLightCone[MAX_LIGHTS];
|
|
188
193
|
${FOG_GLSL}
|
|
189
194
|
uniform vec3 uCameraPos;
|
|
190
195
|
uniform sampler2D uReflectionMap;
|
|
@@ -241,7 +246,11 @@ void main() {
|
|
|
241
246
|
vec3 toLight = uLightPos[i] - vWorldPos;
|
|
242
247
|
float lightDist = length(toLight);
|
|
243
248
|
float falloff;
|
|
244
|
-
|
|
249
|
+
float exponent = uLightCone[i].z;
|
|
250
|
+
if (exponent > 0.0) {
|
|
251
|
+
float reach = lightDist / max(uLightRadius[i], 1e-4);
|
|
252
|
+
falloff = pow(clamp(1.0 - reach * reach, 0.0, 1.0), exponent);
|
|
253
|
+
} else if (uLightFalloff == 1) {
|
|
245
254
|
float window = clamp(1.0 - pow(lightDist / max(uLightRadius[i], 1e-4), 4.0), 0.0, 1.0);
|
|
246
255
|
falloff = window * window / max(lightDist * lightDist, 0.01);
|
|
247
256
|
} else {
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* neighbours' `N·L` as well as its own: the clamped cosine averaged around a ring of the surface's
|
|
8
8
|
* curvature, weighted by the diffusion profile at each neighbour's distance. The profile is Burley's
|
|
9
9
|
* normalised diffusion, one per channel, at a scatter distance `d` — the model's `radius` times its
|
|
10
|
-
* scatter colour
|
|
10
|
+
* scatter colour, over `SKIN_MEAN_EXIT`, since `radius` is how far light travels and the profile
|
|
11
|
+
* carries it two and a half `d` — projected onto the ring's plane, as a cylinder's cross-section
|
|
12
|
+
* sees it. The
|
|
11
13
|
* result depends on `N·L` and on one number per channel, `k = d × curvature`, and at `k = 0` it is
|
|
12
14
|
* Lambert's.
|
|
13
15
|
*
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* neighbours' `N·L` as well as its own: the clamped cosine averaged around a ring of the surface's
|
|
8
8
|
* curvature, weighted by the diffusion profile at each neighbour's distance. The profile is Burley's
|
|
9
9
|
* normalised diffusion, one per channel, at a scatter distance `d` — the model's `radius` times its
|
|
10
|
-
* scatter colour
|
|
10
|
+
* scatter colour, over `SKIN_MEAN_EXIT`, since `radius` is how far light travels and the profile
|
|
11
|
+
* carries it two and a half `d` — projected onto the ring's plane, as a cylinder's cross-section
|
|
12
|
+
* sees it. The
|
|
11
13
|
* result depends on `N·L` and on one number per channel, `k = d × curvature`, and at `k = 0` it is
|
|
12
14
|
* Lambert's.
|
|
13
15
|
*
|
package/dist/render/ssrPass.js
CHANGED
|
@@ -133,6 +133,7 @@ export class SsrPass {
|
|
|
133
133
|
gl.uniform3fv(u['uSsrAxis'] ?? null, surface.axis);
|
|
134
134
|
gl.uniform3fv(u['uSsrTint'] ?? null, surface.tint);
|
|
135
135
|
gl.uniform1f(u['uSsrStrength'] ?? null, surface.strength);
|
|
136
|
+
gl.uniform2f(u['uSsrFresnel'] ?? null, surface.fresnel ? 1 : 0, surface.roughness);
|
|
136
137
|
gl.uniform1f(u['uSsrFacingCos'] ?? null, surface.facingCos);
|
|
137
138
|
gl.uniform1f(u['uSsrReach'] ?? null, surface.reachM);
|
|
138
139
|
gl.uniform1f(u['uSsrThickness'] ?? null, surface.thicknessM);
|
|
@@ -50,7 +50,12 @@ export interface SkinModel {
|
|
|
50
50
|
readonly kind: 'skin';
|
|
51
51
|
/** The colour light keeps travelling beneath the surface — the mean free path, as a colour. */
|
|
52
52
|
readonly scatterColor: Vec3;
|
|
53
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Metres: how far light travels beneath it, on average, before it leaves — from where it entered
|
|
55
|
+
* to where it leaves, for a channel whose share of `scatterColor` is 1. Burley's profile, which
|
|
56
|
+
* the lit stage and the screen-space blur both scatter by, does that at a distance `d` two fifths
|
|
57
|
+
* of it, and carries a tail of light out to about three and a half times it.
|
|
58
|
+
*/
|
|
54
59
|
readonly radius: number;
|
|
55
60
|
/** 0 to 1: how much a light behind a thin part shows through it. */
|
|
56
61
|
readonly transmission: number;
|
|
@@ -104,7 +109,21 @@ export declare function hairModel(options?: {
|
|
|
104
109
|
scatter?: number;
|
|
105
110
|
backlit?: number;
|
|
106
111
|
}): HairModel;
|
|
107
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* How far Burley's normalised diffusion carries light on average, in units of its own distance
|
|
114
|
+
* `d`: its radial density `(e^(−r/d) + e^(−r/3d)) / 4d` has the mean `(d² + 9d²) / 4d = 2.5 d`. So a
|
|
115
|
+
* skin whose light travels `radius` scatters by a profile at `d = radius / 2.5`.
|
|
116
|
+
*
|
|
117
|
+
* **Until 4.8.7 `radius` was `d` itself**, so light travelled two and a half times as far as the
|
|
118
|
+
* model said, and the screen-space blur — whose taps reach nine `d` — spread a face at arm's length
|
|
119
|
+
* over sixty pixels: a nose, lips and brows washed into one blur where a centimetre covers a few.
|
|
120
|
+
* The lit stage's pre-integrated fit, transmission and penumbra take the same `d`, so the two paths
|
|
121
|
+
* still agree with each other.
|
|
122
|
+
*/
|
|
123
|
+
export declare const SKIN_MEAN_EXIT = 2.5;
|
|
124
|
+
/** Burley's distance `d` for one channel of a skin, in metres: see `SKIN_MEAN_EXIT`. */
|
|
125
|
+
export declare function skinProfileDistance(model: SkinModel, channel: 0 | 1 | 2): number;
|
|
126
|
+
/** Skin. The defaults are a fair skin's: red light leaving a little over a centimetre away. */
|
|
108
127
|
export declare function skinModel(options?: {
|
|
109
128
|
scatterColor?: Vec3;
|
|
110
129
|
radius?: number;
|
|
@@ -130,16 +149,18 @@ export declare function lightmapModel(options?: {
|
|
|
130
149
|
}): LightmapModel;
|
|
131
150
|
/**
|
|
132
151
|
* A model's numbers into `out` — eight floats, `uModelParams[0]` then `[1]` — and whether a model
|
|
133
|
-
* map is bound into the last.
|
|
134
|
-
*
|
|
152
|
+
* map is bound into the last. An eye's axis goes in with the descriptor's own until the draw turns
|
|
153
|
+
* it: see `eyeAxisInWorld`. `physical` is `SurfaceMaterial.physicalSpecular`, in the lane the
|
|
154
|
+
* models that read it leave free; skin, hair and the eye shade their own highlights and never do.
|
|
135
155
|
*
|
|
136
|
-
* -
|
|
156
|
+
* - standard (null): − · −, −, physical, −
|
|
157
|
+
* - anisotropic: strength, cos and sin of the rotation · −, −, physical, map
|
|
137
158
|
* - hair: shift, scatter, backlit, − · −, −, −, map
|
|
138
|
-
* - skin: the scatter colour, radius · transmission, profile, −, map
|
|
159
|
+
* - skin: the scatter colour, radius / `SKIN_MEAN_EXIT` · transmission, profile, −, map
|
|
139
160
|
* - eye: iris radius, iris depth, ior, cornea roughness · the axis, map
|
|
140
|
-
* - lightmap: the region's scale and bias · −, −,
|
|
161
|
+
* - lightmap: the region's scale and bias · −, −, physical, map
|
|
141
162
|
*/
|
|
142
|
-
export declare function packModel(model: SurfaceModel | null, mapped: boolean, out: Float32Array): void;
|
|
163
|
+
export declare function packModel(model: SurfaceModel | null, mapped: boolean, out: Float32Array, physical?: boolean): void;
|
|
143
164
|
/**
|
|
144
165
|
* An eye's axis in the world for one draw, into `out[at…at+2]`: the descriptor's mesh-local axis
|
|
145
166
|
* turned by its joint's palette entry, where it names one and a palette is set, and then by the
|
|
@@ -46,7 +46,23 @@ export function hairModel(options = {}) {
|
|
|
46
46
|
backlit: check('hairModel', 'backlit', options.backlit ?? 1, 0, 4),
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* How far Burley's normalised diffusion carries light on average, in units of its own distance
|
|
51
|
+
* `d`: its radial density `(e^(−r/d) + e^(−r/3d)) / 4d` has the mean `(d² + 9d²) / 4d = 2.5 d`. So a
|
|
52
|
+
* skin whose light travels `radius` scatters by a profile at `d = radius / 2.5`.
|
|
53
|
+
*
|
|
54
|
+
* **Until 4.8.7 `radius` was `d` itself**, so light travelled two and a half times as far as the
|
|
55
|
+
* model said, and the screen-space blur — whose taps reach nine `d` — spread a face at arm's length
|
|
56
|
+
* over sixty pixels: a nose, lips and brows washed into one blur where a centimetre covers a few.
|
|
57
|
+
* The lit stage's pre-integrated fit, transmission and penumbra take the same `d`, so the two paths
|
|
58
|
+
* still agree with each other.
|
|
59
|
+
*/
|
|
60
|
+
export const SKIN_MEAN_EXIT = 2.5;
|
|
61
|
+
/** Burley's distance `d` for one channel of a skin, in metres: see `SKIN_MEAN_EXIT`. */
|
|
62
|
+
export function skinProfileDistance(model, channel) {
|
|
63
|
+
return (model.scatterColor[channel] * model.radius) / SKIN_MEAN_EXIT;
|
|
64
|
+
}
|
|
65
|
+
/** Skin. The defaults are a fair skin's: red light leaving a little over a centimetre away. */
|
|
50
66
|
export function skinModel(options = {}) {
|
|
51
67
|
const color = options.scatterColor ?? [0.85, 0.35, 0.22];
|
|
52
68
|
for (let c = 0; c < 3; c++)
|
|
@@ -92,25 +108,31 @@ export function lightmapModel(options = {}) {
|
|
|
92
108
|
}
|
|
93
109
|
/**
|
|
94
110
|
* A model's numbers into `out` — eight floats, `uModelParams[0]` then `[1]` — and whether a model
|
|
95
|
-
* map is bound into the last.
|
|
96
|
-
*
|
|
111
|
+
* map is bound into the last. An eye's axis goes in with the descriptor's own until the draw turns
|
|
112
|
+
* it: see `eyeAxisInWorld`. `physical` is `SurfaceMaterial.physicalSpecular`, in the lane the
|
|
113
|
+
* models that read it leave free; skin, hair and the eye shade their own highlights and never do.
|
|
97
114
|
*
|
|
98
|
-
* -
|
|
115
|
+
* - standard (null): − · −, −, physical, −
|
|
116
|
+
* - anisotropic: strength, cos and sin of the rotation · −, −, physical, map
|
|
99
117
|
* - hair: shift, scatter, backlit, − · −, −, −, map
|
|
100
|
-
* - skin: the scatter colour, radius · transmission, profile, −, map
|
|
118
|
+
* - skin: the scatter colour, radius / `SKIN_MEAN_EXIT` · transmission, profile, −, map
|
|
101
119
|
* - eye: iris radius, iris depth, ior, cornea roughness · the axis, map
|
|
102
|
-
* - lightmap: the region's scale and bias · −, −,
|
|
120
|
+
* - lightmap: the region's scale and bias · −, −, physical, map
|
|
103
121
|
*/
|
|
104
|
-
export function packModel(model, mapped, out) {
|
|
122
|
+
export function packModel(model, mapped, out, physical = false) {
|
|
105
123
|
out.fill(0);
|
|
106
|
-
|
|
124
|
+
const highlight = physical ? 1 : 0;
|
|
125
|
+
if (model === null) {
|
|
126
|
+
out[6] = highlight;
|
|
107
127
|
return;
|
|
128
|
+
}
|
|
108
129
|
out[7] = mapped ? 1 : 0;
|
|
109
130
|
switch (model.kind) {
|
|
110
131
|
case 'anisotropic':
|
|
111
132
|
out[0] = model.strength;
|
|
112
133
|
out[1] = Math.cos(model.rotation);
|
|
113
134
|
out[2] = Math.sin(model.rotation);
|
|
135
|
+
out[6] = highlight;
|
|
114
136
|
return;
|
|
115
137
|
case 'hair':
|
|
116
138
|
out[0] = model.shift;
|
|
@@ -121,7 +143,8 @@ export function packModel(model, mapped, out) {
|
|
|
121
143
|
out[0] = model.scatterColor[0];
|
|
122
144
|
out[1] = model.scatterColor[1];
|
|
123
145
|
out[2] = model.scatterColor[2];
|
|
124
|
-
|
|
146
|
+
/* Burley's distance for a channel of share 1, which the shader scales by each share. */
|
|
147
|
+
out[3] = model.radius / SKIN_MEAN_EXIT;
|
|
125
148
|
out[4] = model.transmission;
|
|
126
149
|
out[5] = model.profile;
|
|
127
150
|
return;
|
|
@@ -139,6 +162,7 @@ export function packModel(model, mapped, out) {
|
|
|
139
162
|
out[1] = model.region[1];
|
|
140
163
|
out[2] = model.region[2];
|
|
141
164
|
out[3] = model.region[3];
|
|
165
|
+
out[6] = highlight;
|
|
142
166
|
return;
|
|
143
167
|
}
|
|
144
168
|
}
|
|
@@ -13,6 +13,7 @@ import type { CompressedTextureFormat, SurfaceSource } from './compressedSource.
|
|
|
13
13
|
import type { SurfaceLayerEffect } from './surfaceEffects.ts';
|
|
14
14
|
import type { CutoutMode } from './cutoutDither.ts';
|
|
15
15
|
import type { LightmapTexels } from './lightmap.ts';
|
|
16
|
+
import type { SceneCaptureTexels } from './sceneCapture.ts';
|
|
16
17
|
/**
|
|
17
18
|
* How a texture behaves past its edges and between its texels.
|
|
18
19
|
*
|
|
@@ -161,6 +162,24 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
|
|
|
161
162
|
* that is not a colour.
|
|
162
163
|
*/
|
|
163
164
|
modelMap?: Texture | null;
|
|
165
|
+
/**
|
|
166
|
+
* Whether a lamp's and the sun's highlight on this surface is GGX's own, `π · D · Vis · F · N·L`,
|
|
167
|
+
* as Unreal and this engine's skin and eye shade it, rather than the engine's lobe scaled to a
|
|
168
|
+
* peak of one. **False by default**, every surface as it was.
|
|
169
|
+
*
|
|
170
|
+
* The peak-normalised lobe is a look control: the specular attribute says how strong a highlight
|
|
171
|
+
* is and roughness how wide, apart. Physical, they are entangled as they are in a real surface —
|
|
172
|
+
* the peak is `1 / (4 α²)` of the light head-on, so a polished surface's highlight is many times
|
|
173
|
+
* the look's and a rough one's lower and broader — and the specular attribute is read as the
|
|
174
|
+
* reflectance at normal incidence, F0: Unreal's `0.08 × Specular`, 0.04 at its default. Fresnel
|
|
175
|
+
* then brightens every surface toward grazing, not only a metal. A rectangle's highlight is the
|
|
176
|
+
* lobe integrated over it with the attribute as F0 already, so it is unchanged either way.
|
|
177
|
+
*
|
|
178
|
+
* On the standard, lightmap and anisotropic models. Skin and the eye are physical already; hair
|
|
179
|
+
* keeps its own three lobes. **What it gives up**: the look's independence of strength and width,
|
|
180
|
+
* and an 8-bit frame clips the brighter peak without `hdrScene`.
|
|
181
|
+
*/
|
|
182
|
+
physicalSpecular?: boolean;
|
|
164
183
|
}
|
|
165
184
|
export interface SurfaceTextureOptions {
|
|
166
185
|
/**
|
|
@@ -262,7 +281,7 @@ export declare class SurfaceTexture {
|
|
|
262
281
|
* Uploaded unflipped either way; see the constructor body for why. Or blocks, uploaded as they are
|
|
263
282
|
* where `compressed` — the formats this context samples — has theirs; see `compressedSource.ts`.
|
|
264
283
|
*/
|
|
265
|
-
constructor(gl: WebGL2RenderingContext, source: SurfaceSource | readonly SurfaceSource[] | LightmapTexels, options?: SurfaceTextureOptions, compressed?: readonly CompressedTextureFormat[]);
|
|
284
|
+
constructor(gl: WebGL2RenderingContext, source: SurfaceSource | readonly SurfaceSource[] | LightmapTexels | SceneCaptureTexels, options?: SurfaceTextureOptions, compressed?: readonly CompressedTextureFormat[]);
|
|
266
285
|
/** Whether this texture carries an effects table, which the lit program reads only once one exists. */
|
|
267
286
|
get hasEffects(): boolean;
|
|
268
287
|
bindEffects(gl: WebGL2RenderingContext, unit: number, fallback: WebGLTexture): void;
|
|
@@ -278,6 +297,11 @@ export declare class SurfaceTexture {
|
|
|
278
297
|
* doing this every frame at any size wants to know that it costs what it costs.
|
|
279
298
|
*/
|
|
280
299
|
update(gl: WebGL2RenderingContext, source: TexImageSource): void;
|
|
300
|
+
/**
|
|
301
|
+
* Attach the only layer as the bound framebuffer's colour: a scene capture's target, drawn into
|
|
302
|
+
* by `captureScene`. Engine-internal, as `bind` is.
|
|
303
|
+
*/
|
|
304
|
+
attachColor(gl: WebGL2RenderingContext): void;
|
|
281
305
|
/** Bind to a unit for sampling. Engine-internal: the renderer owns unit assignment. */
|
|
282
306
|
bind(gl: WebGL2RenderingContext, unit: number): void;
|
|
283
307
|
dispose(gl: WebGL2RenderingContext): void;
|
|
@@ -3,6 +3,7 @@ import { compressedLayers, planBlocks, refuseBlockUpdate } from './compressedSou
|
|
|
3
3
|
import { uploadCompressedArray } from './glCompressed.js';
|
|
4
4
|
import { SURFACE_EFFECT_TEXELS, packSurfaceEffects } from './surfaceEffects.js';
|
|
5
5
|
import { isLightmapTexels } from './lightmap.js';
|
|
6
|
+
import { isSceneCaptureTexels } from './sceneCapture.js';
|
|
6
7
|
const DEFAULT_ANISOTROPY = 4;
|
|
7
8
|
/**
|
|
8
9
|
* An image uploaded once and bound per draw.
|
|
@@ -30,9 +31,11 @@ export class SurfaceTexture {
|
|
|
30
31
|
* where `compressed` — the formats this context samples — has theirs; see `compressedSource.ts`.
|
|
31
32
|
*/
|
|
32
33
|
constructor(gl, source, options = {}, compressed = []) {
|
|
33
|
-
/* A
|
|
34
|
+
/* A scene capture's target: one empty layer the renderer draws into. See `sceneCapture.ts`. */
|
|
35
|
+
const capture = isSceneCaptureTexels(source) ? source : null;
|
|
36
|
+
/* A lightmap's page: two layers of bytes, uploaded as they are. See `lightmap.ts`. */
|
|
34
37
|
const baked = isLightmapTexels(source) ? source : null;
|
|
35
|
-
const listed = (baked !== null ? [] : Array.isArray(source) ? source : [source]);
|
|
38
|
+
const listed = (baked !== null || capture !== null ? [] : Array.isArray(source) ? source : [source]);
|
|
36
39
|
const blocks = compressedLayers(listed);
|
|
37
40
|
this.srgb = (options.colorSpace ?? 'linear') === 'srgb';
|
|
38
41
|
/* Refused before anything is allocated, so a refusal leaves no texture behind. */
|
|
@@ -43,15 +46,27 @@ export class SurfaceTexture {
|
|
|
43
46
|
this.texture = texture;
|
|
44
47
|
const wrap = (options.wrap ?? 'repeat') === 'repeat' ? gl.REPEAT : gl.CLAMP_TO_EDGE;
|
|
45
48
|
gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
|
|
46
|
-
if (
|
|
49
|
+
if (capture !== null) {
|
|
50
|
+
/* Immutable storage, one level: a framebuffer attachment, and radiance read linear. */
|
|
51
|
+
this.layers = 1;
|
|
52
|
+
this.blockFormat = null;
|
|
53
|
+
this.mipmapped = false;
|
|
54
|
+
gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, capture.float ? gl.RGBA16F : gl.RGBA8, capture.width, capture.height, 1);
|
|
55
|
+
}
|
|
56
|
+
else if (baked !== null) {
|
|
47
57
|
/* No chain: a page is read at its own resolution, and a level of it would blend regions. */
|
|
48
|
-
this.layers = baked.layers;
|
|
58
|
+
this.layers = baked.layers.length;
|
|
49
59
|
this.blockFormat = null;
|
|
50
60
|
this.mipmapped = false;
|
|
51
61
|
/* A typed array refuses either flag set, so both are put down here rather than assumed. */
|
|
52
62
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
53
63
|
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
54
|
-
|
|
64
|
+
/* Bytes, and read by `texelFetch` alone: the shader decodes and filters. See `lightmap.ts`. */
|
|
65
|
+
gl.texImage3D(gl.TEXTURE_2D_ARRAY, 0, gl.RGBA8, baked.width, baked.height, this.layers, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
66
|
+
/* A layer at a time, so a page's arrays are uploaded where they lie rather than joined. */
|
|
67
|
+
for (let layer = 0; layer < this.layers; layer++) {
|
|
68
|
+
gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, layer, baked.width, baked.height, 1, gl.RGBA, gl.UNSIGNED_BYTE, baked.layers[layer]);
|
|
69
|
+
}
|
|
55
70
|
}
|
|
56
71
|
else if (blocks !== null && plan !== null) {
|
|
57
72
|
/* The stored chain, or level 0 alone where no chain was asked for. Nothing is generated:
|
|
@@ -135,6 +150,13 @@ export class SurfaceTexture {
|
|
|
135
150
|
gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
|
|
136
151
|
gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
|
|
137
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Attach the only layer as the bound framebuffer's colour: a scene capture's target, drawn into
|
|
155
|
+
* by `captureScene`. Engine-internal, as `bind` is.
|
|
156
|
+
*/
|
|
157
|
+
attachColor(gl) {
|
|
158
|
+
gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, this.texture, 0, 0);
|
|
159
|
+
}
|
|
138
160
|
/** Bind to a unit for sampling. Engine-internal: the renderer owns unit assignment. */
|
|
139
161
|
bind(gl, unit) {
|
|
140
162
|
gl.activeTexture(gl.TEXTURE0 + unit);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/core",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.7",
|
|
4
4
|
"description": "The runtime of DriftEngine, a WebGPU game engine with a WebGL2 fallback: renderer, loop, input, cameras, scene graph and physics",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@driftengine/drft": "4.8.
|
|
33
|
-
"@driftengine/physics": "4.8.
|
|
32
|
+
"@driftengine/drft": "4.8.7",
|
|
33
|
+
"@driftengine/physics": "4.8.7",
|
|
34
34
|
"gl-matrix": "^3.4.3"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"prepack": "node ../../scripts/build.mjs @driftengine/core"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@driftengine/nav": "4.8.
|
|
71
|
-
"@driftengine/network": "4.8.
|
|
72
|
-
"@driftengine/texture": "4.8.
|
|
70
|
+
"@driftengine/nav": "4.8.7",
|
|
71
|
+
"@driftengine/network": "4.8.7",
|
|
72
|
+
"@driftengine/texture": "4.8.7"
|
|
73
73
|
}
|
|
74
74
|
}
|