@driftengine/core 4.8.5 → 4.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +4392 -4250
- package/README.md +1 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.js +12 -2
- package/dist/render/ambientHarmonics.d.ts +46 -0
- package/dist/render/ambientHarmonics.js +80 -0
- package/dist/render/anisotropicLobe.d.ts +10 -0
- package/dist/render/anisotropicLobe.js +19 -0
- package/dist/render/areaLights.d.ts +54 -0
- package/dist/render/areaLights.js +55 -0
- package/dist/render/backend/api.d.ts +43 -2
- package/dist/render/backend/webgl2/instanced.d.ts +2 -0
- package/dist/render/backend/webgl2/instanced.js +11 -2
- package/dist/render/backend/webgl2/renderer.d.ts +120 -11
- package/dist/render/backend/webgl2/renderer.js +356 -53
- package/dist/render/backend/webgl2/sceneCaptureTarget.d.ts +29 -0
- package/dist/render/backend/webgl2/sceneCaptureTarget.js +96 -0
- package/dist/render/backend/webgl2/skinScatter.d.ts +14 -0
- package/dist/render/backend/webgl2/skinScatter.js +88 -26
- package/dist/render/backend/webgpu/buffers.js +2 -1
- package/dist/render/backend/webgpu/clusterBinner.js +3 -2
- package/dist/render/backend/webgpu/flatPass.d.ts +7 -5
- package/dist/render/backend/webgpu/flatPass.js +15 -13
- package/dist/render/backend/webgpu/particlePass.d.ts +37 -4
- package/dist/render/backend/webgpu/particlePass.js +81 -12
- package/dist/render/backend/webgpu/postPass.d.ts +6 -1
- package/dist/render/backend/webgpu/postPass.js +7 -2
- package/dist/render/backend/webgpu/renderer.d.ts +93 -8
- package/dist/render/backend/webgpu/renderer.js +483 -93
- package/dist/render/backend/webgpu/sceneCaptureTarget.d.ts +24 -0
- package/dist/render/backend/webgpu/sceneCaptureTarget.js +59 -0
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.js +41 -1
- package/dist/render/backend/webgpu/skinPaletteRing.d.ts +40 -28
- package/dist/render/backend/webgpu/skinPaletteRing.js +69 -32
- package/dist/render/backend/webgpu/skinScatterPass.d.ts +10 -2
- package/dist/render/backend/webgpu/skinScatterPass.js +59 -9
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +5 -1
- package/dist/render/backend/webgpu/surfaceTexturePass.js +45 -5
- package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
- package/dist/render/bloomChain.d.ts +44 -0
- package/dist/render/bloomChain.js +28 -0
- package/dist/render/bloomPass.d.ts +3 -1
- package/dist/render/bloomPass.js +16 -2
- package/dist/render/clusteredLights.d.ts +78 -6
- package/dist/render/clusteredLights.js +214 -13
- package/dist/render/driftLight/bake.d.ts +2 -0
- package/dist/render/driftLight/bake.js +1 -1
- package/dist/render/driftLight/falloff.d.ts +4 -2
- package/dist/render/driftLight/falloff.js +8 -2
- package/dist/render/fixtureAtlas.js +1 -1
- package/dist/render/gi/bounceLights.d.ts +3 -2
- package/dist/render/gi/bounceLights.js +8 -3
- package/dist/render/instanceCull.js +12 -0
- package/dist/render/instances.d.ts +47 -11
- package/dist/render/instances.js +24 -9
- package/dist/render/lightBudget.d.ts +38 -2
- package/dist/render/lightBudget.js +48 -5
- package/dist/render/lightmap.d.ts +116 -0
- package/dist/render/lightmap.js +140 -0
- package/dist/render/mesh.js +2 -1
- package/dist/render/particleBatch.d.ts +59 -3
- package/dist/render/particleBatch.js +53 -2
- package/dist/render/particleMaterial.d.ts +6 -1
- package/dist/render/particleMaterial.js +10 -2
- package/dist/render/particlePool.d.ts +32 -0
- package/dist/render/particlePool.js +9 -0
- package/dist/render/particleSort.d.ts +34 -0
- package/dist/render/particleSort.js +96 -0
- package/dist/render/pointLightSelection.d.ts +13 -0
- package/dist/render/pointLightSelection.js +3 -1
- package/dist/render/renderQuality.d.ts +8 -7
- package/dist/render/rendererHarness.js +3 -0
- package/dist/render/rgb9e5.d.ts +22 -0
- package/dist/render/rgb9e5.js +47 -0
- package/dist/render/sceneCapture.d.ts +29 -0
- package/dist/render/sceneCapture.js +36 -0
- package/dist/render/sceneTarget.d.ts +5 -0
- package/dist/render/sceneTarget.js +8 -1
- package/dist/render/screenSpaceReflection.d.ts +27 -1
- package/dist/render/screenSpaceReflection.js +9 -0
- package/dist/render/shaders/bloom.d.ts +8 -4
- package/dist/render/shaders/bloom.js +20 -3
- package/dist/render/shaders/bolt.d.ts +1 -1
- package/dist/render/shaders/bolt.js +7 -1
- package/dist/render/shaders/depth.js +11 -0
- package/dist/render/shaders/flat/anisotropic.d.ts +1 -1
- package/dist/render/shaders/flat/anisotropic.js +37 -3
- package/dist/render/shaders/flat/areaLight.d.ts +10 -0
- package/dist/render/shaders/flat/areaLight.js +142 -0
- package/dist/render/shaders/flat/glassTint.d.ts +3 -2
- package/dist/render/shaders/flat/glassTint.js +3 -1
- package/dist/render/shaders/flat/index.d.ts +5 -0
- package/dist/render/shaders/flat/index.js +46 -6
- package/dist/render/shaders/flat/lobes.d.ts +6 -0
- package/dist/render/shaders/flat/lobes.js +49 -43
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +158 -121
- package/dist/render/shaders/flat/models.d.ts +21 -6
- package/dist/render/shaders/flat/models.js +161 -17
- package/dist/render/shaders/flat/normalMap.d.ts +10 -0
- package/dist/render/shaders/flat/normalMap.js +25 -0
- package/dist/render/shaders/flat/preamble.js +84 -40
- package/dist/render/shaders/flat/skin.js +6 -5
- package/dist/render/shaders/generated/bloom.wgsl.d.ts +13 -3
- package/dist/render/shaders/generated/bloom.wgsl.js +13 -3
- package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
- package/dist/render/shaders/generated/depth.wgsl.d.ts +4 -4
- package/dist/render/shaders/generated/depth.wgsl.js +4 -4
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1647 -2044
- package/dist/render/shaders/generated/flat.wgsl.js +1685 -2082
- package/dist/render/shaders/generated/particle.wgsl.d.ts +115 -1
- package/dist/render/shaders/generated/particle.wgsl.js +115 -1
- package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/skinBlur.wgsl.js +12 -2
- package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +11 -6
- package/dist/render/shaders/generated/ssrTrace.wgsl.js +11 -6
- package/dist/render/shaders/generated/water.wgsl.d.ts +23 -16
- package/dist/render/shaders/generated/water.wgsl.js +23 -16
- package/dist/render/shaders/gi/probeBake.wgsl.js +9 -5
- package/dist/render/shaders/particle.d.ts +17 -1
- package/dist/render/shaders/particle.js +133 -1
- package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
- package/dist/render/shaders/recon/motion.wgsl.js +13 -2
- package/dist/render/shaders/skinBlur.js +25 -3
- package/dist/render/shaders/ssrTrace.js +20 -1
- package/dist/render/shaders/water.d.ts +1 -1
- package/dist/render/shaders/water.js +12 -3
- package/dist/render/skinBlur.d.ts +8 -5
- package/dist/render/skinBlur.js +8 -5
- package/dist/render/skinScatter.d.ts +3 -1
- package/dist/render/skinScatter.js +3 -1
- package/dist/render/ssrPass.js +1 -0
- package/dist/render/surfaceModel.d.ts +49 -8
- package/dist/render/surfaceModel.js +54 -8
- package/dist/render/surfaceTexture.d.ts +38 -1
- package/dist/render/surfaceTexture.js +37 -2
- package/package.json +6 -6
- package/src/index.ts +26 -3
- package/src/render/ambientHarmonics.ts +92 -0
- package/src/render/anisotropicLobe.ts +29 -0
- package/src/render/areaLights.ts +97 -0
- package/src/render/backend/api.ts +61 -1
- package/src/render/backend/webgl2/instanced.ts +12 -2
- package/src/render/backend/webgl2/renderer.ts +470 -59
- package/src/render/backend/webgl2/sceneCaptureTarget.ts +130 -0
- package/src/render/backend/webgl2/skinScatter.ts +120 -43
- package/src/render/backend/webgpu/buffers.ts +2 -1
- package/src/render/backend/webgpu/clusterBinner.ts +3 -1
- package/src/render/backend/webgpu/flatPass.ts +18 -13
- package/src/render/backend/webgpu/particlePass.ts +119 -12
- package/src/render/backend/webgpu/postPass.ts +7 -2
- package/src/render/backend/webgpu/renderer.ts +589 -113
- package/src/render/backend/webgpu/sceneCaptureTarget.ts +63 -0
- package/src/render/backend/webgpu/shaders/clusterBin.wgsl.ts +41 -1
- package/src/render/backend/webgpu/skinPaletteRing.ts +65 -33
- package/src/render/backend/webgpu/skinScatterPass.ts +59 -9
- package/src/render/backend/webgpu/surfaceTexturePass.ts +57 -6
- package/src/render/bloomChain.ts +69 -0
- package/src/render/bloomPass.ts +26 -2
- package/src/render/clusteredLights.ts +259 -12
- package/src/render/driftLight/bake.ts +3 -1
- package/src/render/driftLight/falloff.ts +13 -2
- package/src/render/fixtureAtlas.ts +1 -1
- package/src/render/gi/bounceLights.ts +12 -3
- package/src/render/instanceCull.ts +9 -0
- package/src/render/instances.ts +61 -12
- package/src/render/lightBudget.ts +88 -4
- package/src/render/lightmap.ts +243 -0
- package/src/render/mesh.ts +2 -1
- package/src/render/particleBatch.ts +95 -4
- package/src/render/particleMaterial.ts +11 -1
- package/src/render/particlePool.ts +36 -0
- package/src/render/particleSort.ts +133 -0
- package/src/render/pointLightSelection.ts +20 -1
- package/src/render/renderQuality.ts +8 -7
- package/src/render/rendererHarness.ts +3 -0
- package/src/render/rgb9e5.ts +52 -0
- package/src/render/sceneCapture.ts +56 -0
- package/src/render/sceneTarget.ts +18 -1
- package/src/render/screenSpaceReflection.ts +33 -1
- package/src/render/shaders/bloom.ts +20 -3
- package/src/render/shaders/bolt.ts +7 -1
- package/src/render/shaders/depth.ts +11 -0
- package/src/render/shaders/flat/anisotropic.ts +37 -3
- package/src/render/shaders/flat/areaLight.ts +142 -0
- package/src/render/shaders/flat/glassTint.ts +9 -2
- package/src/render/shaders/flat/index.ts +51 -6
- package/src/render/shaders/flat/lobes.ts +50 -43
- package/src/render/shaders/flat/main.ts +158 -121
- package/src/render/shaders/flat/models.ts +170 -21
- package/src/render/shaders/flat/normalMap.ts +25 -0
- package/src/render/shaders/flat/preamble.ts +84 -40
- package/src/render/shaders/flat/skin.ts +6 -5
- package/src/render/shaders/generated/bloom.wgsl.ts +13 -3
- package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
- package/src/render/shaders/generated/depth.wgsl.ts +4 -4
- package/src/render/shaders/generated/flat.wgsl.ts +1685 -2082
- package/src/render/shaders/generated/particle.wgsl.ts +116 -1
- package/src/render/shaders/generated/skinBlur.wgsl.ts +12 -2
- package/src/render/shaders/generated/ssrTrace.wgsl.ts +11 -6
- package/src/render/shaders/generated/water.wgsl.ts +23 -16
- package/src/render/shaders/gi/probeBake.wgsl.ts +9 -5
- package/src/render/shaders/particle.ts +134 -1
- package/src/render/shaders/recon/motion.wgsl.ts +13 -2
- package/src/render/shaders/skinBlur.ts +25 -3
- package/src/render/shaders/ssrTrace.ts +20 -1
- package/src/render/shaders/water.ts +12 -3
- package/src/render/skinBlur.ts +8 -5
- package/src/render/skinScatter.ts +3 -1
- package/src/render/ssrPass.ts +1 -0
- package/src/render/surfaceModel.ts +82 -10
- package/src/render/surfaceTexture.ts +101 -3
|
@@ -3,6 +3,27 @@ import { particleShaders, type ParticleMaterial } from './particleMaterial.ts';
|
|
|
3
3
|
import { PARTICLE_BLADES, PARTICLE_INDICES, PARTICLE_VERTS } from './shaders/particle.ts';
|
|
4
4
|
import type { ParticleInstances } from './particlePool.ts';
|
|
5
5
|
import { blendCovering, blendKeeping } from './sceneCoverage.ts';
|
|
6
|
+
import type { SurfaceTexture } from './surfaceTexture.ts';
|
|
7
|
+
import { PARTICLE_SPRITE_UNIT } from './lightBudget.ts';
|
|
8
|
+
import { createParticleSort, sortBackToFront, type ParticleSort } from './particleSort.ts';
|
|
9
|
+
|
|
10
|
+
/** The vertex stage's `uCameraFacing` for a facing: the cross 0, the camera 1, the travel 2. */
|
|
11
|
+
export function facingCode(facing: ParticleFacing): number {
|
|
12
|
+
return facing === 'cross' ? 0 : facing === 'velocity' ? 2 : 1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A sprite's frame and half-height, two floats a particle, into `out` — the stream the vertex
|
|
17
|
+
* stage reads at location 10. Shared by both backends, so the two read one layout.
|
|
18
|
+
*/
|
|
19
|
+
export function packSpriteStream(data: ParticleInstances, count: number, out: Float32Array): void {
|
|
20
|
+
const frames = data.frames;
|
|
21
|
+
const heights = data.heights;
|
|
22
|
+
for (let i = 0; i < count; i++) {
|
|
23
|
+
out[i * 2] = frames?.[i] ?? 0;
|
|
24
|
+
out[i * 2 + 1] = heights?.[i] ?? 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
6
27
|
|
|
7
28
|
/**
|
|
8
29
|
* One draw call for a live particle pool: one camera-facing quad per particle, or a
|
|
@@ -26,9 +47,11 @@ export type ParticleBlend = 'additive' | 'alpha';
|
|
|
26
47
|
* `'camera'` is one quad facing the viewer, and the default. `'cross'` is the pair of
|
|
27
48
|
* world-fixed blades this batch drew before the option existed — see `uCameraFacing` in
|
|
28
49
|
* `shaders/particle.ts` for the artifact that made the default the other one, and for what
|
|
29
|
-
* the cross still buys a caller who wants it back.
|
|
50
|
+
* the cross still buys a caller who wants it back. `'velocity'` faces the camera with its up along
|
|
51
|
+
* the particle's travel across the view — a streak, a spark stretched by its motion — and a
|
|
52
|
+
* particle at rest faces the camera as `'camera'` does.
|
|
30
53
|
*/
|
|
31
|
-
export type ParticleFacing = 'camera' | 'cross';
|
|
54
|
+
export type ParticleFacing = 'camera' | 'cross' | 'velocity';
|
|
32
55
|
|
|
33
56
|
export interface ParticleBatchOptions {
|
|
34
57
|
/**
|
|
@@ -78,6 +101,41 @@ export interface ParticleBatchOptions {
|
|
|
78
101
|
* shader's own comment on `uCameraFacing` lays out.
|
|
79
102
|
*/
|
|
80
103
|
facing?: ParticleFacing;
|
|
104
|
+
/**
|
|
105
|
+
* The image a `'sprite'` draws: a surface texture, its first layer. **Required by the sprite** and
|
|
106
|
+
* ignored by the procedural materials. Colour is the particle's times the image's, unlit.
|
|
107
|
+
*/
|
|
108
|
+
texture?: SurfaceTexture;
|
|
109
|
+
/**
|
|
110
|
+
* A `'sprite'`'s flipbook: the image as `[columns, rows]` cells, numbered across then down from
|
|
111
|
+
* the top-left, a particle's `frames` naming one. `[1, 1]`, the default, is the whole image.
|
|
112
|
+
*/
|
|
113
|
+
cells?: readonly [number, number];
|
|
114
|
+
/**
|
|
115
|
+
* Whether a `'sprite'` blends from a particle's cell toward the next by the fraction of its
|
|
116
|
+
* frame, so a slow flipbook moves rather than steps. Off by default: one cell, sampled once.
|
|
117
|
+
*/
|
|
118
|
+
blendCells?: boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Metres over which a `'sprite'` fades where it meets the opaque scene: the hard line a flat card
|
|
121
|
+
* draws where it cuts a floor or a body, softened. **0, the default, is a hard edge.** It reads a
|
|
122
|
+
* copy of the frame's depth, which needs `screenEffects`; without one the edge stays hard and
|
|
123
|
+
* the renderer says so once.
|
|
124
|
+
*/
|
|
125
|
+
softDepth?: number;
|
|
126
|
+
/** Metres in front of the eye over which a `'sprite'` fades in. 0, the default, is no fade. */
|
|
127
|
+
cameraFade?: number;
|
|
128
|
+
/**
|
|
129
|
+
* Metres each particle is moved toward the eye before it is drawn, so a sprite born inside a
|
|
130
|
+
* surface is drawn in front of it. 0 by default. Every material reads it.
|
|
131
|
+
*/
|
|
132
|
+
cameraOffset?: number;
|
|
133
|
+
/**
|
|
134
|
+
* Whether the live particles are drawn farthest first, the order an alpha blend assumes. Off
|
|
135
|
+
* by default, because sorting costs a pass over the particles on the CPU each frame and an
|
|
136
|
+
* additive blend is a sum that does not care. See `particleSort.ts`.
|
|
137
|
+
*/
|
|
138
|
+
sort?: boolean;
|
|
81
139
|
/**
|
|
82
140
|
* Another batch whose compiled program this one should reuse.
|
|
83
141
|
*
|
|
@@ -118,6 +176,17 @@ export class ParticleBatch {
|
|
|
118
176
|
private readonly ageBuffer: WebGLBuffer;
|
|
119
177
|
private readonly seedBuffer: WebGLBuffer;
|
|
120
178
|
private readonly velocityBuffer: WebGLBuffer;
|
|
179
|
+
private readonly spriteBuffer: WebGLBuffer;
|
|
180
|
+
private readonly spriteStaging: Float32Array;
|
|
181
|
+
/** Room to draw farthest first, where the batch asked. See `ParticleBatchOptions.sort`. */
|
|
182
|
+
private readonly order: ParticleSort | null;
|
|
183
|
+
/** The sprite's own: its image, its flipbook and its fades. See `ParticleBatchOptions`. */
|
|
184
|
+
readonly texture: SurfaceTexture | null;
|
|
185
|
+
readonly cells: readonly [number, number];
|
|
186
|
+
readonly blendCells: boolean;
|
|
187
|
+
readonly softDepth: number;
|
|
188
|
+
readonly cameraFade: number;
|
|
189
|
+
readonly cameraOffset: number;
|
|
121
190
|
private readonly blend: ParticleBlend;
|
|
122
191
|
private readonly stretchSec: number;
|
|
123
192
|
private readonly erosion: number;
|
|
@@ -152,6 +221,17 @@ export class ParticleBatch {
|
|
|
152
221
|
this.coreGain = options.coreGain ?? 1;
|
|
153
222
|
this.fog = options.fog ?? false;
|
|
154
223
|
this.facing = options.facing ?? 'camera';
|
|
224
|
+
this.texture = options.texture ?? null;
|
|
225
|
+
this.cells = options.cells ?? [1, 1];
|
|
226
|
+
this.blendCells = options.blendCells === true;
|
|
227
|
+
this.softDepth = Math.max(0, options.softDepth ?? 0);
|
|
228
|
+
this.cameraFade = Math.max(0, options.cameraFade ?? 0);
|
|
229
|
+
this.cameraOffset = options.cameraOffset ?? 0;
|
|
230
|
+
this.order = options.sort === true ? createParticleSort(capacity) : null;
|
|
231
|
+
this.spriteStaging = new Float32Array(capacity * 2);
|
|
232
|
+
if (options.material === 'sprite' && this.texture === null) {
|
|
233
|
+
throw new Error("createParticles: a 'sprite' draws an image, and was given no `texture`");
|
|
234
|
+
}
|
|
155
235
|
|
|
156
236
|
const corners = new Float32Array(PARTICLE_VERTS * 2);
|
|
157
237
|
const blades = new Float32Array(PARTICLE_VERTS);
|
|
@@ -189,6 +269,7 @@ export class ParticleBatch {
|
|
|
189
269
|
this.ageBuffer = this.attach(gl, 7, capacity, 1, 1, shaders.label);
|
|
190
270
|
this.seedBuffer = this.attach(gl, 8, capacity, 1, 1, shaders.label);
|
|
191
271
|
this.velocityBuffer = this.attach(gl, 9, capacity * 3, 3, 1, shaders.label);
|
|
272
|
+
this.spriteBuffer = this.attach(gl, 10, capacity * 2, 2, 1, shaders.label);
|
|
192
273
|
|
|
193
274
|
const indexBuffer = gl.createBuffer();
|
|
194
275
|
if (indexBuffer === null) throw new Error(`${shaders.label}: createBuffer failed`);
|
|
@@ -204,9 +285,12 @@ export class ParticleBatch {
|
|
|
204
285
|
* Only the live prefix of each array, which is what the pool's compaction
|
|
205
286
|
* exists for: a pool at a tenth of its capacity uploads a tenth of the bytes.
|
|
206
287
|
*/
|
|
207
|
-
upload(gl: WebGL2RenderingContext,
|
|
288
|
+
upload(gl: WebGL2RenderingContext, given: ParticleInstances, eye: ArrayLike<number>): void {
|
|
289
|
+
const data = this.order === null ? given : sortBackToFront(given, eye, this.order);
|
|
208
290
|
const n = data.count;
|
|
209
291
|
if (n === 0) return;
|
|
292
|
+
packSpriteStream(data, n, this.spriteStaging);
|
|
293
|
+
this.send(gl, this.spriteBuffer, this.spriteStaging, n * 2);
|
|
210
294
|
this.send(gl, this.positionBuffer, data.positions, n * 3);
|
|
211
295
|
this.send(gl, this.sizeBuffer, data.sizes, n);
|
|
212
296
|
this.send(gl, this.spinBuffer, data.spins, n);
|
|
@@ -221,7 +305,14 @@ export class ParticleBatch {
|
|
|
221
305
|
bindMaterial(gl: WebGL2RenderingContext): void {
|
|
222
306
|
const u = this.uniforms;
|
|
223
307
|
gl.uniform1f(u['uStretchSec'] ?? null, this.stretchSec);
|
|
224
|
-
gl.uniform1f(u['uCameraFacing'] ?? null, this.facing
|
|
308
|
+
gl.uniform1f(u['uCameraFacing'] ?? null, facingCode(this.facing));
|
|
309
|
+
gl.uniform1f(u['uCameraOffset'] ?? null, this.cameraOffset);
|
|
310
|
+
/* The sprite's flipbook and its image; the fades are the renderer's, which owns the depth. */
|
|
311
|
+
if (this.texture !== null) {
|
|
312
|
+
gl.uniform4f(u['uCells'] ?? null, this.cells[0], this.cells[1], this.blendCells ? 1 : 0, 0);
|
|
313
|
+
this.texture.bind(gl, PARTICLE_SPRITE_UNIT);
|
|
314
|
+
gl.uniform1i(u['uSprite'] ?? null, PARTICLE_SPRITE_UNIT);
|
|
315
|
+
}
|
|
225
316
|
gl.uniform1f(u['uErosion'] ?? null, this.erosion);
|
|
226
317
|
gl.uniform1f(u['uCoreGain'] ?? null, this.coreGain);
|
|
227
318
|
/* Only the mote program declares uFogEnabled, so this is a no-op — not a leak into the
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
PARTICLE_MOTE_FRAG,
|
|
3
3
|
PARTICLE_SMOKE_FRAG,
|
|
4
4
|
PARTICLE_SPARK_FRAG,
|
|
5
|
+
PARTICLE_SPRITE_FRAG,
|
|
5
6
|
PARTICLE_VERT,
|
|
6
7
|
} from './shaders/particle.ts';
|
|
7
8
|
import {
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
PARTICLE_MOTE_FRAG_WGSL,
|
|
10
11
|
PARTICLE_SMOKE_FRAG_WGSL,
|
|
11
12
|
PARTICLE_SPARK_FRAG_WGSL,
|
|
13
|
+
PARTICLE_SPRITE_FRAG_WGSL,
|
|
12
14
|
PARTICLE_VERT_WGSL,
|
|
13
15
|
} from './shaders/generated/particle.wgsl.ts';
|
|
14
16
|
|
|
@@ -27,7 +29,7 @@ import {
|
|
|
27
29
|
* document that. Naming them makes the pairing explicit instead of leaving it to a caller to
|
|
28
30
|
* match a source with the constants that suit it.
|
|
29
31
|
*/
|
|
30
|
-
export type ParticleMaterial = 'smoke' | 'spark' | 'mote';
|
|
32
|
+
export type ParticleMaterial = 'smoke' | 'spark' | 'mote' | 'sprite';
|
|
31
33
|
|
|
32
34
|
/** Both compilations of one material, and what the generator recorded about each stage. */
|
|
33
35
|
export interface ParticleShaders {
|
|
@@ -66,6 +68,14 @@ const MATERIALS: Readonly<Record<ParticleMaterial, ParticleShaders>> = {
|
|
|
66
68
|
vertexWgsl: PARTICLE_VERT_WGSL,
|
|
67
69
|
fragmentWgsl: PARTICLE_MOTE_FRAG_WGSL,
|
|
68
70
|
},
|
|
71
|
+
/* An image a consumer supplies, not a shape this engine draws: see `PARTICLE_SPRITE_FRAG`. */
|
|
72
|
+
sprite: {
|
|
73
|
+
label: 'particle:sprite',
|
|
74
|
+
vertexSource: PARTICLE_VERT,
|
|
75
|
+
fragmentSource: PARTICLE_SPRITE_FRAG,
|
|
76
|
+
vertexWgsl: PARTICLE_VERT_WGSL,
|
|
77
|
+
fragmentWgsl: PARTICLE_SPRITE_FRAG_WGSL,
|
|
78
|
+
},
|
|
69
79
|
};
|
|
70
80
|
|
|
71
81
|
/** What the generator recorded about the shared vertex stage. */
|
|
@@ -38,10 +38,44 @@ export interface ParticleInstances {
|
|
|
38
38
|
seeds: Float32Array;
|
|
39
39
|
/** Current velocity, three floats each: what a streak is stretched along. */
|
|
40
40
|
velocities: Float32Array;
|
|
41
|
+
/**
|
|
42
|
+
* A sprite's flipbook frame, one float each: its whole part names a cell of the image, counted
|
|
43
|
+
* across then down from the top-left, and its fraction is how far toward the next a batch that
|
|
44
|
+
* blends cells has come. Read by the `'sprite'` material alone. Absent is frame 0.
|
|
45
|
+
*/
|
|
46
|
+
frames?: Float32Array;
|
|
47
|
+
/**
|
|
48
|
+
* A sprite's half-height in metres, one float each, beside `sizes`' half-width — a sprite need
|
|
49
|
+
* not be square. Read by the `'sprite'` material alone; absent or 0 is as tall as it is wide.
|
|
50
|
+
*/
|
|
51
|
+
heights?: Float32Array;
|
|
41
52
|
count: number;
|
|
42
53
|
capacity: number;
|
|
43
54
|
}
|
|
44
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Floats one particle takes in a buffer a caller's own compute writes (`DeviceParticles`), in this
|
|
58
|
+
* order: position 3, half-width 1, roll 1, colour 3, opacity 1, age 1, seed 1, velocity 3, a
|
|
59
|
+
* sprite's frame 1 and its half-height 1 — `ParticleInstances`' streams, interleaved as the pool's
|
|
60
|
+
* own upload interleaves them. 64 bytes a particle.
|
|
61
|
+
*/
|
|
62
|
+
export const DEVICE_PARTICLE_FLOATS = 16;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Particles a caller's own compute shader wrote into a buffer of its own (`registerCompute`), drawn
|
|
66
|
+
* where they lie by `drawDeviceParticles` rather than read back and uploaded again. WebGPU only.
|
|
67
|
+
*
|
|
68
|
+
* **Not sorted**, as a pool's are where its batch asks: the device holds them, so an alpha-blended
|
|
69
|
+
* set that must be drawn far to near sorts itself. A slot with a half-width of 0 draws nothing, which
|
|
70
|
+
* is how a fixed-size buffer holds fewer live particles than it has room for.
|
|
71
|
+
*/
|
|
72
|
+
export interface DeviceParticles {
|
|
73
|
+
/** A buffer with `VERTEX` usage, `DEVICE_PARTICLE_FLOATS` floats a particle. */
|
|
74
|
+
readonly buffer: GPUBuffer;
|
|
75
|
+
/** How many particles from its start to draw; no more than the buffer holds. */
|
|
76
|
+
readonly count: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
45
79
|
function createParticleInstances(capacity: number): ParticleInstances {
|
|
46
80
|
return {
|
|
47
81
|
positions: new Float32Array(capacity * 3),
|
|
@@ -52,6 +86,8 @@ function createParticleInstances(capacity: number): ParticleInstances {
|
|
|
52
86
|
ages: new Float32Array(capacity),
|
|
53
87
|
seeds: new Float32Array(capacity),
|
|
54
88
|
velocities: new Float32Array(capacity * 3),
|
|
89
|
+
frames: new Float32Array(capacity),
|
|
90
|
+
heights: new Float32Array(capacity),
|
|
55
91
|
count: 0,
|
|
56
92
|
capacity,
|
|
57
93
|
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A pool's particles, farthest from the eye first, for a blended batch that asks to be sorted.
|
|
3
|
+
*
|
|
4
|
+
* **Why a blended batch wants it.** An alpha blend is order-dependent: a near puff drawn before a
|
|
5
|
+
* far one is covered by it, and a cloud of them drawn in the order they were born flickers as
|
|
6
|
+
* that order and the camera disagree. Back to front is the order the blend assumes. Additive
|
|
7
|
+
* blending is a sum and needs none of it, which is why sorting is the batch's choice.
|
|
8
|
+
*
|
|
9
|
+
* **Backend-neutral, as a copy**: the sorted particles are written into a second set of streams the
|
|
10
|
+
* batch owns, and each backend uploads that set exactly as it uploads an unsorted one — so the
|
|
11
|
+
* order is one decision, not two, by the 2026-08-13 rule. A merge sort into preallocated scratch,
|
|
12
|
+
* so nothing is allocated per frame however many particles there are; stable, so two particles at
|
|
13
|
+
* one distance keep their order and do not trade places from frame to frame.
|
|
14
|
+
*
|
|
15
|
+
* **What it gives up** is order between batches and within one particle: two sorted batches still
|
|
16
|
+
* draw one after the other, and two crossing cards cut through each other. **What would make it
|
|
17
|
+
* wrong** is a field of particles large enough that sorting it on the CPU costs a frame; the GPU
|
|
18
|
+
* sorts it then.
|
|
19
|
+
*/
|
|
20
|
+
import type { ParticleInstances } from './particlePool.ts';
|
|
21
|
+
|
|
22
|
+
/** What sorting a batch keeps between frames: the order, its keys and the sorted copy. */
|
|
23
|
+
export interface ParticleSort {
|
|
24
|
+
readonly order: Uint32Array;
|
|
25
|
+
readonly scratch: Uint32Array;
|
|
26
|
+
readonly keys: Float32Array;
|
|
27
|
+
readonly sorted: ParticleInstances;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Room to sort `capacity` particles, allocated once with the batch. */
|
|
31
|
+
export function createParticleSort(capacity: number): ParticleSort {
|
|
32
|
+
return {
|
|
33
|
+
order: new Uint32Array(capacity),
|
|
34
|
+
scratch: new Uint32Array(capacity),
|
|
35
|
+
keys: new Float32Array(capacity),
|
|
36
|
+
sorted: {
|
|
37
|
+
positions: new Float32Array(capacity * 3),
|
|
38
|
+
sizes: new Float32Array(capacity),
|
|
39
|
+
spins: new Float32Array(capacity),
|
|
40
|
+
colors: new Float32Array(capacity * 3),
|
|
41
|
+
alphas: new Float32Array(capacity),
|
|
42
|
+
ages: new Float32Array(capacity),
|
|
43
|
+
seeds: new Float32Array(capacity),
|
|
44
|
+
velocities: new Float32Array(capacity * 3),
|
|
45
|
+
frames: new Float32Array(capacity),
|
|
46
|
+
heights: new Float32Array(capacity),
|
|
47
|
+
count: 0,
|
|
48
|
+
capacity,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `data`'s live particles into `sort.sorted`, farthest from `eye` first, and that copy returned.
|
|
55
|
+
* At most the sort's capacity are kept.
|
|
56
|
+
*/
|
|
57
|
+
export function sortBackToFront(
|
|
58
|
+
data: ParticleInstances,
|
|
59
|
+
eye: ArrayLike<number>,
|
|
60
|
+
sort: ParticleSort,
|
|
61
|
+
): ParticleInstances {
|
|
62
|
+
const count = Math.min(data.count, sort.order.length);
|
|
63
|
+
const { order, keys } = sort;
|
|
64
|
+
const ex = eye[0] ?? 0;
|
|
65
|
+
const ey = eye[1] ?? 0;
|
|
66
|
+
const ez = eye[2] ?? 0;
|
|
67
|
+
for (let i = 0; i < count; i++) {
|
|
68
|
+
const dx = (data.positions[i * 3] as number) - ex;
|
|
69
|
+
const dy = (data.positions[i * 3 + 1] as number) - ey;
|
|
70
|
+
const dz = (data.positions[i * 3 + 2] as number) - ez;
|
|
71
|
+
keys[i] = dx * dx + dy * dy + dz * dz;
|
|
72
|
+
order[i] = i;
|
|
73
|
+
}
|
|
74
|
+
mergeSortFarFirst(order, sort.scratch, keys, count);
|
|
75
|
+
|
|
76
|
+
const out = sort.sorted;
|
|
77
|
+
for (let k = 0; k < count; k++) {
|
|
78
|
+
const i = order[k] as number;
|
|
79
|
+
copy3(data.positions, i, out.positions, k);
|
|
80
|
+
copy3(data.colors, i, out.colors, k);
|
|
81
|
+
copy3(data.velocities, i, out.velocities, k);
|
|
82
|
+
out.sizes[k] = data.sizes[i] as number;
|
|
83
|
+
out.spins[k] = data.spins[i] as number;
|
|
84
|
+
out.alphas[k] = data.alphas[i] as number;
|
|
85
|
+
out.ages[k] = data.ages[i] as number;
|
|
86
|
+
out.seeds[k] = data.seeds[i] as number;
|
|
87
|
+
(out.frames as Float32Array)[k] = data.frames?.[i] ?? 0;
|
|
88
|
+
(out.heights as Float32Array)[k] = data.heights?.[i] ?? 0;
|
|
89
|
+
}
|
|
90
|
+
out.count = count;
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function copy3(from: Float32Array, i: number, to: Float32Array, k: number): void {
|
|
95
|
+
to[k * 3] = from[i * 3] as number;
|
|
96
|
+
to[k * 3 + 1] = from[i * 3 + 1] as number;
|
|
97
|
+
to[k * 3 + 2] = from[i * 3 + 2] as number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A bottom-up merge sort of `order` by `keys`, largest first, stable, through `scratch`. */
|
|
101
|
+
function mergeSortFarFirst(
|
|
102
|
+
order: Uint32Array,
|
|
103
|
+
scratch: Uint32Array,
|
|
104
|
+
keys: Float32Array,
|
|
105
|
+
count: number,
|
|
106
|
+
): void {
|
|
107
|
+
let from = order;
|
|
108
|
+
let to = scratch;
|
|
109
|
+
for (let width = 1; width < count; width *= 2) {
|
|
110
|
+
for (let start = 0; start < count; start += 2 * width) {
|
|
111
|
+
const middle = Math.min(start + width, count);
|
|
112
|
+
const end = Math.min(start + 2 * width, count);
|
|
113
|
+
let a = start;
|
|
114
|
+
let b = middle;
|
|
115
|
+
let at = start;
|
|
116
|
+
while (a < middle && b < end) {
|
|
117
|
+
/* `>=` keeps the left run's particle first on a tie, which is what makes this stable. */
|
|
118
|
+
if ((keys[from[a] as number] as number) >= (keys[from[b] as number] as number)) {
|
|
119
|
+
to[at++] = from[a++] as number;
|
|
120
|
+
} else {
|
|
121
|
+
to[at++] = from[b++] as number;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
while (a < middle) to[at++] = from[a++] as number;
|
|
125
|
+
while (b < end) to[at++] = from[b++] as number;
|
|
126
|
+
}
|
|
127
|
+
const swap = from;
|
|
128
|
+
from = to;
|
|
129
|
+
to = swap;
|
|
130
|
+
}
|
|
131
|
+
/* Copied by hand: a subarray would be a view allocated every frame. */
|
|
132
|
+
if (from !== order) for (let i = 0; i < count; i++) order[i] = from[i] as number;
|
|
133
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/** Allocation-free selection and packing of point lights for a fixed shader budget. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
POINT_LIGHT_COS_INNER,
|
|
5
|
+
POINT_LIGHT_COS_OUTER,
|
|
6
|
+
lightFalloffExponentOf,
|
|
7
|
+
} from './clusteredLights.ts';
|
|
4
8
|
import { MAX_POINT_LIGHTS, POINT_SHADOW_POOL } from './lightBudget.ts';
|
|
5
9
|
import { gatherLights } from './lightGrid.ts';
|
|
6
10
|
import type { LightGrid } from './lightGrid.ts';
|
|
@@ -198,6 +202,14 @@ export interface PointLightSource {
|
|
|
198
202
|
* whose uniform budget has none to spare.
|
|
199
203
|
*/
|
|
200
204
|
inLightField?: boolean;
|
|
205
|
+
/**
|
|
206
|
+
* How the light fades with distance when it names its own way: `(1 - (d/R)^2)^n` with no distance
|
|
207
|
+
* term, `R` its radius and `n` this. Unreal's falloff for a light without inverse-square falloff,
|
|
208
|
+
* so a rig authored there — a fill of brightness 160 across 164 m, a key at exponent 8 — lights
|
|
209
|
+
* the space it was authored for rather than one that ends a few metres out. Absent or 0, the
|
|
210
|
+
* frame's own falloff (`pointLightFalloff`), which is every light before 4.8.7.
|
|
211
|
+
*/
|
|
212
|
+
falloffExponent?: number;
|
|
201
213
|
}
|
|
202
214
|
|
|
203
215
|
export interface PointLightBuffer {
|
|
@@ -256,6 +268,11 @@ export interface PointLightBuffer {
|
|
|
256
268
|
readonly directions: Float32Array;
|
|
257
269
|
/** Two per slot: the cosine of the inner cone angle, then of the outer. */
|
|
258
270
|
readonly coneCos: Float32Array;
|
|
271
|
+
/**
|
|
272
|
+
* One per slot: the light's own falloff exponent, or 0 for the frame's falloff. See
|
|
273
|
+
* `PointLightSource.falloffExponent`.
|
|
274
|
+
*/
|
|
275
|
+
readonly falloffExponents: Float32Array;
|
|
259
276
|
/** One per slot: a row of the photometric atlas, or −1 for a light with no profile. */
|
|
260
277
|
readonly iesProfiles: Float32Array;
|
|
261
278
|
/** Internal insertion-sort storage; exposed only to keep one flat object. */
|
|
@@ -307,6 +324,7 @@ export function createPointLightBuffer(capacity: number = MAX_POINT_LIGHTS): Poi
|
|
|
307
324
|
weights: new Float32Array(shaded),
|
|
308
325
|
directions: new Float32Array(shaded * 3),
|
|
309
326
|
coneCos: new Float32Array(shaded * 2),
|
|
327
|
+
falloffExponents: new Float32Array(shaded),
|
|
310
328
|
/* −1 rather than 0, because row 0 is a real fixture and a light that asked for none must not
|
|
311
329
|
be shaped by whichever profile happened to load first. */
|
|
312
330
|
iesProfiles: new Float32Array(shaded).fill(-1),
|
|
@@ -587,6 +605,7 @@ function choose(
|
|
|
587
605
|
* Restricting it to spots would be a smaller feature for no saving.
|
|
588
606
|
*/
|
|
589
607
|
out.iesProfiles[slot] = light.iesProfile ?? -1;
|
|
608
|
+
out.falloffExponents[slot] = lightFalloffExponentOf(light.falloffExponent);
|
|
590
609
|
}
|
|
591
610
|
out.count = chosen;
|
|
592
611
|
|
|
@@ -219,13 +219,14 @@ export interface RenderQuality {
|
|
|
219
219
|
* **`'pre-integrated'`, the default**: the lit stage's fit alone — a terminator softened and
|
|
220
220
|
* reddened by the surface's curvature, light through thin parts, a penumbra each channel crosses
|
|
221
221
|
* as far as it travels — at no cost beyond the skin pipeline itself. **`'screen-space'`** adds
|
|
222
|
-
* Burley's blur in the picture as well: a skin draw writes its diffuse
|
|
223
|
-
* is drawn
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
222
|
+
* Burley's blur in the picture as well: a skin draw writes the light its diffuse is made of to a
|
|
223
|
+
* target of its own and its colour to another (it is drawn three times to do so), and the light is
|
|
224
|
+
* spread across the pixels its scatter distance covers, multiplied by the colour and added back
|
|
225
|
+
* before anything blended is drawn, so light crosses a shadow's edge and a nostril's rim in the
|
|
226
|
+
* picture rather than in a fit. See `skinBlur.ts`.
|
|
227
|
+
*
|
|
228
|
+
* Construction-time: three half-float targets the size of the frame, and two multisampled ones
|
|
229
|
+
* under multisampling, made at the first skin draw. It needs the composite (`screenEffects`), which is
|
|
229
230
|
* where the frame is still linear light to add to; without one, skin is pre-integrated and the
|
|
230
231
|
* renderer says so once. What would make the default wrong is a close-up face that reads as wax
|
|
231
232
|
* at a shadow's edge — that is what the blur is for.
|
|
@@ -119,6 +119,9 @@ export function recordingGl(
|
|
|
119
119
|
ONE: 1,
|
|
120
120
|
ONE_MINUS_SRC_ALPHA: 0x0303,
|
|
121
121
|
TRIANGLES: 0x0004,
|
|
122
|
+
/* The winding a pass names, so a test can tell which one it chose. */
|
|
123
|
+
CW: 0x0900,
|
|
124
|
+
CCW: 0x0901,
|
|
122
125
|
TEXTURE0: 0x84c0,
|
|
123
126
|
DEPTH_BUFFER_BIT: 0x00000100,
|
|
124
127
|
DEPTH_TEST: 0x0b71,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rgb9e5: three 9-bit mantissas over one shared 5-bit exponent, an unsigned float colour in four
|
|
3
|
+
* bytes. `EXT_texture_shared_exponent`'s encoding, the one a GPU's `rgb9e5ufloat` reads, written
|
|
4
|
+
* out so a lightmap page can hold its irradiance in four bytes a texel against a half float's eight.
|
|
5
|
+
*
|
|
6
|
+
* **What it gives up**: precision in the smaller channels, which share the largest one's exponent —
|
|
7
|
+
* a channel a thousandth of its neighbour keeps about one bit — and anything negative or past
|
|
8
|
+
* 65,408, which it clamps. Light is neither, and a bake's colours rarely span more than a few
|
|
9
|
+
* octaves within one texel, so a lightmap loses nothing visible; a value that needs each channel
|
|
10
|
+
* exact wants a format of its own.
|
|
11
|
+
*
|
|
12
|
+
* The bits, low to high: red in 0–8, green in 9–17, blue in 18–26, the exponent in 27–31, biased by
|
|
13
|
+
* 15, and a value is `mantissa * 2^(exponent - 15 - 9)`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const BIAS = 15;
|
|
17
|
+
const MANTISSA_BITS = 9;
|
|
18
|
+
const MANTISSA_VALUES = 1 << MANTISSA_BITS;
|
|
19
|
+
const MAX_EXPONENT = 31;
|
|
20
|
+
|
|
21
|
+
/** The largest value the format holds: 511/512 of 2^16. */
|
|
22
|
+
export const RGB9E5_MAX = ((MANTISSA_VALUES - 1) / MANTISSA_VALUES) * 2 ** (MAX_EXPONENT - BIAS);
|
|
23
|
+
|
|
24
|
+
const clampChannel = (v: number): number => (v > 0 ? Math.min(v, RGB9E5_MAX) : 0);
|
|
25
|
+
|
|
26
|
+
/** One colour packed into its 32 bits, rounding to nearest; NaN and negatives are zero. */
|
|
27
|
+
export function packRgb9e5(red: number, green: number, blue: number): number {
|
|
28
|
+
const r = clampChannel(red);
|
|
29
|
+
const g = clampChannel(green);
|
|
30
|
+
const b = clampChannel(blue);
|
|
31
|
+
const largest = Math.max(r, g, b);
|
|
32
|
+
let exponent = Math.max(-BIAS - 1, Math.floor(Math.log2(largest))) + 1 + BIAS;
|
|
33
|
+
let scale = 2 ** (exponent - BIAS - MANTISSA_BITS);
|
|
34
|
+
/* Rounding the largest channel up to 512 needs one more exponent, which the spec allows for. */
|
|
35
|
+
if (Math.floor(largest / scale + 0.5) === MANTISSA_VALUES) {
|
|
36
|
+
exponent += 1;
|
|
37
|
+
scale *= 2;
|
|
38
|
+
}
|
|
39
|
+
/* Inline rather than a helper: a page packs millions of texels, and a closure a call is one each. */
|
|
40
|
+
const mr = Math.min(MANTISSA_VALUES - 1, Math.floor(r / scale + 0.5));
|
|
41
|
+
const mg = Math.min(MANTISSA_VALUES - 1, Math.floor(g / scale + 0.5));
|
|
42
|
+
const mb = Math.min(MANTISSA_VALUES - 1, Math.floor(b / scale + 0.5));
|
|
43
|
+
return (mr | (mg << 9) | (mb << 18) | (exponent << 27)) >>> 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A packed colour's three channels, into `out[at…at+2]`. */
|
|
47
|
+
export function unpackRgb9e5(bits: number, out: { [index: number]: number }, at = 0): void {
|
|
48
|
+
const scale = 2 ** ((bits >>> 27) - BIAS - MANTISSA_BITS);
|
|
49
|
+
out[at] = (bits & 511) * scale;
|
|
50
|
+
out[at + 1] = ((bits >>> 9) & 511) * scale;
|
|
51
|
+
out[at + 2] = ((bits >>> 18) & 511) * scale;
|
|
52
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scene capture: a texture the world is drawn into from a camera of the caller's, which a material
|
|
3
|
+
* then shows like any image — a screen in the world showing what a camera elsewhere sees, a monitor,
|
|
4
|
+
* a security feed. The planar reflection's pass, for a caller's camera rather than a mirrored one.
|
|
5
|
+
*
|
|
6
|
+
* **It holds radiance, not display pixels**, as a probe does: drawn with the output transform held
|
|
7
|
+
* off, at the colour format a probe takes — half floats under `hdrScene`, bytes otherwise — and
|
|
8
|
+
* sampled linear, so a material showing it as its `emissive` emits the light the capture saw and the
|
|
9
|
+
* frame's grade then applies once, to the screen as to everything around it.
|
|
10
|
+
*
|
|
11
|
+
* **What it gives up**: it is the mesh pass alone, as a probe bake is — the sky, water, particles and
|
|
12
|
+
* anything else a scene draws after its world are in the capture only if the callback draws them;
|
|
13
|
+
* no temporal resolve, bloom or other effect after the world runs on it; and it has no mip chain, so
|
|
14
|
+
* a screen seen small and far shimmers where a mipmapped image would not.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** What a capture's texture is made from, in place of an image: its size and colour format. */
|
|
18
|
+
export interface SceneCaptureTexels {
|
|
19
|
+
readonly kind: 'scene-capture';
|
|
20
|
+
readonly width: number;
|
|
21
|
+
readonly height: number;
|
|
22
|
+
/** Half floats, which hold light past one; bytes otherwise. */
|
|
23
|
+
readonly float: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The largest side a capture may have, the size every WebGL2 device must take. */
|
|
27
|
+
export const MAX_SCENE_CAPTURE_SIZE = 4096;
|
|
28
|
+
|
|
29
|
+
/** A capture's description, refused by name where a side is not a whole number of pixels it can be. */
|
|
30
|
+
export function sceneCaptureTexels(
|
|
31
|
+
width: number,
|
|
32
|
+
height: number,
|
|
33
|
+
float: boolean,
|
|
34
|
+
): SceneCaptureTexels {
|
|
35
|
+
for (const [name, side] of [
|
|
36
|
+
['width', width],
|
|
37
|
+
['height', height],
|
|
38
|
+
] as const) {
|
|
39
|
+
if (!Number.isInteger(side) || side < 1 || side > MAX_SCENE_CAPTURE_SIZE) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`createSceneCapture: ${name} must be a whole number from 1 to ${MAX_SCENE_CAPTURE_SIZE}, ` +
|
|
42
|
+
`not ${String(side)}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { kind: 'scene-capture', width, height, float };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Whether a texture source is a capture's description rather than an image. */
|
|
50
|
+
export function isSceneCaptureTexels(source: unknown): source is SceneCaptureTexels {
|
|
51
|
+
return (
|
|
52
|
+
typeof source === 'object' &&
|
|
53
|
+
source !== null &&
|
|
54
|
+
(source as { kind?: unknown }).kind === 'scene-capture'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -497,6 +497,15 @@ export class SceneTarget {
|
|
|
497
497
|
* must reject it in the same place or the accumulation and the revealage cover different
|
|
498
498
|
* fragments.
|
|
499
499
|
*/
|
|
500
|
+
/** The frame's size in pixels, as its targets are allocated. */
|
|
501
|
+
get frameWidth(): number {
|
|
502
|
+
return this.width;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
get frameHeight(): number {
|
|
506
|
+
return this.height;
|
|
507
|
+
}
|
|
508
|
+
|
|
500
509
|
/**
|
|
501
510
|
* The depth the frame draws against, for a second framebuffer to share: skin's diffuse half,
|
|
502
511
|
* which tests against it and writes none. The multisampled renderbuffer where the frame is
|
|
@@ -783,6 +792,8 @@ export class SceneTarget {
|
|
|
783
792
|
readonly strength: number;
|
|
784
793
|
/** In scene units, which is why this wants `keepsRange`. See `BloomPass`. */
|
|
785
794
|
readonly threshold: number;
|
|
795
|
+
/** The ramp and each level's tint, as `resolveBloomResponse` writes them. */
|
|
796
|
+
readonly response: Float32Array;
|
|
786
797
|
},
|
|
787
798
|
/*
|
|
788
799
|
* Depth of field, between the camera smear and the occlusion in the shader's own order. Like
|
|
@@ -966,7 +977,13 @@ export class SceneTarget {
|
|
|
966
977
|
let bloomTexture: WebGLTexture | null = null;
|
|
967
978
|
if (bloom !== undefined && scene !== null) {
|
|
968
979
|
this.bloom ??= new BloomPass(gl);
|
|
969
|
-
bloomTexture = this.bloom.run(
|
|
980
|
+
bloomTexture = this.bloom.run(
|
|
981
|
+
scene,
|
|
982
|
+
this.width,
|
|
983
|
+
this.height,
|
|
984
|
+
bloom.threshold,
|
|
985
|
+
bloom.response,
|
|
986
|
+
);
|
|
970
987
|
}
|
|
971
988
|
|
|
972
989
|
/*
|