@driftengine/core 4.4.2 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +97 -0
- package/README.md +1 -1
- package/dist/geometry/boxCsg.d.ts +28 -0
- package/dist/geometry/boxCsg.js +139 -0
- package/dist/geometry/csg.d.ts +36 -0
- package/dist/geometry/csg.js +329 -0
- package/dist/geometry/meshBuilder.js +13 -0
- package/dist/geometry/meshConcat.js +1 -0
- package/dist/geometry/pixelFont.js +2 -0
- package/dist/geometry/solid.d.ts +61 -0
- package/dist/geometry/solid.js +152 -0
- package/dist/geometry/solidFlat.d.ts +35 -0
- package/dist/geometry/solidFlat.js +428 -0
- package/dist/geometry/solidHarness.d.ts +19 -0
- package/dist/geometry/solidHarness.js +66 -0
- package/dist/geometry/solidNormals.d.ts +17 -0
- package/dist/geometry/solidNormals.js +86 -0
- package/dist/geometry/solidRound.d.ts +41 -0
- package/dist/geometry/solidRound.js +323 -0
- package/dist/geometry/solidWriter.d.ts +16 -0
- package/dist/geometry/solidWriter.js +42 -0
- package/dist/index.d.ts +28 -3
- package/dist/index.js +20 -1
- package/dist/render/backend/api.d.ts +34 -14
- package/dist/render/backend/drawFog.d.ts +26 -0
- package/dist/render/backend/drawFog.js +30 -0
- package/dist/render/backend/materialChanges.d.ts +2 -1
- package/dist/render/backend/materialChanges.js +6 -1
- package/dist/render/backend/webgl2/glassTintPrograms.d.ts +13 -0
- package/dist/render/backend/webgl2/glassTintPrograms.js +38 -0
- package/dist/render/backend/webgl2/instanced.d.ts +24 -5
- package/dist/render/backend/webgl2/instanced.js +72 -15
- package/dist/render/backend/webgl2/renderer.d.ts +201 -16
- package/dist/render/backend/webgl2/renderer.js +596 -224
- package/dist/render/backend/webgpu/buffers.d.ts +29 -1
- package/dist/render/backend/webgpu/buffers.js +61 -4
- package/dist/render/backend/webgpu/depthPass.d.ts +1 -1
- package/dist/render/backend/webgpu/depthPass.js +7 -2
- package/dist/render/backend/webgpu/drawCommand.d.ts +5 -0
- package/dist/render/backend/webgpu/drawCommand.js +1 -0
- package/dist/render/backend/webgpu/flatPass.d.ts +6 -1
- package/dist/render/backend/webgpu/flatPass.js +57 -9
- package/dist/render/backend/webgpu/glassTintPass.d.ts +28 -0
- package/dist/render/backend/webgpu/glassTintPass.js +206 -0
- package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +60 -1
- package/dist/render/backend/webgpu/gpuDrivenPass.js +305 -5
- package/dist/render/backend/webgpu/instanceCullPass.d.ts +34 -0
- package/dist/render/backend/webgpu/instanceCullPass.js +144 -0
- package/dist/render/backend/webgpu/instanced.d.ts +23 -2
- package/dist/render/backend/webgpu/instanced.js +41 -4
- package/dist/render/backend/webgpu/latePass.d.ts +30 -0
- package/dist/render/backend/webgpu/latePass.js +140 -0
- package/dist/render/backend/webgpu/lightVolumePass.js +5 -1
- package/dist/render/backend/webgpu/motionPass.d.ts +95 -0
- package/dist/render/backend/webgpu/motionPass.js +491 -0
- package/dist/render/backend/webgpu/pipelineCache.d.ts +10 -1
- package/dist/render/backend/webgpu/pipelineCache.js +11 -1
- package/dist/render/backend/webgpu/pointGlassTintPass.d.ts +32 -0
- package/dist/render/backend/webgpu/pointGlassTintPass.js +193 -0
- package/dist/render/backend/webgpu/pointShadowPass.d.ts +35 -6
- package/dist/render/backend/webgpu/pointShadowPass.js +105 -20
- package/dist/render/backend/webgpu/postPass.d.ts +3 -13
- package/dist/render/backend/webgpu/renderer.d.ts +271 -64
- package/dist/render/backend/webgpu/renderer.js +1395 -580
- package/dist/render/backend/webgpu/scatterPass.js +2 -1
- package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
- package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
- package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
- package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
- package/dist/render/backend/webgpu/skinPaletteRing.js +4 -0
- package/dist/render/backend/webgpu/snapshotMips.d.ts +29 -0
- package/dist/render/backend/webgpu/snapshotMips.js +52 -0
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +22 -3
- package/dist/render/backend/webgpu/surfaceTexturePass.js +78 -39
- package/dist/render/driftLight/bake.d.ts +7 -0
- package/dist/render/driftLight/bake.js +61 -56
- package/dist/render/driftLight/denseField.d.ts +46 -0
- package/dist/render/driftLight/denseField.js +109 -0
- package/dist/render/driftLight/lightField.d.ts +10 -5
- package/dist/render/driftLight/lightField.js +25 -19
- package/dist/render/driftLight/presence.d.ts +50 -0
- package/dist/render/driftLight/presence.js +43 -0
- package/dist/render/driftLight/uniforms.d.ts +3 -3
- package/dist/render/driftLight/uniforms.js +5 -6
- package/dist/render/driftLight/worldLightField.d.ts +41 -0
- package/dist/render/driftLight/worldLightField.js +66 -0
- package/dist/render/fixtureAtlas.d.ts +20 -0
- package/dist/render/fixtureAtlas.js +49 -0
- package/dist/render/glass.d.ts +45 -0
- package/dist/render/glass.js +39 -0
- package/dist/render/glassCasters.d.ts +40 -0
- package/dist/render/glassCasters.js +84 -0
- package/dist/render/glassShadow.d.ts +78 -0
- package/dist/render/glassShadow.js +125 -0
- package/dist/render/globalMediumPass.d.ts +2 -3
- package/dist/render/globalMediumPass.js +2 -8
- package/dist/render/gpudriven/lit.d.ts +12 -0
- package/dist/render/gpudriven/lit.js +23 -0
- package/dist/render/gpudriven/materialTable.d.ts +15 -5
- package/dist/render/gpudriven/materialTable.js +26 -6
- package/dist/render/gpudriven/pipeline.d.ts +1 -1
- package/dist/render/gpudriven/pipeline.js +4 -3
- package/dist/render/gpudriven/sunGlass.d.ts +35 -0
- package/dist/render/gpudriven/sunGlass.js +89 -0
- package/dist/render/instanceCull.d.ts +63 -0
- package/dist/render/instanceCull.js +160 -0
- package/dist/render/instances.d.ts +15 -0
- package/dist/render/lightBudget.d.ts +36 -35
- package/dist/render/lightBudget.js +38 -37
- package/dist/render/lightGrid.d.ts +42 -0
- package/dist/render/lightGrid.js +87 -0
- package/dist/render/mesh.d.ts +18 -24
- package/dist/render/mesh.js +51 -68
- package/dist/render/occlusion.d.ts +10 -0
- package/dist/render/occlusion.js +36 -7
- package/dist/render/pointGlassTint.d.ts +59 -0
- package/dist/render/pointGlassTint.js +198 -0
- package/dist/render/pointLightSelection.d.ts +7 -0
- package/dist/render/pointLightSelection.js +29 -4
- package/dist/render/pointShadowArray.d.ts +56 -3
- package/dist/render/pointShadowArray.js +127 -16
- package/dist/render/pointShadowMap.d.ts +23 -1
- package/dist/render/pointShadowMap.js +27 -1
- package/dist/render/rainField.d.ts +13 -0
- package/dist/render/rainField.js +27 -5
- package/dist/render/recon/changeFrames.d.ts +15 -0
- package/dist/render/recon/changeFrames.js +21 -0
- package/dist/render/recon/lateDepth.d.ts +11 -0
- package/dist/render/recon/lateDepth.js +70 -0
- package/dist/render/recon/lateRouting.d.ts +14 -0
- package/dist/render/recon/lateRouting.js +18 -0
- package/dist/render/recon/mover.d.ts +57 -0
- package/dist/render/recon/mover.js +86 -0
- package/dist/render/recon/resolve.d.ts +34 -4
- package/dist/render/recon/resolve.js +161 -20
- package/dist/render/renderQuality.d.ts +35 -0
- package/dist/render/renderQuality.js +7 -0
- package/dist/render/rendererHarness.js +16 -0
- package/dist/render/sceneTarget.d.ts +9 -1
- package/dist/render/sceneTarget.js +60 -12
- package/dist/render/shaders/depth.d.ts +16 -1
- package/dist/render/shaders/depth.js +94 -15
- package/dist/render/shaders/flat/directionalShadow.js +91 -6
- package/dist/render/shaders/flat/driftLight.d.ts +1 -1
- package/dist/render/shaders/flat/driftLight.js +32 -7
- package/dist/render/shaders/flat/glassTint.d.ts +6 -0
- package/dist/render/shaders/flat/glassTint.js +25 -0
- package/dist/render/shaders/flat/index.d.ts +13 -0
- package/dist/render/shaders/flat/index.js +9 -3
- package/dist/render/shaders/flat/lampGlass.d.ts +1 -0
- package/dist/render/shaders/flat/lampGlass.js +183 -0
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +190 -38
- package/dist/render/shaders/flat/pointShadow.d.ts +1 -1
- package/dist/render/shaders/flat/pointShadow.js +2 -0
- package/dist/render/shaders/flat/preamble.js +96 -26
- package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
- package/dist/render/shaders/flat/surfaceEffects.js +152 -0
- package/dist/render/shaders/generated/depth.wgsl.d.ts +230 -7
- package/dist/render/shaders/generated/depth.wgsl.js +230 -7
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1644 -1596
- package/dist/render/shaders/generated/flat.wgsl.js +1683 -1621
- package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +3 -13
- package/dist/render/shaders/generated/globalMedium.wgsl.js +3 -13
- package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +3 -13
- package/dist/render/shaders/generated/lightVolume.wgsl.js +21 -17
- package/dist/render/shaders/generated/octahedralResolve.wgsl.d.ts +29 -0
- package/dist/render/shaders/generated/octahedralResolve.wgsl.js +29 -0
- package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
- package/dist/render/shaders/generated/rush.wgsl.js +21 -16
- package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
- package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
- package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/text.wgsl.js +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
- package/dist/render/shaders/globalMedium.js +6 -6
- package/dist/render/shaders/gpudriven/blendRaster.wgsl.d.ts +7 -0
- package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +86 -1
- package/dist/render/shaders/gpudriven/cull.wgsl.d.ts +1 -1
- package/dist/render/shaders/gpudriven/cull.wgsl.js +11 -2
- package/dist/render/shaders/gpudriven/materialTable.wgsl.d.ts +1 -1
- package/dist/render/shaders/gpudriven/materialTable.wgsl.js +3 -1
- package/dist/render/shaders/gpudriven/shade.wgsl.js +50 -1
- package/dist/render/shaders/gpudriven/sunGlass.wgsl.d.ts +9 -0
- package/dist/render/shaders/gpudriven/sunGlass.wgsl.js +133 -0
- package/dist/render/shaders/lightVolume.js +6 -6
- package/dist/render/shaders/octahedralResolve.d.ts +8 -0
- package/dist/render/shaders/octahedralResolve.js +48 -17
- package/dist/render/shaders/recon/lateDepth.wgsl.d.ts +1 -0
- package/dist/render/shaders/recon/lateDepth.wgsl.js +69 -0
- package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
- package/dist/render/shaders/recon/motion.wgsl.js +84 -0
- package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
- package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
- package/dist/render/shaders/rush.js +11 -1
- package/dist/render/shaders/sdfText.d.ts +1 -1
- package/dist/render/shaders/sdfText.js +3 -2
- package/dist/render/shaders/text.d.ts +1 -1
- package/dist/render/shaders/text.js +18 -9
- package/dist/render/shadowCasters.d.ts +20 -1
- package/dist/render/shadowCasters.js +15 -1
- package/dist/render/shadowMap.d.ts +54 -5
- package/dist/render/shadowMap.js +190 -36
- package/dist/render/sunGlassTint.d.ts +25 -0
- package/dist/render/sunGlassTint.js +79 -0
- package/dist/render/surfaceEffects.d.ts +83 -0
- package/dist/render/surfaceEffects.js +75 -0
- package/dist/render/surfaceScene.d.ts +8 -0
- package/dist/render/surfaceScene.js +8 -0
- package/dist/render/surfaceTexture.d.ts +18 -2
- package/dist/render/surfaceTexture.js +68 -15
- package/dist/render/temporalAa.d.ts +25 -6
- package/dist/render/temporalAa.js +45 -15
- package/dist/render/textureSource.d.ts +30 -0
- package/dist/render/textureSource.js +53 -0
- package/dist/render/translucentQueue.d.ts +6 -0
- package/dist/render/translucentQueue.js +15 -1
- package/dist/render/vertexDefaults.js +2 -0
- package/dist/world/hlod.d.ts +104 -0
- package/dist/world/hlod.js +214 -0
- package/package.json +6 -6
- package/src/geometry/boxCsg.ts +186 -0
- package/src/geometry/csg.ts +384 -0
- package/src/geometry/meshBuilder.ts +15 -0
- package/src/geometry/meshConcat.ts +1 -0
- package/src/geometry/pixelFont.ts +2 -0
- package/src/geometry/solid.ts +189 -0
- package/src/geometry/solidFlat.ts +522 -0
- package/src/geometry/solidHarness.ts +83 -0
- package/src/geometry/solidNormals.ts +103 -0
- package/src/geometry/solidRound.ts +443 -0
- package/src/geometry/solidWriter.ts +58 -0
- package/src/index.ts +61 -1
- package/src/render/backend/api.ts +36 -11
- package/src/render/backend/drawFog.ts +31 -0
- package/src/render/backend/materialChanges.ts +9 -2
- package/src/render/backend/webgl2/glassTintPrograms.ts +58 -0
- package/src/render/backend/webgl2/instanced.ts +80 -14
- package/src/render/backend/webgl2/renderer.ts +700 -250
- package/src/render/backend/webgpu/buffers.ts +92 -5
- package/src/render/backend/webgpu/depthPass.ts +7 -2
- package/src/render/backend/webgpu/drawCommand.ts +6 -0
- package/src/render/backend/webgpu/flatPass.ts +62 -11
- package/src/render/backend/webgpu/glassTintPass.ts +245 -0
- package/src/render/backend/webgpu/gpuDrivenPass.ts +349 -5
- package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
- package/src/render/backend/webgpu/instanced.ts +42 -4
- package/src/render/backend/webgpu/latePass.ts +161 -0
- package/src/render/backend/webgpu/lightVolumePass.ts +5 -1
- package/src/render/backend/webgpu/motionPass.ts +535 -0
- package/src/render/backend/webgpu/pipelineCache.ts +12 -1
- package/src/render/backend/webgpu/pointGlassTintPass.ts +233 -0
- package/src/render/backend/webgpu/pointShadowPass.ts +158 -26
- package/src/render/backend/webgpu/renderer.ts +1671 -670
- package/src/render/backend/webgpu/scatterPass.ts +2 -1
- package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
- package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
- package/src/render/backend/webgpu/skinPaletteRing.ts +4 -0
- package/src/render/backend/webgpu/snapshotMips.ts +80 -0
- package/src/render/backend/webgpu/surfaceTexturePass.ts +91 -51
- package/src/render/driftLight/bake.ts +85 -62
- package/src/render/driftLight/denseField.ts +149 -0
- package/src/render/driftLight/lightField.ts +33 -18
- package/src/render/driftLight/presence.ts +69 -0
- package/src/render/driftLight/uniforms.ts +8 -9
- package/src/render/driftLight/worldLightField.ts +83 -0
- package/src/render/fixtureAtlas.ts +63 -0
- package/src/render/glass.ts +74 -0
- package/src/render/glassCasters.ts +164 -0
- package/src/render/glassShadow.ts +153 -0
- package/src/render/globalMediumPass.ts +4 -11
- package/src/render/gpudriven/lit.ts +31 -0
- package/src/render/gpudriven/materialTable.ts +28 -6
- package/src/render/gpudriven/pipeline.ts +4 -3
- package/src/render/gpudriven/sunGlass.ts +117 -0
- package/src/render/instanceCull.ts +205 -0
- package/src/render/instances.ts +16 -0
- package/src/render/lightBudget.ts +41 -38
- package/src/render/lightGrid.ts +130 -0
- package/src/render/mesh.ts +59 -72
- package/src/render/occlusion.ts +51 -7
- package/src/render/pointGlassTint.ts +250 -0
- package/src/render/pointLightSelection.ts +72 -4
- package/src/render/pointShadowArray.ts +188 -22
- package/src/render/pointShadowMap.ts +41 -0
- package/src/render/rainField.ts +39 -5
- package/src/render/recon/changeFrames.ts +24 -0
- package/src/render/recon/lateDepth.ts +83 -0
- package/src/render/recon/lateRouting.ts +29 -0
- package/src/render/recon/mover.ts +118 -0
- package/src/render/recon/resolve.ts +206 -20
- package/src/render/renderQuality.ts +47 -0
- package/src/render/rendererHarness.ts +16 -0
- package/src/render/sceneTarget.ts +62 -11
- package/src/render/shaders/depth.ts +118 -15
- package/src/render/shaders/flat/directionalShadow.ts +97 -6
- package/src/render/shaders/flat/driftLight.ts +32 -7
- package/src/render/shaders/flat/glassTint.ts +25 -0
- package/src/render/shaders/flat/index.ts +22 -3
- package/src/render/shaders/flat/lampGlass.ts +185 -0
- package/src/render/shaders/flat/main.ts +190 -38
- package/src/render/shaders/flat/pointShadow.ts +2 -0
- package/src/render/shaders/flat/preamble.ts +96 -26
- package/src/render/shaders/flat/surfaceEffects.ts +152 -0
- package/src/render/shaders/generated/depth.wgsl.ts +237 -7
- package/src/render/shaders/generated/flat.wgsl.ts +1685 -1621
- package/src/render/shaders/generated/globalMedium.wgsl.ts +3 -13
- package/src/render/shaders/generated/lightVolume.wgsl.ts +23 -17
- package/src/render/shaders/generated/octahedralResolve.wgsl.ts +30 -0
- package/src/render/shaders/generated/rush.wgsl.ts +21 -16
- package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
- package/src/render/shaders/generated/text.wgsl.ts +1 -1
- package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
- package/src/render/shaders/globalMedium.ts +6 -6
- package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +88 -1
- package/src/render/shaders/gpudriven/cull.wgsl.ts +11 -2
- package/src/render/shaders/gpudriven/materialTable.wgsl.ts +3 -1
- package/src/render/shaders/gpudriven/shade.wgsl.ts +50 -1
- package/src/render/shaders/gpudriven/sunGlass.wgsl.ts +139 -0
- package/src/render/shaders/lightVolume.ts +6 -6
- package/src/render/shaders/octahedralResolve.ts +50 -17
- package/src/render/shaders/recon/lateDepth.wgsl.ts +72 -0
- package/src/render/shaders/recon/motion.wgsl.ts +84 -0
- package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
- package/src/render/shaders/rush.ts +11 -1
- package/src/render/shaders/sdfText.ts +3 -2
- package/src/render/shaders/text.ts +18 -9
- package/src/render/shadowCasters.ts +28 -1
- package/src/render/shadowMap.ts +231 -37
- package/src/render/sunGlassTint.ts +86 -0
- package/src/render/surfaceEffects.ts +132 -0
- package/src/render/surfaceScene.ts +16 -0
- package/src/render/surfaceTexture.ts +110 -19
- package/src/render/temporalAa.ts +78 -14
- package/src/render/textureSource.ts +72 -0
- package/src/render/translucentQueue.ts +20 -1
- package/src/render/vertexDefaults.ts +2 -0
- package/src/world/hlod.ts +290 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DriftLight for a whole world: every fixed light summed into one dense grid, baked offline.
|
|
3
|
+
*
|
|
4
|
+
* **Dense, where the courtyard field is sparse, because a city is lit nearly everywhere.** A brick
|
|
5
|
+
* earns its index and its shared faces where light is rare; street lamps reaching thirty metres
|
|
6
|
+
* forty metres apart leave almost no cell dark, so the sparseness buys nothing and the shared
|
|
7
|
+
* faces cost 2.4× the samples. At 8 m a sample, a two-kilometre city 128 m tall is 256 × 16 × 256
|
|
8
|
+
* samples, two half-float texels each: 16 MB on the GPU, and the renderer's lookup is one filtered
|
|
9
|
+
* fetch for the light and one for its direction.
|
|
10
|
+
*
|
|
11
|
+
* **The same sample as every other bake.** `sampleFieldLight` is shared with `bakeBrick`, so a dense
|
|
12
|
+
* volume and a brick agree bit for bit wherever both have a sample. The order a sample's lights are
|
|
13
|
+
* summed in does not have to match for that: the sum runs in double precision and is stored in
|
|
14
|
+
* single, which the last bits of a few terms in another order do not reach.
|
|
15
|
+
*
|
|
16
|
+
* **Lights are bucketed first**, four samples a bucket on every axis, so a sample sums only the
|
|
17
|
+
* lights whose reach touches its bucket rather than all of them: a city of ten thousand lamps is a
|
|
18
|
+
* few dozen candidates a sample. Offline and allocation-free per sample; it may take seconds.
|
|
19
|
+
*
|
|
20
|
+
* What it gives up is resolution: at 8 m a lamp's pool is a few samples across, which is why this
|
|
21
|
+
* stands in only past the exact lights, where a pool is a few dozen pixels.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { FieldLight } from './bake.ts';
|
|
25
|
+
import { sampleFieldLight } from './bake.ts';
|
|
26
|
+
import type { PointLightFalloff } from './falloff.ts';
|
|
27
|
+
import type { DistanceAt } from './visibility.ts';
|
|
28
|
+
|
|
29
|
+
/** The most samples along x or z: WebGL2 promises 256 on every axis of a 3D texture. */
|
|
30
|
+
export const DENSE_MAX_AXIS = 256;
|
|
31
|
+
/** The most along y, which holds the light and then the direction: two layers in 256. */
|
|
32
|
+
export const DENSE_MAX_HEIGHT = 128;
|
|
33
|
+
/** Samples a bucket spans on each axis. */
|
|
34
|
+
const BUCKET = 4;
|
|
35
|
+
|
|
36
|
+
export interface DenseLightVolume {
|
|
37
|
+
/** The world position of sample (0, 0, 0). */
|
|
38
|
+
readonly origin: readonly [number, number, number];
|
|
39
|
+
/** Metres between two samples. */
|
|
40
|
+
readonly spacing: number;
|
|
41
|
+
/** Samples on each axis. */
|
|
42
|
+
readonly dims: readonly [number, number, number];
|
|
43
|
+
/** Four floats a sample, x fastest then y then z: the colour, then 1 for a sample and 0 inside. */
|
|
44
|
+
readonly light: Float32Array;
|
|
45
|
+
/** Four floats a sample: the direction light arrives from, weighted by brightness, then 0. */
|
|
46
|
+
readonly direction: Float32Array;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sum `lights` over the box `bounds` (min xyz, max xyz) at `spacing` metres. `distance` occludes, or
|
|
51
|
+
* null for none. The first sample stands on the box's min corner.
|
|
52
|
+
*/
|
|
53
|
+
export function bakeDenseField(
|
|
54
|
+
lights: readonly FieldLight[],
|
|
55
|
+
falloff: PointLightFalloff,
|
|
56
|
+
distance: DistanceAt | null,
|
|
57
|
+
bounds: ArrayLike<number>,
|
|
58
|
+
spacing: number,
|
|
59
|
+
): DenseLightVolume {
|
|
60
|
+
if (!(spacing > 0)) throw new Error(`bakeDenseField: spacing is ${String(spacing)}`);
|
|
61
|
+
const origin: [number, number, number] = [
|
|
62
|
+
bounds[0] as number,
|
|
63
|
+
bounds[1] as number,
|
|
64
|
+
bounds[2] as number,
|
|
65
|
+
];
|
|
66
|
+
const dims: [number, number, number] = [0, 0, 0];
|
|
67
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
68
|
+
const extent = (bounds[axis + 3] as number) - (origin[axis] as number);
|
|
69
|
+
dims[axis] = Math.max(1, Math.floor(extent / spacing + 1e-9) + 1);
|
|
70
|
+
}
|
|
71
|
+
if (dims[0] > DENSE_MAX_AXIS || dims[2] > DENSE_MAX_AXIS || dims[1] > DENSE_MAX_HEIGHT) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`bakeDenseField: ${dims.join(' × ')} samples, and a volume holds ${DENSE_MAX_AXIS} across, ` +
|
|
74
|
+
`${DENSE_MAX_HEIGHT} up and ${DENSE_MAX_AXIS} deep; a coarser spacing fits it.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Each light listed in every bucket its reach touches, in the order the scene listed them. */
|
|
79
|
+
const buckets = [
|
|
80
|
+
Math.ceil(dims[0] / BUCKET),
|
|
81
|
+
Math.ceil(dims[1] / BUCKET),
|
|
82
|
+
Math.ceil(dims[2] / BUCKET),
|
|
83
|
+
] as const;
|
|
84
|
+
const bucketCount = buckets[0] * buckets[1] * buckets[2];
|
|
85
|
+
const span = spacing * BUCKET;
|
|
86
|
+
const range = (light: FieldLight, axis: number, out: Int32Array): boolean => {
|
|
87
|
+
const at = axis === 0 ? light.x : axis === 1 ? light.y : light.z;
|
|
88
|
+
const lo = Math.floor((at - light.radius - (origin[axis] as number)) / span);
|
|
89
|
+
const hi = Math.floor((at + light.radius - (origin[axis] as number)) / span);
|
|
90
|
+
out[axis * 2] = Math.max(0, lo);
|
|
91
|
+
out[axis * 2 + 1] = Math.min((buckets[axis] as number) - 1, hi);
|
|
92
|
+
return (out[axis * 2] as number) <= (out[axis * 2 + 1] as number);
|
|
93
|
+
};
|
|
94
|
+
const cells = new Int32Array(6);
|
|
95
|
+
const start = new Uint32Array(bucketCount + 1);
|
|
96
|
+
const each = (fill: (bucket: number, light: number) => void): void => {
|
|
97
|
+
for (let index = 0; index < lights.length; index++) {
|
|
98
|
+
const light = lights[index] as FieldLight;
|
|
99
|
+
if (!(light.radius > 0)) continue;
|
|
100
|
+
if (!range(light, 0, cells) || !range(light, 1, cells) || !range(light, 2, cells)) continue;
|
|
101
|
+
for (let z = cells[4] as number; z <= (cells[5] as number); z++) {
|
|
102
|
+
for (let y = cells[2] as number; y <= (cells[3] as number); y++) {
|
|
103
|
+
for (let x = cells[0] as number; x <= (cells[1] as number); x++) {
|
|
104
|
+
fill(x + buckets[0] * (y + buckets[1] * z), index);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
each((bucket) => {
|
|
111
|
+
start[bucket + 1] = (start[bucket + 1] as number) + 1;
|
|
112
|
+
});
|
|
113
|
+
for (let b = 0; b < bucketCount; b++)
|
|
114
|
+
start[b + 1] = (start[b + 1] as number) + (start[b] as number);
|
|
115
|
+
const listed = new Uint32Array(start[bucketCount] as number);
|
|
116
|
+
const cursor = start.slice(0, bucketCount);
|
|
117
|
+
each((bucket, light) => {
|
|
118
|
+
listed[cursor[bucket] as number] = light;
|
|
119
|
+
cursor[bucket] = (cursor[bucket] as number) + 1;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const count = dims[0] * dims[1] * dims[2];
|
|
123
|
+
const light = new Float32Array(count * 4);
|
|
124
|
+
const direction = new Float32Array(count * 4);
|
|
125
|
+
for (let k = 0; k < dims[2]; k++) {
|
|
126
|
+
for (let j = 0; j < dims[1]; j++) {
|
|
127
|
+
for (let i = 0; i < dims[0]; i++) {
|
|
128
|
+
const bucket =
|
|
129
|
+
Math.floor(i / BUCKET) +
|
|
130
|
+
buckets[0] * (Math.floor(j / BUCKET) + buckets[1] * Math.floor(k / BUCKET));
|
|
131
|
+
sampleFieldLight(
|
|
132
|
+
origin[0] + i * spacing,
|
|
133
|
+
origin[1] + j * spacing,
|
|
134
|
+
origin[2] + k * spacing,
|
|
135
|
+
lights,
|
|
136
|
+
listed,
|
|
137
|
+
start[bucket] as number,
|
|
138
|
+
start[bucket + 1] as number,
|
|
139
|
+
falloff,
|
|
140
|
+
distance,
|
|
141
|
+
light,
|
|
142
|
+
direction,
|
|
143
|
+
(i + dims[0] * (j + dims[1] * k)) * 4,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { origin, spacing, dims, light, direction };
|
|
149
|
+
}
|
|
@@ -34,11 +34,11 @@ import type { FieldLight } from './bake.ts';
|
|
|
34
34
|
import type { PointLightFalloff } from './falloff.ts';
|
|
35
35
|
import { BRICK_SAMPLES, layoutLightField } from './layout.ts';
|
|
36
36
|
import type { LightFieldLayout } from './layout.ts';
|
|
37
|
+
import { FieldPresence } from './presence.ts';
|
|
38
|
+
import type { DriftLightVolumes } from './presence.ts';
|
|
37
39
|
|
|
38
40
|
/** Seconds the summed light takes to fade in once the field is whole. */
|
|
39
41
|
export const FIELD_FADE_SEC = 0.5;
|
|
40
|
-
/** How fast the shading radius grows back, per second, when the choice reaches further again. */
|
|
41
|
-
const RADIUS_GROWTH_PER_SEC = 2;
|
|
42
42
|
/** Bricks along each axis of the atlas: 256 texels a side, the least 3D texture WebGL2 promises. */
|
|
43
43
|
const ATLAS_BRICKS: readonly [number, number, number] = [32, 64, 64];
|
|
44
44
|
/** A brick's footprint in the atlas: its light block and its direction block side by side. */
|
|
@@ -66,7 +66,7 @@ export interface LightFieldOptions {
|
|
|
66
66
|
readonly fadeSec?: number;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export class LightField {
|
|
69
|
+
export class LightField implements DriftLightVolumes {
|
|
70
70
|
readonly layout: LightFieldLayout;
|
|
71
71
|
/** Texels on each axis of the atlas. */
|
|
72
72
|
readonly atlasSize: readonly [number, number, number];
|
|
@@ -82,18 +82,15 @@ export class LightField {
|
|
|
82
82
|
readonly fadeSec: number;
|
|
83
83
|
/** The summed light's brightness, for a scene that dims all its lamps at once: dawn, a switch. */
|
|
84
84
|
scale = 1;
|
|
85
|
-
/** Where the frame's choice was centred, which the shader measures the radius from. */
|
|
86
|
-
readonly centre = new Float32Array(3);
|
|
87
85
|
|
|
88
86
|
private readonly lights: readonly FieldLight[];
|
|
89
87
|
private readonly falloff: PointLightFalloff;
|
|
90
88
|
private readonly distance: ((x: number, y: number, z: number) => number) | null;
|
|
91
89
|
private readonly lightBlock = new Float32Array(BRICK_TEXELS * 4);
|
|
92
90
|
private readonly directionBlock = new Float32Array(BRICK_TEXELS * 4);
|
|
91
|
+
private readonly showing: FieldPresence;
|
|
93
92
|
private baked = 0;
|
|
94
93
|
private taken = 0;
|
|
95
|
-
private eased = 0;
|
|
96
|
-
private shown = 0;
|
|
97
94
|
|
|
98
95
|
constructor(lights: readonly LightFieldSource[], options: LightFieldOptions) {
|
|
99
96
|
for (const light of lights) light.inLightField = true;
|
|
@@ -110,6 +107,7 @@ export class LightField {
|
|
|
110
107
|
this.falloff = options.falloff;
|
|
111
108
|
this.band = options.band ?? 2;
|
|
112
109
|
this.fadeSec = Math.max(0, options.fadeSec ?? FIELD_FADE_SEC);
|
|
110
|
+
this.showing = new FieldPresence(this.fadeSec);
|
|
113
111
|
const fields = options.fields ?? [];
|
|
114
112
|
this.occluded = fields.length > 0;
|
|
115
113
|
this.distance = this.occluded ? distanceToInstances(fields) : null;
|
|
@@ -143,12 +141,37 @@ export class LightField {
|
|
|
143
141
|
|
|
144
142
|
/** The radius, in metres from `centre`, inside which lights are shaded exactly. */
|
|
145
143
|
get radius(): number {
|
|
146
|
-
return this.
|
|
144
|
+
return this.showing.radius;
|
|
147
145
|
}
|
|
148
146
|
|
|
149
147
|
/** How much of the summed light is in the frame, 0 until the field is whole, then up to 1. */
|
|
150
148
|
get presence(): number {
|
|
151
|
-
return this.
|
|
149
|
+
return this.showing.presence;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Where the frame's choice was centred, which the shader measures the radius from. */
|
|
153
|
+
get centre(): Float32Array {
|
|
154
|
+
return this.showing.centre;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
get empty(): boolean {
|
|
158
|
+
return this.layout.count === 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
get indexDims(): readonly [number, number, number] {
|
|
162
|
+
return this.layout.dims;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get index(): Uint32Array {
|
|
166
|
+
return this.layout.index;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
get sampleOrigin(): readonly [number, number, number] {
|
|
170
|
+
return this.layout.origin;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
get signedSpacing(): number {
|
|
174
|
+
return this.layout.spacing;
|
|
152
175
|
}
|
|
153
176
|
|
|
154
177
|
/** The texel at which brick `brick`'s light block starts; its direction block is 4 along x. */
|
|
@@ -195,15 +218,7 @@ export class LightField {
|
|
|
195
218
|
* which is only less exact. While nothing is summed the radius simply follows.
|
|
196
219
|
*/
|
|
197
220
|
follow(complete: number, x: number, y: number, z: number, dtSec: number): void {
|
|
198
|
-
this.
|
|
199
|
-
this.centre[1] = y;
|
|
200
|
-
this.centre[2] = z;
|
|
201
|
-
const showing = this.shown > 0;
|
|
202
|
-
if (!showing || complete < this.eased) this.eased = complete;
|
|
203
|
-
else this.eased += (complete - this.eased) * (1 - Math.exp(-RADIUS_GROWTH_PER_SEC * dtSec));
|
|
204
|
-
if (this.ready) {
|
|
205
|
-
this.shown = this.fadeSec > 0 ? Math.min(1, this.shown + dtSec / this.fadeSec) : 1;
|
|
206
|
-
}
|
|
221
|
+
this.showing.follow(this.ready, complete, x, y, z, dtSec);
|
|
207
222
|
}
|
|
208
223
|
|
|
209
224
|
/** Write the brick just baked into its place in the atlas, as half floats. */
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much of a DriftLight field is in the frame, and the radius inside which the frame's exact
|
|
3
|
+
* lights take over from it — the part every kind of field shares, kept once.
|
|
4
|
+
*
|
|
5
|
+
* **The radius shrinks at once and grows back slowly.** A pixel inside it that a left-out light
|
|
6
|
+
* reaches would be shaded short, so it cannot lag a shrink; a pixel it has not reached yet takes the
|
|
7
|
+
* summed light, which is only less exact, so a grow may ease. While nothing is summed the radius
|
|
8
|
+
* simply follows. The presence fades in over `fadeSec` once the field is whole, so a field
|
|
9
|
+
* completing in view does not pop — and a held clock never finishes a fade, so a field behind a
|
|
10
|
+
* loading screen asks for none.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** How fast the shading radius grows back, per second, when the choice reaches further again. */
|
|
14
|
+
const RADIUS_GROWTH_PER_SEC = 2;
|
|
15
|
+
|
|
16
|
+
export class FieldPresence {
|
|
17
|
+
/** Where the frame's choice was centred, which the shader measures the radius from. */
|
|
18
|
+
readonly centre = new Float32Array(3);
|
|
19
|
+
private eased = 0;
|
|
20
|
+
private shown = 0;
|
|
21
|
+
|
|
22
|
+
constructor(readonly fadeSec: number) {}
|
|
23
|
+
|
|
24
|
+
/** The radius, in metres from `centre`, inside which lights are shaded exactly. */
|
|
25
|
+
get radius(): number {
|
|
26
|
+
return this.eased;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** How much of the summed light is in the frame, 0 until the field is whole, then up to 1. */
|
|
30
|
+
get presence(): number {
|
|
31
|
+
return this.shown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Follow the frame's choice: `complete` from the selection, centred at (x, y, z). */
|
|
35
|
+
follow(ready: boolean, complete: number, x: number, y: number, z: number, dtSec: number): void {
|
|
36
|
+
this.centre[0] = x;
|
|
37
|
+
this.centre[1] = y;
|
|
38
|
+
this.centre[2] = z;
|
|
39
|
+
if (this.shown <= 0 || complete < this.eased) this.eased = complete;
|
|
40
|
+
else this.eased += (complete - this.eased) * (1 - Math.exp(-RADIUS_GROWTH_PER_SEC * dtSec));
|
|
41
|
+
if (ready) this.shown = this.fadeSec > 0 ? Math.min(1, this.shown + dtSec / this.fadeSec) : 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What a backend uploads and binds for a field of either kind, and what the uniforms are made of.
|
|
47
|
+
*
|
|
48
|
+
* A sparse field's index finds its bricks; a dense one brings a 1×1×1 index of nothing, so the
|
|
49
|
+
* upload and the bind are one code path, and says it is dense by a negative spacing.
|
|
50
|
+
*/
|
|
51
|
+
export interface DriftLightVolumes {
|
|
52
|
+
readonly ready: boolean;
|
|
53
|
+
/** Nothing summed at all: nothing to upload. */
|
|
54
|
+
readonly empty: boolean;
|
|
55
|
+
readonly indexDims: readonly [number, number, number];
|
|
56
|
+
readonly index: Uint32Array;
|
|
57
|
+
readonly atlasSize: readonly [number, number, number];
|
|
58
|
+
/** Half floats, four channels a texel, x fastest. */
|
|
59
|
+
readonly atlas: Uint16Array;
|
|
60
|
+
readonly presence: number;
|
|
61
|
+
readonly radius: number;
|
|
62
|
+
readonly band: number;
|
|
63
|
+
readonly scale: number;
|
|
64
|
+
readonly centre: Float32Array;
|
|
65
|
+
/** The world position of the first sample. */
|
|
66
|
+
readonly sampleOrigin: readonly [number, number, number];
|
|
67
|
+
/** Metres between samples, negative for a dense volume: the shader's mode. */
|
|
68
|
+
readonly signedSpacing: number;
|
|
69
|
+
}
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
* from the centre, gets a smaller radius and more of the volume, which is the safe direction.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type {
|
|
17
|
+
import type { DriftLightVolumes } from './presence.ts';
|
|
18
18
|
|
|
19
19
|
export interface DriftLightUniforms {
|
|
20
20
|
/** How much of the summed light is in, the radius, the band, the scale. */
|
|
21
21
|
readonly light: Float32Array;
|
|
22
|
-
/** The first
|
|
22
|
+
/** The first sample, and the metres between samples — negative for a dense volume. */
|
|
23
23
|
readonly origin: Float32Array;
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -35,16 +35,15 @@ export function createDriftLightUniforms(): DriftLightUniforms {
|
|
|
35
35
|
* being drawn.
|
|
36
36
|
*/
|
|
37
37
|
export function resolveDriftLight(
|
|
38
|
-
field:
|
|
38
|
+
field: DriftLightVolumes | null,
|
|
39
39
|
probePass: boolean,
|
|
40
40
|
eye: ArrayLike<number>,
|
|
41
41
|
out: DriftLightUniforms,
|
|
42
42
|
): void {
|
|
43
|
-
if (field === null || !field.ready || field.presence <= 0 || field.
|
|
43
|
+
if (field === null || !field.ready || field.presence <= 0 || field.empty) {
|
|
44
44
|
out.light.fill(0);
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
|
-
const { layout } = field;
|
|
48
47
|
const offset = Math.hypot(
|
|
49
48
|
(eye[0] ?? 0) - (field.centre[0] as number),
|
|
50
49
|
(eye[1] ?? 0) - (field.centre[1] as number),
|
|
@@ -54,8 +53,8 @@ export function resolveDriftLight(
|
|
|
54
53
|
out.light[1] = probePass ? 0 : Math.max(0, field.radius - offset);
|
|
55
54
|
out.light[2] = field.band;
|
|
56
55
|
out.light[3] = field.scale;
|
|
57
|
-
out.origin[0] =
|
|
58
|
-
out.origin[1] =
|
|
59
|
-
out.origin[2] =
|
|
60
|
-
out.origin[3] =
|
|
56
|
+
out.origin[0] = field.sampleOrigin[0];
|
|
57
|
+
out.origin[1] = field.sampleOrigin[1];
|
|
58
|
+
out.origin[2] = field.sampleOrigin[2];
|
|
59
|
+
out.origin[3] = field.signedSpacing;
|
|
61
60
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A world's DriftLight: a dense volume baked offline (`bakeDenseField`), handed to the renderer
|
|
3
|
+
* whole, standing in for every fixed light past the frame's exact choice.
|
|
4
|
+
*
|
|
5
|
+
* **Whole from the start**, because nothing is baked in the page: `ready` is true on construction
|
|
6
|
+
* and the volume fades in over `fadeSec` from the first `follow`. The lights it summed are the
|
|
7
|
+
* scene's to mark `inLightField`, so the selection knows not to count them twice — a volume carries
|
|
8
|
+
* no list of the lights it came from.
|
|
9
|
+
*
|
|
10
|
+
* **Light and direction side by side along y**, the short axis: a city is 256 samples across and a
|
|
11
|
+
* few dozen up, and a 3D texture promises 256 on every axis, so stacking the two there keeps both
|
|
12
|
+
* inside one texture and one sampler. The shader keeps each lookup inside its own half.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { halfBits } from '../halfFloat.ts';
|
|
16
|
+
import type { DenseLightVolume } from './denseField.ts';
|
|
17
|
+
import { FieldPresence } from './presence.ts';
|
|
18
|
+
import type { DriftLightVolumes } from './presence.ts';
|
|
19
|
+
|
|
20
|
+
export interface WorldLightFieldOptions {
|
|
21
|
+
/** Metres over which exact and summed light crossfade at the shading radius. Two by default. */
|
|
22
|
+
readonly band?: number;
|
|
23
|
+
/** Seconds the volume fades in over; zero behind a loading screen, whose clock may be held. */
|
|
24
|
+
readonly fadeSec?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The index a dense volume binds in the sparse one's place: one cell, no brick. */
|
|
28
|
+
const NO_INDEX = new Uint32Array(1);
|
|
29
|
+
|
|
30
|
+
export class WorldLightField implements DriftLightVolumes {
|
|
31
|
+
readonly ready = true;
|
|
32
|
+
readonly empty = false;
|
|
33
|
+
readonly indexDims: readonly [number, number, number] = [1, 1, 1];
|
|
34
|
+
readonly index = NO_INDEX;
|
|
35
|
+
readonly atlasSize: readonly [number, number, number];
|
|
36
|
+
readonly atlas: Uint16Array;
|
|
37
|
+
readonly band: number;
|
|
38
|
+
readonly sampleOrigin: readonly [number, number, number];
|
|
39
|
+
readonly signedSpacing: number;
|
|
40
|
+
/** The summed light's brightness, for a scene that dims all its lamps at once. */
|
|
41
|
+
scale = 1;
|
|
42
|
+
private readonly showing: FieldPresence;
|
|
43
|
+
|
|
44
|
+
constructor(volume: DenseLightVolume, options: WorldLightFieldOptions = {}) {
|
|
45
|
+
const [x, y, z] = volume.dims;
|
|
46
|
+
this.atlasSize = [x, y * 2, z];
|
|
47
|
+
this.atlas = new Uint16Array(x * y * 2 * z * 4);
|
|
48
|
+
for (let k = 0; k < z; k++) {
|
|
49
|
+
for (let j = 0; j < y; j++) {
|
|
50
|
+
for (let i = 0; i < x; i++) {
|
|
51
|
+
const from = (i + x * (j + y * k)) * 4;
|
|
52
|
+
const light = (i + x * (j + 2 * y * k)) * 4;
|
|
53
|
+
const direction = (i + x * (j + y + 2 * y * k)) * 4;
|
|
54
|
+
for (let c = 0; c < 4; c++) {
|
|
55
|
+
this.atlas[light + c] = halfBits(volume.light[from + c] as number);
|
|
56
|
+
this.atlas[direction + c] = halfBits(volume.direction[from + c] as number);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this.band = options.band ?? 2;
|
|
62
|
+
this.sampleOrigin = volume.origin;
|
|
63
|
+
this.signedSpacing = -volume.spacing;
|
|
64
|
+
this.showing = new FieldPresence(Math.max(0, options.fadeSec ?? 0.5));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get radius(): number {
|
|
68
|
+
return this.showing.radius;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get presence(): number {
|
|
72
|
+
return this.showing.presence;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get centre(): Float32Array {
|
|
76
|
+
return this.showing.centre;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Follow the frame's choice, as `LightField.follow` does. */
|
|
80
|
+
follow(complete: number, x: number, y: number, z: number, dtSec: number): void {
|
|
81
|
+
this.showing.follow(true, complete, x, y, z, dtSec);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a light fixture throws, in one texture: its spot cookies in the top band, its photometric
|
|
3
|
+
* profiles in the rows below.
|
|
4
|
+
*
|
|
5
|
+
* **One binding for two tables, because the lit stage had one sampler left.** The cookie atlas and
|
|
6
|
+
* the photometric atlas were two small float textures read with `textureLod`, and the widest lit
|
|
7
|
+
* shader bound fifteen of the sixteen fragment samplers WebGL2 guarantees — so the next sampler had
|
|
8
|
+
* to be found by folding two into one, which is what this is. Both describe a fixture's emission
|
|
9
|
+
* pattern, and both are 128 texels across, which is what lets them share a texture exactly.
|
|
10
|
+
*
|
|
11
|
+
* **Cookies on top, profiles at the bottom, and the order is load-bearing.** A profile's row blend
|
|
12
|
+
* only ever reaches forward — to the next plane, and from the last to the repeat of the first — so
|
|
13
|
+
* past its last row it meets the texture's edge and the clamp, never a cookie. A cookie's own
|
|
14
|
+
* half-texel inset keeps it inside its band on both axes. The shader finds the bands from
|
|
15
|
+
* `textureSize` and `uIesAtlasRows`, so the fold cost no uniform.
|
|
16
|
+
*
|
|
17
|
+
* **What it gives up**: every texel is four half floats where a profile needed one, a few kilobytes
|
|
18
|
+
* a scene; and setting either table rebuilds the whole texture, which a consumer does once per set.
|
|
19
|
+
*/
|
|
20
|
+
import { IES_ATLAS_WIDTH } from './iesProfile.ts';
|
|
21
|
+
import { COOKIE_TILE } from './lightBudget.ts';
|
|
22
|
+
|
|
23
|
+
export interface FixtureAtlasLayout {
|
|
24
|
+
readonly width: number;
|
|
25
|
+
readonly height: number;
|
|
26
|
+
/** Rows of cookie tiles at the top: `COOKIE_TILE`, or 0 with none loaded. */
|
|
27
|
+
readonly cookieRows: number;
|
|
28
|
+
/** The first row of the profiles, below the cookies. */
|
|
29
|
+
readonly iesTop: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The atlas for `iesRows` rows of profile and `cookieTiles` cookies. */
|
|
33
|
+
export function fixtureAtlasLayout(iesRows: number, cookieTiles: number): FixtureAtlasLayout {
|
|
34
|
+
const cookieRows = cookieTiles > 0 ? COOKIE_TILE : 0;
|
|
35
|
+
return {
|
|
36
|
+
width: Math.max(IES_ATLAS_WIDTH, COOKIE_TILE * cookieTiles),
|
|
37
|
+
height: cookieRows + iesRows,
|
|
38
|
+
cookieRows,
|
|
39
|
+
iesTop: cookieRows,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The profiles' rows as the atlas holds them: `data` one float a texel, `width` across and `rows`
|
|
45
|
+
* down, spread into the red channel of a four-channel image the atlas's size, the rest ones. The
|
|
46
|
+
* cookie band is left zero for the backend to fill from its images.
|
|
47
|
+
*/
|
|
48
|
+
export function fixtureAtlasTexels(
|
|
49
|
+
layout: FixtureAtlasLayout,
|
|
50
|
+
ies: { readonly data: Float32Array; readonly width: number; readonly height: number },
|
|
51
|
+
): Float32Array {
|
|
52
|
+
const out = new Float32Array(layout.width * layout.height * 4);
|
|
53
|
+
for (let row = 0; row < ies.height; row++) {
|
|
54
|
+
for (let x = 0; x < ies.width; x++) {
|
|
55
|
+
const at = ((layout.iesTop + row) * layout.width + x) * 4;
|
|
56
|
+
out[at] = ies.data[row * ies.width + x] as number;
|
|
57
|
+
out[at + 1] = 1;
|
|
58
|
+
out[at + 2] = 1;
|
|
59
|
+
out[at + 3] = 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glass: how much light a surface lets through, how milky it is, and the colour it takes.
|
|
3
|
+
*
|
|
4
|
+
* **A thin pane, not a volume.** Glass here is a surface that shows the scene behind it — by
|
|
5
|
+
* `transmission`, blurred by `frost`, tinted — keeps its own highlight and reflection, and glows
|
|
6
|
+
* with the lights behind it in proportion to how frosted it is. The one depth it has is the path
|
|
7
|
+
* absorption refraction already applies (`TranslucentMeshOptions.thicknessM`). What it gives up is
|
|
8
|
+
* everything a solid of glass does inside itself: a sculpture or a thick block under-darkens and
|
|
9
|
+
* does not bend light twice. What would make it wrong is a scene whose glass is mostly that.
|
|
10
|
+
*
|
|
11
|
+
* **Zero transmission is not glass**, whatever the frost says: a surface letting nothing through is
|
|
12
|
+
* opaque, and every branch the shader gates on transmission is skipped for it. This module is also
|
|
13
|
+
* the reference the shader's Fresnel is held to (`schlickFresnel`), stated once in TypeScript.
|
|
14
|
+
*/
|
|
15
|
+
import type { Vec3 } from '../math/color.ts';
|
|
16
|
+
|
|
17
|
+
export interface GlassOptions {
|
|
18
|
+
/** Share of light that passes, 0 (opaque) to 1 (clear). */
|
|
19
|
+
readonly transmission: number;
|
|
20
|
+
/** How milky, 0 (see-through) to 1 (fully diffusing: what is behind is blurred away). */
|
|
21
|
+
readonly frost: number;
|
|
22
|
+
/** The colour light takes through it. White, the default, is clear glass. */
|
|
23
|
+
readonly tint?: Vec3;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A `GlassOptions` clamped and defaulted, in a caller-owned object so a draw allocates nothing. */
|
|
27
|
+
export interface ResolvedGlass {
|
|
28
|
+
transmission: number;
|
|
29
|
+
frost: number;
|
|
30
|
+
tint: [number, number, number];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const clamp01 = (value: number): number => Math.min(1, Math.max(0, value));
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve `options` into `out`, and say whether it is glass at all.
|
|
37
|
+
*
|
|
38
|
+
* Not glass — absent, or letting nothing through — zeroes `out` and tints it white, so a binder that
|
|
39
|
+
* uploads `out` either way uploads the state that switches the shader's glass branch off.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveGlass(options: GlassOptions | undefined, out: ResolvedGlass): boolean {
|
|
42
|
+
const transmission = clamp01(options?.transmission ?? 0);
|
|
43
|
+
if (options === undefined || transmission <= 0) {
|
|
44
|
+
out.transmission = 0;
|
|
45
|
+
out.frost = 0;
|
|
46
|
+
out.tint[0] = 1;
|
|
47
|
+
out.tint[1] = 1;
|
|
48
|
+
out.tint[2] = 1;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
out.transmission = transmission;
|
|
52
|
+
out.frost = clamp01(options.frost);
|
|
53
|
+
const tint = options.tint;
|
|
54
|
+
out.tint[0] = tint === undefined ? 1 : clamp01(tint[0]);
|
|
55
|
+
out.tint[1] = tint === undefined ? 1 : clamp01(tint[1]);
|
|
56
|
+
out.tint[2] = tint === undefined ? 1 : clamp01(tint[2]);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Schlick's Fresnel for glass, `F0 = 0.04`: the share of light a pane reflects at a view angle.
|
|
62
|
+
*
|
|
63
|
+
* `cosView` is clamped at 0.05, the floor the shader's path length already uses, so a silhouette
|
|
64
|
+
* seen exactly edge-on is a very reflective pane rather than a division by zero.
|
|
65
|
+
*/
|
|
66
|
+
export function schlickFresnel(cosView: number): number {
|
|
67
|
+
const c = Math.min(1, Math.max(0.05, cosView));
|
|
68
|
+
return 0.04 + 0.96 * (1 - c) ** 5;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** How much of what is behind a pane is seen through it at a view angle. */
|
|
72
|
+
export function glassSeenShare(transmission: number, cosView: number): number {
|
|
73
|
+
return transmission * (1 - schlickFresnel(cosView));
|
|
74
|
+
}
|