@driftengine/core 4.4.1 → 4.5.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 +72 -0
- package/README.md +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +7 -0
- package/dist/render/backend/api.d.ts +17 -11
- package/dist/render/backend/webgl2/glassTintPrograms.d.ts +13 -0
- package/dist/render/backend/webgl2/glassTintPrograms.js +38 -0
- package/dist/render/backend/webgl2/renderer.d.ts +82 -8
- package/dist/render/backend/webgl2/renderer.js +373 -105
- package/dist/render/backend/webgpu/buffers.d.ts +19 -1
- package/dist/render/backend/webgpu/buffers.js +27 -1
- package/dist/render/backend/webgpu/depthPass.d.ts +1 -1
- package/dist/render/backend/webgpu/depthPass.js +2 -1
- package/dist/render/backend/webgpu/flatPass.d.ts +2 -0
- package/dist/render/backend/webgpu/flatPass.js +20 -1
- package/dist/render/backend/webgpu/glassTintPass.d.ts +28 -0
- package/dist/render/backend/webgpu/glassTintPass.js +205 -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/instanced.d.ts +12 -1
- package/dist/render/backend/webgpu/instanced.js +20 -1
- package/dist/render/backend/webgpu/latePass.d.ts +26 -0
- package/dist/render/backend/webgpu/latePass.js +133 -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 +21 -15
- package/dist/render/backend/webgpu/postPass.js +17 -4
- package/dist/render/backend/webgpu/renderer.d.ts +181 -54
- package/dist/render/backend/webgpu/renderer.js +1011 -394
- package/dist/render/backend/webgpu/scatterPass.js +2 -1
- 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 +5 -0
- package/dist/render/backend/webgpu/surfaceTexturePass.js +1 -1
- package/dist/render/flameLight.js +15 -2
- 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/lightBudget.d.ts +17 -5
- package/dist/render/lightBudget.js +17 -5
- package/dist/render/pointGlassTint.d.ts +59 -0
- package/dist/render/pointGlassTint.js +198 -0
- package/dist/render/pointShadowArray.d.ts +56 -3
- package/dist/render/pointShadowArray.js +127 -16
- package/dist/render/pointShadowBudget.js +51 -41
- package/dist/render/pointShadowMap.d.ts +23 -1
- package/dist/render/pointShadowMap.js +27 -1
- package/dist/render/recon/changeFrames.d.ts +15 -0
- package/dist/render/recon/changeFrames.js +21 -0
- package/dist/render/recon/disocclusion.d.ts +36 -3
- package/dist/render/recon/disocclusion.js +56 -4
- 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.js +95 -12
- package/dist/render/renderQuality.d.ts +35 -0
- package/dist/render/renderQuality.js +7 -0
- package/dist/render/rendererHarness.js +12 -0
- package/dist/render/sceneTarget.d.ts +9 -1
- package/dist/render/sceneTarget.js +59 -13
- package/dist/render/shaders/depth.d.ts +16 -1
- package/dist/render/shaders/depth.js +85 -9
- package/dist/render/shaders/flat/directionalShadow.js +91 -6
- 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 +4 -0
- 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 +115 -15
- 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 +21 -6
- package/dist/render/shaders/generated/depth.wgsl.d.ts +227 -4
- package/dist/render/shaders/generated/depth.wgsl.js +227 -4
- package/dist/render/shaders/generated/flat.wgsl.d.ts +360 -312
- package/dist/render/shaders/generated/flat.wgsl.js +376 -328
- 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 +4 -14
- 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/temporalResolve.wgsl.d.ts +31 -6
- package/dist/render/shaders/generated/temporalResolve.wgsl.js +31 -6
- 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 +44 -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 +1 -1
- package/dist/render/shaders/recon/resolve.wgsl.js +112 -8
- package/dist/render/shaders/temporalResolve.js +133 -5
- 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/temporalAa.d.ts +145 -0
- package/dist/render/temporalAa.js +190 -0
- package/dist/render/temporalPass.d.ts +16 -14
- package/dist/render/temporalPass.js +59 -14
- package/dist/render/translucentQueue.d.ts +6 -0
- package/dist/render/translucentQueue.js +15 -1
- package/package.json +6 -6
- package/src/index.ts +10 -0
- package/src/render/backend/api.ts +15 -9
- package/src/render/backend/webgl2/glassTintPrograms.ts +58 -0
- package/src/render/backend/webgl2/renderer.ts +422 -109
- package/src/render/backend/webgpu/buffers.ts +50 -2
- package/src/render/backend/webgpu/depthPass.ts +2 -1
- package/src/render/backend/webgpu/flatPass.ts +22 -1
- package/src/render/backend/webgpu/glassTintPass.ts +244 -0
- package/src/render/backend/webgpu/gpuDrivenPass.ts +349 -5
- package/src/render/backend/webgpu/instanced.ts +21 -1
- package/src/render/backend/webgpu/latePass.ts +152 -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/postPass.ts +34 -4
- package/src/render/backend/webgpu/renderer.ts +1178 -425
- package/src/render/backend/webgpu/scatterPass.ts +2 -1
- 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 +1 -1
- package/src/render/flameLight.ts +15 -2
- 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/lightBudget.ts +19 -5
- package/src/render/pointGlassTint.ts +250 -0
- package/src/render/pointShadowArray.ts +188 -22
- package/src/render/pointShadowBudget.ts +49 -39
- package/src/render/pointShadowMap.ts +41 -0
- package/src/render/recon/changeFrames.ts +24 -0
- package/src/render/recon/disocclusion.ts +54 -3
- package/src/render/recon/lateRouting.ts +29 -0
- package/src/render/recon/mover.ts +118 -0
- package/src/render/recon/resolve.ts +137 -10
- package/src/render/renderQuality.ts +47 -0
- package/src/render/rendererHarness.ts +12 -0
- package/src/render/sceneTarget.ts +66 -12
- package/src/render/shaders/depth.ts +109 -9
- package/src/render/shaders/flat/directionalShadow.ts +97 -6
- package/src/render/shaders/flat/glassTint.ts +25 -0
- package/src/render/shaders/flat/index.ts +17 -0
- package/src/render/shaders/flat/lampGlass.ts +185 -0
- package/src/render/shaders/flat/main.ts +115 -15
- package/src/render/shaders/flat/pointShadow.ts +2 -0
- package/src/render/shaders/flat/preamble.ts +21 -6
- package/src/render/shaders/generated/depth.wgsl.ts +234 -4
- package/src/render/shaders/generated/flat.wgsl.ts +376 -328
- package/src/render/shaders/generated/globalMedium.wgsl.ts +3 -13
- package/src/render/shaders/generated/lightVolume.wgsl.ts +4 -14
- package/src/render/shaders/generated/octahedralResolve.wgsl.ts +30 -0
- package/src/render/shaders/generated/temporalResolve.wgsl.ts +31 -6
- 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 +47 -0
- package/src/render/shaders/recon/motion.wgsl.ts +84 -0
- package/src/render/shaders/recon/resolve.wgsl.ts +112 -8
- package/src/render/shaders/temporalResolve.ts +141 -5
- package/src/render/shadowCasters.ts +28 -1
- package/src/render/shadowMap.ts +231 -37
- package/src/render/sunGlassTint.ts +86 -0
- package/src/render/temporalAa.ts +227 -0
- package/src/render/temporalPass.ts +88 -12
- package/src/render/translucentQueue.ts +20 -1
|
@@ -255,7 +255,8 @@ export function createScatterDepthBindGroupLayout(device, fragBinding, fragSize,
|
|
|
255
255
|
{
|
|
256
256
|
binding: previous.texture,
|
|
257
257
|
visibility: VISIBILITY_FRAGMENT,
|
|
258
|
-
|
|
258
|
+
/* The depth program's peel read, an array since the sun's maps became one. */
|
|
259
|
+
texture: { sampleType: 'unfilterable-float', viewDimension: '2d-array' },
|
|
259
260
|
},
|
|
260
261
|
{
|
|
261
262
|
binding: previous.sampler,
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* **What would make it wrong** is a genuine crowd — a stadium, a swarm of rigged birds — and the
|
|
14
14
|
* fix is then the one `skinPalette.ts` names: one texture holding every palette with a per-draw
|
|
15
15
|
* row offset, which spends a uniform and a shader change to stop spending textures.
|
|
16
|
+
*
|
|
17
|
+
* **A reconstructing frame spends one more per skinned mover**: last frame's pose is uploaded into
|
|
18
|
+
* a slot of its own for the motion pass. So the same budget is about twelve such characters with a
|
|
19
|
+
* reflection and three shadow layers each.
|
|
16
20
|
*/
|
|
17
21
|
export declare const MAX_SKIN_PALETTES = 128;
|
|
18
22
|
/**
|
|
@@ -14,6 +14,10 @@ import { SkinPaletteTexture } from './skinPaletteTexture.js';
|
|
|
14
14
|
* **What would make it wrong** is a genuine crowd — a stadium, a swarm of rigged birds — and the
|
|
15
15
|
* fix is then the one `skinPalette.ts` names: one texture holding every palette with a per-draw
|
|
16
16
|
* row offset, which spends a uniform and a shader change to stop spending textures.
|
|
17
|
+
*
|
|
18
|
+
* **A reconstructing frame spends one more per skinned mover**: last frame's pose is uploaded into
|
|
19
|
+
* a slot of its own for the motion pass. So the same budget is about twelve such characters with a
|
|
20
|
+
* reflection and three shadow layers each.
|
|
17
21
|
*/
|
|
18
22
|
export const MAX_SKIN_PALETTES = 128;
|
|
19
23
|
/**
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mip chain of the frame's refraction copy, recorded into the frame's own encoder.
|
|
3
|
+
*
|
|
4
|
+
* **Frosted glass reads a blurrier level of what is behind it** (`glass.ts`), so on a frame that draws
|
|
5
|
+
* glass the copy needs every level filled from the one above. `generateMipChain` does that for an
|
|
6
|
+
* upload, on an encoder of its own that it submits at once — which here would run before the frame's
|
|
7
|
+
* encoder had even made the copy, and would build views and bind groups on every call. So this
|
|
8
|
+
* makes them once per copy texture and records one pass per level wherever the copy was taken.
|
|
9
|
+
*
|
|
10
|
+
* What it costs is a render pass per level on every frame that takes the copy — about a dozen for a
|
|
11
|
+
* 1080p copy, each a quarter of the one before, together about a third of the copy itself — and
|
|
12
|
+
* nothing on a frame that shows nothing through anything. **A frame that refracts and draws no glass
|
|
13
|
+
* pays it too**, and that is the trade: the copy is taken at the first draw that asks and shared by
|
|
14
|
+
* every later one, so a glass pane drawn after a refracting one would otherwise read levels nothing
|
|
15
|
+
* filled. WebGL2 fills its chain on the first glass draw instead, because a `generateMipmap` there
|
|
16
|
+
* ends no pass. What would change it is knowing at the copy whether glass follows in the frame.
|
|
17
|
+
*/
|
|
18
|
+
import type { MipPipelines } from './surfaceTexturePass.ts';
|
|
19
|
+
export declare class SnapshotMips {
|
|
20
|
+
private readonly label;
|
|
21
|
+
private readonly pipeline;
|
|
22
|
+
private readonly targets;
|
|
23
|
+
private readonly groups;
|
|
24
|
+
constructor(device: GPUDevice, pipelines: MipPipelines, texture: GPUTexture, format: GPUTextureFormat,
|
|
25
|
+
/** One layer of an array texture, where the chain is a layer's rather than the texture's. */
|
|
26
|
+
layer?: number | null, label?: string);
|
|
27
|
+
/** Fill every level from level 0, in `encoder`, after whatever wrote level 0 there. */
|
|
28
|
+
record(encoder: GPUCommandEncoder): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mipBlitPipeline } from './surfaceTexturePass.js';
|
|
2
|
+
export class SnapshotMips {
|
|
3
|
+
label;
|
|
4
|
+
pipeline;
|
|
5
|
+
targets = [];
|
|
6
|
+
groups = [];
|
|
7
|
+
constructor(device, pipelines, texture, format,
|
|
8
|
+
/** One layer of an array texture, where the chain is a layer's rather than the texture's. */
|
|
9
|
+
layer = null, label = 'refract.mips') {
|
|
10
|
+
this.label = label;
|
|
11
|
+
this.pipeline = mipBlitPipeline(device, pipelines, format);
|
|
12
|
+
const sampler = device.createSampler({
|
|
13
|
+
label: 'refract.mips.sampler',
|
|
14
|
+
magFilter: 'linear',
|
|
15
|
+
minFilter: 'linear',
|
|
16
|
+
});
|
|
17
|
+
const layout = this.pipeline.getBindGroupLayout(0);
|
|
18
|
+
const at = layer === null ? {} : { dimension: '2d', baseArrayLayer: layer, arrayLayerCount: 1 };
|
|
19
|
+
for (let level = 1; level < texture.mipLevelCount; level++) {
|
|
20
|
+
const source = texture.createView({ ...at, baseMipLevel: level - 1, mipLevelCount: 1 });
|
|
21
|
+
this.targets.push(texture.createView({ ...at, baseMipLevel: level, mipLevelCount: 1 }));
|
|
22
|
+
this.groups.push(device.createBindGroup({
|
|
23
|
+
label: `refract.mips.${String(level)}`,
|
|
24
|
+
layout,
|
|
25
|
+
entries: [
|
|
26
|
+
{ binding: 0, resource: source },
|
|
27
|
+
{ binding: 1, resource: sampler },
|
|
28
|
+
],
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Fill every level from level 0, in `encoder`, after whatever wrote level 0 there. */
|
|
33
|
+
record(encoder) {
|
|
34
|
+
for (let i = 0; i < this.targets.length; i++) {
|
|
35
|
+
const pass = encoder.beginRenderPass({
|
|
36
|
+
label: this.label,
|
|
37
|
+
colorAttachments: [
|
|
38
|
+
{
|
|
39
|
+
view: this.targets[i],
|
|
40
|
+
loadOp: 'clear',
|
|
41
|
+
storeOp: 'store',
|
|
42
|
+
clearValue: [0, 0, 0, 0],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
pass.setPipeline(this.pipeline);
|
|
47
|
+
pass.setBindGroup(0, this.groups[i]);
|
|
48
|
+
pass.draw(3);
|
|
49
|
+
pass.end();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -30,6 +30,11 @@ export interface MipPipelines {
|
|
|
30
30
|
* thing, and the second copy is the one that would quietly disagree about the format.
|
|
31
31
|
*/
|
|
32
32
|
export declare function generateMipChain(device: GPUDevice, pipelines: MipPipelines, texture: GPUTexture, format: GPUTextureFormat, levels: number): void;
|
|
33
|
+
/**
|
|
34
|
+
* Cached per format, because the two differ: an `-srgb` target encodes on write, and blitting a
|
|
35
|
+
* linear chain through a pipeline declared for the other one is a gamma error per level.
|
|
36
|
+
*/
|
|
37
|
+
export declare function mipBlitPipeline(device: GPUDevice, pipelines: MipPipelines, format: GPUTextureFormat): GPURenderPipeline;
|
|
33
38
|
/** An image uploaded once and bound per material. */
|
|
34
39
|
export declare class GpuSurfaceTexture {
|
|
35
40
|
private readonly device;
|
|
@@ -101,7 +101,7 @@ export function generateMipChain(device, pipelines, texture, format, levels) {
|
|
|
101
101
|
* Cached per format, because the two differ: an `-srgb` target encodes on write, and blitting a
|
|
102
102
|
* linear chain through a pipeline declared for the other one is a gamma error per level.
|
|
103
103
|
*/
|
|
104
|
-
function mipBlitPipeline(device, pipelines, format) {
|
|
104
|
+
export function mipBlitPipeline(device, pipelines, format) {
|
|
105
105
|
return pipelines.get(`surface.mip:${format}`, () => ({
|
|
106
106
|
label: `surface.mip:${format}`,
|
|
107
107
|
layout: 'auto',
|
|
@@ -11,6 +11,7 @@ export function flameFrequencyHz(diameterM) {
|
|
|
11
11
|
export function createFlame(options) {
|
|
12
12
|
const height = HEIGHT_PER_DIAMETER * options.diameterM;
|
|
13
13
|
const centre = [options.x, options.y + height / 3, options.z];
|
|
14
|
+
const swayM = (options.sway ?? SWAY_PER_HEIGHT) * height;
|
|
14
15
|
/* A seed spread over the whole turn, so seeds 1 and 2 are not neighbours in phase. */
|
|
15
16
|
const phase = ((options.seed ?? 0) * 2.399963) % (Math.PI * 2);
|
|
16
17
|
const light = {
|
|
@@ -23,7 +24,19 @@ export function createFlame(options) {
|
|
|
23
24
|
radius: options.radius,
|
|
24
25
|
/* Off, because this helper is the flicker; the selection's own would apply a second one. */
|
|
25
26
|
flicker: 0,
|
|
26
|
-
|
|
27
|
+
/*
|
|
28
|
+
* **Clear of what the fire burns from wherever the light has wandered**, which is the sway added
|
|
29
|
+
* to the near plane a still flame would want. The shadow map is baked from where the light
|
|
30
|
+
* stood, so the cut moves with it, and each axis moves by at most `swayM`: a near plane of D
|
|
31
|
+
* alone left a brazier's bowl 7 mm inside it at the top of the sway and 13 cm outside at the
|
|
32
|
+
* bottom, and the bowl's shadow on the floor under the brazier came and went with every
|
|
33
|
+
* re-bake — a dark disc on some, none on others.
|
|
34
|
+
*
|
|
35
|
+
* What it gives up is `swayM` of shadow nearest the light, which is inside the fixture. What
|
|
36
|
+
* would make it wrong is a fixture reaching further below the flame than D / 6: this clears
|
|
37
|
+
* that far and no further, so a deep bowl sets its own `shadowNear` from its own depth.
|
|
38
|
+
*/
|
|
39
|
+
shadowNear: Math.max(0.05, options.diameterM) + swayM,
|
|
27
40
|
sourceRadius: options.diameterM / 2,
|
|
28
41
|
...(options.castsShadow === false ? { castsShadow: false } : {}),
|
|
29
42
|
};
|
|
@@ -41,7 +54,7 @@ export function createFlame(options) {
|
|
|
41
54
|
color: [options.color[0], options.color[1], options.color[2]],
|
|
42
55
|
flicker: options.flicker ?? 0.18,
|
|
43
56
|
phase,
|
|
44
|
-
swayM
|
|
57
|
+
swayM,
|
|
45
58
|
};
|
|
46
59
|
}
|
|
47
60
|
/** Where the flame's light is and how bright, at `timeSeconds`. Writes `flame.light` in place. */
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
export interface GlassOptions {
|
|
17
|
+
/** Share of light that passes, 0 (opaque) to 1 (clear). */
|
|
18
|
+
readonly transmission: number;
|
|
19
|
+
/** How milky, 0 (see-through) to 1 (fully diffusing: what is behind is blurred away). */
|
|
20
|
+
readonly frost: number;
|
|
21
|
+
/** The colour light takes through it. White, the default, is clear glass. */
|
|
22
|
+
readonly tint?: Vec3;
|
|
23
|
+
}
|
|
24
|
+
/** A `GlassOptions` clamped and defaulted, in a caller-owned object so a draw allocates nothing. */
|
|
25
|
+
export interface ResolvedGlass {
|
|
26
|
+
transmission: number;
|
|
27
|
+
frost: number;
|
|
28
|
+
tint: [number, number, number];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve `options` into `out`, and say whether it is glass at all.
|
|
32
|
+
*
|
|
33
|
+
* Not glass — absent, or letting nothing through — zeroes `out` and tints it white, so a binder that
|
|
34
|
+
* uploads `out` either way uploads the state that switches the shader's glass branch off.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveGlass(options: GlassOptions | undefined, out: ResolvedGlass): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Schlick's Fresnel for glass, `F0 = 0.04`: the share of light a pane reflects at a view angle.
|
|
39
|
+
*
|
|
40
|
+
* `cosView` is clamped at 0.05, the floor the shader's path length already uses, so a silhouette
|
|
41
|
+
* seen exactly edge-on is a very reflective pane rather than a division by zero.
|
|
42
|
+
*/
|
|
43
|
+
export declare function schlickFresnel(cosView: number): number;
|
|
44
|
+
/** How much of what is behind a pane is seen through it at a view angle. */
|
|
45
|
+
export declare function glassSeenShare(transmission: number, cosView: number): number;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const clamp01 = (value) => Math.min(1, Math.max(0, value));
|
|
2
|
+
/**
|
|
3
|
+
* Resolve `options` into `out`, and say whether it is glass at all.
|
|
4
|
+
*
|
|
5
|
+
* Not glass — absent, or letting nothing through — zeroes `out` and tints it white, so a binder that
|
|
6
|
+
* uploads `out` either way uploads the state that switches the shader's glass branch off.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveGlass(options, out) {
|
|
9
|
+
const transmission = clamp01(options?.transmission ?? 0);
|
|
10
|
+
if (options === undefined || transmission <= 0) {
|
|
11
|
+
out.transmission = 0;
|
|
12
|
+
out.frost = 0;
|
|
13
|
+
out.tint[0] = 1;
|
|
14
|
+
out.tint[1] = 1;
|
|
15
|
+
out.tint[2] = 1;
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
out.transmission = transmission;
|
|
19
|
+
out.frost = clamp01(options.frost);
|
|
20
|
+
const tint = options.tint;
|
|
21
|
+
out.tint[0] = tint === undefined ? 1 : clamp01(tint[0]);
|
|
22
|
+
out.tint[1] = tint === undefined ? 1 : clamp01(tint[1]);
|
|
23
|
+
out.tint[2] = tint === undefined ? 1 : clamp01(tint[2]);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Schlick's Fresnel for glass, `F0 = 0.04`: the share of light a pane reflects at a view angle.
|
|
28
|
+
*
|
|
29
|
+
* `cosView` is clamped at 0.05, the floor the shader's path length already uses, so a silhouette
|
|
30
|
+
* seen exactly edge-on is a very reflective pane rather than a division by zero.
|
|
31
|
+
*/
|
|
32
|
+
export function schlickFresnel(cosView) {
|
|
33
|
+
const c = Math.min(1, Math.max(0.05, cosView));
|
|
34
|
+
return 0.04 + 0.96 * (1 - c) ** 5;
|
|
35
|
+
}
|
|
36
|
+
/** How much of what is behind a pane is seen through it at a view angle. */
|
|
37
|
+
export function glassSeenShare(transmission, cosView) {
|
|
38
|
+
return transmission * (1 - schlickFresnel(cosView));
|
|
39
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The glass a shadow pass was offered, kept aside to be drawn twice once the opaque casters are in:
|
|
3
|
+
* where the nearest pane is, and what the panes let through. See `glassShadow.ts`.
|
|
4
|
+
*
|
|
5
|
+
* **Recorded rather than drawn at once**, because the two glass passes each need the whole set and
|
|
6
|
+
* a pass of their own, and the caster enumeration is the consumer's to run exactly once per pass.
|
|
7
|
+
* A pool of entries grown on demand and never shrunk, so a steady scene records into the entries
|
|
8
|
+
* the frame before it made — no allocation per frame, the house rule for a per-pass path.
|
|
9
|
+
*
|
|
10
|
+
* What it keeps by reference is only what outlives the pass that recorded it: the mesh, the batch
|
|
11
|
+
* and its instance data, a skinned caster's palette, and the material. The model matrix is copied,
|
|
12
|
+
* because a caller is entitled to reuse one scratch matrix between sink calls.
|
|
13
|
+
*/
|
|
14
|
+
import type { ReadonlyMat4 } from 'gl-matrix';
|
|
15
|
+
import type { ResolvedGlass } from './glass.ts';
|
|
16
|
+
import type { InstancedHandle, MeshHandle } from './backend/api.ts';
|
|
17
|
+
import type { MeshInstances } from './instances.ts';
|
|
18
|
+
import type { SceneCasterMaterial } from './shadowCasters.ts';
|
|
19
|
+
/** Where a replay goes: each entry with its own glass, as the depth and tint passes want it. */
|
|
20
|
+
export interface GlassReplaySink {
|
|
21
|
+
mesh(mesh: MeshHandle, model: ReadonlyMat4, material: SceneCasterMaterial | undefined, glass: ResolvedGlass): void;
|
|
22
|
+
instanced(batch: InstancedHandle, data: MeshInstances, material: SceneCasterMaterial | undefined, glass: ResolvedGlass): void;
|
|
23
|
+
skinnedMesh(mesh: MeshHandle, model: ReadonlyMat4, palette: Float32Array, material: SceneCasterMaterial | undefined, glass: ResolvedGlass): void;
|
|
24
|
+
}
|
|
25
|
+
export declare class GlassCasterList {
|
|
26
|
+
private readonly pool;
|
|
27
|
+
private used;
|
|
28
|
+
/** How many glass casters this pass has recorded. */
|
|
29
|
+
get count(): number;
|
|
30
|
+
/** How many entries the pool holds, recorded or not: what a steady scene stops growing. */
|
|
31
|
+
get pooled(): number;
|
|
32
|
+
recordMesh(mesh: MeshHandle, model: ReadonlyMat4, material: SceneCasterMaterial | undefined, glass: ResolvedGlass): void;
|
|
33
|
+
recordInstanced(batch: InstancedHandle, data: MeshInstances, material: SceneCasterMaterial | undefined, glass: ResolvedGlass): void;
|
|
34
|
+
recordSkinned(mesh: MeshHandle, model: ReadonlyMat4, palette: Float32Array, material: SceneCasterMaterial | undefined, glass: ResolvedGlass): void;
|
|
35
|
+
/** Every recorded caster, in the order it was offered. */
|
|
36
|
+
replay(sink: GlassReplaySink): void;
|
|
37
|
+
/** Forget this pass's casters; the entries stay for the next one. */
|
|
38
|
+
clear(): void;
|
|
39
|
+
private take;
|
|
40
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const MESH = 0;
|
|
2
|
+
const INSTANCED = 1;
|
|
3
|
+
const SKINNED = 2;
|
|
4
|
+
export class GlassCasterList {
|
|
5
|
+
pool = [];
|
|
6
|
+
used = 0;
|
|
7
|
+
/** How many glass casters this pass has recorded. */
|
|
8
|
+
get count() {
|
|
9
|
+
return this.used;
|
|
10
|
+
}
|
|
11
|
+
/** How many entries the pool holds, recorded or not: what a steady scene stops growing. */
|
|
12
|
+
get pooled() {
|
|
13
|
+
return this.pool.length;
|
|
14
|
+
}
|
|
15
|
+
recordMesh(mesh, model, material, glass) {
|
|
16
|
+
const entry = this.take(MESH, material, glass);
|
|
17
|
+
entry.mesh = mesh;
|
|
18
|
+
entry.model.set(model);
|
|
19
|
+
}
|
|
20
|
+
recordInstanced(batch, data, material, glass) {
|
|
21
|
+
const entry = this.take(INSTANCED, material, glass);
|
|
22
|
+
entry.batch = batch;
|
|
23
|
+
entry.data = data;
|
|
24
|
+
}
|
|
25
|
+
recordSkinned(mesh, model, palette, material, glass) {
|
|
26
|
+
const entry = this.take(SKINNED, material, glass);
|
|
27
|
+
entry.mesh = mesh;
|
|
28
|
+
entry.palette = palette;
|
|
29
|
+
entry.model.set(model);
|
|
30
|
+
}
|
|
31
|
+
/** Every recorded caster, in the order it was offered. */
|
|
32
|
+
replay(sink) {
|
|
33
|
+
for (let i = 0; i < this.used; i++) {
|
|
34
|
+
const entry = this.pool[i];
|
|
35
|
+
if (entry.kind === INSTANCED && entry.batch !== null && entry.data !== null) {
|
|
36
|
+
sink.instanced(entry.batch, entry.data, entry.material, entry.glass);
|
|
37
|
+
}
|
|
38
|
+
else if (entry.kind === SKINNED && entry.mesh !== null && entry.palette !== null) {
|
|
39
|
+
sink.skinnedMesh(entry.mesh, entry.model, entry.palette, entry.material, entry.glass);
|
|
40
|
+
}
|
|
41
|
+
else if (entry.mesh !== null) {
|
|
42
|
+
sink.mesh(entry.mesh, entry.model, entry.material, entry.glass);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Forget this pass's casters; the entries stay for the next one. */
|
|
47
|
+
clear() {
|
|
48
|
+
for (let i = 0; i < this.used; i++) {
|
|
49
|
+
const entry = this.pool[i];
|
|
50
|
+
/* Released so a disposed mesh is not held alive by a list nobody will replay again. */
|
|
51
|
+
entry.mesh = null;
|
|
52
|
+
entry.batch = null;
|
|
53
|
+
entry.data = null;
|
|
54
|
+
entry.palette = null;
|
|
55
|
+
entry.material = undefined;
|
|
56
|
+
}
|
|
57
|
+
this.used = 0;
|
|
58
|
+
}
|
|
59
|
+
take(kind, material, glass) {
|
|
60
|
+
let entry = this.pool[this.used];
|
|
61
|
+
if (entry === undefined) {
|
|
62
|
+
entry = {
|
|
63
|
+
kind,
|
|
64
|
+
mesh: null,
|
|
65
|
+
batch: null,
|
|
66
|
+
data: null,
|
|
67
|
+
palette: null,
|
|
68
|
+
model: new Float32Array(16),
|
|
69
|
+
material,
|
|
70
|
+
glass: { transmission: 0, frost: 0, tint: [1, 1, 1] },
|
|
71
|
+
};
|
|
72
|
+
this.pool.push(entry);
|
|
73
|
+
}
|
|
74
|
+
this.used++;
|
|
75
|
+
entry.kind = kind;
|
|
76
|
+
entry.material = material;
|
|
77
|
+
entry.glass.transmission = glass.transmission;
|
|
78
|
+
entry.glass.frost = glass.frost;
|
|
79
|
+
entry.glass.tint[0] = glass.tint[0];
|
|
80
|
+
entry.glass.tint[1] = glass.tint[1];
|
|
81
|
+
entry.glass.tint[2] = glass.tint[2];
|
|
82
|
+
return entry;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coloured light through glass: how one pane filters light, how panes combine, which receivers a
|
|
3
|
+
* pane tints, and how far frost spreads the colour — the reference the shaders state term for term.
|
|
4
|
+
*
|
|
5
|
+
* **Frost spreads light, it does not remove it.** Etched glass passes 88–91% of visible light
|
|
6
|
+
* against clear float glass's ~90%, so a frosted pane passes `transmission × (1 − F) × tint` like a
|
|
7
|
+
* clear one and only the filter widens. What it gives up: the spread stays inside the opaque frame's
|
|
8
|
+
* shadow, so light from a frosted pane does not reach past the outline of the frame around it. What
|
|
9
|
+
* would make it wrong is glass that genuinely absorbs as it diffuses — opal glass, milk glass — for
|
|
10
|
+
* which a lower `transmission` is the honest statement.
|
|
11
|
+
*
|
|
12
|
+
* **What else it does not reach**: light that bounces. DriftRay's probe bake and DriftLight's summed
|
|
13
|
+
* far field see a pane as absent, so the light a tinted window sends round a room by bouncing is its
|
|
14
|
+
* sun's own colour; only the direct light through the pane takes the tint.
|
|
15
|
+
*/
|
|
16
|
+
import { type ResolvedGlass } from './glass.ts';
|
|
17
|
+
/** One tint texel: the light a stack of panes lets through, and how clear the stack is. */
|
|
18
|
+
export interface GlassTexel {
|
|
19
|
+
r: number;
|
|
20
|
+
g: number;
|
|
21
|
+
b: number;
|
|
22
|
+
clarity: number;
|
|
23
|
+
}
|
|
24
|
+
/** No glass: everything passes, perfectly clear. What a tint map is cleared to. */
|
|
25
|
+
export declare const CLEAR_TEXEL: Readonly<GlassTexel>;
|
|
26
|
+
/** How widely a fully frosted pane spreads its colour: a 30° half-angle cone behind it. */
|
|
27
|
+
export declare const FROST_SPREAD: number;
|
|
28
|
+
/**
|
|
29
|
+
* The spread never exceeds this many base filter radii, and the tap count never changes.
|
|
30
|
+
*
|
|
31
|
+
* **Sixty-four, because the tint is read from the mip level the spread asks for**: each tap is then
|
|
32
|
+
* already an average over its own share of the spread, so taps far apart do not alias into grain.
|
|
33
|
+
* It was eight before the tint had mips, and on a sun map a few centimetres a texel that capped a
|
|
34
|
+
* frosted pane's spread at seven centimetres — frost that nobody saw. What the cap still bounds is
|
|
35
|
+
* a pane so frosted and so far above a floor that its colour would wash over a room.
|
|
36
|
+
*/
|
|
37
|
+
export declare const FROST_RADIUS_CAP = 64;
|
|
38
|
+
/** One pane as a tint texel, lit at `cosLight` to its normal. Fresnel as `glass.ts` states it. */
|
|
39
|
+
export declare function paneTexel(glass: ResolvedGlass, cosLight: number, out: GlassTexel): GlassTexel;
|
|
40
|
+
/** Two stacks of glass on one ray: a product, so the order they are drawn in cannot matter. */
|
|
41
|
+
export declare function combinePanes(a: GlassTexel, b: GlassTexel, out: GlassTexel): GlassTexel;
|
|
42
|
+
/**
|
|
43
|
+
* What one filter tap contributes: the glass's texel where the receiver is behind the pane, and
|
|
44
|
+
* nothing where it is on the light's side of it — or is the pane, which the bias keeps untinted.
|
|
45
|
+
*/
|
|
46
|
+
export declare function tapTint(receiverDepth: number, glassDepth: number, bias: number, glass: GlassTexel, out: GlassTexel): GlassTexel;
|
|
47
|
+
/**
|
|
48
|
+
* A frosted pane's tint at a receiver: the outline from the taps at the base radius, the colour
|
|
49
|
+
* from the spread taps that found glass — nothing else.
|
|
50
|
+
*
|
|
51
|
+
* **Split because a tint cannot carry light out past a pane.** Blurring the tint map whole pulls in
|
|
52
|
+
* the white of the unobstructed ground around a pane, which brightens the patch — measured at 10%
|
|
53
|
+
* more light over a frosted window than a clear one — while the pane's own light, which should
|
|
54
|
+
* spread onto that ground, has no way to exceed full sun there. So the outline stays where the
|
|
55
|
+
* clear pane's is (`coverage`, the share of outline taps behind glass) and only the colour spreads,
|
|
56
|
+
* averaged over glass alone (`colourSum` over `colourCount`, the summed pane shares `unmixTint`
|
|
57
|
+
* reports — continuous, so a colour crossing between panes blends rather than stepping): neighbouring
|
|
58
|
+
* panes mix, a pattern blurs, and the light through the patch is what it was. What it gives up is
|
|
59
|
+
* spread past the outline, which the spec states; with no glass found by the spread the colour is
|
|
60
|
+
* clear.
|
|
61
|
+
*/
|
|
62
|
+
export declare function spreadTint(coverage: number, colourSum: GlassTexel, colourCount: number, out: GlassTexel): GlassTexel;
|
|
63
|
+
/**
|
|
64
|
+
* A coarse level of the tint, with the open ground's white taken back out: returns the pane's share
|
|
65
|
+
* of the texel and writes the pane's own colour to `out`.
|
|
66
|
+
*
|
|
67
|
+
* **Why a level needs this**: it averages a pane with whatever lies beside it, and beside a pane is
|
|
68
|
+
* usually open ground, which the tint holds as white — so a frosted patch read coarse is paler than
|
|
69
|
+
* the pane, and measured 6% more light than the clear pane over the same floor. The clarity channel
|
|
70
|
+
* says how much of the texel was pane: open ground holds 1, the pane its own `centreClarity`, so
|
|
71
|
+
* the pane's share is `(1 − a) / (1 − centreClarity)` and its colour follows by subtraction. What it
|
|
72
|
+
* assumes is that the frost around a receiver is the frost at its centre, which is true within a
|
|
73
|
+
* pane and blends where two panes of different frost meet; a clear pane (`centreClarity` 1) is
|
|
74
|
+
* never read coarse, so it is returned as it stands.
|
|
75
|
+
*/
|
|
76
|
+
export declare function unmixTint(mip: GlassTexel, centreClarity: number, out: GlassTexel): number;
|
|
77
|
+
/** The filter radius for a receiver `paneToReceiver` behind a pane of this clarity. */
|
|
78
|
+
export declare function frostRadius(clarity: number, paneToReceiver: number, baseRadius: number): number;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coloured light through glass: how one pane filters light, how panes combine, which receivers a
|
|
3
|
+
* pane tints, and how far frost spreads the colour — the reference the shaders state term for term.
|
|
4
|
+
*
|
|
5
|
+
* **Frost spreads light, it does not remove it.** Etched glass passes 88–91% of visible light
|
|
6
|
+
* against clear float glass's ~90%, so a frosted pane passes `transmission × (1 − F) × tint` like a
|
|
7
|
+
* clear one and only the filter widens. What it gives up: the spread stays inside the opaque frame's
|
|
8
|
+
* shadow, so light from a frosted pane does not reach past the outline of the frame around it. What
|
|
9
|
+
* would make it wrong is glass that genuinely absorbs as it diffuses — opal glass, milk glass — for
|
|
10
|
+
* which a lower `transmission` is the honest statement.
|
|
11
|
+
*
|
|
12
|
+
* **What else it does not reach**: light that bounces. DriftRay's probe bake and DriftLight's summed
|
|
13
|
+
* far field see a pane as absent, so the light a tinted window sends round a room by bouncing is its
|
|
14
|
+
* sun's own colour; only the direct light through the pane takes the tint.
|
|
15
|
+
*/
|
|
16
|
+
import { schlickFresnel } from './glass.js';
|
|
17
|
+
/** No glass: everything passes, perfectly clear. What a tint map is cleared to. */
|
|
18
|
+
export const CLEAR_TEXEL = { r: 1, g: 1, b: 1, clarity: 1 };
|
|
19
|
+
/** How widely a fully frosted pane spreads its colour: a 30° half-angle cone behind it. */
|
|
20
|
+
export const FROST_SPREAD = Math.tan(Math.PI / 6);
|
|
21
|
+
/**
|
|
22
|
+
* The spread never exceeds this many base filter radii, and the tap count never changes.
|
|
23
|
+
*
|
|
24
|
+
* **Sixty-four, because the tint is read from the mip level the spread asks for**: each tap is then
|
|
25
|
+
* already an average over its own share of the spread, so taps far apart do not alias into grain.
|
|
26
|
+
* It was eight before the tint had mips, and on a sun map a few centimetres a texel that capped a
|
|
27
|
+
* frosted pane's spread at seven centimetres — frost that nobody saw. What the cap still bounds is
|
|
28
|
+
* a pane so frosted and so far above a floor that its colour would wash over a room.
|
|
29
|
+
*/
|
|
30
|
+
export const FROST_RADIUS_CAP = 64;
|
|
31
|
+
/** One pane as a tint texel, lit at `cosLight` to its normal. Fresnel as `glass.ts` states it. */
|
|
32
|
+
export function paneTexel(glass, cosLight, out) {
|
|
33
|
+
const passes = glass.transmission * (1 - schlickFresnel(cosLight));
|
|
34
|
+
out.r = passes * glass.tint[0];
|
|
35
|
+
out.g = passes * glass.tint[1];
|
|
36
|
+
out.b = passes * glass.tint[2];
|
|
37
|
+
out.clarity = 1 - glass.frost;
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
/** Two stacks of glass on one ray: a product, so the order they are drawn in cannot matter. */
|
|
41
|
+
export function combinePanes(a, b, out) {
|
|
42
|
+
out.r = a.r * b.r;
|
|
43
|
+
out.g = a.g * b.g;
|
|
44
|
+
out.b = a.b * b.b;
|
|
45
|
+
out.clarity = a.clarity * b.clarity;
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* What one filter tap contributes: the glass's texel where the receiver is behind the pane, and
|
|
50
|
+
* nothing where it is on the light's side of it — or is the pane, which the bias keeps untinted.
|
|
51
|
+
*/
|
|
52
|
+
export function tapTint(receiverDepth, glassDepth, bias, glass, out) {
|
|
53
|
+
const behind = receiverDepth > glassDepth + bias;
|
|
54
|
+
out.r = behind ? glass.r : 1;
|
|
55
|
+
out.g = behind ? glass.g : 1;
|
|
56
|
+
out.b = behind ? glass.b : 1;
|
|
57
|
+
out.clarity = behind ? glass.clarity : 1;
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A frosted pane's tint at a receiver: the outline from the taps at the base radius, the colour
|
|
62
|
+
* from the spread taps that found glass — nothing else.
|
|
63
|
+
*
|
|
64
|
+
* **Split because a tint cannot carry light out past a pane.** Blurring the tint map whole pulls in
|
|
65
|
+
* the white of the unobstructed ground around a pane, which brightens the patch — measured at 10%
|
|
66
|
+
* more light over a frosted window than a clear one — while the pane's own light, which should
|
|
67
|
+
* spread onto that ground, has no way to exceed full sun there. So the outline stays where the
|
|
68
|
+
* clear pane's is (`coverage`, the share of outline taps behind glass) and only the colour spreads,
|
|
69
|
+
* averaged over glass alone (`colourSum` over `colourCount`, the summed pane shares `unmixTint`
|
|
70
|
+
* reports — continuous, so a colour crossing between panes blends rather than stepping): neighbouring
|
|
71
|
+
* panes mix, a pattern blurs, and the light through the patch is what it was. What it gives up is
|
|
72
|
+
* spread past the outline, which the spec states; with no glass found by the spread the colour is
|
|
73
|
+
* clear.
|
|
74
|
+
*/
|
|
75
|
+
export function spreadTint(coverage, colourSum, colourCount, out) {
|
|
76
|
+
const r = colourCount > 0 ? colourSum.r / colourCount : 1;
|
|
77
|
+
const g = colourCount > 0 ? colourSum.g / colourCount : 1;
|
|
78
|
+
const b = colourCount > 0 ? colourSum.b / colourCount : 1;
|
|
79
|
+
out.r = 1 + (r - 1) * coverage;
|
|
80
|
+
out.g = 1 + (g - 1) * coverage;
|
|
81
|
+
out.b = 1 + (b - 1) * coverage;
|
|
82
|
+
out.clarity = 1;
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A coarse level of the tint, with the open ground's white taken back out: returns the pane's share
|
|
87
|
+
* of the texel and writes the pane's own colour to `out`.
|
|
88
|
+
*
|
|
89
|
+
* **Why a level needs this**: it averages a pane with whatever lies beside it, and beside a pane is
|
|
90
|
+
* usually open ground, which the tint holds as white — so a frosted patch read coarse is paler than
|
|
91
|
+
* the pane, and measured 6% more light than the clear pane over the same floor. The clarity channel
|
|
92
|
+
* says how much of the texel was pane: open ground holds 1, the pane its own `centreClarity`, so
|
|
93
|
+
* the pane's share is `(1 − a) / (1 − centreClarity)` and its colour follows by subtraction. What it
|
|
94
|
+
* assumes is that the frost around a receiver is the frost at its centre, which is true within a
|
|
95
|
+
* pane and blends where two panes of different frost meet; a clear pane (`centreClarity` 1) is
|
|
96
|
+
* never read coarse, so it is returned as it stands.
|
|
97
|
+
*/
|
|
98
|
+
export function unmixTint(mip, centreClarity, out) {
|
|
99
|
+
if (centreClarity >= 1) {
|
|
100
|
+
out.r = mip.r;
|
|
101
|
+
out.g = mip.g;
|
|
102
|
+
out.b = mip.b;
|
|
103
|
+
out.clarity = mip.clarity;
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
const share = Math.min(1, Math.max(0, (1 - mip.clarity) / (1 - centreClarity)));
|
|
107
|
+
if (share <= 0) {
|
|
108
|
+
out.r = 1;
|
|
109
|
+
out.g = 1;
|
|
110
|
+
out.b = 1;
|
|
111
|
+
out.clarity = 1;
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
const ground = 1 - share;
|
|
115
|
+
out.r = Math.min(1, Math.max(0, (mip.r - ground) / share));
|
|
116
|
+
out.g = Math.min(1, Math.max(0, (mip.g - ground) / share));
|
|
117
|
+
out.b = Math.min(1, Math.max(0, (mip.b - ground) / share));
|
|
118
|
+
out.clarity = centreClarity;
|
|
119
|
+
return share;
|
|
120
|
+
}
|
|
121
|
+
/** The filter radius for a receiver `paneToReceiver` behind a pane of this clarity. */
|
|
122
|
+
export function frostRadius(clarity, paneToReceiver, baseRadius) {
|
|
123
|
+
const spread = (1 - clarity) * FROST_SPREAD * Math.max(paneToReceiver, 0);
|
|
124
|
+
return Math.min(Math.max(baseRadius, spread), baseRadius * FROST_RADIUS_CAP);
|
|
125
|
+
}
|
|
@@ -11,9 +11,8 @@ export interface MediumLight {
|
|
|
11
11
|
readonly sunShadow: number;
|
|
12
12
|
/** World → light clip, **uncorrected**: the shader does its own `* 0.5 + 0.5`. */
|
|
13
13
|
readonly lightViewProj: ReadonlyMat4;
|
|
14
|
-
|
|
15
|
-
readonly
|
|
16
|
-
readonly dynamicShadowMap: WebGLTexture;
|
|
14
|
+
/** The sun's static, moving and peeled maps, as one array. See `shadowMap.ts`. */
|
|
15
|
+
readonly sunShadows: WebGLTexture;
|
|
17
16
|
readonly peeledEnabled: boolean;
|
|
18
17
|
}
|
|
19
18
|
/**
|
|
@@ -158,14 +158,8 @@ export class GlobalMediumPass {
|
|
|
158
158
|
gl.bindTexture(gl.TEXTURE_2D, depth);
|
|
159
159
|
gl.uniform1i(u['uDepth'] ?? null, 0);
|
|
160
160
|
gl.activeTexture(gl.TEXTURE1);
|
|
161
|
-
gl.bindTexture(gl.
|
|
162
|
-
gl.uniform1i(u['
|
|
163
|
-
gl.activeTexture(gl.TEXTURE2);
|
|
164
|
-
gl.bindTexture(gl.TEXTURE_2D, light.peeledShadowMap);
|
|
165
|
-
gl.uniform1i(u['uPeeledShadowMap'] ?? null, 2);
|
|
166
|
-
gl.activeTexture(gl.TEXTURE3);
|
|
167
|
-
gl.bindTexture(gl.TEXTURE_2D, light.dynamicShadowMap);
|
|
168
|
-
gl.uniform1i(u['uDynamicShadowMap'] ?? null, 3);
|
|
161
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, light.sunShadows);
|
|
162
|
+
gl.uniform1i(u['uSunShadows'] ?? null, 1);
|
|
169
163
|
gl.uniformMatrix4fv(u['uDepthToWorld'] ?? null, false, this.depthToWorld);
|
|
170
164
|
gl.uniform3f(u['uCameraPos'] ?? null, eye[0] ?? 0, eye[1] ?? 0, eye[2] ?? 0);
|
|
171
165
|
gl.uniform3f(u['uSunDir'] ?? null, light.sunDir[0] ?? 0, light.sunDir[1] ?? 1, light.sunDir[2] ?? 0);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** What the second pipeline's shading pass computes, in TypeScript, so something can disagree. */
|
|
2
|
+
import { type ResolvedGlass } from '../glass.ts';
|
|
2
3
|
/**
|
|
3
4
|
* **The lit expression had no reference until 2026-09-16, and that was a gap rather than a policy.**
|
|
4
5
|
*
|
|
@@ -163,3 +164,14 @@ export declare function newLitColour(): Float32Array;
|
|
|
163
164
|
* so the two orders had never been asked to differ.
|
|
164
165
|
*/
|
|
165
166
|
export declare function litColour(surface: LitSurface, environment: LitEnvironment, out: Float32Array): Float32Array;
|
|
167
|
+
/**
|
|
168
|
+
* Glass over its own shading: what is behind it shown by how much it lets through and how little it
|
|
169
|
+
* reflects at this angle, and the light behind it glowing through by how frosted it is — the forward
|
|
170
|
+
* path's glass branch (`flat/main.ts`) and `glass.ts`'s arithmetic, for the blended raster's twin.
|
|
171
|
+
*
|
|
172
|
+
* **`cosView` is taken unsigned**, because a pane passes the same light seen from either side.
|
|
173
|
+
* `sunGlow` is the light arriving at the pane's far side from the eye, which frost scatters toward
|
|
174
|
+
* it; a clear pane shows that light's source in `seen` instead. Allocates nothing: `out` is the
|
|
175
|
+
* caller's, and may be `own`.
|
|
176
|
+
*/
|
|
177
|
+
export declare function glassShade(own: readonly [number, number, number], seen: readonly [number, number, number], glass: ResolvedGlass, cosView: number, sunGlow: readonly [number, number, number], out: [number, number, number]): [number, number, number];
|