@driftengine/core 4.2.1 → 4.4.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 +272 -0
- package/README.md +1 -1
- package/dist/cinematic/cameraPath.d.ts +53 -0
- package/dist/cinematic/cameraPath.js +118 -0
- package/dist/environment/daylight.d.ts +77 -0
- package/dist/environment/daylight.js +77 -0
- package/dist/geometry/meshConcat.d.ts +9 -0
- package/dist/geometry/meshConcat.js +101 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +14 -1
- package/dist/render/backend/createRenderer.d.ts +13 -1
- package/dist/render/backend/createRenderer.js +6 -5
- package/dist/render/backend/webgl2/renderer.d.ts +166 -1
- package/dist/render/backend/webgl2/renderer.js +424 -23
- package/dist/render/backend/webgpu/buffers.js +46 -8
- package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
- package/dist/render/backend/webgpu/depthPass.js +75 -14
- package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
- package/dist/render/backend/webgpu/exposurePass.js +190 -0
- package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
- package/dist/render/backend/webgpu/fieldCompose.js +13 -0
- package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
- package/dist/render/backend/webgpu/flatPass.js +12 -5
- package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
- package/dist/render/backend/webgpu/particlePass.js +1 -0
- package/dist/render/backend/webgpu/postPass.d.ts +12 -2
- package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
- package/dist/render/backend/webgpu/probeBake.js +97 -7
- package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
- package/dist/render/backend/webgpu/probeTargets.js +180 -0
- package/dist/render/backend/webgpu/renderer.d.ts +176 -11
- package/dist/render/backend/webgpu/renderer.js +680 -250
- package/dist/render/backend/webgpu/scatterPass.js +7 -7
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
- package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
- package/dist/render/backend/webgpu/skyPass.js +5 -1
- package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
- package/dist/render/backend/webgpu/uniformRing.js +53 -3
- package/dist/render/bloomChain.d.ts +6 -0
- package/dist/render/bloomChain.js +8 -0
- package/dist/render/clusteredLights.d.ts +19 -11
- package/dist/render/clusteredLights.js +96 -45
- package/dist/render/cutoutCaster.d.ts +23 -0
- package/dist/render/cutoutCaster.js +20 -0
- package/dist/render/driftLight/bake.d.ts +34 -0
- package/dist/render/driftLight/bake.js +95 -0
- package/dist/render/driftLight/falloff.d.ts +16 -0
- package/dist/render/driftLight/falloff.js +23 -0
- package/dist/render/driftLight/layout.d.ts +59 -0
- package/dist/render/driftLight/layout.js +138 -0
- package/dist/render/driftLight/lightField.d.ts +108 -0
- package/dist/render/driftLight/lightField.js +179 -0
- package/dist/render/driftLight/uniforms.d.ts +28 -0
- package/dist/render/driftLight/uniforms.js +41 -0
- package/dist/render/driftLight/visibility.d.ts +18 -0
- package/dist/render/driftLight/visibility.js +47 -0
- package/dist/render/emptyTexture.d.ts +6 -0
- package/dist/render/emptyTexture.js +19 -0
- package/dist/render/exposurePass.d.ts +48 -0
- package/dist/render/exposurePass.js +172 -0
- package/dist/render/flameLight.d.ts +71 -0
- package/dist/render/flameLight.js +63 -0
- package/dist/render/gi/bounceLights.d.ts +34 -0
- package/dist/render/gi/bounceLights.js +66 -0
- package/dist/render/gi/fieldReach.d.ts +12 -0
- package/dist/render/gi/fieldReach.js +62 -0
- package/dist/render/gi/globalField.d.ts +8 -0
- package/dist/render/gi/globalField.js +31 -0
- package/dist/render/gi/probeTrace.d.ts +3 -1
- package/dist/render/gi/probeTrace.js +3 -1
- package/dist/render/gpuCapability.js +19 -4
- package/dist/render/lightBudget.d.ts +11 -0
- package/dist/render/lightBudget.js +11 -0
- package/dist/render/pointLightSelection.d.ts +16 -0
- package/dist/render/pointLightSelection.js +178 -83
- package/dist/render/probeGrid.d.ts +11 -1
- package/dist/render/probeGrid.js +5 -2
- package/dist/render/probeSweeps.d.ts +45 -0
- package/dist/render/probeSweeps.js +77 -0
- package/dist/render/reflectionProbe.d.ts +43 -1
- package/dist/render/reflectionProbe.js +17 -3
- package/dist/render/renderQuality.d.ts +3 -1
- package/dist/render/rendererHarness.d.ts +9 -0
- package/dist/render/rendererHarness.js +18 -3
- package/dist/render/sceneTarget.d.ts +20 -0
- package/dist/render/sceneTarget.js +62 -1
- package/dist/render/shaders/agx.d.ts +31 -0
- package/dist/render/shaders/agx.js +81 -0
- package/dist/render/shaders/ambientOcclusion.d.ts +24 -5
- package/dist/render/shaders/ambientOcclusion.js +120 -63
- package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
- package/dist/render/shaders/cutoutCoverage.js +40 -0
- package/dist/render/shaders/depth.d.ts +25 -0
- package/dist/render/shaders/depth.js +69 -3
- package/dist/render/shaders/exposure.d.ts +52 -0
- package/dist/render/shaders/exposure.js +124 -0
- package/dist/render/shaders/filmLook.d.ts +47 -0
- package/dist/render/shaders/filmLook.js +98 -0
- package/dist/render/shaders/flat/directionalShadow.js +35 -4
- package/dist/render/shaders/flat/driftLight.d.ts +29 -0
- package/dist/render/shaders/flat/driftLight.js +75 -0
- package/dist/render/shaders/flat/index.js +5 -0
- package/dist/render/shaders/flat/lobes.js +13 -0
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +68 -9
- package/dist/render/shaders/flat/preamble.js +23 -5
- package/dist/render/shaders/flat/probeGrid.js +46 -3
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +1 -1
- package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
- package/dist/render/shaders/generated/depth.wgsl.js +100 -0
- package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
- package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
- package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
- package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
- package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
- package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
- package/dist/render/shaders/generated/rush.wgsl.js +40 -10
- package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
- package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
- package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
- package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
- package/dist/render/shaders/globalMedium.js +4 -0
- package/dist/render/shaders/localExposure.d.ts +35 -0
- package/dist/render/shaders/localExposure.js +138 -0
- package/dist/render/shaders/prefilter.d.ts +1 -1
- package/dist/render/shaders/prefilter.js +6 -0
- package/dist/render/shaders/rush.js +33 -5
- package/dist/render/shaders/tangentFrame.d.ts +1 -1
- package/dist/render/shaders/tangentFrame.js +15 -3
- package/dist/render/shaders/temporalResolve.js +39 -1
- package/dist/render/shadowCasters.d.ts +5 -3
- package/dist/render/shadowMap.js +13 -0
- package/dist/render/surfaceTexture.d.ts +5 -0
- package/package.json +6 -6
- package/scripts/gpuCompute.mjs +24 -3
- package/scripts/png.mjs +23 -12
- package/src/cinematic/cameraPath.ts +178 -0
- package/src/environment/daylight.ts +152 -0
- package/src/geometry/meshConcat.ts +100 -0
- package/src/index.ts +32 -0
- package/src/render/backend/createRenderer.ts +22 -6
- package/src/render/backend/webgl2/renderer.ts +462 -20
- package/src/render/backend/webgpu/buffers.ts +51 -8
- package/src/render/backend/webgpu/depthPass.ts +78 -10
- package/src/render/backend/webgpu/exposurePass.ts +234 -0
- package/src/render/backend/webgpu/fieldCompose.ts +15 -0
- package/src/render/backend/webgpu/flatPass.ts +10 -2
- package/src/render/backend/webgpu/particlePass.ts +9 -0
- package/src/render/backend/webgpu/probeBake.ts +143 -9
- package/src/render/backend/webgpu/probeTargets.ts +211 -0
- package/src/render/backend/webgpu/renderer.ts +807 -271
- package/src/render/backend/webgpu/scatterPass.ts +7 -7
- package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
- package/src/render/backend/webgpu/skyPass.ts +5 -1
- package/src/render/backend/webgpu/uniformRing.ts +49 -3
- package/src/render/bloomChain.ts +9 -0
- package/src/render/clusteredLights.ts +108 -63
- package/src/render/cutoutCaster.ts +44 -0
- package/src/render/driftLight/bake.ts +125 -0
- package/src/render/driftLight/falloff.ts +26 -0
- package/src/render/driftLight/layout.ts +177 -0
- package/src/render/driftLight/lightField.ts +227 -0
- package/src/render/driftLight/uniforms.ts +61 -0
- package/src/render/driftLight/visibility.ts +58 -0
- package/src/render/emptyTexture.ts +41 -0
- package/src/render/exposurePass.ts +186 -0
- package/src/render/flameLight.ts +135 -0
- package/src/render/gi/bounceLights.ts +68 -0
- package/src/render/gi/fieldReach.ts +74 -0
- package/src/render/gi/globalField.ts +39 -0
- package/src/render/gi/probeTrace.ts +3 -1
- package/src/render/gpuCapability.ts +18 -5
- package/src/render/lightBudget.ts +12 -0
- package/src/render/pointLightSelection.ts +191 -79
- package/src/render/probeGrid.ts +13 -2
- package/src/render/probeSweeps.ts +80 -0
- package/src/render/reflectionProbe.ts +54 -2
- package/src/render/renderQuality.ts +3 -1
- package/src/render/rendererHarness.ts +25 -3
- package/src/render/sceneTarget.ts +75 -1
- package/src/render/shaders/ambientOcclusion.ts +125 -63
- package/src/render/shaders/cutoutCoverage.ts +43 -0
- package/src/render/shaders/depth.ts +80 -3
- package/src/render/shaders/exposure.ts +128 -0
- package/src/render/shaders/filmLook.ts +111 -0
- package/src/render/shaders/flat/directionalShadow.ts +35 -4
- package/src/render/shaders/flat/driftLight.ts +75 -0
- package/src/render/shaders/flat/index.ts +5 -0
- package/src/render/shaders/flat/lobes.ts +13 -0
- package/src/render/shaders/flat/main.ts +68 -9
- package/src/render/shaders/flat/preamble.ts +23 -5
- package/src/render/shaders/flat/probeGrid.ts +46 -3
- package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +1 -1
- package/src/render/shaders/generated/depth.wgsl.ts +103 -0
- package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
- package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
- package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
- package/src/render/shaders/generated/rush.wgsl.ts +40 -10
- package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
- package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
- package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
- package/src/render/shaders/globalMedium.ts +4 -0
- package/src/render/shaders/localExposure.ts +154 -0
- package/src/render/shaders/prefilter.ts +6 -0
- package/src/render/shaders/rush.ts +33 -5
- package/src/render/shaders/tangentFrame.ts +15 -3
- package/src/render/shaders/temporalResolve.ts +39 -1
- package/src/render/shadowCasters.ts +5 -3
- package/src/render/shadowMap.ts +14 -0
- package/src/render/surfaceTexture.ts +5 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the lit shader is told about a DriftLight field this pass, decided once for both backends.
|
|
3
|
+
*
|
|
4
|
+
* **Decided here and bound there**, by the 2026-08-13 rule: each backend only uploads these numbers
|
|
5
|
+
* to the names `driftLight.ts` declares, so the two cannot come to disagree about when the field is
|
|
6
|
+
* on or what radius a pass uses. Off, which the shader reads as `light.x` of zero, until a field is
|
|
7
|
+
* whole; and during a probe bake the radius is zero, because the exact lights were chosen for the
|
|
8
|
+
* camera and a probe stands somewhere else: a probe sees every fixed light through the volume.
|
|
9
|
+
*
|
|
10
|
+
* **The radius is handed over measured from the eye**, because the shader measures from the
|
|
11
|
+
* `uCameraPos` it already has rather than spending a uniform on the choice's centre. A point within
|
|
12
|
+
* `r − |eye − centre|` of the eye is within `r` of the centre, so taking the offset off keeps every
|
|
13
|
+
* pixel the shader shades exactly inside the radius the selection promised. A mirror's camera, far
|
|
14
|
+
* from the centre, gets a smaller radius and more of the volume, which is the safe direction.
|
|
15
|
+
*/
|
|
16
|
+
import type { LightField } from './lightField.ts';
|
|
17
|
+
export interface DriftLightUniforms {
|
|
18
|
+
/** How much of the summed light is in, the radius, the band, the scale. */
|
|
19
|
+
readonly light: Float32Array;
|
|
20
|
+
/** The first brick's first sample, and the metres between samples. */
|
|
21
|
+
readonly origin: Float32Array;
|
|
22
|
+
}
|
|
23
|
+
export declare function createDriftLightUniforms(): DriftLightUniforms;
|
|
24
|
+
/**
|
|
25
|
+
* Fill `out` for `field`, or for no field, seen from `eye`; `probePass` while a probe's faces are
|
|
26
|
+
* being drawn.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveDriftLight(field: LightField | null, probePass: boolean, eye: ArrayLike<number>, out: DriftLightUniforms): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the lit shader is told about a DriftLight field this pass, decided once for both backends.
|
|
3
|
+
*
|
|
4
|
+
* **Decided here and bound there**, by the 2026-08-13 rule: each backend only uploads these numbers
|
|
5
|
+
* to the names `driftLight.ts` declares, so the two cannot come to disagree about when the field is
|
|
6
|
+
* on or what radius a pass uses. Off, which the shader reads as `light.x` of zero, until a field is
|
|
7
|
+
* whole; and during a probe bake the radius is zero, because the exact lights were chosen for the
|
|
8
|
+
* camera and a probe stands somewhere else: a probe sees every fixed light through the volume.
|
|
9
|
+
*
|
|
10
|
+
* **The radius is handed over measured from the eye**, because the shader measures from the
|
|
11
|
+
* `uCameraPos` it already has rather than spending a uniform on the choice's centre. A point within
|
|
12
|
+
* `r − |eye − centre|` of the eye is within `r` of the centre, so taking the offset off keeps every
|
|
13
|
+
* pixel the shader shades exactly inside the radius the selection promised. A mirror's camera, far
|
|
14
|
+
* from the centre, gets a smaller radius and more of the volume, which is the safe direction.
|
|
15
|
+
*/
|
|
16
|
+
export function createDriftLightUniforms() {
|
|
17
|
+
return {
|
|
18
|
+
light: new Float32Array(4),
|
|
19
|
+
origin: new Float32Array(4),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Fill `out` for `field`, or for no field, seen from `eye`; `probePass` while a probe's faces are
|
|
24
|
+
* being drawn.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveDriftLight(field, probePass, eye, out) {
|
|
27
|
+
if (field === null || !field.ready || field.presence <= 0 || field.layout.count === 0) {
|
|
28
|
+
out.light.fill(0);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const { layout } = field;
|
|
32
|
+
const offset = Math.hypot((eye[0] ?? 0) - field.centre[0], (eye[1] ?? 0) - field.centre[1], (eye[2] ?? 0) - field.centre[2]);
|
|
33
|
+
out.light[0] = field.presence;
|
|
34
|
+
out.light[1] = probePass ? 0 : Math.max(0, field.radius - offset);
|
|
35
|
+
out.light[2] = field.band;
|
|
36
|
+
out.light[3] = field.scale;
|
|
37
|
+
out.origin[0] = layout.origin[0];
|
|
38
|
+
out.origin[1] = layout.origin[1];
|
|
39
|
+
out.origin[2] = layout.origin[2];
|
|
40
|
+
out.origin[3] = layout.spacing;
|
|
41
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much of a light a point sees, through a signed distance field: DriftLight's occlusion.
|
|
3
|
+
*
|
|
4
|
+
* **Sphere-traced, with a penumbra from the light's own size.** A spherical light of radius R seen
|
|
5
|
+
* from a distance L fills a cone that is R * t / L across at a distance t along the way, so a surface
|
|
6
|
+
* a clearance h from the ray covers the fraction of it that h falls short of that radius. The least
|
|
7
|
+
* such ratio along the ray is the visibility: 1 in open air, 0 behind a wall, and a soft ramp at an
|
|
8
|
+
* edge that widens with the light, which is what `AGENTS.md` asks of a light with a physical size.
|
|
9
|
+
*
|
|
10
|
+
* The trace stops before the light by its own radius, or 5 cm where that is smaller, so a candle's
|
|
11
|
+
* wax does not shadow the flame sitting on it. What it gives up is exactness at a grazing corner,
|
|
12
|
+
* where one ratio stands for an area the cone only partly crosses; what would make it wrong is a
|
|
13
|
+
* field coarser than the lights it shadows, which blurs every occluder smaller than a cell.
|
|
14
|
+
*/
|
|
15
|
+
/** A signed distance: metres to the nearest surface, negative inside. */
|
|
16
|
+
export type DistanceAt = (x: number, y: number, z: number) => number;
|
|
17
|
+
/** The visibility, 0 to 1, of a light of radius `sourceRadius` at `to` from the point `from`. */
|
|
18
|
+
export declare function softVisibility(distance: DistanceAt, fx: number, fy: number, fz: number, tx: number, ty: number, tz: number, sourceRadius: number): number;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much of a light a point sees, through a signed distance field: DriftLight's occlusion.
|
|
3
|
+
*
|
|
4
|
+
* **Sphere-traced, with a penumbra from the light's own size.** A spherical light of radius R seen
|
|
5
|
+
* from a distance L fills a cone that is R * t / L across at a distance t along the way, so a surface
|
|
6
|
+
* a clearance h from the ray covers the fraction of it that h falls short of that radius. The least
|
|
7
|
+
* such ratio along the ray is the visibility: 1 in open air, 0 behind a wall, and a soft ramp at an
|
|
8
|
+
* edge that widens with the light, which is what `AGENTS.md` asks of a light with a physical size.
|
|
9
|
+
*
|
|
10
|
+
* The trace stops before the light by its own radius, or 5 cm where that is smaller, so a candle's
|
|
11
|
+
* wax does not shadow the flame sitting on it. What it gives up is exactness at a grazing corner,
|
|
12
|
+
* where one ratio stands for an area the cone only partly crosses; what would make it wrong is a
|
|
13
|
+
* field coarser than the lights it shadows, which blurs every occluder smaller than a cell.
|
|
14
|
+
*/
|
|
15
|
+
/** Steps before the trace gives up and answers what it has; a light a metre off needs a dozen. */
|
|
16
|
+
const MAX_STEPS = 64;
|
|
17
|
+
/** The least a step advances, so a ray grazing a surface still reaches the light. */
|
|
18
|
+
const MIN_STEP_M = 0.01;
|
|
19
|
+
/** Where the trace starts, clear of the surface the point may be lying on. */
|
|
20
|
+
const START_M = 0.02;
|
|
21
|
+
/** The visibility, 0 to 1, of a light of radius `sourceRadius` at `to` from the point `from`. */
|
|
22
|
+
export function softVisibility(distance, fx, fy, fz, tx, ty, tz, sourceRadius) {
|
|
23
|
+
const dx = tx - fx;
|
|
24
|
+
const dy = ty - fy;
|
|
25
|
+
const dz = tz - fz;
|
|
26
|
+
const length = Math.hypot(dx, dy, dz);
|
|
27
|
+
if (length < 1e-6)
|
|
28
|
+
return 1;
|
|
29
|
+
const ux = dx / length;
|
|
30
|
+
const uy = dy / length;
|
|
31
|
+
const uz = dz / length;
|
|
32
|
+
const radius = Math.max(sourceRadius, 1e-3);
|
|
33
|
+
const end = length - Math.max(radius, 0.05);
|
|
34
|
+
let seen = 1;
|
|
35
|
+
let t = START_M;
|
|
36
|
+
for (let step = 0; step < MAX_STEPS && t < end; step++) {
|
|
37
|
+
const h = distance(fx + ux * t, fy + uy * t, fz + uz * t);
|
|
38
|
+
if (h <= 0)
|
|
39
|
+
return 0;
|
|
40
|
+
const cone = (radius * t) / length;
|
|
41
|
+
seen = Math.min(seen, h / cone);
|
|
42
|
+
if (seen <= 0)
|
|
43
|
+
return 0;
|
|
44
|
+
t += Math.max(h, MIN_STEP_M);
|
|
45
|
+
}
|
|
46
|
+
return Math.min(1, seen);
|
|
47
|
+
}
|
|
@@ -58,3 +58,9 @@ export declare function createEmptyTexture2DArray(gl: WebGL2RenderingContext): W
|
|
|
58
58
|
* complete the other five are.
|
|
59
59
|
*/
|
|
60
60
|
export declare function createEmptyTextureCube(gl: WebGL2RenderingContext): WebGLTexture;
|
|
61
|
+
/**
|
|
62
|
+
* The same for DriftLight's two volumes while a scene has no field: one texel of an unsigned
|
|
63
|
+
* integer, reading "no brick here", or of zero light. GLSL ES gives a 3D sampler no default and a
|
|
64
|
+
* lookup through an unbound one is the undefined read this file exists to prevent.
|
|
65
|
+
*/
|
|
66
|
+
export declare function createEmptyTexture3D(gl: WebGL2RenderingContext, integer: boolean): WebGLTexture;
|
|
@@ -101,3 +101,22 @@ export function createEmptyTextureCube(gl) {
|
|
|
101
101
|
gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
|
|
102
102
|
return texture;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* The same for DriftLight's two volumes while a scene has no field: one texel of an unsigned
|
|
106
|
+
* integer, reading "no brick here", or of zero light. GLSL ES gives a 3D sampler no default and a
|
|
107
|
+
* lookup through an unbound one is the undefined read this file exists to prevent.
|
|
108
|
+
*/
|
|
109
|
+
export function createEmptyTexture3D(gl, integer) {
|
|
110
|
+
const texture = allocate(gl, 'emptyTexture3D');
|
|
111
|
+
gl.bindTexture(gl.TEXTURE_3D, texture);
|
|
112
|
+
if (integer) {
|
|
113
|
+
gl.texImage3D(gl.TEXTURE_3D, 0, gl.R32UI, 1, 1, 1, 0, gl.RED_INTEGER, gl.UNSIGNED_INT, new Uint32Array(1));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
gl.texImage3D(gl.TEXTURE_3D, 0, gl.RGBA8, 1, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(4));
|
|
117
|
+
}
|
|
118
|
+
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
119
|
+
gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
120
|
+
gl.bindTexture(gl.TEXTURE_3D, null);
|
|
121
|
+
return texture;
|
|
122
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eye adaptation on WebGL2: meter the finished scene, and follow it with a held brightness the
|
|
3
|
+
* composite reads. See `shaders/exposure.ts` for the design; this owns the targets and the draws.
|
|
4
|
+
*
|
|
5
|
+
* **Two one-texel targets that swap roles each frame**, because the adaptation reads what it held
|
|
6
|
+
* and a texture cannot be read and written in one draw. The composite is handed whichever was
|
|
7
|
+
* written last. `R16F`, because the held value is in stops and eight bits of a range twenty stops
|
|
8
|
+
* wide is an exposure that steps visibly as it moves.
|
|
9
|
+
*
|
|
10
|
+
* **And local exposure's grid, when that is asked for**: `RG16F`, a sum and a count per tile per
|
|
11
|
+
* band, filtered linearly because the composite reads it between tiles. See `localExposure.ts`.
|
|
12
|
+
*/
|
|
13
|
+
export declare class ExposurePass {
|
|
14
|
+
private readonly gl;
|
|
15
|
+
private meter;
|
|
16
|
+
private meterFramebuffer;
|
|
17
|
+
private localGrid;
|
|
18
|
+
private localFramebuffer;
|
|
19
|
+
private readonly held;
|
|
20
|
+
private readonly heldFramebuffers;
|
|
21
|
+
/** Which of the two held texels the next adaptation writes. */
|
|
22
|
+
private writing;
|
|
23
|
+
/** Whether anything is held yet: the first frame, and the first after a cut, snaps. */
|
|
24
|
+
private holding;
|
|
25
|
+
private allocated;
|
|
26
|
+
/** Set once if a half-float target will not complete; the pass is then off and says so once. */
|
|
27
|
+
private unavailable;
|
|
28
|
+
private readonly meterProgram;
|
|
29
|
+
private readonly meterUniforms;
|
|
30
|
+
private readonly adaptProgram;
|
|
31
|
+
private readonly adaptUniforms;
|
|
32
|
+
private readonly localProgram;
|
|
33
|
+
private readonly localUniforms;
|
|
34
|
+
private readonly vao;
|
|
35
|
+
constructor(gl: WebGL2RenderingContext);
|
|
36
|
+
/** Forget what is held, so the next frame meters afresh rather than easing from another shot. */
|
|
37
|
+
cut(): void;
|
|
38
|
+
private ensure;
|
|
39
|
+
/**
|
|
40
|
+
* Meter `scene` and move the held brightness toward it by `dtSec` of adaptation, and with `local`
|
|
41
|
+
* draw local exposure's grid too. Returns the texel the composite reads, or null where this pass
|
|
42
|
+
* could not run, which the caller reads as off.
|
|
43
|
+
*/
|
|
44
|
+
run(scene: WebGLTexture, dtSec: number, local?: boolean): WebGLTexture | null;
|
|
45
|
+
/** Local exposure's grid as the last `run` drew it, or null before one has. */
|
|
46
|
+
get localTexture(): WebGLTexture | null;
|
|
47
|
+
dispose(): void;
|
|
48
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { EXPOSURE_ADAPT_FRAG, EXPOSURE_METER_FRAG, METER_GRID, adaptBlend, } from './shaders/exposure.js';
|
|
2
|
+
import { EXPOSURE_LOCAL_FRAG, LOCAL_GRID_WIDTH } from './shaders/localExposure.js';
|
|
3
|
+
import { FULLSCREEN_VERT } from './shaders/fullscreen.js';
|
|
4
|
+
import { compileProgram, uniformLocations } from './shader.js';
|
|
5
|
+
/**
|
|
6
|
+
* Eye adaptation on WebGL2: meter the finished scene, and follow it with a held brightness the
|
|
7
|
+
* composite reads. See `shaders/exposure.ts` for the design; this owns the targets and the draws.
|
|
8
|
+
*
|
|
9
|
+
* **Two one-texel targets that swap roles each frame**, because the adaptation reads what it held
|
|
10
|
+
* and a texture cannot be read and written in one draw. The composite is handed whichever was
|
|
11
|
+
* written last. `R16F`, because the held value is in stops and eight bits of a range twenty stops
|
|
12
|
+
* wide is an exposure that steps visibly as it moves.
|
|
13
|
+
*
|
|
14
|
+
* **And local exposure's grid, when that is asked for**: `RG16F`, a sum and a count per tile per
|
|
15
|
+
* band, filtered linearly because the composite reads it between tiles. See `localExposure.ts`.
|
|
16
|
+
*/
|
|
17
|
+
export class ExposurePass {
|
|
18
|
+
gl;
|
|
19
|
+
meter = null;
|
|
20
|
+
meterFramebuffer = null;
|
|
21
|
+
localGrid = null;
|
|
22
|
+
localFramebuffer = null;
|
|
23
|
+
held = [null, null];
|
|
24
|
+
heldFramebuffers = [null, null];
|
|
25
|
+
/** Which of the two held texels the next adaptation writes. */
|
|
26
|
+
writing = 0;
|
|
27
|
+
/** Whether anything is held yet: the first frame, and the first after a cut, snaps. */
|
|
28
|
+
holding = false;
|
|
29
|
+
allocated = false;
|
|
30
|
+
/** Set once if a half-float target will not complete; the pass is then off and says so once. */
|
|
31
|
+
unavailable = false;
|
|
32
|
+
meterProgram;
|
|
33
|
+
meterUniforms;
|
|
34
|
+
adaptProgram;
|
|
35
|
+
adaptUniforms;
|
|
36
|
+
localProgram;
|
|
37
|
+
localUniforms;
|
|
38
|
+
vao;
|
|
39
|
+
constructor(gl) {
|
|
40
|
+
this.gl = gl;
|
|
41
|
+
this.meterProgram = compileProgram(gl, FULLSCREEN_VERT, EXPOSURE_METER_FRAG, 'exposureMeter');
|
|
42
|
+
this.meterUniforms = uniformLocations(gl, this.meterProgram, 'exposureMeter');
|
|
43
|
+
this.adaptProgram = compileProgram(gl, FULLSCREEN_VERT, EXPOSURE_ADAPT_FRAG, 'exposureAdapt');
|
|
44
|
+
this.adaptUniforms = uniformLocations(gl, this.adaptProgram, 'exposureAdapt');
|
|
45
|
+
this.localProgram = compileProgram(gl, FULLSCREEN_VERT, EXPOSURE_LOCAL_FRAG, 'exposureLocal');
|
|
46
|
+
this.localUniforms = uniformLocations(gl, this.localProgram, 'exposureLocal');
|
|
47
|
+
const vao = gl.createVertexArray();
|
|
48
|
+
if (vao === null)
|
|
49
|
+
throw new Error('ExposurePass: createVertexArray failed');
|
|
50
|
+
this.vao = vao;
|
|
51
|
+
}
|
|
52
|
+
/** Forget what is held, so the next frame meters afresh rather than easing from another shot. */
|
|
53
|
+
cut() {
|
|
54
|
+
this.holding = false;
|
|
55
|
+
}
|
|
56
|
+
ensure() {
|
|
57
|
+
if (this.unavailable)
|
|
58
|
+
return false;
|
|
59
|
+
if (this.allocated)
|
|
60
|
+
return true;
|
|
61
|
+
const { gl } = this;
|
|
62
|
+
const target = (size, height = size, pair = false) => {
|
|
63
|
+
const texture = gl.createTexture();
|
|
64
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
65
|
+
if (pair)
|
|
66
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RG16F, size, height, 0, gl.RG, gl.HALF_FLOAT, null);
|
|
67
|
+
else
|
|
68
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.R16F, size, height, 0, gl.RED, gl.HALF_FLOAT, null);
|
|
69
|
+
const filter = pair ? gl.LINEAR : gl.NEAREST;
|
|
70
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
|
|
71
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
|
|
72
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
73
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
74
|
+
const framebuffer = gl.createFramebuffer();
|
|
75
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
76
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
77
|
+
const ok = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
|
|
78
|
+
return { texture, framebuffer, ok };
|
|
79
|
+
};
|
|
80
|
+
const meter = target(METER_GRID);
|
|
81
|
+
const first = target(1);
|
|
82
|
+
const second = target(1);
|
|
83
|
+
const local = target(LOCAL_GRID_WIDTH, METER_GRID, true);
|
|
84
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
85
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
86
|
+
this.meter = meter.texture;
|
|
87
|
+
this.meterFramebuffer = meter.framebuffer;
|
|
88
|
+
this.held[0] = first.texture;
|
|
89
|
+
this.held[1] = second.texture;
|
|
90
|
+
this.heldFramebuffers[0] = first.framebuffer;
|
|
91
|
+
this.heldFramebuffers[1] = second.framebuffer;
|
|
92
|
+
this.localGrid = local.texture;
|
|
93
|
+
this.localFramebuffer = local.framebuffer;
|
|
94
|
+
this.allocated = true;
|
|
95
|
+
if (!meter.ok || !first.ok || !second.ok || !local.ok) {
|
|
96
|
+
this.unavailable = true;
|
|
97
|
+
console.warn('ExposurePass: this driver will not render to a half-float target, so eye adaptation is ' +
|
|
98
|
+
'off and the exposure is the one set. The frame itself is unaffected.');
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Meter `scene` and move the held brightness toward it by `dtSec` of adaptation, and with `local`
|
|
105
|
+
* draw local exposure's grid too. Returns the texel the composite reads, or null where this pass
|
|
106
|
+
* could not run, which the caller reads as off.
|
|
107
|
+
*/
|
|
108
|
+
run(scene, dtSec, local = false) {
|
|
109
|
+
if (!this.ensure())
|
|
110
|
+
return null;
|
|
111
|
+
const { gl } = this;
|
|
112
|
+
gl.disable(gl.DEPTH_TEST);
|
|
113
|
+
gl.disable(gl.BLEND);
|
|
114
|
+
gl.disable(gl.CULL_FACE);
|
|
115
|
+
gl.depthMask(false);
|
|
116
|
+
gl.bindVertexArray(this.vao);
|
|
117
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.meterFramebuffer);
|
|
118
|
+
gl.viewport(0, 0, METER_GRID, METER_GRID);
|
|
119
|
+
gl.useProgram(this.meterProgram);
|
|
120
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
121
|
+
gl.bindTexture(gl.TEXTURE_2D, scene);
|
|
122
|
+
gl.uniform1i(this.meterUniforms['uScene'] ?? null, 0);
|
|
123
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
124
|
+
if (local) {
|
|
125
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.localFramebuffer);
|
|
126
|
+
gl.viewport(0, 0, LOCAL_GRID_WIDTH, METER_GRID);
|
|
127
|
+
gl.useProgram(this.localProgram);
|
|
128
|
+
gl.uniform1i(this.localUniforms['uScene'] ?? null, 0);
|
|
129
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
130
|
+
}
|
|
131
|
+
const into = this.writing;
|
|
132
|
+
const from = 1 - into;
|
|
133
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.heldFramebuffers[into] ?? null);
|
|
134
|
+
gl.viewport(0, 0, 1, 1);
|
|
135
|
+
gl.useProgram(this.adaptProgram);
|
|
136
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
137
|
+
gl.bindTexture(gl.TEXTURE_2D, this.meter);
|
|
138
|
+
gl.uniform1i(this.adaptUniforms['uMeter'] ?? null, 0);
|
|
139
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
140
|
+
gl.bindTexture(gl.TEXTURE_2D, this.held[from] ?? null);
|
|
141
|
+
gl.uniform1i(this.adaptUniforms['uHeld'] ?? null, 1);
|
|
142
|
+
gl.uniform1f(this.adaptUniforms['uBlend'] ?? null, adaptBlend(dtSec, !this.holding));
|
|
143
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
144
|
+
this.holding = true;
|
|
145
|
+
this.writing = from;
|
|
146
|
+
gl.bindVertexArray(null);
|
|
147
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
148
|
+
return this.held[into] ?? null;
|
|
149
|
+
}
|
|
150
|
+
/** Local exposure's grid as the last `run` drew it, or null before one has. */
|
|
151
|
+
get localTexture() {
|
|
152
|
+
return this.localGrid;
|
|
153
|
+
}
|
|
154
|
+
dispose() {
|
|
155
|
+
const { gl } = this;
|
|
156
|
+
for (const framebuffer of [
|
|
157
|
+
this.meterFramebuffer,
|
|
158
|
+
this.localFramebuffer,
|
|
159
|
+
...this.heldFramebuffers,
|
|
160
|
+
]) {
|
|
161
|
+
if (framebuffer !== null)
|
|
162
|
+
gl.deleteFramebuffer(framebuffer);
|
|
163
|
+
}
|
|
164
|
+
for (const texture of [this.meter, this.localGrid, ...this.held])
|
|
165
|
+
if (texture !== null)
|
|
166
|
+
gl.deleteTexture(texture);
|
|
167
|
+
gl.deleteProgram(this.meterProgram);
|
|
168
|
+
gl.deleteProgram(this.localProgram);
|
|
169
|
+
gl.deleteProgram(this.adaptProgram);
|
|
170
|
+
gl.deleteVertexArray(this.vao);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A flame: one point light and one plume from a single description, flickering at the rate a flame
|
|
3
|
+
* of that size flickers.
|
|
4
|
+
*
|
|
5
|
+
* **The rate is physics, not taste.** A buoyant flame puffs at about 1.5 / √D times a second for a
|
|
6
|
+
* burning area D metres across (Cetegen and Ahmed, 1993). That is 15 Hz for a candle's centimetre,
|
|
7
|
+
* which is why a candle trembles, and 2.4 Hz for a 40 cm brazier, which is why a fire breathes.
|
|
8
|
+
* Before this, a scene wired its plume, its light and a flicker amplitude by hand. The selection's
|
|
9
|
+
* shared wobble ran every light at the same two rates, so a candle and a bonfire pulsed together.
|
|
10
|
+
*
|
|
11
|
+
* **What the description fixes.** The flame stands 2.5 D tall, which is a candle's 2.5 cm and a
|
|
12
|
+
* brazier's metre. The light sits a third of the way up it, where a flame is brightest, and its
|
|
13
|
+
* physical radius is D / 2, so its shadows soften as a fire's do. It sways by up to 6 per cent of
|
|
14
|
+
* the flame's height on each axis, a few centimetres for a brazier, so a shadow it casts travels
|
|
15
|
+
* rather than only dimming.
|
|
16
|
+
*
|
|
17
|
+
* **What it gives up.** The waveform is two sines, the pulse and a harmonic, which never gust. A
|
|
18
|
+
* fire in wind wants its sway driven by the scene's `WindField`, and nothing here reads one. What
|
|
19
|
+
* would make the rate wrong is a flame far outside the law's range: a jet, or a pool fire of
|
|
20
|
+
* several metres, which puffs slower than the formula says.
|
|
21
|
+
*
|
|
22
|
+
* Plain data and two functions. `updateFlame` writes into the light it made and allocates nothing,
|
|
23
|
+
* so a scene updates every flame in its frame and hands the lights to `selectPointLights`.
|
|
24
|
+
*/
|
|
25
|
+
import type { Vec3 } from '../math/color.ts';
|
|
26
|
+
import type { PlumePlacement } from './plumeRenderer.ts';
|
|
27
|
+
import type { PointLightSource } from './pointLightSelection.ts';
|
|
28
|
+
export interface FlameOptions {
|
|
29
|
+
/** Where the fire burns from, world space: the wick's tip, the top of the coals. */
|
|
30
|
+
readonly x: number;
|
|
31
|
+
readonly y: number;
|
|
32
|
+
readonly z: number;
|
|
33
|
+
/** How wide the burning area is, metres: a candle 0.01, a lantern 0.03, a brazier's bowl 0.4. */
|
|
34
|
+
readonly diameterM: number;
|
|
35
|
+
/** The light's colour at its mean brightness, in the caller's light units. */
|
|
36
|
+
readonly color: Vec3;
|
|
37
|
+
/** Metres at which the light ends. */
|
|
38
|
+
readonly radius: number;
|
|
39
|
+
/** How far the brightness swings either side of its mean, 0 to 1. Defaults to 0.18. */
|
|
40
|
+
readonly flicker?: number;
|
|
41
|
+
/** Keeps two flames of one size out of step. Defaults to 0. */
|
|
42
|
+
readonly seed?: number;
|
|
43
|
+
/** Whether the light may take a shadow slot. Defaults to true, as `PointLightSource` does. */
|
|
44
|
+
readonly castsShadow?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* How far the light wanders on each axis, as a share of the flame's height. Defaults to 0.06.
|
|
47
|
+
*
|
|
48
|
+
* 0 holds it still. A point light's static shadow map is baked from where the light stood, so a
|
|
49
|
+
* light that wanders reads that map from a few centimetres off. What would make the default wrong
|
|
50
|
+
* is a flame near a surface it shadows, where that offset shows as a band.
|
|
51
|
+
*/
|
|
52
|
+
readonly sway?: number;
|
|
53
|
+
}
|
|
54
|
+
export interface Flame {
|
|
55
|
+
/** Rewritten by `updateFlame`. Offer it to `selectPointLights` with the scene's other lights. */
|
|
56
|
+
readonly light: PointLightSource;
|
|
57
|
+
/** Where the plume stands and how big it is, for `createPlumes`. */
|
|
58
|
+
readonly plume: PlumePlacement;
|
|
59
|
+
/** Pulses a second: 1.5 / √D. */
|
|
60
|
+
readonly frequencyHz: number;
|
|
61
|
+
readonly centre: Vec3;
|
|
62
|
+
readonly color: Vec3;
|
|
63
|
+
readonly flicker: number;
|
|
64
|
+
readonly phase: number;
|
|
65
|
+
readonly swayM: number;
|
|
66
|
+
}
|
|
67
|
+
/** The puffing rate of a flame burning from `diameterM` metres across. */
|
|
68
|
+
export declare function flameFrequencyHz(diameterM: number): number;
|
|
69
|
+
export declare function createFlame(options: FlameOptions): Flame;
|
|
70
|
+
/** Where the flame's light is and how bright, at `timeSeconds`. Writes `flame.light` in place. */
|
|
71
|
+
export declare function updateFlame(flame: Flame, timeSeconds: number): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** How tall a flame stands, as a multiple of the width it burns from. */
|
|
2
|
+
const HEIGHT_PER_DIAMETER = 2.5;
|
|
3
|
+
/** How far the light sways on each axis, as a share of the flame's height. */
|
|
4
|
+
const SWAY_PER_HEIGHT = 0.06;
|
|
5
|
+
/** The harmonic's rate against the pulse's, chosen irrational so the two never lock. */
|
|
6
|
+
const HARMONIC = 2.37;
|
|
7
|
+
/** The puffing rate of a flame burning from `diameterM` metres across. */
|
|
8
|
+
export function flameFrequencyHz(diameterM) {
|
|
9
|
+
return 1.5 / Math.sqrt(Math.max(diameterM, 1e-3));
|
|
10
|
+
}
|
|
11
|
+
export function createFlame(options) {
|
|
12
|
+
const height = HEIGHT_PER_DIAMETER * options.diameterM;
|
|
13
|
+
const centre = [options.x, options.y + height / 3, options.z];
|
|
14
|
+
/* A seed spread over the whole turn, so seeds 1 and 2 are not neighbours in phase. */
|
|
15
|
+
const phase = ((options.seed ?? 0) * 2.399963) % (Math.PI * 2);
|
|
16
|
+
const light = {
|
|
17
|
+
x: centre[0],
|
|
18
|
+
y: centre[1],
|
|
19
|
+
z: centre[2],
|
|
20
|
+
r: options.color[0],
|
|
21
|
+
g: options.color[1],
|
|
22
|
+
b: options.color[2],
|
|
23
|
+
radius: options.radius,
|
|
24
|
+
/* Off, because this helper is the flicker; the selection's own would apply a second one. */
|
|
25
|
+
flicker: 0,
|
|
26
|
+
shadowNear: Math.max(0.05, options.diameterM),
|
|
27
|
+
sourceRadius: options.diameterM / 2,
|
|
28
|
+
...(options.castsShadow === false ? { castsShadow: false } : {}),
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
light,
|
|
32
|
+
plume: {
|
|
33
|
+
x: options.x,
|
|
34
|
+
y: options.y,
|
|
35
|
+
z: options.z,
|
|
36
|
+
width: options.diameterM / 2,
|
|
37
|
+
height,
|
|
38
|
+
},
|
|
39
|
+
frequencyHz: flameFrequencyHz(options.diameterM),
|
|
40
|
+
centre,
|
|
41
|
+
color: [options.color[0], options.color[1], options.color[2]],
|
|
42
|
+
flicker: options.flicker ?? 0.18,
|
|
43
|
+
phase,
|
|
44
|
+
swayM: (options.sway ?? SWAY_PER_HEIGHT) * height,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Where the flame's light is and how bright, at `timeSeconds`. Writes `flame.light` in place. */
|
|
48
|
+
export function updateFlame(flame, timeSeconds) {
|
|
49
|
+
const w = 2 * Math.PI * flame.frequencyHz * timeSeconds;
|
|
50
|
+
const p = flame.phase;
|
|
51
|
+
const pulse = 0.65 * Math.sin(w + p) + 0.35 * Math.sin(HARMONIC * w + 1.9 * p);
|
|
52
|
+
const scale = 1 + flame.flicker * pulse;
|
|
53
|
+
const light = flame.light;
|
|
54
|
+
light.r = flame.color[0] * scale;
|
|
55
|
+
light.g = flame.color[1] * scale;
|
|
56
|
+
light.b = flame.color[2] * scale;
|
|
57
|
+
/* The sway runs slower than the pulse and on its own rates, so the light wanders rather than
|
|
58
|
+
tracing one loop. Up when the flame puffs up. */
|
|
59
|
+
const sway = flame.swayM;
|
|
60
|
+
light.x = flame.centre[0] + sway * Math.sin(0.83 * w + 2.1 * p);
|
|
61
|
+
light.y = flame.centre[1] + sway * Math.sin(w + p);
|
|
62
|
+
light.z = flame.centre[2] + sway * Math.cos(0.71 * w + 3.3 * p);
|
|
63
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame's exact lights as DriftRay's probe bake reads them: what a surface a probe's ray
|
|
3
|
+
* strikes is lit by, besides the sun, the bounce already in the grid and a DriftLight field.
|
|
4
|
+
*
|
|
5
|
+
* **Why the trace needs them at all.** A probe ray shades what it hits itself rather than reading
|
|
6
|
+
* a capture, which is what lets the bounce follow a light that moves; but it shaded with the sun
|
|
7
|
+
* alone, so a courtyard at night bounced no firelight into the corners the fires could not see. A
|
|
8
|
+
* field carries the many fixed lights; these are the rest, the lights the frame shades one by one,
|
|
9
|
+
* flickering and moving as they do in the frame.
|
|
10
|
+
*
|
|
11
|
+
* **Decided here and bound there**, by the 2026-08-13 rule: which lights, in what order and at what
|
|
12
|
+
* weight is one decision for any backend that traces. A light a field sums carries a negative
|
|
13
|
+
* weight and is left out, because the field already brings it to the bounce; a fading light goes in
|
|
14
|
+
* at its weight, as the lit shader scales it.
|
|
15
|
+
*
|
|
16
|
+
* **At most `MAX_BOUNCE_LIGHTS`, the first the selection chose**, which is nearest the camera: each
|
|
17
|
+
* one is a march per ray that reaches it, and a probe far from the camera is lit by lights further
|
|
18
|
+
* down the list that this leaves out. What would change that is a scene whose bounce is dominated
|
|
19
|
+
* by lights far from where it is looked at, which would want the selection run again from the grid.
|
|
20
|
+
*
|
|
21
|
+
* What it gives up against the frame: a photometric profile and a cookie, which the bounce reads as
|
|
22
|
+
* a plain cone, and a light's specular, which a diffuse bounce has no use for.
|
|
23
|
+
*/
|
|
24
|
+
import type { PointLightSet } from '../lightBudget.ts';
|
|
25
|
+
/** Lights the probe bake shades a hit with. */
|
|
26
|
+
export declare const MAX_BOUNCE_LIGHTS = 32;
|
|
27
|
+
/**
|
|
28
|
+
* Floats a light takes: where it is and how far it reaches; its colour at its weight and the cosine
|
|
29
|
+
* of its inner cone; the way it points and the cosine of its outer cone.
|
|
30
|
+
*/
|
|
31
|
+
export declare const BOUNCE_LIGHT_FLOATS = 12;
|
|
32
|
+
export declare function createBounceLights(): Float32Array;
|
|
33
|
+
/** Pack the lights of `lights` the bounce reads into `out`, and answer how many. */
|
|
34
|
+
export declare function resolveBounceLights(lights: PointLightSet | null, out: Float32Array): number;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame's exact lights as DriftRay's probe bake reads them: what a surface a probe's ray
|
|
3
|
+
* strikes is lit by, besides the sun, the bounce already in the grid and a DriftLight field.
|
|
4
|
+
*
|
|
5
|
+
* **Why the trace needs them at all.** A probe ray shades what it hits itself rather than reading
|
|
6
|
+
* a capture, which is what lets the bounce follow a light that moves; but it shaded with the sun
|
|
7
|
+
* alone, so a courtyard at night bounced no firelight into the corners the fires could not see. A
|
|
8
|
+
* field carries the many fixed lights; these are the rest, the lights the frame shades one by one,
|
|
9
|
+
* flickering and moving as they do in the frame.
|
|
10
|
+
*
|
|
11
|
+
* **Decided here and bound there**, by the 2026-08-13 rule: which lights, in what order and at what
|
|
12
|
+
* weight is one decision for any backend that traces. A light a field sums carries a negative
|
|
13
|
+
* weight and is left out, because the field already brings it to the bounce; a fading light goes in
|
|
14
|
+
* at its weight, as the lit shader scales it.
|
|
15
|
+
*
|
|
16
|
+
* **At most `MAX_BOUNCE_LIGHTS`, the first the selection chose**, which is nearest the camera: each
|
|
17
|
+
* one is a march per ray that reaches it, and a probe far from the camera is lit by lights further
|
|
18
|
+
* down the list that this leaves out. What would change that is a scene whose bounce is dominated
|
|
19
|
+
* by lights far from where it is looked at, which would want the selection run again from the grid.
|
|
20
|
+
*
|
|
21
|
+
* What it gives up against the frame: a photometric profile and a cookie, which the bounce reads as
|
|
22
|
+
* a plain cone, and a light's specular, which a diffuse bounce has no use for.
|
|
23
|
+
*/
|
|
24
|
+
import { POINT_LIGHT_COS_INNER, POINT_LIGHT_COS_OUTER } from '../clusteredLights.js';
|
|
25
|
+
/** Lights the probe bake shades a hit with. */
|
|
26
|
+
export const MAX_BOUNCE_LIGHTS = 32;
|
|
27
|
+
/**
|
|
28
|
+
* Floats a light takes: where it is and how far it reaches; its colour at its weight and the cosine
|
|
29
|
+
* of its inner cone; the way it points and the cosine of its outer cone.
|
|
30
|
+
*/
|
|
31
|
+
export const BOUNCE_LIGHT_FLOATS = 12;
|
|
32
|
+
export function createBounceLights() {
|
|
33
|
+
return new Float32Array(MAX_BOUNCE_LIGHTS * BOUNCE_LIGHT_FLOATS);
|
|
34
|
+
}
|
|
35
|
+
/** Pack the lights of `lights` the bounce reads into `out`, and answer how many. */
|
|
36
|
+
export function resolveBounceLights(lights, out) {
|
|
37
|
+
if (lights === null)
|
|
38
|
+
return 0;
|
|
39
|
+
let count = 0;
|
|
40
|
+
for (let i = 0; i < lights.lightCount && count < MAX_BOUNCE_LIGHTS; i++) {
|
|
41
|
+
const weight = lights.lightWeights[i] ?? 0;
|
|
42
|
+
const radius = lights.lightRadii[i] ?? 0;
|
|
43
|
+
if (!(weight > 0) || !(radius > 0))
|
|
44
|
+
continue;
|
|
45
|
+
const r = lights.lightColors[i * 3] ?? 0;
|
|
46
|
+
const g = lights.lightColors[i * 3 + 1] ?? 0;
|
|
47
|
+
const b = lights.lightColors[i * 3 + 2] ?? 0;
|
|
48
|
+
if (!(r + g + b > 0))
|
|
49
|
+
continue;
|
|
50
|
+
const at = count * BOUNCE_LIGHT_FLOATS;
|
|
51
|
+
out[at] = lights.lightPositions[i * 3] ?? 0;
|
|
52
|
+
out[at + 1] = lights.lightPositions[i * 3 + 1] ?? 0;
|
|
53
|
+
out[at + 2] = lights.lightPositions[i * 3 + 2] ?? 0;
|
|
54
|
+
out[at + 3] = radius;
|
|
55
|
+
out[at + 4] = r * weight;
|
|
56
|
+
out[at + 5] = g * weight;
|
|
57
|
+
out[at + 6] = b * weight;
|
|
58
|
+
out[at + 7] = lights.lightConeCos?.[i * 2] ?? POINT_LIGHT_COS_INNER;
|
|
59
|
+
out[at + 8] = lights.lightDirections?.[i * 3] ?? 0;
|
|
60
|
+
out[at + 9] = lights.lightDirections?.[i * 3 + 1] ?? 0;
|
|
61
|
+
out[at + 10] = lights.lightDirections?.[i * 3 + 2] ?? 0;
|
|
62
|
+
out[at + 11] = lights.lightConeCos?.[i * 2 + 1] ?? POINT_LIGHT_COS_OUTER;
|
|
63
|
+
count++;
|
|
64
|
+
}
|
|
65
|
+
return count;
|
|
66
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProbeGrid } from '../probeGrid.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Cascades enough for the outermost to hold the whole grid, and one probe step past it, within the
|
|
4
|
+
* part of it that is trusted, wherever inside the grid the camera stands. At least `least` and at
|
|
5
|
+
* most `most`: each cascade is the same number of samples, so the bound is on memory, not reach.
|
|
6
|
+
*/
|
|
7
|
+
export declare function cascadesForGrid(grid: ProbeGrid, radius: number, least: number, most: number): number;
|
|
8
|
+
/**
|
|
9
|
+
* Keeps, in place and in order, the scheduled probes inside the trusted part of the outermost
|
|
10
|
+
* cascade, `bounds` as `[minX, minY, minZ, maxX, maxY, maxZ]`. Returns how many are kept.
|
|
11
|
+
*/
|
|
12
|
+
export declare function probesTheFieldReaches(grid: ProbeGrid, scheduled: Int32Array, count: number, bounds: ArrayLike<number>): number;
|