@driftengine/core 4.5.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +51 -0
- package/README.md +1 -1
- package/dist/geometry/boxCsg.d.ts +28 -0
- package/dist/geometry/boxCsg.js +139 -0
- package/dist/geometry/csg.d.ts +36 -0
- package/dist/geometry/csg.js +329 -0
- package/dist/geometry/meshBuilder.js +13 -0
- package/dist/geometry/meshConcat.js +1 -0
- package/dist/geometry/pixelFont.js +2 -0
- package/dist/geometry/solid.d.ts +61 -0
- package/dist/geometry/solid.js +152 -0
- package/dist/geometry/solidFlat.d.ts +35 -0
- package/dist/geometry/solidFlat.js +428 -0
- package/dist/geometry/solidHarness.d.ts +19 -0
- package/dist/geometry/solidHarness.js +66 -0
- package/dist/geometry/solidNormals.d.ts +17 -0
- package/dist/geometry/solidNormals.js +86 -0
- package/dist/geometry/solidRound.d.ts +41 -0
- package/dist/geometry/solidRound.js +323 -0
- package/dist/geometry/solidWriter.d.ts +16 -0
- package/dist/geometry/solidWriter.js +42 -0
- package/dist/index.d.ts +23 -2
- package/dist/index.js +13 -1
- package/dist/render/backend/api.d.ts +17 -3
- package/dist/render/backend/drawFog.d.ts +26 -0
- package/dist/render/backend/drawFog.js +30 -0
- package/dist/render/backend/materialChanges.d.ts +2 -1
- package/dist/render/backend/materialChanges.js +6 -1
- package/dist/render/backend/webgl2/instanced.d.ts +24 -5
- package/dist/render/backend/webgl2/instanced.js +72 -15
- package/dist/render/backend/webgl2/renderer.d.ts +122 -8
- package/dist/render/backend/webgl2/renderer.js +238 -121
- package/dist/render/backend/webgpu/buffers.d.ts +10 -0
- package/dist/render/backend/webgpu/buffers.js +34 -3
- package/dist/render/backend/webgpu/depthPass.js +5 -1
- package/dist/render/backend/webgpu/drawCommand.d.ts +5 -0
- package/dist/render/backend/webgpu/drawCommand.js +1 -0
- package/dist/render/backend/webgpu/flatPass.d.ts +4 -1
- package/dist/render/backend/webgpu/flatPass.js +38 -9
- package/dist/render/backend/webgpu/glassTintPass.js +2 -1
- package/dist/render/backend/webgpu/instanceCullPass.d.ts +34 -0
- package/dist/render/backend/webgpu/instanceCullPass.js +144 -0
- package/dist/render/backend/webgpu/instanced.d.ts +11 -1
- package/dist/render/backend/webgpu/instanced.js +21 -3
- package/dist/render/backend/webgpu/latePass.d.ts +6 -2
- package/dist/render/backend/webgpu/latePass.js +11 -4
- package/dist/render/backend/webgpu/postPass.d.ts +1 -1
- package/dist/render/backend/webgpu/renderer.d.ts +100 -10
- package/dist/render/backend/webgpu/renderer.js +462 -201
- package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
- package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
- package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +17 -3
- package/dist/render/backend/webgpu/surfaceTexturePass.js +77 -38
- package/dist/render/driftLight/bake.d.ts +7 -0
- package/dist/render/driftLight/bake.js +61 -56
- package/dist/render/driftLight/denseField.d.ts +46 -0
- package/dist/render/driftLight/denseField.js +109 -0
- package/dist/render/driftLight/lightField.d.ts +10 -5
- package/dist/render/driftLight/lightField.js +25 -19
- package/dist/render/driftLight/presence.d.ts +50 -0
- package/dist/render/driftLight/presence.js +43 -0
- package/dist/render/driftLight/uniforms.d.ts +3 -3
- package/dist/render/driftLight/uniforms.js +5 -6
- package/dist/render/driftLight/worldLightField.d.ts +41 -0
- package/dist/render/driftLight/worldLightField.js +66 -0
- package/dist/render/fixtureAtlas.d.ts +20 -0
- package/dist/render/fixtureAtlas.js +49 -0
- package/dist/render/instanceCull.d.ts +63 -0
- package/dist/render/instanceCull.js +160 -0
- package/dist/render/instances.d.ts +15 -0
- package/dist/render/lightBudget.d.ts +22 -33
- package/dist/render/lightBudget.js +24 -35
- package/dist/render/lightGrid.d.ts +42 -0
- package/dist/render/lightGrid.js +87 -0
- package/dist/render/mesh.d.ts +18 -24
- package/dist/render/mesh.js +51 -68
- package/dist/render/occlusion.d.ts +10 -0
- package/dist/render/occlusion.js +36 -7
- package/dist/render/pointLightSelection.d.ts +7 -0
- package/dist/render/pointLightSelection.js +29 -4
- package/dist/render/rainField.d.ts +13 -0
- package/dist/render/rainField.js +27 -5
- package/dist/render/recon/lateDepth.d.ts +11 -0
- package/dist/render/recon/lateDepth.js +70 -0
- package/dist/render/recon/resolve.d.ts +34 -4
- package/dist/render/recon/resolve.js +161 -20
- package/dist/render/rendererHarness.js +4 -0
- package/dist/render/sceneTarget.d.ts +2 -0
- package/dist/render/sceneTarget.js +2 -0
- package/dist/render/shaders/depth.js +12 -9
- package/dist/render/shaders/flat/driftLight.d.ts +1 -1
- package/dist/render/shaders/flat/driftLight.js +32 -7
- package/dist/render/shaders/flat/index.js +5 -3
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +75 -23
- package/dist/render/shaders/flat/preamble.js +75 -20
- package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
- package/dist/render/shaders/flat/surfaceEffects.js +152 -0
- package/dist/render/shaders/generated/depth.wgsl.d.ts +8 -8
- package/dist/render/shaders/generated/depth.wgsl.js +8 -8
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1320 -1320
- package/dist/render/shaders/generated/flat.wgsl.js +1359 -1345
- package/dist/render/shaders/generated/lightVolume.wgsl.js +18 -4
- package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
- package/dist/render/shaders/generated/rush.wgsl.js +21 -16
- package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
- package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
- package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/text.wgsl.js +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
- package/dist/render/shaders/recon/lateDepth.wgsl.js +38 -13
- package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
- package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
- package/dist/render/shaders/rush.js +11 -1
- package/dist/render/shaders/sdfText.d.ts +1 -1
- package/dist/render/shaders/sdfText.js +3 -2
- package/dist/render/shaders/text.d.ts +1 -1
- package/dist/render/shaders/text.js +18 -9
- package/dist/render/surfaceEffects.d.ts +83 -0
- package/dist/render/surfaceEffects.js +75 -0
- package/dist/render/surfaceScene.d.ts +8 -0
- package/dist/render/surfaceScene.js +8 -0
- package/dist/render/surfaceTexture.d.ts +18 -2
- package/dist/render/surfaceTexture.js +68 -15
- package/dist/render/temporalAa.d.ts +25 -6
- package/dist/render/temporalAa.js +45 -15
- package/dist/render/textureSource.d.ts +30 -0
- package/dist/render/textureSource.js +53 -0
- package/dist/render/vertexDefaults.js +2 -0
- package/dist/world/hlod.d.ts +104 -0
- package/dist/world/hlod.js +214 -0
- package/package.json +6 -6
- package/src/geometry/boxCsg.ts +186 -0
- package/src/geometry/csg.ts +384 -0
- package/src/geometry/meshBuilder.ts +15 -0
- package/src/geometry/meshConcat.ts +1 -0
- package/src/geometry/pixelFont.ts +2 -0
- package/src/geometry/solid.ts +189 -0
- package/src/geometry/solidFlat.ts +522 -0
- package/src/geometry/solidHarness.ts +83 -0
- package/src/geometry/solidNormals.ts +103 -0
- package/src/geometry/solidRound.ts +443 -0
- package/src/geometry/solidWriter.ts +58 -0
- package/src/index.ts +51 -1
- package/src/render/backend/api.ts +21 -2
- package/src/render/backend/drawFog.ts +31 -0
- package/src/render/backend/materialChanges.ts +9 -2
- package/src/render/backend/webgl2/instanced.ts +80 -14
- package/src/render/backend/webgl2/renderer.ts +300 -143
- package/src/render/backend/webgpu/buffers.ts +42 -3
- package/src/render/backend/webgpu/depthPass.ts +5 -1
- package/src/render/backend/webgpu/drawCommand.ts +6 -0
- package/src/render/backend/webgpu/flatPass.ts +41 -11
- package/src/render/backend/webgpu/glassTintPass.ts +2 -1
- package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
- package/src/render/backend/webgpu/instanced.ts +21 -3
- package/src/render/backend/webgpu/latePass.ts +12 -3
- package/src/render/backend/webgpu/renderer.ts +540 -226
- package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
- package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
- package/src/render/backend/webgpu/surfaceTexturePass.ts +90 -50
- package/src/render/driftLight/bake.ts +85 -62
- package/src/render/driftLight/denseField.ts +149 -0
- package/src/render/driftLight/lightField.ts +33 -18
- package/src/render/driftLight/presence.ts +69 -0
- package/src/render/driftLight/uniforms.ts +8 -9
- package/src/render/driftLight/worldLightField.ts +83 -0
- package/src/render/fixtureAtlas.ts +63 -0
- package/src/render/instanceCull.ts +205 -0
- package/src/render/instances.ts +16 -0
- package/src/render/lightBudget.ts +25 -36
- package/src/render/lightGrid.ts +130 -0
- package/src/render/mesh.ts +59 -72
- package/src/render/occlusion.ts +51 -7
- package/src/render/pointLightSelection.ts +72 -4
- package/src/render/rainField.ts +39 -5
- package/src/render/recon/lateDepth.ts +83 -0
- package/src/render/recon/resolve.ts +206 -20
- package/src/render/rendererHarness.ts +4 -0
- package/src/render/sceneTarget.ts +4 -0
- package/src/render/shaders/depth.ts +12 -9
- package/src/render/shaders/flat/driftLight.ts +32 -7
- package/src/render/shaders/flat/index.ts +5 -3
- package/src/render/shaders/flat/main.ts +75 -23
- package/src/render/shaders/flat/preamble.ts +75 -20
- package/src/render/shaders/flat/surfaceEffects.ts +152 -0
- package/src/render/shaders/generated/depth.wgsl.ts +8 -8
- package/src/render/shaders/generated/flat.wgsl.ts +1361 -1345
- package/src/render/shaders/generated/lightVolume.wgsl.ts +20 -4
- package/src/render/shaders/generated/rush.wgsl.ts +21 -16
- package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
- package/src/render/shaders/generated/text.wgsl.ts +1 -1
- package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
- package/src/render/shaders/recon/lateDepth.wgsl.ts +38 -13
- package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
- package/src/render/shaders/rush.ts +11 -1
- package/src/render/shaders/sdfText.ts +3 -2
- package/src/render/shaders/text.ts +18 -9
- package/src/render/surfaceEffects.ts +132 -0
- package/src/render/surfaceScene.ts +16 -0
- package/src/render/surfaceTexture.ts +110 -19
- package/src/render/temporalAa.ts +78 -14
- package/src/render/textureSource.ts +72 -0
- package/src/render/vertexDefaults.ts +2 -0
- package/src/world/hlod.ts +290 -0
|
@@ -29,6 +29,7 @@ import type { GlobalFieldInstance } from '../gi/globalField.ts';
|
|
|
29
29
|
import type { FieldLight } from './bake.ts';
|
|
30
30
|
import type { PointLightFalloff } from './falloff.ts';
|
|
31
31
|
import type { LightFieldLayout } from './layout.ts';
|
|
32
|
+
import type { DriftLightVolumes } from './presence.ts';
|
|
32
33
|
/** Seconds the summed light takes to fade in once the field is whole. */
|
|
33
34
|
export declare const FIELD_FADE_SEC = 0.5;
|
|
34
35
|
/** A light a field sums: a `PointLightSource`, whose `inLightField` the field sets. */
|
|
@@ -51,7 +52,7 @@ export interface LightFieldOptions {
|
|
|
51
52
|
*/
|
|
52
53
|
readonly fadeSec?: number;
|
|
53
54
|
}
|
|
54
|
-
export declare class LightField {
|
|
55
|
+
export declare class LightField implements DriftLightVolumes {
|
|
55
56
|
readonly layout: LightFieldLayout;
|
|
56
57
|
/** Texels on each axis of the atlas. */
|
|
57
58
|
readonly atlasSize: readonly [number, number, number];
|
|
@@ -67,17 +68,14 @@ export declare class LightField {
|
|
|
67
68
|
readonly fadeSec: number;
|
|
68
69
|
/** The summed light's brightness, for a scene that dims all its lamps at once: dawn, a switch. */
|
|
69
70
|
scale: number;
|
|
70
|
-
/** Where the frame's choice was centred, which the shader measures the radius from. */
|
|
71
|
-
readonly centre: Float32Array<ArrayBuffer>;
|
|
72
71
|
private readonly lights;
|
|
73
72
|
private readonly falloff;
|
|
74
73
|
private readonly distance;
|
|
75
74
|
private readonly lightBlock;
|
|
76
75
|
private readonly directionBlock;
|
|
76
|
+
private readonly showing;
|
|
77
77
|
private baked;
|
|
78
78
|
private taken;
|
|
79
|
-
private eased;
|
|
80
|
-
private shown;
|
|
81
79
|
constructor(lights: readonly LightFieldSource[], options: LightFieldOptions);
|
|
82
80
|
/** Whether every brick has been baked. */
|
|
83
81
|
get ready(): boolean;
|
|
@@ -87,6 +85,13 @@ export declare class LightField {
|
|
|
87
85
|
get radius(): number;
|
|
88
86
|
/** How much of the summed light is in the frame, 0 until the field is whole, then up to 1. */
|
|
89
87
|
get presence(): number;
|
|
88
|
+
/** Where the frame's choice was centred, which the shader measures the radius from. */
|
|
89
|
+
get centre(): Float32Array;
|
|
90
|
+
get empty(): boolean;
|
|
91
|
+
get indexDims(): readonly [number, number, number];
|
|
92
|
+
get index(): Uint32Array;
|
|
93
|
+
get sampleOrigin(): readonly [number, number, number];
|
|
94
|
+
get signedSpacing(): number;
|
|
90
95
|
/** The texel at which brick `brick`'s light block starts; its direction block is 4 along x. */
|
|
91
96
|
atlasCorner(brick: number): [number, number, number];
|
|
92
97
|
/** Bake up to `bricks` more bricks into the atlas, and answer how many were baked. */
|
|
@@ -29,10 +29,9 @@ import { distanceToInstances } from '../gi/globalField.js';
|
|
|
29
29
|
import { halfBits } from '../halfFloat.js';
|
|
30
30
|
import { BRICK_TEXELS, bakeBrick } from './bake.js';
|
|
31
31
|
import { BRICK_SAMPLES, layoutLightField } from './layout.js';
|
|
32
|
+
import { FieldPresence } from './presence.js';
|
|
32
33
|
/** Seconds the summed light takes to fade in once the field is whole. */
|
|
33
34
|
export const FIELD_FADE_SEC = 0.5;
|
|
34
|
-
/** How fast the shading radius grows back, per second, when the choice reaches further again. */
|
|
35
|
-
const RADIUS_GROWTH_PER_SEC = 2;
|
|
36
35
|
/** Bricks along each axis of the atlas: 256 texels a side, the least 3D texture WebGL2 promises. */
|
|
37
36
|
const ATLAS_BRICKS = [32, 64, 64];
|
|
38
37
|
/** A brick's footprint in the atlas: its light block and its direction block side by side. */
|
|
@@ -53,17 +52,14 @@ export class LightField {
|
|
|
53
52
|
fadeSec;
|
|
54
53
|
/** The summed light's brightness, for a scene that dims all its lamps at once: dawn, a switch. */
|
|
55
54
|
scale = 1;
|
|
56
|
-
/** Where the frame's choice was centred, which the shader measures the radius from. */
|
|
57
|
-
centre = new Float32Array(3);
|
|
58
55
|
lights;
|
|
59
56
|
falloff;
|
|
60
57
|
distance;
|
|
61
58
|
lightBlock = new Float32Array(BRICK_TEXELS * 4);
|
|
62
59
|
directionBlock = new Float32Array(BRICK_TEXELS * 4);
|
|
60
|
+
showing;
|
|
63
61
|
baked = 0;
|
|
64
62
|
taken = 0;
|
|
65
|
-
eased = 0;
|
|
66
|
-
shown = 0;
|
|
67
63
|
constructor(lights, options) {
|
|
68
64
|
for (const light of lights)
|
|
69
65
|
light.inLightField = true;
|
|
@@ -80,6 +76,7 @@ export class LightField {
|
|
|
80
76
|
this.falloff = options.falloff;
|
|
81
77
|
this.band = options.band ?? 2;
|
|
82
78
|
this.fadeSec = Math.max(0, options.fadeSec ?? FIELD_FADE_SEC);
|
|
79
|
+
this.showing = new FieldPresence(this.fadeSec);
|
|
83
80
|
const fields = options.fields ?? [];
|
|
84
81
|
this.occluded = fields.length > 0;
|
|
85
82
|
this.distance = this.occluded ? distanceToInstances(fields) : null;
|
|
@@ -107,11 +104,30 @@ export class LightField {
|
|
|
107
104
|
}
|
|
108
105
|
/** The radius, in metres from `centre`, inside which lights are shaded exactly. */
|
|
109
106
|
get radius() {
|
|
110
|
-
return this.
|
|
107
|
+
return this.showing.radius;
|
|
111
108
|
}
|
|
112
109
|
/** How much of the summed light is in the frame, 0 until the field is whole, then up to 1. */
|
|
113
110
|
get presence() {
|
|
114
|
-
return this.
|
|
111
|
+
return this.showing.presence;
|
|
112
|
+
}
|
|
113
|
+
/** Where the frame's choice was centred, which the shader measures the radius from. */
|
|
114
|
+
get centre() {
|
|
115
|
+
return this.showing.centre;
|
|
116
|
+
}
|
|
117
|
+
get empty() {
|
|
118
|
+
return this.layout.count === 0;
|
|
119
|
+
}
|
|
120
|
+
get indexDims() {
|
|
121
|
+
return this.layout.dims;
|
|
122
|
+
}
|
|
123
|
+
get index() {
|
|
124
|
+
return this.layout.index;
|
|
125
|
+
}
|
|
126
|
+
get sampleOrigin() {
|
|
127
|
+
return this.layout.origin;
|
|
128
|
+
}
|
|
129
|
+
get signedSpacing() {
|
|
130
|
+
return this.layout.spacing;
|
|
115
131
|
}
|
|
116
132
|
/** The texel at which brick `brick`'s light block starts; its direction block is 4 along x. */
|
|
117
133
|
atlasCorner(brick) {
|
|
@@ -146,17 +162,7 @@ export class LightField {
|
|
|
146
162
|
* which is only less exact. While nothing is summed the radius simply follows.
|
|
147
163
|
*/
|
|
148
164
|
follow(complete, x, y, z, dtSec) {
|
|
149
|
-
this.
|
|
150
|
-
this.centre[1] = y;
|
|
151
|
-
this.centre[2] = z;
|
|
152
|
-
const showing = this.shown > 0;
|
|
153
|
-
if (!showing || complete < this.eased)
|
|
154
|
-
this.eased = complete;
|
|
155
|
-
else
|
|
156
|
-
this.eased += (complete - this.eased) * (1 - Math.exp(-RADIUS_GROWTH_PER_SEC * dtSec));
|
|
157
|
-
if (this.ready) {
|
|
158
|
-
this.shown = this.fadeSec > 0 ? Math.min(1, this.shown + dtSec / this.fadeSec) : 1;
|
|
159
|
-
}
|
|
165
|
+
this.showing.follow(this.ready, complete, x, y, z, dtSec);
|
|
160
166
|
}
|
|
161
167
|
/** Write the brick just baked into its place in the atlas, as half floats. */
|
|
162
168
|
place(brick) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much of a DriftLight field is in the frame, and the radius inside which the frame's exact
|
|
3
|
+
* lights take over from it — the part every kind of field shares, kept once.
|
|
4
|
+
*
|
|
5
|
+
* **The radius shrinks at once and grows back slowly.** A pixel inside it that a left-out light
|
|
6
|
+
* reaches would be shaded short, so it cannot lag a shrink; a pixel it has not reached yet takes the
|
|
7
|
+
* summed light, which is only less exact, so a grow may ease. While nothing is summed the radius
|
|
8
|
+
* simply follows. The presence fades in over `fadeSec` once the field is whole, so a field
|
|
9
|
+
* completing in view does not pop — and a held clock never finishes a fade, so a field behind a
|
|
10
|
+
* loading screen asks for none.
|
|
11
|
+
*/
|
|
12
|
+
export declare class FieldPresence {
|
|
13
|
+
readonly fadeSec: number;
|
|
14
|
+
/** Where the frame's choice was centred, which the shader measures the radius from. */
|
|
15
|
+
readonly centre: Float32Array<ArrayBuffer>;
|
|
16
|
+
private eased;
|
|
17
|
+
private shown;
|
|
18
|
+
constructor(fadeSec: number);
|
|
19
|
+
/** The radius, in metres from `centre`, inside which lights are shaded exactly. */
|
|
20
|
+
get radius(): number;
|
|
21
|
+
/** How much of the summed light is in the frame, 0 until the field is whole, then up to 1. */
|
|
22
|
+
get presence(): number;
|
|
23
|
+
/** Follow the frame's choice: `complete` from the selection, centred at (x, y, z). */
|
|
24
|
+
follow(ready: boolean, complete: number, x: number, y: number, z: number, dtSec: number): void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* What a backend uploads and binds for a field of either kind, and what the uniforms are made of.
|
|
28
|
+
*
|
|
29
|
+
* A sparse field's index finds its bricks; a dense one brings a 1×1×1 index of nothing, so the
|
|
30
|
+
* upload and the bind are one code path, and says it is dense by a negative spacing.
|
|
31
|
+
*/
|
|
32
|
+
export interface DriftLightVolumes {
|
|
33
|
+
readonly ready: boolean;
|
|
34
|
+
/** Nothing summed at all: nothing to upload. */
|
|
35
|
+
readonly empty: boolean;
|
|
36
|
+
readonly indexDims: readonly [number, number, number];
|
|
37
|
+
readonly index: Uint32Array;
|
|
38
|
+
readonly atlasSize: readonly [number, number, number];
|
|
39
|
+
/** Half floats, four channels a texel, x fastest. */
|
|
40
|
+
readonly atlas: Uint16Array;
|
|
41
|
+
readonly presence: number;
|
|
42
|
+
readonly radius: number;
|
|
43
|
+
readonly band: number;
|
|
44
|
+
readonly scale: number;
|
|
45
|
+
readonly centre: Float32Array;
|
|
46
|
+
/** The world position of the first sample. */
|
|
47
|
+
readonly sampleOrigin: readonly [number, number, number];
|
|
48
|
+
/** Metres between samples, negative for a dense volume: the shader's mode. */
|
|
49
|
+
readonly signedSpacing: number;
|
|
50
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much of a DriftLight field is in the frame, and the radius inside which the frame's exact
|
|
3
|
+
* lights take over from it — the part every kind of field shares, kept once.
|
|
4
|
+
*
|
|
5
|
+
* **The radius shrinks at once and grows back slowly.** A pixel inside it that a left-out light
|
|
6
|
+
* reaches would be shaded short, so it cannot lag a shrink; a pixel it has not reached yet takes the
|
|
7
|
+
* summed light, which is only less exact, so a grow may ease. While nothing is summed the radius
|
|
8
|
+
* simply follows. The presence fades in over `fadeSec` once the field is whole, so a field
|
|
9
|
+
* completing in view does not pop — and a held clock never finishes a fade, so a field behind a
|
|
10
|
+
* loading screen asks for none.
|
|
11
|
+
*/
|
|
12
|
+
/** How fast the shading radius grows back, per second, when the choice reaches further again. */
|
|
13
|
+
const RADIUS_GROWTH_PER_SEC = 2;
|
|
14
|
+
export class FieldPresence {
|
|
15
|
+
fadeSec;
|
|
16
|
+
/** Where the frame's choice was centred, which the shader measures the radius from. */
|
|
17
|
+
centre = new Float32Array(3);
|
|
18
|
+
eased = 0;
|
|
19
|
+
shown = 0;
|
|
20
|
+
constructor(fadeSec) {
|
|
21
|
+
this.fadeSec = fadeSec;
|
|
22
|
+
}
|
|
23
|
+
/** The radius, in metres from `centre`, inside which lights are shaded exactly. */
|
|
24
|
+
get radius() {
|
|
25
|
+
return this.eased;
|
|
26
|
+
}
|
|
27
|
+
/** How much of the summed light is in the frame, 0 until the field is whole, then up to 1. */
|
|
28
|
+
get presence() {
|
|
29
|
+
return this.shown;
|
|
30
|
+
}
|
|
31
|
+
/** Follow the frame's choice: `complete` from the selection, centred at (x, y, z). */
|
|
32
|
+
follow(ready, complete, x, y, z, dtSec) {
|
|
33
|
+
this.centre[0] = x;
|
|
34
|
+
this.centre[1] = y;
|
|
35
|
+
this.centre[2] = z;
|
|
36
|
+
if (this.shown <= 0 || complete < this.eased)
|
|
37
|
+
this.eased = complete;
|
|
38
|
+
else
|
|
39
|
+
this.eased += (complete - this.eased) * (1 - Math.exp(-RADIUS_GROWTH_PER_SEC * dtSec));
|
|
40
|
+
if (ready)
|
|
41
|
+
this.shown = this.fadeSec > 0 ? Math.min(1, this.shown + dtSec / this.fadeSec) : 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
* pixel the shader shades exactly inside the radius the selection promised. A mirror's camera, far
|
|
14
14
|
* from the centre, gets a smaller radius and more of the volume, which is the safe direction.
|
|
15
15
|
*/
|
|
16
|
-
import type {
|
|
16
|
+
import type { DriftLightVolumes } from './presence.ts';
|
|
17
17
|
export interface DriftLightUniforms {
|
|
18
18
|
/** How much of the summed light is in, the radius, the band, the scale. */
|
|
19
19
|
readonly light: Float32Array;
|
|
20
|
-
/** The first
|
|
20
|
+
/** The first sample, and the metres between samples — negative for a dense volume. */
|
|
21
21
|
readonly origin: Float32Array;
|
|
22
22
|
}
|
|
23
23
|
export declare function createDriftLightUniforms(): DriftLightUniforms;
|
|
@@ -25,4 +25,4 @@ export declare function createDriftLightUniforms(): DriftLightUniforms;
|
|
|
25
25
|
* Fill `out` for `field`, or for no field, seen from `eye`; `probePass` while a probe's faces are
|
|
26
26
|
* being drawn.
|
|
27
27
|
*/
|
|
28
|
-
export declare function resolveDriftLight(field:
|
|
28
|
+
export declare function resolveDriftLight(field: DriftLightVolumes | null, probePass: boolean, eye: ArrayLike<number>, out: DriftLightUniforms): void;
|
|
@@ -24,18 +24,17 @@ export function createDriftLightUniforms() {
|
|
|
24
24
|
* being drawn.
|
|
25
25
|
*/
|
|
26
26
|
export function resolveDriftLight(field, probePass, eye, out) {
|
|
27
|
-
if (field === null || !field.ready || field.presence <= 0 || field.
|
|
27
|
+
if (field === null || !field.ready || field.presence <= 0 || field.empty) {
|
|
28
28
|
out.light.fill(0);
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
|
-
const { layout } = field;
|
|
32
31
|
const offset = Math.hypot((eye[0] ?? 0) - field.centre[0], (eye[1] ?? 0) - field.centre[1], (eye[2] ?? 0) - field.centre[2]);
|
|
33
32
|
out.light[0] = field.presence;
|
|
34
33
|
out.light[1] = probePass ? 0 : Math.max(0, field.radius - offset);
|
|
35
34
|
out.light[2] = field.band;
|
|
36
35
|
out.light[3] = field.scale;
|
|
37
|
-
out.origin[0] =
|
|
38
|
-
out.origin[1] =
|
|
39
|
-
out.origin[2] =
|
|
40
|
-
out.origin[3] =
|
|
36
|
+
out.origin[0] = field.sampleOrigin[0];
|
|
37
|
+
out.origin[1] = field.sampleOrigin[1];
|
|
38
|
+
out.origin[2] = field.sampleOrigin[2];
|
|
39
|
+
out.origin[3] = field.signedSpacing;
|
|
41
40
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A world's DriftLight: a dense volume baked offline (`bakeDenseField`), handed to the renderer
|
|
3
|
+
* whole, standing in for every fixed light past the frame's exact choice.
|
|
4
|
+
*
|
|
5
|
+
* **Whole from the start**, because nothing is baked in the page: `ready` is true on construction
|
|
6
|
+
* and the volume fades in over `fadeSec` from the first `follow`. The lights it summed are the
|
|
7
|
+
* scene's to mark `inLightField`, so the selection knows not to count them twice — a volume carries
|
|
8
|
+
* no list of the lights it came from.
|
|
9
|
+
*
|
|
10
|
+
* **Light and direction side by side along y**, the short axis: a city is 256 samples across and a
|
|
11
|
+
* few dozen up, and a 3D texture promises 256 on every axis, so stacking the two there keeps both
|
|
12
|
+
* inside one texture and one sampler. The shader keeps each lookup inside its own half.
|
|
13
|
+
*/
|
|
14
|
+
import type { DenseLightVolume } from './denseField.ts';
|
|
15
|
+
import type { DriftLightVolumes } from './presence.ts';
|
|
16
|
+
export interface WorldLightFieldOptions {
|
|
17
|
+
/** Metres over which exact and summed light crossfade at the shading radius. Two by default. */
|
|
18
|
+
readonly band?: number;
|
|
19
|
+
/** Seconds the volume fades in over; zero behind a loading screen, whose clock may be held. */
|
|
20
|
+
readonly fadeSec?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare class WorldLightField implements DriftLightVolumes {
|
|
23
|
+
readonly ready = true;
|
|
24
|
+
readonly empty = false;
|
|
25
|
+
readonly indexDims: readonly [number, number, number];
|
|
26
|
+
readonly index: Uint32Array<ArrayBuffer>;
|
|
27
|
+
readonly atlasSize: readonly [number, number, number];
|
|
28
|
+
readonly atlas: Uint16Array;
|
|
29
|
+
readonly band: number;
|
|
30
|
+
readonly sampleOrigin: readonly [number, number, number];
|
|
31
|
+
readonly signedSpacing: number;
|
|
32
|
+
/** The summed light's brightness, for a scene that dims all its lamps at once. */
|
|
33
|
+
scale: number;
|
|
34
|
+
private readonly showing;
|
|
35
|
+
constructor(volume: DenseLightVolume, options?: WorldLightFieldOptions);
|
|
36
|
+
get radius(): number;
|
|
37
|
+
get presence(): number;
|
|
38
|
+
get centre(): Float32Array;
|
|
39
|
+
/** Follow the frame's choice, as `LightField.follow` does. */
|
|
40
|
+
follow(complete: number, x: number, y: number, z: number, dtSec: number): void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A world's DriftLight: a dense volume baked offline (`bakeDenseField`), handed to the renderer
|
|
3
|
+
* whole, standing in for every fixed light past the frame's exact choice.
|
|
4
|
+
*
|
|
5
|
+
* **Whole from the start**, because nothing is baked in the page: `ready` is true on construction
|
|
6
|
+
* and the volume fades in over `fadeSec` from the first `follow`. The lights it summed are the
|
|
7
|
+
* scene's to mark `inLightField`, so the selection knows not to count them twice — a volume carries
|
|
8
|
+
* no list of the lights it came from.
|
|
9
|
+
*
|
|
10
|
+
* **Light and direction side by side along y**, the short axis: a city is 256 samples across and a
|
|
11
|
+
* few dozen up, and a 3D texture promises 256 on every axis, so stacking the two there keeps both
|
|
12
|
+
* inside one texture and one sampler. The shader keeps each lookup inside its own half.
|
|
13
|
+
*/
|
|
14
|
+
import { halfBits } from '../halfFloat.js';
|
|
15
|
+
import { FieldPresence } from './presence.js';
|
|
16
|
+
/** The index a dense volume binds in the sparse one's place: one cell, no brick. */
|
|
17
|
+
const NO_INDEX = new Uint32Array(1);
|
|
18
|
+
export class WorldLightField {
|
|
19
|
+
ready = true;
|
|
20
|
+
empty = false;
|
|
21
|
+
indexDims = [1, 1, 1];
|
|
22
|
+
index = NO_INDEX;
|
|
23
|
+
atlasSize;
|
|
24
|
+
atlas;
|
|
25
|
+
band;
|
|
26
|
+
sampleOrigin;
|
|
27
|
+
signedSpacing;
|
|
28
|
+
/** The summed light's brightness, for a scene that dims all its lamps at once. */
|
|
29
|
+
scale = 1;
|
|
30
|
+
showing;
|
|
31
|
+
constructor(volume, options = {}) {
|
|
32
|
+
const [x, y, z] = volume.dims;
|
|
33
|
+
this.atlasSize = [x, y * 2, z];
|
|
34
|
+
this.atlas = new Uint16Array(x * y * 2 * z * 4);
|
|
35
|
+
for (let k = 0; k < z; k++) {
|
|
36
|
+
for (let j = 0; j < y; j++) {
|
|
37
|
+
for (let i = 0; i < x; i++) {
|
|
38
|
+
const from = (i + x * (j + y * k)) * 4;
|
|
39
|
+
const light = (i + x * (j + 2 * y * k)) * 4;
|
|
40
|
+
const direction = (i + x * (j + y + 2 * y * k)) * 4;
|
|
41
|
+
for (let c = 0; c < 4; c++) {
|
|
42
|
+
this.atlas[light + c] = halfBits(volume.light[from + c]);
|
|
43
|
+
this.atlas[direction + c] = halfBits(volume.direction[from + c]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
this.band = options.band ?? 2;
|
|
49
|
+
this.sampleOrigin = volume.origin;
|
|
50
|
+
this.signedSpacing = -volume.spacing;
|
|
51
|
+
this.showing = new FieldPresence(Math.max(0, options.fadeSec ?? 0.5));
|
|
52
|
+
}
|
|
53
|
+
get radius() {
|
|
54
|
+
return this.showing.radius;
|
|
55
|
+
}
|
|
56
|
+
get presence() {
|
|
57
|
+
return this.showing.presence;
|
|
58
|
+
}
|
|
59
|
+
get centre() {
|
|
60
|
+
return this.showing.centre;
|
|
61
|
+
}
|
|
62
|
+
/** Follow the frame's choice, as `LightField.follow` does. */
|
|
63
|
+
follow(complete, x, y, z, dtSec) {
|
|
64
|
+
this.showing.follow(true, complete, x, y, z, dtSec);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface FixtureAtlasLayout {
|
|
2
|
+
readonly width: number;
|
|
3
|
+
readonly height: number;
|
|
4
|
+
/** Rows of cookie tiles at the top: `COOKIE_TILE`, or 0 with none loaded. */
|
|
5
|
+
readonly cookieRows: number;
|
|
6
|
+
/** The first row of the profiles, below the cookies. */
|
|
7
|
+
readonly iesTop: number;
|
|
8
|
+
}
|
|
9
|
+
/** The atlas for `iesRows` rows of profile and `cookieTiles` cookies. */
|
|
10
|
+
export declare function fixtureAtlasLayout(iesRows: number, cookieTiles: number): FixtureAtlasLayout;
|
|
11
|
+
/**
|
|
12
|
+
* The profiles' rows as the atlas holds them: `data` one float a texel, `width` across and `rows`
|
|
13
|
+
* down, spread into the red channel of a four-channel image the atlas's size, the rest ones. The
|
|
14
|
+
* cookie band is left zero for the backend to fill from its images.
|
|
15
|
+
*/
|
|
16
|
+
export declare function fixtureAtlasTexels(layout: FixtureAtlasLayout, ies: {
|
|
17
|
+
readonly data: Float32Array;
|
|
18
|
+
readonly width: number;
|
|
19
|
+
readonly height: number;
|
|
20
|
+
}): Float32Array;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a light fixture throws, in one texture: its spot cookies in the top band, its photometric
|
|
3
|
+
* profiles in the rows below.
|
|
4
|
+
*
|
|
5
|
+
* **One binding for two tables, because the lit stage had one sampler left.** The cookie atlas and
|
|
6
|
+
* the photometric atlas were two small float textures read with `textureLod`, and the widest lit
|
|
7
|
+
* shader bound fifteen of the sixteen fragment samplers WebGL2 guarantees — so the next sampler had
|
|
8
|
+
* to be found by folding two into one, which is what this is. Both describe a fixture's emission
|
|
9
|
+
* pattern, and both are 128 texels across, which is what lets them share a texture exactly.
|
|
10
|
+
*
|
|
11
|
+
* **Cookies on top, profiles at the bottom, and the order is load-bearing.** A profile's row blend
|
|
12
|
+
* only ever reaches forward — to the next plane, and from the last to the repeat of the first — so
|
|
13
|
+
* past its last row it meets the texture's edge and the clamp, never a cookie. A cookie's own
|
|
14
|
+
* half-texel inset keeps it inside its band on both axes. The shader finds the bands from
|
|
15
|
+
* `textureSize` and `uIesAtlasRows`, so the fold cost no uniform.
|
|
16
|
+
*
|
|
17
|
+
* **What it gives up**: every texel is four half floats where a profile needed one, a few kilobytes
|
|
18
|
+
* a scene; and setting either table rebuilds the whole texture, which a consumer does once per set.
|
|
19
|
+
*/
|
|
20
|
+
import { IES_ATLAS_WIDTH } from './iesProfile.js';
|
|
21
|
+
import { COOKIE_TILE } from './lightBudget.js';
|
|
22
|
+
/** The atlas for `iesRows` rows of profile and `cookieTiles` cookies. */
|
|
23
|
+
export function fixtureAtlasLayout(iesRows, cookieTiles) {
|
|
24
|
+
const cookieRows = cookieTiles > 0 ? COOKIE_TILE : 0;
|
|
25
|
+
return {
|
|
26
|
+
width: Math.max(IES_ATLAS_WIDTH, COOKIE_TILE * cookieTiles),
|
|
27
|
+
height: cookieRows + iesRows,
|
|
28
|
+
cookieRows,
|
|
29
|
+
iesTop: cookieRows,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The profiles' rows as the atlas holds them: `data` one float a texel, `width` across and `rows`
|
|
34
|
+
* down, spread into the red channel of a four-channel image the atlas's size, the rest ones. The
|
|
35
|
+
* cookie band is left zero for the backend to fill from its images.
|
|
36
|
+
*/
|
|
37
|
+
export function fixtureAtlasTexels(layout, ies) {
|
|
38
|
+
const out = new Float32Array(layout.width * layout.height * 4);
|
|
39
|
+
for (let row = 0; row < ies.height; row++) {
|
|
40
|
+
for (let x = 0; x < ies.width; x++) {
|
|
41
|
+
const at = ((layout.iesTop + row) * layout.width + x) * 4;
|
|
42
|
+
out[at] = ies.data[row * ies.width + x];
|
|
43
|
+
out[at + 1] = 1;
|
|
44
|
+
out[at + 2] = 1;
|
|
45
|
+
out[at + 3] = 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which instances of a batch can be seen: the decision both backends make, and the reference the
|
|
3
|
+
* WebGPU compute cull is held to.
|
|
4
|
+
*
|
|
5
|
+
* **An instance's bounds are the mesh's, placed by its matrix** — the mesh's bounding sphere with its
|
|
6
|
+
* centre through the model matrix and its radius scaled by the largest axis, exactly as
|
|
7
|
+
* `boundsVisible` treats one mesh — so a batch needs no bounds of its own and a caller supplies none.
|
|
8
|
+
* The test is `sphereInFrustum`'s: kept unless wholly beyond one plane.
|
|
9
|
+
*
|
|
10
|
+
* WebGL2 runs `cullInstances` on the CPU; WebGPU runs `CULL_INSTANCES_WGSL`, the same arithmetic, on
|
|
11
|
+
* the device. Both keep what this keeps: `gpu-parity.mjs` compares them on a device. What this gives
|
|
12
|
+
* up is shape: a long thin mesh is culled by the sphere around it, which keeps it in more views than
|
|
13
|
+
* its box would. What would make that wrong is a batch of long thin things seen end on, where a box
|
|
14
|
+
* test pays; nothing in the engine draws those instanced today.
|
|
15
|
+
*
|
|
16
|
+
* Per frame and allocation-free: everything writes into arrays the caller owns.
|
|
17
|
+
*/
|
|
18
|
+
import type { MeshInstances } from './instances.ts';
|
|
19
|
+
import type { Bounds } from '../math/bounds.ts';
|
|
20
|
+
import type { FrustumPlanes } from '../math/frustum.ts';
|
|
21
|
+
/**
|
|
22
|
+
* Indices a culling batch must draw, all its instances together, before culling it instance by
|
|
23
|
+
* instance pays; below it the batch is drawn whole once its box is seen (`batchBoxVisible`).
|
|
24
|
+
*
|
|
25
|
+
* **What a per-instance cull costs is fixed a batch, and what it saves grows with the batch.** On
|
|
26
|
+
* WebGPU it is a dispatch and an indirect draw, and Chrome validates every indirect draw in its GPU
|
|
27
|
+
* process — measured at 8 µs apiece in a city frame whose six hundred small prop batches took one
|
|
28
|
+
* each, 52 fps against 70 with those draws gone. On WebGL2 it is a loop over the instances and an
|
|
29
|
+
* upload of the survivors. A few thousand indices cost a device less than either.
|
|
30
|
+
*
|
|
31
|
+
* What it gives up is the instances of a small batch that stand out of view once its box is in it,
|
|
32
|
+
* which the device transforms and clips for nothing. What would make it wrong is a device whose
|
|
33
|
+
* vertex stage limits the frame while the CPU idles — where this wants to come down.
|
|
34
|
+
*/
|
|
35
|
+
export declare const INSTANCE_CULL_MIN_INDICES: number;
|
|
36
|
+
/**
|
|
37
|
+
* Whether a culling batch of `count` instances of a mesh of `indexCount` indices is culled instance
|
|
38
|
+
* by instance, or drawn whole once its box is seen. See `INSTANCE_CULL_MIN_INDICES`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function cullsInstances(count: number, indexCount: number): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Keep the instances of `data` whose spheres meet `frustum`, compacted in order into `out`, and
|
|
43
|
+
* return how many. `out` must hold as many as `data` does; its count is set.
|
|
44
|
+
*/
|
|
45
|
+
export declare function cullInstances(data: MeshInstances, local: Bounds, frustum: FrustumPlanes, out: MeshInstances): number;
|
|
46
|
+
/**
|
|
47
|
+
* The world box around every instance's sphere, for testing a whole batch at once: (minX, minY,
|
|
48
|
+
* minZ, maxX, maxY, maxZ) into `out`. A batch holding nothing writes a min above its max.
|
|
49
|
+
*
|
|
50
|
+
* **A box, not a sphere, and the difference is the whole point for a flat batch.** A region of props
|
|
51
|
+
* a hundred metres square has a sphere seventy metres deep underground, where nothing occludes it,
|
|
52
|
+
* so a sphere test never finds a region hidden. The box stays as flat as the props are.
|
|
53
|
+
*/
|
|
54
|
+
export declare function instancesBox(data: MeshInstances, local: Bounds, out: Float32Array): void;
|
|
55
|
+
/** What the batch test asks of an occlusion buffer. `OcclusionBuffer` and both renderers answer it. */
|
|
56
|
+
export interface BoxOccluder {
|
|
57
|
+
occludedBox(min: ArrayLike<number>, max: ArrayLike<number>): boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Whether a batch's box (`instancesBox`) can be seen at all: inside the frustum, and not behind the
|
|
61
|
+
* declared occluders. One test for a whole region's props, run before any instance is looked at.
|
|
62
|
+
*/
|
|
63
|
+
export declare function batchBoxVisible(box: Float32Array, frustum: FrustumPlanes, occluder: BoxOccluder | null): boolean;
|