@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
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Fixed point-light budget shared by CPU selection and the flat shader. */
|
|
2
1
|
import type { AreaLightBuffer } from './areaLights.ts';
|
|
3
2
|
import type { ResolvedAreaShadows } from './areaShadowSet.ts';
|
|
4
3
|
/**
|
|
@@ -229,12 +228,21 @@ export declare const CLOTH_REST_TEXTURE_UNIT: number;
|
|
|
229
228
|
*/
|
|
230
229
|
export declare const MODEL_MAP_TEXTURE_UNIT: number;
|
|
231
230
|
/**
|
|
232
|
-
* The
|
|
231
|
+
* The units skin's screen-space blur reads on WebGL2 — its target and the frame's depth — past
|
|
233
232
|
* every unit a lit program binds, so spreading skin in the middle of a frame unbinds nothing the
|
|
234
233
|
* draws after it sample. WebGL2 guarantees thirty-two.
|
|
235
234
|
*/
|
|
236
235
|
export declare const SKIN_BLUR_TEXTURE_UNIT: number;
|
|
237
236
|
export declare const SKIN_BLUR_DEPTH_UNIT: number;
|
|
237
|
+
/** And the skin's colour, which the blur's second axis multiplies the spread light by. */
|
|
238
|
+
export declare const SKIN_BLUR_ALBEDO_UNIT: number;
|
|
239
|
+
/**
|
|
240
|
+
* A sprite particle's image and the frame's depth copy its soft edge reads, on WebGL2: past every
|
|
241
|
+
* unit a lit program binds, for the skin blur's reason — a particle is drawn in the middle of a
|
|
242
|
+
* frame, and the draws after it must find their textures where they left them.
|
|
243
|
+
*/
|
|
244
|
+
export declare const PARTICLE_SPRITE_UNIT: number;
|
|
245
|
+
export declare const PARTICLE_DEPTH_UNIT: number;
|
|
238
246
|
/**
|
|
239
247
|
* A cookie's tile, in texels a side.
|
|
240
248
|
*
|
|
@@ -349,6 +357,22 @@ export interface PointLightSet {
|
|
|
349
357
|
* drawn for one spot works in another with a different angle.
|
|
350
358
|
*/
|
|
351
359
|
readonly lightCookies?: Float32Array;
|
|
360
|
+
/**
|
|
361
|
+
* One per light: the lighting channels it lights, as a bitmask in a float — 1, 2, 3 … 255.
|
|
362
|
+
* **Absent, or a short array, is channel 1 for every light**, which is every surface's default
|
|
363
|
+
* too, so a scene that never names a channel lights everything with everything as it did.
|
|
364
|
+
*
|
|
365
|
+
* A light shades a surface only where its mask and the surface's `SurfaceMaterial.lightChannels`
|
|
366
|
+
* share a bit. What it is for: a rig of lights that belongs to some draws and not others — a
|
|
367
|
+
* character's own key and rim lights, which the floor around the character must not take.
|
|
368
|
+
*/
|
|
369
|
+
readonly lightChannels?: Float32Array;
|
|
370
|
+
/**
|
|
371
|
+
* One per light: its own falloff exponent, `(1 - (d/R)^2)^n` with no distance term, or 0 for the
|
|
372
|
+
* frame's falloff. **Absent, or a short array, is 0 for every light**, so a scene that names none
|
|
373
|
+
* falls off as it did. See `PointLightSource.falloffExponent`.
|
|
374
|
+
*/
|
|
375
|
+
readonly lightFalloffExponents?: Float32Array;
|
|
352
376
|
}
|
|
353
377
|
/**
|
|
354
378
|
* A cone that admits every direction, for every light.
|
|
@@ -416,6 +440,18 @@ export interface ResolvedPointLights {
|
|
|
416
440
|
iesProfiles: Float32Array;
|
|
417
441
|
iesAxes: Float32Array;
|
|
418
442
|
cookies: Float32Array;
|
|
443
|
+
/**
|
|
444
|
+
* Owned, four a light: the photometric azimuth in xyz and the light's channels in w, which is how
|
|
445
|
+
* `uLightIesAxis` carries them — the channels in a lane the axis left, rather than in a uniform
|
|
446
|
+
* array of their own, which would cost a row a light of a budget that has none to give.
|
|
447
|
+
*/
|
|
448
|
+
readonly axesAndChannels: Float32Array;
|
|
449
|
+
/**
|
|
450
|
+
* Owned, four a light: the cone's two cosines, then the light's own falloff exponent, as
|
|
451
|
+
* `uLightCone` carries them — a `vec2` array costs a whole row a light on WebGL2 anyway, so the
|
|
452
|
+
* exponent rides the half it left rather than a row of its own.
|
|
453
|
+
*/
|
|
454
|
+
readonly conesAndFalloffs: Float32Array;
|
|
419
455
|
/**
|
|
420
456
|
* Owned, `MAX_POINT_LIGHTS` long: the emitter sizes and weights as the froxel record carries
|
|
421
457
|
* them, at half precision. `sourceRadii` and `weights` point here once a light set resolves, so
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/** Fixed point-light budget shared by CPU selection and the flat shader. */
|
|
2
|
+
import { MAX_AREA_LIGHTS, packAreaSizesAndDoors } from './areaLights.js';
|
|
3
|
+
import { POINT_LIGHT_COS_INNER, POINT_LIGHT_COS_OUTER, lampSourceRadius, lampWeight, lightChannelsOf, lightFalloffExponentOf, } from './clusteredLights.js';
|
|
2
4
|
/**
|
|
3
5
|
* How many point lights the shading pass shades against at once.
|
|
4
6
|
*
|
|
@@ -227,12 +229,21 @@ export const CLOTH_REST_TEXTURE_UNIT = CLOTH_PARTICLES_TEXTURE_UNIT + 1;
|
|
|
227
229
|
*/
|
|
228
230
|
export const MODEL_MAP_TEXTURE_UNIT = CLOTH_REST_TEXTURE_UNIT + 1;
|
|
229
231
|
/**
|
|
230
|
-
* The
|
|
232
|
+
* The units skin's screen-space blur reads on WebGL2 — its target and the frame's depth — past
|
|
231
233
|
* every unit a lit program binds, so spreading skin in the middle of a frame unbinds nothing the
|
|
232
234
|
* draws after it sample. WebGL2 guarantees thirty-two.
|
|
233
235
|
*/
|
|
234
236
|
export const SKIN_BLUR_TEXTURE_UNIT = MODEL_MAP_TEXTURE_UNIT + 1;
|
|
235
237
|
export const SKIN_BLUR_DEPTH_UNIT = SKIN_BLUR_TEXTURE_UNIT + 1;
|
|
238
|
+
/** And the skin's colour, which the blur's second axis multiplies the spread light by. */
|
|
239
|
+
export const SKIN_BLUR_ALBEDO_UNIT = SKIN_BLUR_DEPTH_UNIT + 1;
|
|
240
|
+
/**
|
|
241
|
+
* A sprite particle's image and the frame's depth copy its soft edge reads, on WebGL2: past every
|
|
242
|
+
* unit a lit program binds, for the skin blur's reason — a particle is drawn in the middle of a
|
|
243
|
+
* frame, and the draws after it must find their textures where they left them.
|
|
244
|
+
*/
|
|
245
|
+
export const PARTICLE_SPRITE_UNIT = SKIN_BLUR_ALBEDO_UNIT + 1;
|
|
246
|
+
export const PARTICLE_DEPTH_UNIT = PARTICLE_SPRITE_UNIT + 1;
|
|
236
247
|
/**
|
|
237
248
|
* A cookie's tile, in texels a side.
|
|
238
249
|
*
|
|
@@ -293,6 +304,8 @@ export const SDF_TEXT_TEXTURE_UNIT = SURFACE_TEXTURE_UNIT;
|
|
|
293
304
|
export const POINT_SHADOW_POOL = MAX_POINT_LIGHTS + 4;
|
|
294
305
|
/** Weights are optional per consumer; a short array means every light is fully present. */
|
|
295
306
|
const FULLY_PRESENT = new Float32Array(MAX_POINT_LIGHTS).fill(1);
|
|
307
|
+
/** The fixed arm's rectangles as `uAreaLightSize` holds them; refilled per bind, allocated once. */
|
|
308
|
+
const AREA_SIZES_AND_DOORS = new Float32Array(MAX_AREA_LIGHTS * 4);
|
|
296
309
|
/** No size at all, which is the behaviour every scene had before a light could have one. */
|
|
297
310
|
const POINT_SOURCES = new Float32Array(MAX_POINT_LIGHTS);
|
|
298
311
|
/**
|
|
@@ -385,9 +398,11 @@ export function bindPointLights(gl, uniforms, lights, falloff) {
|
|
|
385
398
|
* every lamp switched off.
|
|
386
399
|
*/
|
|
387
400
|
gl.uniform3fv(uniforms['uLightDir[0]'] ?? null, r.directions);
|
|
388
|
-
|
|
401
|
+
/* The cone and, in z, the light's own falloff exponent. See `conesAndFalloffs`. */
|
|
402
|
+
gl.uniform4fv(uniforms['uLightCone[0]'] ?? null, r.conesAndFalloffs);
|
|
389
403
|
gl.uniform1fv(uniforms['uLightIesProfile[0]'] ?? null, r.iesProfiles);
|
|
390
|
-
|
|
404
|
+
/* The azimuth and, in w, the channels. See `axesAndChannels`. */
|
|
405
|
+
gl.uniform4fv(uniforms['uLightIesAxis[0]'] ?? null, r.axesAndChannels);
|
|
391
406
|
gl.uniform1fv(uniforms['uLightCookie[0]'] ?? null, r.cookies);
|
|
392
407
|
}
|
|
393
408
|
/**
|
|
@@ -411,7 +426,9 @@ export function bindAreaLights(gl, uniforms, lights) {
|
|
|
411
426
|
gl.uniform3fv(uniforms['uAreaLightColor[0]'] ?? null, lights.colors);
|
|
412
427
|
gl.uniform3fv(uniforms['uAreaLightRight[0]'] ?? null, lights.right);
|
|
413
428
|
gl.uniform3fv(uniforms['uAreaLightUp[0]'] ?? null, lights.up);
|
|
414
|
-
|
|
429
|
+
/* The half extents and, in zw, the barn doors. See `packAreaSizesAndDoors`. */
|
|
430
|
+
packAreaSizesAndDoors(lights, AREA_SIZES_AND_DOORS);
|
|
431
|
+
gl.uniform4fv(uniforms['uAreaLightSize[0]'] ?? null, AREA_SIZES_AND_DOORS);
|
|
415
432
|
gl.uniform1fv(uniforms['uAreaLightTwoSided[0]'] ?? null, lights.twoSided);
|
|
416
433
|
}
|
|
417
434
|
/**
|
|
@@ -452,6 +469,8 @@ const glLights = {
|
|
|
452
469
|
iesProfiles: NO_PROFILES,
|
|
453
470
|
iesAxes: NO_IES_AXES,
|
|
454
471
|
cookies: NO_COOKIES,
|
|
472
|
+
axesAndChannels: new Float32Array(MAX_POINT_LIGHTS * 4),
|
|
473
|
+
conesAndFalloffs: new Float32Array(MAX_POINT_LIGHTS * 4),
|
|
455
474
|
heldSourceRadii: new Float32Array(MAX_POINT_LIGHTS),
|
|
456
475
|
heldWeights: new Float32Array(MAX_POINT_LIGHTS),
|
|
457
476
|
};
|
|
@@ -481,6 +500,8 @@ export function resolvePointLights(lights, falloff, out) {
|
|
|
481
500
|
out.iesProfiles = NO_PROFILES;
|
|
482
501
|
out.iesAxes = NO_IES_AXES;
|
|
483
502
|
out.cookies = NO_COOKIES;
|
|
503
|
+
packAxesAndChannels(NO_IES_AXES, undefined, out.axesAndChannels);
|
|
504
|
+
packConesAndFalloffs(OPEN_CONES, undefined, out.conesAndFalloffs);
|
|
484
505
|
return out;
|
|
485
506
|
}
|
|
486
507
|
/*
|
|
@@ -561,5 +582,27 @@ export function resolvePointLights(lights, falloff, out) {
|
|
|
561
582
|
out.iesProfiles = iesProfiles;
|
|
562
583
|
out.iesAxes = iesAxes;
|
|
563
584
|
out.cookies = cookies;
|
|
585
|
+
packAxesAndChannels(iesAxes, lights.lightChannels, out.axesAndChannels);
|
|
586
|
+
packConesAndFalloffs(coneCos, lights.lightFalloffExponents, out.conesAndFalloffs);
|
|
564
587
|
return out;
|
|
565
588
|
}
|
|
589
|
+
/** The cone and the falloff exponent, four a light, as `uLightCone` holds them. */
|
|
590
|
+
function packConesAndFalloffs(cones, exponents, out) {
|
|
591
|
+
const named = (exponents?.length ?? 0) >= MAX_POINT_LIGHTS ? exponents : undefined;
|
|
592
|
+
for (let light = 0; light < MAX_POINT_LIGHTS; light++) {
|
|
593
|
+
out[light * 4] = cones[light * 2] ?? POINT_LIGHT_COS_INNER;
|
|
594
|
+
out[light * 4 + 1] = cones[light * 2 + 1] ?? POINT_LIGHT_COS_OUTER;
|
|
595
|
+
out[light * 4 + 2] = lightFalloffExponentOf(named?.[light]);
|
|
596
|
+
out[light * 4 + 3] = 0;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/** The azimuth and the channels, four a light, as `uLightIesAxis` holds them. */
|
|
600
|
+
function packAxesAndChannels(axes, channels, out) {
|
|
601
|
+
const named = (channels?.length ?? 0) >= MAX_POINT_LIGHTS ? channels : undefined;
|
|
602
|
+
for (let light = 0; light < MAX_POINT_LIGHTS; light++) {
|
|
603
|
+
out[light * 4] = axes[light * 3] ?? 0;
|
|
604
|
+
out[light * 4 + 1] = axes[light * 3 + 1] ?? 0;
|
|
605
|
+
out[light * 4 + 2] = axes[light * 3 + 2] ?? 0;
|
|
606
|
+
out[light * 4 + 3] = lightChannelsOf(named?.[light]);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A baked lightmap: what a consumer hands over, how both backends lay it out, and what a surface
|
|
3
|
+
* reads from it — the backend-neutral half of `createLightmap` and `lightmapModel`, by the
|
|
4
|
+
* 2026-08-13 rule.
|
|
5
|
+
*
|
|
6
|
+
* **What it is for.** A stage built in another engine carries its static light baked: the bounce
|
|
7
|
+
* off its walls, the soft occlusion in its corners, most of the colour a reference render shows.
|
|
8
|
+
* The dynamic lights alone leave everything they do not reach flat. A consumer decodes the bake
|
|
9
|
+
* into pages — one irradiance image and one direction image each — and every lightmapped surface
|
|
10
|
+
* reads its own region of its page, at its second texture coordinates scaled and offset into it.
|
|
11
|
+
*
|
|
12
|
+
* **What a surface adds**, for its shading normal `n` (the normal map's, where it has one):
|
|
13
|
+
*
|
|
14
|
+
* d = direction * 2 - 1
|
|
15
|
+
* light = irradiance(uv) * max(0, dot(d.xyz, n) + d.w)
|
|
16
|
+
* diffuse += albedo * (1 - metal) * light
|
|
17
|
+
*
|
|
18
|
+
* Unreal's high-quality lightmap read, `GetLightMapColorHQ`, once its own vectors have decoded the
|
|
19
|
+
* page — the first-order spherical harmonic its direction carries, in this engine's axes. **Added
|
|
20
|
+
* to what the dynamic lights give**, and to the ambient: a consumer whose bake already holds the
|
|
21
|
+
* sky's light zeroes the ambient for those draws itself, with `setAmbientSH` and nine zeros.
|
|
22
|
+
*
|
|
23
|
+
* **A shading model, because everything it would otherwise take is spent.** The page binds where a
|
|
24
|
+
* model's map does and the region is the model's numbers (`lightmapModel`), so a lightmapped
|
|
25
|
+
* material is a pipeline constant of its own and nothing else pays for it; the surface is otherwise
|
|
26
|
+
* the standard model, unchanged. The second coordinates ride the grain and relief attributes
|
|
27
|
+
* (`MeshData.lightmapUvs`, stored below zero by `withLightmapUvs`), and an instance's region rides
|
|
28
|
+
* its tint and opacity (`MeshInstances.lightmapRegions`). **What it gives up**: a lightmapped
|
|
29
|
+
* surface has no procedural grain or relief, no other shading model, and an instanced batch with
|
|
30
|
+
* regions no tint. **What it does not do**: the GPU-driven pipeline and splats read no page.
|
|
31
|
+
*/
|
|
32
|
+
import type { MeshData } from '@driftengine/drft';
|
|
33
|
+
/** One page as a consumer decoded it. */
|
|
34
|
+
export interface LightmapPage {
|
|
35
|
+
readonly width: number;
|
|
36
|
+
readonly height: number;
|
|
37
|
+
/**
|
|
38
|
+
* Linear irradiance, row-major from the top, in the same units every other light here is in — a
|
|
39
|
+
* bake from another engine scaled by that engine's exposure, as its lights are. Either three
|
|
40
|
+
* floats a texel, or one word a texel already packed as rgb9e5 (`rgb9e5.ts`), which is how such a
|
|
41
|
+
* bake usually arrives and which uploads with no conversion at all.
|
|
42
|
+
*/
|
|
43
|
+
readonly irradiance: Float32Array | Uint32Array;
|
|
44
|
+
/**
|
|
45
|
+
* The direction the light arrives from, four bytes a texel: a first-order spherical harmonic in
|
|
46
|
+
* this engine's world axes, each value stored as `v * 0.5 + 0.5` — xyz the axis, w the constant.
|
|
47
|
+
*/
|
|
48
|
+
readonly direction: Uint8Array;
|
|
49
|
+
}
|
|
50
|
+
/** Layers a page is uploaded as: the irradiance, then the direction. */
|
|
51
|
+
export declare const LIGHTMAP_LAYERS = 2;
|
|
52
|
+
/**
|
|
53
|
+
* Where a surface's region of its page is: `lightmapUv = uv2 * scale + bias`, as
|
|
54
|
+
* `[scaleU, scaleV, biasU, biasV]`. The whole page is the identity.
|
|
55
|
+
*/
|
|
56
|
+
export type LightmapRegion = readonly [number, number, number, number];
|
|
57
|
+
/** The whole page. */
|
|
58
|
+
export declare const WHOLE_PAGE: LightmapRegion;
|
|
59
|
+
/**
|
|
60
|
+
* A page as both backends upload it: two layers of four bytes a texel, row-major from the top. The
|
|
61
|
+
* first is the irradiance's rgb9e5 word, byte by byte from its lowest; the second is the direction.
|
|
62
|
+
*
|
|
63
|
+
* **Eight bytes a texel, and the shader filters it.** Half floats were sixteen: a stage of 49 pages
|
|
64
|
+
* and 21.8 million texels held 348 MB, where this holds 174. The price is that the irradiance cannot
|
|
65
|
+
* be filtered as it is stored — a bilinear blend of two words with different exponents is not the
|
|
66
|
+
* blend of the colours they hold — so the lit stage fetches a texel's four neighbours, decodes each
|
|
67
|
+
* and blends the colours (`modelBaked` in `shaders/flat/models.ts`): four fetches a layer where one
|
|
68
|
+
* filtered sample was, on lightmapped surfaces alone. **What would make it wrong** is every device
|
|
69
|
+
* a page reaches sampling `rgb9e5ufloat`, which WebGL2 does as `RGB9_E5` too; it would then be a
|
|
70
|
+
* texture of its own, filtered by the device, and a binding the lit stage has no room for.
|
|
71
|
+
*/
|
|
72
|
+
export interface LightmapTexels {
|
|
73
|
+
readonly kind: 'lightmap-texels';
|
|
74
|
+
readonly width: number;
|
|
75
|
+
readonly height: number;
|
|
76
|
+
/** Each layer's bytes, in layer order: the irradiance, then the direction. */
|
|
77
|
+
readonly layers: readonly Uint8Array[];
|
|
78
|
+
}
|
|
79
|
+
/** Whether a surface texture's source is a page's texels rather than an image or blocks. */
|
|
80
|
+
export declare function isLightmapTexels(source: unknown): source is LightmapTexels;
|
|
81
|
+
/**
|
|
82
|
+
* A page as both backends upload it (see `LightmapTexels`). Refuses a page whose arrays are not its
|
|
83
|
+
* size, naming the one that is short. Packed irradiance is read where it lies, with no copy; float
|
|
84
|
+
* irradiance is packed once, here.
|
|
85
|
+
*/
|
|
86
|
+
export declare function lightmapTexels(page: LightmapPage): LightmapTexels;
|
|
87
|
+
/**
|
|
88
|
+
* A mesh with its second coordinates moved onto the grain and relief streams they ride, or the
|
|
89
|
+
* mesh itself where it has none. Called by both backends' `createMesh`, before anything records
|
|
90
|
+
* which streams the mesh has.
|
|
91
|
+
*
|
|
92
|
+
* **Stored as `-1 - uv`, below zero, and that is what keeps the lanes safe to share.** Grain takes
|
|
93
|
+
* effect only above zero and relief is read through `max(relief, 0)`, so the same mesh drawn by a
|
|
94
|
+
* material that is not lightmapped is a mesh with no grain and no relief — rather than one mottled
|
|
95
|
+
* and bumped by its own coordinates, which is what storing them as they are drew. **What it gives
|
|
96
|
+
* up** is coordinates at or below −1, which read as nothing; a page's coordinates are from 0 to 1.
|
|
97
|
+
*/
|
|
98
|
+
export declare function withLightmapUvs(data: MeshData): MeshData;
|
|
99
|
+
/** A material's region, checked: four finite numbers, the whole page where none was named. */
|
|
100
|
+
export declare function lightmapRegionOf(region: LightmapRegion | undefined): LightmapRegion;
|
|
101
|
+
/**
|
|
102
|
+
* Says once that a lightmapped material was set with no page. The lit stage adds nothing for one,
|
|
103
|
+
* which is the defined state; this is what keeps it from also being a silent one.
|
|
104
|
+
*/
|
|
105
|
+
export declare function warnUnpagedLightmap(model: {
|
|
106
|
+
readonly kind: string;
|
|
107
|
+
} | null | undefined, map: unknown): void;
|
|
108
|
+
/**
|
|
109
|
+
* Says once that an instanced batch and its material disagree about lightmap regions: a batch with
|
|
110
|
+
* regions drawn by a material that is not lightmapped, or a lightmapped one drawn without them. The
|
|
111
|
+
* regions ride the lane a tint and an opacity do (`MeshInstances.lightmapRegions`), so either draw
|
|
112
|
+
* reads one as the other: drawn, and wrong, which is why it is said rather than left.
|
|
113
|
+
*/
|
|
114
|
+
export declare function warnRegionsWithoutPage(model: {
|
|
115
|
+
readonly kind: string;
|
|
116
|
+
} | null | undefined, regions: Float32Array | undefined): void;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { packRgb9e5 } from './rgb9e5.js';
|
|
2
|
+
/** Layers a page is uploaded as: the irradiance, then the direction. */
|
|
3
|
+
export const LIGHTMAP_LAYERS = 2;
|
|
4
|
+
/** The whole page. */
|
|
5
|
+
export const WHOLE_PAGE = [1, 1, 0, 0];
|
|
6
|
+
/** Whether a surface texture's source is a page's texels rather than an image or blocks. */
|
|
7
|
+
export function isLightmapTexels(source) {
|
|
8
|
+
return (typeof source === 'object' &&
|
|
9
|
+
source !== null &&
|
|
10
|
+
source.kind === 'lightmap-texels');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A page as both backends upload it (see `LightmapTexels`). Refuses a page whose arrays are not its
|
|
14
|
+
* size, naming the one that is short. Packed irradiance is read where it lies, with no copy; float
|
|
15
|
+
* irradiance is packed once, here.
|
|
16
|
+
*/
|
|
17
|
+
export function lightmapTexels(page) {
|
|
18
|
+
const { width, height } = page;
|
|
19
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) {
|
|
20
|
+
throw new Error(`createLightmap: a ${width} by ${height} page has nothing in it`);
|
|
21
|
+
}
|
|
22
|
+
const texels = width * height;
|
|
23
|
+
const irradiance = page.irradiance;
|
|
24
|
+
const packed = irradiance instanceof Uint32Array;
|
|
25
|
+
const wanted = packed ? texels : texels * 3;
|
|
26
|
+
if (irradiance.length !== wanted) {
|
|
27
|
+
throw new Error(`createLightmap: ${irradiance.length} irradiance ${packed ? 'words' : 'floats'} for a ` +
|
|
28
|
+
`${width} by ${height} page, which wants ${wanted}: ` +
|
|
29
|
+
(packed ? 'one rgb9e5 word a texel' : 'three a texel'));
|
|
30
|
+
}
|
|
31
|
+
if (page.direction.length !== texels * 4) {
|
|
32
|
+
throw new Error(`createLightmap: ${page.direction.length} direction bytes for a ${width} by ${height} ` +
|
|
33
|
+
`page, which wants ${texels * 4}: four a texel`);
|
|
34
|
+
}
|
|
35
|
+
let words;
|
|
36
|
+
if (packed) {
|
|
37
|
+
words = irradiance;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
words = new Uint32Array(texels);
|
|
41
|
+
for (let t = 0; t < texels; t++) {
|
|
42
|
+
words[t] = packRgb9e5(irradiance[t * 3], irradiance[t * 3 + 1], irradiance[t * 3 + 2]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
kind: 'lightmap-texels',
|
|
47
|
+
width,
|
|
48
|
+
height,
|
|
49
|
+
layers: [littleEndianBytes(words), page.direction],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Whether this platform lays a word out lowest byte first, which the shader's decode assumes. */
|
|
53
|
+
const LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
|
|
54
|
+
/**
|
|
55
|
+
* A page's words as bytes, lowest first: a view of the same memory on every little-endian platform,
|
|
56
|
+
* which is every one a browser runs on, and a reordered copy anywhere else.
|
|
57
|
+
*/
|
|
58
|
+
function littleEndianBytes(words) {
|
|
59
|
+
const bytes = new Uint8Array(words.buffer, words.byteOffset, words.length * 4);
|
|
60
|
+
if (LITTLE_ENDIAN)
|
|
61
|
+
return bytes;
|
|
62
|
+
const swapped = new Uint8Array(bytes.length);
|
|
63
|
+
for (let i = 0; i < bytes.length; i += 4) {
|
|
64
|
+
swapped[i] = bytes[i + 3];
|
|
65
|
+
swapped[i + 1] = bytes[i + 2];
|
|
66
|
+
swapped[i + 2] = bytes[i + 1];
|
|
67
|
+
swapped[i + 3] = bytes[i];
|
|
68
|
+
}
|
|
69
|
+
return swapped;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A mesh with its second coordinates moved onto the grain and relief streams they ride, or the
|
|
73
|
+
* mesh itself where it has none. Called by both backends' `createMesh`, before anything records
|
|
74
|
+
* which streams the mesh has.
|
|
75
|
+
*
|
|
76
|
+
* **Stored as `-1 - uv`, below zero, and that is what keeps the lanes safe to share.** Grain takes
|
|
77
|
+
* effect only above zero and relief is read through `max(relief, 0)`, so the same mesh drawn by a
|
|
78
|
+
* material that is not lightmapped is a mesh with no grain and no relief — rather than one mottled
|
|
79
|
+
* and bumped by its own coordinates, which is what storing them as they are drew. **What it gives
|
|
80
|
+
* up** is coordinates at or below −1, which read as nothing; a page's coordinates are from 0 to 1.
|
|
81
|
+
*/
|
|
82
|
+
export function withLightmapUvs(data) {
|
|
83
|
+
const uvs = data.lightmapUvs;
|
|
84
|
+
if (uvs === undefined)
|
|
85
|
+
return data;
|
|
86
|
+
const vertices = uvs.length / 2;
|
|
87
|
+
const grain = new Float32Array(vertices);
|
|
88
|
+
const relief = new Float32Array(vertices);
|
|
89
|
+
for (let v = 0; v < vertices; v++) {
|
|
90
|
+
grain[v] = -1 - uvs[v * 2];
|
|
91
|
+
relief[v] = -1 - uvs[v * 2 + 1];
|
|
92
|
+
}
|
|
93
|
+
return { ...data, grain, relief, lightmapUvs: undefined };
|
|
94
|
+
}
|
|
95
|
+
/** A material's region, checked: four finite numbers, the whole page where none was named. */
|
|
96
|
+
export function lightmapRegionOf(region) {
|
|
97
|
+
if (region === undefined)
|
|
98
|
+
return WHOLE_PAGE;
|
|
99
|
+
let finite = region.length === 4;
|
|
100
|
+
for (let i = 0; i < region.length; i++)
|
|
101
|
+
finite &&= Number.isFinite(region[i]);
|
|
102
|
+
if (!finite) {
|
|
103
|
+
throw new Error('lightmapModel: a region is four numbers, [scaleU, scaleV, biasU, biasV], ' +
|
|
104
|
+
`and was given ${String(region)}`);
|
|
105
|
+
}
|
|
106
|
+
return region;
|
|
107
|
+
}
|
|
108
|
+
let warnedUnpaged = false;
|
|
109
|
+
/**
|
|
110
|
+
* Says once that a lightmapped material was set with no page. The lit stage adds nothing for one,
|
|
111
|
+
* which is the defined state; this is what keeps it from also being a silent one.
|
|
112
|
+
*/
|
|
113
|
+
export function warnUnpagedLightmap(model, map) {
|
|
114
|
+
if (warnedUnpaged || model?.kind !== 'lightmap' || (map !== null && map !== undefined))
|
|
115
|
+
return;
|
|
116
|
+
warnedUnpaged = true;
|
|
117
|
+
console.warn('setMaterial: a lightmapModel material with no modelMap reads no page and adds no light. ' +
|
|
118
|
+
'Pass the page createLightmap returned as its modelMap.');
|
|
119
|
+
}
|
|
120
|
+
let warnedRegions = false;
|
|
121
|
+
/**
|
|
122
|
+
* Says once that an instanced batch and its material disagree about lightmap regions: a batch with
|
|
123
|
+
* regions drawn by a material that is not lightmapped, or a lightmapped one drawn without them. The
|
|
124
|
+
* regions ride the lane a tint and an opacity do (`MeshInstances.lightmapRegions`), so either draw
|
|
125
|
+
* reads one as the other: drawn, and wrong, which is why it is said rather than left.
|
|
126
|
+
*/
|
|
127
|
+
export function warnRegionsWithoutPage(model, regions) {
|
|
128
|
+
if (warnedRegions)
|
|
129
|
+
return;
|
|
130
|
+
const lightmapped = model?.kind === 'lightmap';
|
|
131
|
+
if (lightmapped === (regions !== undefined))
|
|
132
|
+
return;
|
|
133
|
+
warnedRegions = true;
|
|
134
|
+
console.warn(lightmapped
|
|
135
|
+
? 'drawInstanced: a lightmapModel material drawing a batch with no lightmapRegions reads ' +
|
|
136
|
+
'its tints as regions. Give each instance its region of the page.'
|
|
137
|
+
: 'drawInstanced: a batch with lightmapRegions drawn by a material that is not a ' +
|
|
138
|
+
'lightmapModel reads its regions as its tints and opacities. Draw it with its page, or ' +
|
|
139
|
+
'without regions.');
|
|
140
|
+
}
|
package/dist/render/mesh.js
CHANGED
|
@@ -380,7 +380,8 @@ export class Mesh {
|
|
|
380
380
|
gl.vertexAttribDivisor(location, 1);
|
|
381
381
|
}
|
|
382
382
|
gl.enableVertexAttribArray(15);
|
|
383
|
-
|
|
383
|
+
/* The tint and, in w, the opacity: one attribute for both. See `INSTANCE_FLOATS`. */
|
|
384
|
+
gl.vertexAttribPointer(15, 4, gl.FLOAT, false, stride, 64);
|
|
384
385
|
gl.vertexAttribDivisor(15, 1);
|
|
385
386
|
gl.bindVertexArray(null);
|
|
386
387
|
return vao;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { type ParticleMaterial } from './particleMaterial.ts';
|
|
2
2
|
import type { ParticleInstances } from './particlePool.ts';
|
|
3
|
+
import type { SurfaceTexture } from './surfaceTexture.ts';
|
|
4
|
+
/** The vertex stage's `uCameraFacing` for a facing: the cross 0, the camera 1, the travel 2. */
|
|
5
|
+
export declare function facingCode(facing: ParticleFacing): number;
|
|
6
|
+
/**
|
|
7
|
+
* A sprite's frame and half-height, two floats a particle, into `out` — the stream the vertex
|
|
8
|
+
* stage reads at location 10. Shared by both backends, so the two read one layout.
|
|
9
|
+
*/
|
|
10
|
+
export declare function packSpriteStream(data: ParticleInstances, count: number, out: Float32Array): void;
|
|
3
11
|
/**
|
|
4
12
|
* One draw call for a live particle pool: one camera-facing quad per particle, or a
|
|
5
13
|
* crossed pair of world-fixed ones when `facing` asks for them.
|
|
@@ -21,9 +29,11 @@ export type ParticleBlend = 'additive' | 'alpha';
|
|
|
21
29
|
* `'camera'` is one quad facing the viewer, and the default. `'cross'` is the pair of
|
|
22
30
|
* world-fixed blades this batch drew before the option existed — see `uCameraFacing` in
|
|
23
31
|
* `shaders/particle.ts` for the artifact that made the default the other one, and for what
|
|
24
|
-
* the cross still buys a caller who wants it back.
|
|
32
|
+
* the cross still buys a caller who wants it back. `'velocity'` faces the camera with its up along
|
|
33
|
+
* the particle's travel across the view — a streak, a spark stretched by its motion — and a
|
|
34
|
+
* particle at rest faces the camera as `'camera'` does.
|
|
25
35
|
*/
|
|
26
|
-
export type ParticleFacing = 'camera' | 'cross';
|
|
36
|
+
export type ParticleFacing = 'camera' | 'cross' | 'velocity';
|
|
27
37
|
export interface ParticleBatchOptions {
|
|
28
38
|
/**
|
|
29
39
|
* Which particle this is, by name.
|
|
@@ -72,6 +82,41 @@ export interface ParticleBatchOptions {
|
|
|
72
82
|
* shader's own comment on `uCameraFacing` lays out.
|
|
73
83
|
*/
|
|
74
84
|
facing?: ParticleFacing;
|
|
85
|
+
/**
|
|
86
|
+
* The image a `'sprite'` draws: a surface texture, its first layer. **Required by the sprite** and
|
|
87
|
+
* ignored by the procedural materials. Colour is the particle's times the image's, unlit.
|
|
88
|
+
*/
|
|
89
|
+
texture?: SurfaceTexture;
|
|
90
|
+
/**
|
|
91
|
+
* A `'sprite'`'s flipbook: the image as `[columns, rows]` cells, numbered across then down from
|
|
92
|
+
* the top-left, a particle's `frames` naming one. `[1, 1]`, the default, is the whole image.
|
|
93
|
+
*/
|
|
94
|
+
cells?: readonly [number, number];
|
|
95
|
+
/**
|
|
96
|
+
* Whether a `'sprite'` blends from a particle's cell toward the next by the fraction of its
|
|
97
|
+
* frame, so a slow flipbook moves rather than steps. Off by default: one cell, sampled once.
|
|
98
|
+
*/
|
|
99
|
+
blendCells?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Metres over which a `'sprite'` fades where it meets the opaque scene: the hard line a flat card
|
|
102
|
+
* draws where it cuts a floor or a body, softened. **0, the default, is a hard edge.** It reads a
|
|
103
|
+
* copy of the frame's depth, which needs `screenEffects`; without one the edge stays hard and
|
|
104
|
+
* the renderer says so once.
|
|
105
|
+
*/
|
|
106
|
+
softDepth?: number;
|
|
107
|
+
/** Metres in front of the eye over which a `'sprite'` fades in. 0, the default, is no fade. */
|
|
108
|
+
cameraFade?: number;
|
|
109
|
+
/**
|
|
110
|
+
* Metres each particle is moved toward the eye before it is drawn, so a sprite born inside a
|
|
111
|
+
* surface is drawn in front of it. 0 by default. Every material reads it.
|
|
112
|
+
*/
|
|
113
|
+
cameraOffset?: number;
|
|
114
|
+
/**
|
|
115
|
+
* Whether the live particles are drawn farthest first, the order an alpha blend assumes. Off
|
|
116
|
+
* by default, because sorting costs a pass over the particles on the CPU each frame and an
|
|
117
|
+
* additive blend is a sum that does not care. See `particleSort.ts`.
|
|
118
|
+
*/
|
|
119
|
+
sort?: boolean;
|
|
75
120
|
/**
|
|
76
121
|
* Another batch whose compiled program this one should reuse.
|
|
77
122
|
*
|
|
@@ -102,6 +147,17 @@ export declare class ParticleBatch {
|
|
|
102
147
|
private readonly ageBuffer;
|
|
103
148
|
private readonly seedBuffer;
|
|
104
149
|
private readonly velocityBuffer;
|
|
150
|
+
private readonly spriteBuffer;
|
|
151
|
+
private readonly spriteStaging;
|
|
152
|
+
/** Room to draw farthest first, where the batch asked. See `ParticleBatchOptions.sort`. */
|
|
153
|
+
private readonly order;
|
|
154
|
+
/** The sprite's own: its image, its flipbook and its fades. See `ParticleBatchOptions`. */
|
|
155
|
+
readonly texture: SurfaceTexture | null;
|
|
156
|
+
readonly cells: readonly [number, number];
|
|
157
|
+
readonly blendCells: boolean;
|
|
158
|
+
readonly softDepth: number;
|
|
159
|
+
readonly cameraFade: number;
|
|
160
|
+
readonly cameraOffset: number;
|
|
105
161
|
private readonly blend;
|
|
106
162
|
private readonly stretchSec;
|
|
107
163
|
private readonly erosion;
|
|
@@ -119,7 +175,7 @@ export declare class ParticleBatch {
|
|
|
119
175
|
* Only the live prefix of each array, which is what the pool's compaction
|
|
120
176
|
* exists for: a pool at a tenth of its capacity uploads a tenth of the bytes.
|
|
121
177
|
*/
|
|
122
|
-
upload(gl: WebGL2RenderingContext,
|
|
178
|
+
upload(gl: WebGL2RenderingContext, given: ParticleInstances, eye: ArrayLike<number>): void;
|
|
123
179
|
/** Bind this material's own constants. Called once per draw, before `drawTo`. */
|
|
124
180
|
bindMaterial(gl: WebGL2RenderingContext): void;
|
|
125
181
|
/**
|
|
@@ -2,6 +2,24 @@ import { compileProgram, uniformLocations } from './shader.js';
|
|
|
2
2
|
import { particleShaders } from './particleMaterial.js';
|
|
3
3
|
import { PARTICLE_BLADES, PARTICLE_INDICES, PARTICLE_VERTS } from './shaders/particle.js';
|
|
4
4
|
import { blendCovering, blendKeeping } from './sceneCoverage.js';
|
|
5
|
+
import { PARTICLE_SPRITE_UNIT } from './lightBudget.js';
|
|
6
|
+
import { createParticleSort, sortBackToFront } from './particleSort.js';
|
|
7
|
+
/** The vertex stage's `uCameraFacing` for a facing: the cross 0, the camera 1, the travel 2. */
|
|
8
|
+
export function facingCode(facing) {
|
|
9
|
+
return facing === 'cross' ? 0 : facing === 'velocity' ? 2 : 1;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A sprite's frame and half-height, two floats a particle, into `out` — the stream the vertex
|
|
13
|
+
* stage reads at location 10. Shared by both backends, so the two read one layout.
|
|
14
|
+
*/
|
|
15
|
+
export function packSpriteStream(data, count, out) {
|
|
16
|
+
const frames = data.frames;
|
|
17
|
+
const heights = data.heights;
|
|
18
|
+
for (let i = 0; i < count; i++) {
|
|
19
|
+
out[i * 2] = frames?.[i] ?? 0;
|
|
20
|
+
out[i * 2 + 1] = heights?.[i] ?? 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
5
23
|
/** Corners of one blade, as a triangle-strip-friendly quad. */
|
|
6
24
|
const CORNERS = [
|
|
7
25
|
[-1, -1],
|
|
@@ -22,6 +40,17 @@ export class ParticleBatch {
|
|
|
22
40
|
ageBuffer;
|
|
23
41
|
seedBuffer;
|
|
24
42
|
velocityBuffer;
|
|
43
|
+
spriteBuffer;
|
|
44
|
+
spriteStaging;
|
|
45
|
+
/** Room to draw farthest first, where the batch asked. See `ParticleBatchOptions.sort`. */
|
|
46
|
+
order;
|
|
47
|
+
/** The sprite's own: its image, its flipbook and its fades. See `ParticleBatchOptions`. */
|
|
48
|
+
texture;
|
|
49
|
+
cells;
|
|
50
|
+
blendCells;
|
|
51
|
+
softDepth;
|
|
52
|
+
cameraFade;
|
|
53
|
+
cameraOffset;
|
|
25
54
|
blend;
|
|
26
55
|
stretchSec;
|
|
27
56
|
erosion;
|
|
@@ -53,6 +82,17 @@ export class ParticleBatch {
|
|
|
53
82
|
this.coreGain = options.coreGain ?? 1;
|
|
54
83
|
this.fog = options.fog ?? false;
|
|
55
84
|
this.facing = options.facing ?? 'camera';
|
|
85
|
+
this.texture = options.texture ?? null;
|
|
86
|
+
this.cells = options.cells ?? [1, 1];
|
|
87
|
+
this.blendCells = options.blendCells === true;
|
|
88
|
+
this.softDepth = Math.max(0, options.softDepth ?? 0);
|
|
89
|
+
this.cameraFade = Math.max(0, options.cameraFade ?? 0);
|
|
90
|
+
this.cameraOffset = options.cameraOffset ?? 0;
|
|
91
|
+
this.order = options.sort === true ? createParticleSort(capacity) : null;
|
|
92
|
+
this.spriteStaging = new Float32Array(capacity * 2);
|
|
93
|
+
if (options.material === 'sprite' && this.texture === null) {
|
|
94
|
+
throw new Error("createParticles: a 'sprite' draws an image, and was given no `texture`");
|
|
95
|
+
}
|
|
56
96
|
const corners = new Float32Array(PARTICLE_VERTS * 2);
|
|
57
97
|
const blades = new Float32Array(PARTICLE_VERTS);
|
|
58
98
|
const indices = new Uint16Array(PARTICLE_INDICES);
|
|
@@ -88,6 +128,7 @@ export class ParticleBatch {
|
|
|
88
128
|
this.ageBuffer = this.attach(gl, 7, capacity, 1, 1, shaders.label);
|
|
89
129
|
this.seedBuffer = this.attach(gl, 8, capacity, 1, 1, shaders.label);
|
|
90
130
|
this.velocityBuffer = this.attach(gl, 9, capacity * 3, 3, 1, shaders.label);
|
|
131
|
+
this.spriteBuffer = this.attach(gl, 10, capacity * 2, 2, 1, shaders.label);
|
|
91
132
|
const indexBuffer = gl.createBuffer();
|
|
92
133
|
if (indexBuffer === null)
|
|
93
134
|
throw new Error(`${shaders.label}: createBuffer failed`);
|
|
@@ -102,10 +143,13 @@ export class ParticleBatch {
|
|
|
102
143
|
* Only the live prefix of each array, which is what the pool's compaction
|
|
103
144
|
* exists for: a pool at a tenth of its capacity uploads a tenth of the bytes.
|
|
104
145
|
*/
|
|
105
|
-
upload(gl,
|
|
146
|
+
upload(gl, given, eye) {
|
|
147
|
+
const data = this.order === null ? given : sortBackToFront(given, eye, this.order);
|
|
106
148
|
const n = data.count;
|
|
107
149
|
if (n === 0)
|
|
108
150
|
return;
|
|
151
|
+
packSpriteStream(data, n, this.spriteStaging);
|
|
152
|
+
this.send(gl, this.spriteBuffer, this.spriteStaging, n * 2);
|
|
109
153
|
this.send(gl, this.positionBuffer, data.positions, n * 3);
|
|
110
154
|
this.send(gl, this.sizeBuffer, data.sizes, n);
|
|
111
155
|
this.send(gl, this.spinBuffer, data.spins, n);
|
|
@@ -119,7 +163,14 @@ export class ParticleBatch {
|
|
|
119
163
|
bindMaterial(gl) {
|
|
120
164
|
const u = this.uniforms;
|
|
121
165
|
gl.uniform1f(u['uStretchSec'] ?? null, this.stretchSec);
|
|
122
|
-
gl.uniform1f(u['uCameraFacing'] ?? null, this.facing
|
|
166
|
+
gl.uniform1f(u['uCameraFacing'] ?? null, facingCode(this.facing));
|
|
167
|
+
gl.uniform1f(u['uCameraOffset'] ?? null, this.cameraOffset);
|
|
168
|
+
/* The sprite's flipbook and its image; the fades are the renderer's, which owns the depth. */
|
|
169
|
+
if (this.texture !== null) {
|
|
170
|
+
gl.uniform4f(u['uCells'] ?? null, this.cells[0], this.cells[1], this.blendCells ? 1 : 0, 0);
|
|
171
|
+
this.texture.bind(gl, PARTICLE_SPRITE_UNIT);
|
|
172
|
+
gl.uniform1i(u['uSprite'] ?? null, PARTICLE_SPRITE_UNIT);
|
|
173
|
+
}
|
|
123
174
|
gl.uniform1f(u['uErosion'] ?? null, this.erosion);
|
|
124
175
|
gl.uniform1f(u['uCoreGain'] ?? null, this.coreGain);
|
|
125
176
|
/* Only the mote program declares uFogEnabled, so this is a no-op — not a leak into the
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* document that. Naming them makes the pairing explicit instead of leaving it to a caller to
|
|
14
14
|
* match a source with the constants that suit it.
|
|
15
15
|
*/
|
|
16
|
-
export type ParticleMaterial = 'smoke' | 'spark' | 'mote';
|
|
16
|
+
export type ParticleMaterial = 'smoke' | 'spark' | 'mote' | 'sprite';
|
|
17
17
|
/** Both compilations of one material, and what the generator recorded about each stage. */
|
|
18
18
|
export interface ParticleShaders {
|
|
19
19
|
readonly label: string;
|
|
@@ -47,6 +47,11 @@ export declare const PARTICLE_VERT_BINDINGS: {
|
|
|
47
47
|
readonly size: 4;
|
|
48
48
|
readonly type: "float";
|
|
49
49
|
};
|
|
50
|
+
readonly uCameraOffset: {
|
|
51
|
+
readonly offset: 84;
|
|
52
|
+
readonly size: 4;
|
|
53
|
+
readonly type: "float";
|
|
54
|
+
};
|
|
50
55
|
};
|
|
51
56
|
readonly textures: {};
|
|
52
57
|
};
|