@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
|
@@ -84,7 +84,7 @@ export const MAX_CLUSTERED_LIGHTS = 320;
|
|
|
84
84
|
* it cannot. That is what lets a light index the shadow uniforms by a number read from a texture,
|
|
85
85
|
* and it is the same fact the octahedral array texture was built on.
|
|
86
86
|
*/
|
|
87
|
-
export const LIGHT_TEXELS =
|
|
87
|
+
export const LIGHT_TEXELS = 6;
|
|
88
88
|
/**
|
|
89
89
|
* Where each field of a light's record sits, as an offset in `uint`s from `lightBase`.
|
|
90
90
|
*
|
|
@@ -114,7 +114,11 @@ export const LIGHT_TEXELS = 5;
|
|
|
114
114
|
* **Reordered 2026-10-02 so a plain light's fields are the first eight**, which is what lets the
|
|
115
115
|
* shader stop after two texels. Nine values are needed by every light and eight fit in two
|
|
116
116
|
* texels, so the emitter size and the weight share slot 7 as two halves; see `packSizeAndWeight`.
|
|
117
|
-
*
|
|
117
|
+
*
|
|
118
|
+
* **A sixth texel arrived in 4.8.7 for a light's own falloff exponent**, the fifth's last slot
|
|
119
|
+
* having gone to the channels: 6 KB more of a table near 300, read only for a light whose record
|
|
120
|
+
* carries a fixture, which a light with an exponent does. Slots 21 and 22 are a rectangle's barn
|
|
121
|
+
* doors and spare on a lamp; slot 23 is spare.
|
|
118
122
|
*/
|
|
119
123
|
export const LIGHT_RECORD = {
|
|
120
124
|
positionX: 0,
|
|
@@ -139,11 +143,43 @@ export const LIGHT_RECORD = {
|
|
|
139
143
|
iesAxisX: 16,
|
|
140
144
|
iesAxisY: 17,
|
|
141
145
|
iesAxisZ: 18,
|
|
146
|
+
/**
|
|
147
|
+
* The light's lighting channels, a mask (`PointLightSet.lightChannels`), in the slot that was
|
|
148
|
+
* spare. A light naming channels other than the default is read as carrying a fixture, so its
|
|
149
|
+
* last texels are fetched; a plain light's are not, and the shader takes the default for it.
|
|
150
|
+
*/
|
|
151
|
+
channels: 19,
|
|
152
|
+
/** The light's own falloff exponent, or 0 for the frame's falloff. `PointLightSource.falloffExponent`. */
|
|
153
|
+
falloffExponent: 20,
|
|
154
|
+
/** A rectangle's barn doors: the cosine of their angle from its normal, then their length in metres. */
|
|
155
|
+
barnDoorCos: 21,
|
|
156
|
+
barnDoorLength: 22,
|
|
142
157
|
};
|
|
158
|
+
/** The lighting channel every light and every surface is on unless it says otherwise. */
|
|
159
|
+
export const DEFAULT_LIGHT_CHANNELS = 1;
|
|
160
|
+
/**
|
|
161
|
+
* A light's channels as the shader compares them: a whole mask from 1 to 255, or the default where
|
|
162
|
+
* none was named or what was named is not one. The froxel record and the uniform path both write
|
|
163
|
+
* this, so a light named badly is on channel 1 in both rather than on two different guesses.
|
|
164
|
+
*/
|
|
165
|
+
export function lightChannelsOf(value) {
|
|
166
|
+
if (value === undefined || !Number.isInteger(value) || value < 1 || value > 255) {
|
|
167
|
+
return DEFAULT_LIGHT_CHANNELS;
|
|
168
|
+
}
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* A light's own falloff exponent as the shader reads it: a positive finite number, or 0 for the
|
|
173
|
+
* frame's falloff where none was named or what was named is not one. The record and the uniform
|
|
174
|
+
* path both write this, so a light named badly falls off by the frame's rule in both.
|
|
175
|
+
*/
|
|
176
|
+
export function lightFalloffExponentOf(value) {
|
|
177
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? value : 0;
|
|
178
|
+
}
|
|
143
179
|
/** Texels the lit pass reads for a light whose record carries no fixture. */
|
|
144
180
|
export const PLAIN_LIGHT_TEXELS = 2;
|
|
145
181
|
/**
|
|
146
|
-
* The bit of `sizeAndWeight` that says the record's last
|
|
182
|
+
* The bit of `sizeAndWeight` that says the record's last four texels hold something: the sign of
|
|
147
183
|
* the emitter size's half, which a size never needs.
|
|
148
184
|
*/
|
|
149
185
|
export const LIGHT_FIXTURE_FLAG = 0x8000;
|
|
@@ -354,8 +390,14 @@ export function createClusterLightSet() {
|
|
|
354
390
|
* radius in its place, an emitter as wide as the light reaches, which widened every lamp's
|
|
355
391
|
* highlight on a specular surface on the clustered arm alone.
|
|
356
392
|
*/
|
|
357
|
-
export function fillClusterLightSet(env, out
|
|
393
|
+
export function fillClusterLightSet(env, out,
|
|
394
|
+
/** How many rectangles the fixed arm shades; the rest of the buffer is clustered. */
|
|
395
|
+
areaFrom = 0) {
|
|
358
396
|
out.count = Math.min(env.lightCount ?? 0, MAX_CLUSTERED_LIGHTS);
|
|
397
|
+
const areas = env.areaLights ?? null;
|
|
398
|
+
out.areas = areas;
|
|
399
|
+
out.areaFrom = areaFrom;
|
|
400
|
+
out.areaCount = Math.max(0, Math.min((areas?.count ?? 0) - areaFrom, MAX_CLUSTERED_LIGHTS - out.count));
|
|
359
401
|
out.positions = env.lightPositions;
|
|
360
402
|
out.colors = env.lightColors;
|
|
361
403
|
out.radii = env.lightRadii;
|
|
@@ -366,6 +408,8 @@ export function fillClusterLightSet(env, out) {
|
|
|
366
408
|
out.iesProfiles = env.lightIesProfiles;
|
|
367
409
|
out.iesAxes = env.lightIesAxes;
|
|
368
410
|
out.cookies = env.lightCookies;
|
|
411
|
+
out.channels = env.lightChannels;
|
|
412
|
+
out.falloffExponents = env.lightFalloffExponents;
|
|
369
413
|
return out;
|
|
370
414
|
}
|
|
371
415
|
/*
|
|
@@ -410,20 +454,25 @@ function distanceSqToBounds(x, y, z, b) {
|
|
|
410
454
|
return dx * dx + dy * dy + dz * dz;
|
|
411
455
|
}
|
|
412
456
|
/**
|
|
413
|
-
* Whether a light's record needs its last
|
|
414
|
-
* or a
|
|
457
|
+
* Whether a light's record needs its last four texels: it holds a shadow slot, a cone, a profile,
|
|
458
|
+
* a cookie, channels of its own or a falloff exponent of its own. Everything else is a plain light, read in `PLAIN_LIGHT_TEXELS`.
|
|
415
459
|
*
|
|
416
460
|
* **`shadowSlots` is zero where the lit shader has no point shadows**, and both binners are handed
|
|
417
461
|
* it that way: a slot is only read under `POINT_SHADOWS`, so a light holding one in a build without
|
|
418
462
|
* them was paying for three texels the shader then ignored.
|
|
419
463
|
*/
|
|
420
464
|
export function lightHasFixture(lights, light, shadowSlots) {
|
|
465
|
+
/* A rectangle's record always carries its axes and extents past the second texel. */
|
|
466
|
+
if (light >= lights.count)
|
|
467
|
+
return true;
|
|
421
468
|
const cone = lights.coneCos;
|
|
422
469
|
return (light < shadowSlots ||
|
|
423
470
|
(cone?.[light * 2] ?? POINT_LIGHT_COS_INNER) !== POINT_LIGHT_COS_INNER ||
|
|
424
471
|
(cone?.[light * 2 + 1] ?? POINT_LIGHT_COS_OUTER) !== POINT_LIGHT_COS_OUTER ||
|
|
425
472
|
(lights.iesProfiles?.[light] ?? NO_IES_PROFILE) >= 0 ||
|
|
426
|
-
(lights.cookies?.[light] ?? NO_IES_PROFILE) >= 0
|
|
473
|
+
(lights.cookies?.[light] ?? NO_IES_PROFILE) >= 0 ||
|
|
474
|
+
lightChannelsOf(lights.channels?.[light]) !== DEFAULT_LIGHT_CHANNELS ||
|
|
475
|
+
lightFalloffExponentOf(lights.falloffExponents?.[light]) > 0);
|
|
427
476
|
}
|
|
428
477
|
/**
|
|
429
478
|
* The value of `uClustered` for a frame: 0 with clustering off, 1 when every light is plain, 2 when
|
|
@@ -459,6 +508,10 @@ export function clusteredMode(lights, count, shadowSlots) {
|
|
|
459
508
|
* is a light without a map, which is what a scene with two hundred lamps has anyway.
|
|
460
509
|
*/
|
|
461
510
|
export function writeLightRecord(lights, light, shadowSlots, out, at) {
|
|
511
|
+
if (light >= lights.count) {
|
|
512
|
+
writeAreaRecord(lights, light, out, at);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
462
515
|
const directions = lights.directions;
|
|
463
516
|
const cone = lights.coneCos;
|
|
464
517
|
const axes = lights.iesAxes;
|
|
@@ -490,7 +543,72 @@ export function writeLightRecord(lights, light, shadowSlots, out, at) {
|
|
|
490
543
|
out[at + LIGHT_RECORD.iesAxisX] = floatBits(axes?.[light * 3] ?? 0);
|
|
491
544
|
out[at + LIGHT_RECORD.iesAxisY] = floatBits(axes?.[light * 3 + 1] ?? 0);
|
|
492
545
|
out[at + LIGHT_RECORD.iesAxisZ] = floatBits(axes?.[light * 3 + 2] ?? 0);
|
|
493
|
-
out[at +
|
|
546
|
+
out[at + LIGHT_RECORD.channels] = floatBits(lightChannelsOf(lights.channels?.[light]));
|
|
547
|
+
out[at + LIGHT_RECORD.falloffExponent] = floatBits(lightFalloffExponentOf(lights.falloffExponents?.[light]));
|
|
548
|
+
out[at + LIGHT_RECORD.barnDoorCos] = 0;
|
|
549
|
+
out[at + LIGHT_RECORD.barnDoorLength] = 0;
|
|
550
|
+
out[at + LIGHT_RECORD.barnDoorLength + 1] = 0;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Which rectangle of the area buffer clustered light `light` is, for a light past the point
|
|
554
|
+
* lights. See `ClusterLightSet.areas`.
|
|
555
|
+
*/
|
|
556
|
+
function areaSlot(lights, light) {
|
|
557
|
+
return (lights.areaFrom ?? 0) + light - lights.count;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* A rectangle's record, which the shader tells from a lamp's by its **negative radius** — its
|
|
561
|
+
* reach, negated — and reads to its sixth texel: sidedness where a lamp keeps its shadow slot,
|
|
562
|
+
* the right axis where a spot keeps its direction, the half extents where its cone is, the up
|
|
563
|
+
* axis where the azimuth is, the channels, and the barn doors in the sixth. Never shadowed: a rectangle's occlusion is
|
|
564
|
+
* the fixed arm's, which is why the first `maxAreaLights` stay there.
|
|
565
|
+
*/
|
|
566
|
+
function writeAreaRecord(lights, light, out, at) {
|
|
567
|
+
const areas = lights.areas;
|
|
568
|
+
const a = areaSlot(lights, light);
|
|
569
|
+
const pos = areas?.positions;
|
|
570
|
+
const color = areas?.colors;
|
|
571
|
+
const right = areas?.right;
|
|
572
|
+
const up = areas?.up;
|
|
573
|
+
const sizes = areas?.sizes;
|
|
574
|
+
out[at + LIGHT_RECORD.positionX] = floatBits(pos?.[a * 3] ?? 0);
|
|
575
|
+
out[at + LIGHT_RECORD.positionY] = floatBits(pos?.[a * 3 + 1] ?? 0);
|
|
576
|
+
out[at + LIGHT_RECORD.positionZ] = floatBits(pos?.[a * 3 + 2] ?? 0);
|
|
577
|
+
out[at + LIGHT_RECORD.radius] = floatBits(-areaReach(lights, light));
|
|
578
|
+
out[at + LIGHT_RECORD.colorR] = floatBits(color?.[a * 3] ?? 0);
|
|
579
|
+
out[at + LIGHT_RECORD.colorG] = floatBits(color?.[a * 3 + 1] ?? 0);
|
|
580
|
+
out[at + LIGHT_RECORD.colorB] = floatBits(color?.[a * 3 + 2] ?? 0);
|
|
581
|
+
out[at + LIGHT_RECORD.sizeAndWeight] = packSizeAndWeight(0, 1, true);
|
|
582
|
+
out[at + LIGHT_RECORD.shadowSlot] = floatBits(areas?.twoSided[a] ?? 0);
|
|
583
|
+
out[at + LIGHT_RECORD.directionX] = floatBits(right?.[a * 3] ?? 1);
|
|
584
|
+
out[at + LIGHT_RECORD.directionY] = floatBits(right?.[a * 3 + 1] ?? 0);
|
|
585
|
+
out[at + LIGHT_RECORD.directionZ] = floatBits(right?.[a * 3 + 2] ?? 0);
|
|
586
|
+
out[at + LIGHT_RECORD.cosInner] = floatBits(sizes?.[a * 2] ?? 0);
|
|
587
|
+
out[at + LIGHT_RECORD.cosOuter] = floatBits(sizes?.[a * 2 + 1] ?? 0);
|
|
588
|
+
out[at + LIGHT_RECORD.iesProfile] = floatBits(NO_IES_PROFILE);
|
|
589
|
+
out[at + LIGHT_RECORD.cookie] = floatBits(NO_IES_PROFILE);
|
|
590
|
+
out[at + LIGHT_RECORD.iesAxisX] = floatBits(up?.[a * 3] ?? 0);
|
|
591
|
+
out[at + LIGHT_RECORD.iesAxisY] = floatBits(up?.[a * 3 + 1] ?? 1);
|
|
592
|
+
out[at + LIGHT_RECORD.iesAxisZ] = floatBits(up?.[a * 3 + 2] ?? 0);
|
|
593
|
+
out[at + LIGHT_RECORD.channels] = floatBits(DEFAULT_LIGHT_CHANNELS);
|
|
594
|
+
out[at + LIGHT_RECORD.falloffExponent] = 0;
|
|
595
|
+
out[at + LIGHT_RECORD.barnDoorCos] = floatBits(areas?.barnDoors?.[a * 2] ?? 0);
|
|
596
|
+
out[at + LIGHT_RECORD.barnDoorLength] = floatBits(areas?.barnDoors?.[a * 2 + 1] ?? 0);
|
|
597
|
+
out[at + LIGHT_RECORD.barnDoorLength + 1] = 0;
|
|
598
|
+
}
|
|
599
|
+
/** A clustered rectangle's reach, which its record carries negated. See `AreaLightSource.range`. */
|
|
600
|
+
function areaReach(lights, light) {
|
|
601
|
+
const areas = lights.areas;
|
|
602
|
+
const a = areaSlot(lights, light);
|
|
603
|
+
const named = areas?.ranges?.[a] ?? 0;
|
|
604
|
+
if (named > 0)
|
|
605
|
+
return named;
|
|
606
|
+
const sizes = areas?.sizes;
|
|
607
|
+
return Math.hypot(sizes?.[a * 2] ?? 0, sizes?.[a * 2 + 1] ?? 0);
|
|
608
|
+
}
|
|
609
|
+
/** How many lights the table carries: the point lights and the clustered rectangles after them. */
|
|
610
|
+
export function clusteredLightTotal(lights) {
|
|
611
|
+
return Math.min(lights.count + (lights.areaCount ?? 0), MAX_CLUSTERED_LIGHTS);
|
|
494
612
|
}
|
|
495
613
|
/**
|
|
496
614
|
* Fill the table with this frame's light assignment, and answer how many lights it carried.
|
|
@@ -538,7 +656,7 @@ export function writeLightRecord(lights, light, shadowSlots, out, at) {
|
|
|
538
656
|
* Fills a caller-owned table and allocates nothing.
|
|
539
657
|
*/
|
|
540
658
|
export function buildLightClusters(lights, view, near, far, tanHalfFovY, aspect, table, shadowSlots) {
|
|
541
|
-
const count =
|
|
659
|
+
const count = clusteredLightTotal(lights);
|
|
542
660
|
/*
|
|
543
661
|
* The whole cluster region, not just the counts.
|
|
544
662
|
*
|
|
@@ -553,10 +671,14 @@ export function buildLightClusters(lights, view, near, far, tanHalfFovY, aspect,
|
|
|
553
671
|
for (let k = 0; k <= CLUSTER_Z; k++)
|
|
554
672
|
sliceDepths[k] = sliceNearDepth(k, near, far);
|
|
555
673
|
for (let light = 0; light < count; light++) {
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
const
|
|
674
|
+
/* A rectangle past the lamps is placed by its centre and binned by its reach. */
|
|
675
|
+
const area = light >= lights.count;
|
|
676
|
+
const from = area ? (lights.areas?.positions ?? lights.positions) : lights.positions;
|
|
677
|
+
const slot = area ? areaSlot(lights, light) : light;
|
|
678
|
+
const x = from[slot * 3] ?? 0;
|
|
679
|
+
const y = from[slot * 3 + 1] ?? 0;
|
|
680
|
+
const z = from[slot * 3 + 2] ?? 0;
|
|
681
|
+
const radius = area ? areaReach(lights, light) : (lights.radii[light] ?? 0);
|
|
560
682
|
/* The record, as the shader reads it. World space; see the note above. */
|
|
561
683
|
writeLightRecord(lights, light, shadowSlots, table, lightBase(light));
|
|
562
684
|
/* Column-major, as gl-matrix builds it. The view looks down -z, so depth is -vz. */
|
|
@@ -570,6 +692,39 @@ export function buildLightClusters(lights, view, near, far, tanHalfFovY, aspect,
|
|
|
570
692
|
/* Entirely behind the camera, so no cluster can reach it. */
|
|
571
693
|
if (depth + radius <= 0)
|
|
572
694
|
continue;
|
|
695
|
+
/*
|
|
696
|
+
* **A spot is binned by its cone as well as its sphere.** A 10° spot reaching thirty metres
|
|
697
|
+
* touches every froxel its sphere does and lights a sliver of them, and a stage rigged with
|
|
698
|
+
* hundreds of such fixtures put every one of them in nearly every froxel near the floor: 313
|
|
699
|
+
* lights cost 11.7 ms of a 1600x900 frame. The cone is tested against each froxel's bounding
|
|
700
|
+
* sphere in view space, which is conservative, by the construction `coneBoxGap` describes.
|
|
701
|
+
*/
|
|
702
|
+
const cosOuter = lights.coneCos?.[light * 2 + 1] ?? POINT_LIGHT_COS_OUTER;
|
|
703
|
+
const directions = lights.directions;
|
|
704
|
+
let coneCos = POINT_LIGHT_COS_OUTER;
|
|
705
|
+
let coneSin = 0;
|
|
706
|
+
let axisX = 0;
|
|
707
|
+
let axisY = 0;
|
|
708
|
+
let axisZ = 0;
|
|
709
|
+
if (!area && directions !== undefined && cosOuter > -1) {
|
|
710
|
+
const dx = directions[light * 3] ?? 0;
|
|
711
|
+
const dy = directions[light * 3 + 1] ?? 0;
|
|
712
|
+
const dz = directions[light * 3 + 2] ?? 0;
|
|
713
|
+
const ax = (view[0] ?? 0) * dx + (view[4] ?? 0) * dy + (view[8] ?? 0) * dz;
|
|
714
|
+
const ay = (view[1] ?? 0) * dx + (view[5] ?? 0) * dy + (view[9] ?? 0) * dz;
|
|
715
|
+
const az = -((view[2] ?? 0) * dx + (view[6] ?? 0) * dy + (view[10] ?? 0) * dz);
|
|
716
|
+
const length = Math.sqrt(ax * ax + ay * ay + az * az);
|
|
717
|
+
/* The shader measures the cone against the direction as given, so a long one is a wider
|
|
718
|
+
cone: `dot(L, d) >= c` is `dot(L, d / |d|) >= c / |d|`. */
|
|
719
|
+
if (length > 1e-6 && cosOuter / length > -1) {
|
|
720
|
+
coneCos = cosOuter / length;
|
|
721
|
+
coneSin = Math.sqrt(Math.max(0, 1 - coneCos * coneCos));
|
|
722
|
+
axisX = ax / length;
|
|
723
|
+
axisY = ay / length;
|
|
724
|
+
axisZ = az / length;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const spot = coneCos > -1;
|
|
573
728
|
/*
|
|
574
729
|
* The slices the sphere could possibly touch. The tiles are derived per slice, below.
|
|
575
730
|
*
|
|
@@ -617,6 +772,10 @@ export function buildLightClusters(lights, view, near, far, tanHalfFovY, aspect,
|
|
|
617
772
|
sliceBounds(i, j, zNear, zFar, halfWNear, halfWFar, halfHNear, halfHFar, bounds);
|
|
618
773
|
if (distanceSqToBounds(vx, vy, depth, bounds) > radiusSq)
|
|
619
774
|
continue;
|
|
775
|
+
if (spot &&
|
|
776
|
+
coneBoxGap(vx, vy, depth, axisX, axisY, axisZ, coneCos, coneSin, bounds) > 0) {
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
620
779
|
insert(table, i + j * CLUSTER_X + k * CLUSTER_X * CLUSTER_Y, light, bounds);
|
|
621
780
|
}
|
|
622
781
|
}
|
|
@@ -624,6 +783,48 @@ export function buildLightClusters(lights, view, near, far, tanHalfFovY, aspect,
|
|
|
624
783
|
}
|
|
625
784
|
return count;
|
|
626
785
|
}
|
|
786
|
+
/**
|
|
787
|
+
* How far a froxel lies outside a spot's cone, in metres, or a negative number where the cone
|
|
788
|
+
* reaches it: positive is a froxel the light cannot shade. Everything in view space with depth
|
|
789
|
+
* positive, as the binner holds it; `axis` is unit length and `cosA`, `sinA` are the outer cone's.
|
|
790
|
+
*
|
|
791
|
+
* **The froxel's bounding sphere against the infinite cone**, after Wronski's cone test for
|
|
792
|
+
* clustered spots. With `V` from the apex to the sphere's centre at an angle `θ` from the axis and
|
|
793
|
+
* `A` the cone's half-angle, the centre's distance from the cone is `|V| sin(θ − A)` — which is
|
|
794
|
+
* `cosA |V⊥| − (V·axis) sinA` — while `θ − A` is under a right angle, and `|V|` past it, where the
|
|
795
|
+
* nearest point of the cone is its apex. Inside the cone it is negative. So the gap is exact for
|
|
796
|
+
* the sphere, and the sphere holds the froxel: the test can only keep a light, never drop one the
|
|
797
|
+
* shader would draw. The apex case is what drops a froxel behind a spot whose sphere reaches it,
|
|
798
|
+
* which the formula alone underestimates.
|
|
799
|
+
*
|
|
800
|
+
* **What it gives up** is tightness at the near end of the frustum, where a froxel is a long thin
|
|
801
|
+
* box whose sphere is much larger than it: a spot grazing such a froxel is kept. **What would make
|
|
802
|
+
* it wrong** is a profile showing those near froxels hold the cost; testing the box's corners is
|
|
803
|
+
* the tight version, at eight dot products a pair.
|
|
804
|
+
*
|
|
805
|
+
* Exported so the conformance page can tell a disagreement at the cone's edge from a real one; the
|
|
806
|
+
* GPU binner writes the same arithmetic in WGSL.
|
|
807
|
+
*/
|
|
808
|
+
export function coneBoxGap(px, py, pz, axisX, axisY, axisZ, cosA, sinA, box) {
|
|
809
|
+
const b0 = box[0] ?? 0;
|
|
810
|
+
const b1 = box[1] ?? 0;
|
|
811
|
+
const b2 = box[2] ?? 0;
|
|
812
|
+
const b3 = box[3] ?? 0;
|
|
813
|
+
const b4 = box[4] ?? 0;
|
|
814
|
+
const b5 = box[5] ?? 0;
|
|
815
|
+
const ex = (b3 - b0) * 0.5;
|
|
816
|
+
const ey = (b4 - b1) * 0.5;
|
|
817
|
+
const ez = (b5 - b2) * 0.5;
|
|
818
|
+
const vx = (b0 + b3) * 0.5 - px;
|
|
819
|
+
const vy = (b1 + b4) * 0.5 - py;
|
|
820
|
+
const vz = (b2 + b5) * 0.5 - pz;
|
|
821
|
+
const along = vx * axisX + vy * axisY + vz * axisZ;
|
|
822
|
+
const lengthSq = vx * vx + vy * vy + vz * vz;
|
|
823
|
+
const across = Math.sqrt(Math.max(lengthSq - along * along, 0));
|
|
824
|
+
const reach = Math.sqrt(lengthSq);
|
|
825
|
+
const distance = along < -sinA * reach ? reach : cosA * across - along * sinA;
|
|
826
|
+
return distance - Math.sqrt(ex * ex + ey * ey + ez * ez);
|
|
827
|
+
}
|
|
627
828
|
/** An NDC coordinate to a tile index, clamped. */
|
|
628
829
|
function tileOf(ndc, tiles) {
|
|
629
830
|
const tile = Math.floor((ndc + 1) * 0.5 * tiles);
|
|
@@ -26,6 +26,8 @@ export interface FieldLight extends LightReach {
|
|
|
26
26
|
readonly b: number;
|
|
27
27
|
/** The emitter's physical radius, which widens its penumbra. */
|
|
28
28
|
readonly sourceRadius: number;
|
|
29
|
+
/** Its own falloff exponent, overriding the frame's above zero. See `pointLightShape`. */
|
|
30
|
+
readonly falloffExponent?: number;
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
33
|
* Fill `light` and `direction`, `BRICK_TEXELS * 4` floats each, x fastest, for brick `brick`.
|
|
@@ -66,7 +66,7 @@ export function sampleFieldLight(px, py, pz, lights, candidates, first, last, fa
|
|
|
66
66
|
const dist = Math.hypot(dx, dy, dz);
|
|
67
67
|
if (dist >= source.radius)
|
|
68
68
|
continue;
|
|
69
|
-
const shape = pointLightShape(dist, source.radius, falloff);
|
|
69
|
+
const shape = pointLightShape(dist, source.radius, falloff, source.falloffExponent);
|
|
70
70
|
if (shape <= 0)
|
|
71
71
|
continue;
|
|
72
72
|
const seen = distance === null
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Both of the renderer's falloffs: `smooth`, the default, is a linear ramp squared, reaching zero at
|
|
11
11
|
* the radius; `inverseSquare` is physical, windowed so it still reaches zero at the radius, and
|
|
12
|
-
* floored at ten centimetres so a surface touching a light is not infinitely bright.
|
|
12
|
+
* floored at ten centimetres so a surface touching a light is not infinitely bright. And a light's
|
|
13
|
+
* own, which overrides both: `(1 - (d/R)^2)^n` with no distance term, for an exponent `n` above zero
|
|
14
|
+
* (`PointLightSource.falloffExponent`).
|
|
13
15
|
*/
|
|
14
16
|
export type PointLightFalloff = 'smooth' | 'inverseSquare';
|
|
15
17
|
/** The shader's `shape` for an unshaped point light: no cone, no photometric profile. */
|
|
16
|
-
export declare function pointLightShape(dist: number, radius: number, falloff: PointLightFalloff): number;
|
|
18
|
+
export declare function pointLightShape(dist: number, radius: number, falloff: PointLightFalloff, exponent?: number): number;
|
|
@@ -9,10 +9,16 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Both of the renderer's falloffs: `smooth`, the default, is a linear ramp squared, reaching zero at
|
|
11
11
|
* the radius; `inverseSquare` is physical, windowed so it still reaches zero at the radius, and
|
|
12
|
-
* floored at ten centimetres so a surface touching a light is not infinitely bright.
|
|
12
|
+
* floored at ten centimetres so a surface touching a light is not infinitely bright. And a light's
|
|
13
|
+
* own, which overrides both: `(1 - (d/R)^2)^n` with no distance term, for an exponent `n` above zero
|
|
14
|
+
* (`PointLightSource.falloffExponent`).
|
|
13
15
|
*/
|
|
14
16
|
/** The shader's `shape` for an unshaped point light: no cone, no photometric profile. */
|
|
15
|
-
export function pointLightShape(dist, radius, falloff) {
|
|
17
|
+
export function pointLightShape(dist, radius, falloff, exponent = 0) {
|
|
18
|
+
if (exponent > 0) {
|
|
19
|
+
const reach = dist / Math.max(radius, 1e-4);
|
|
20
|
+
return Math.min(1, Math.max(0, 1 - reach * reach)) ** exponent;
|
|
21
|
+
}
|
|
16
22
|
if (falloff === 'inverseSquare') {
|
|
17
23
|
const ratio = dist / Math.max(radius, 1e-4);
|
|
18
24
|
const window = Math.min(1, Math.max(0, 1 - ratio ** 4));
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* only ever reaches forward — to the next plane, and from the last to the repeat of the first — so
|
|
13
13
|
* past its last row it meets the texture's edge and the clamp, never a cookie. A cookie's own
|
|
14
14
|
* half-texel inset keeps it inside its band on both axes. The shader finds the bands from
|
|
15
|
-
* `textureSize` and `
|
|
15
|
+
* `textureSize` and `uFixtureShape.x`, so the fold cost no uniform.
|
|
16
16
|
*
|
|
17
17
|
* **What it gives up**: every texel is four half floats where a profile needed one, a few kilobytes
|
|
18
18
|
* a scene; and setting either table rebuilds the whole texture, which a consumer does once per set.
|
|
@@ -26,9 +26,10 @@ import type { PointLightSet } from '../lightBudget.ts';
|
|
|
26
26
|
export declare const MAX_BOUNCE_LIGHTS = 32;
|
|
27
27
|
/**
|
|
28
28
|
* Floats a light takes: where it is and how far it reaches; its colour at its weight and the cosine
|
|
29
|
-
* of its inner cone; the way it points and the cosine of its outer cone
|
|
29
|
+
* of its inner cone; the way it points and the cosine of its outer cone; and its own falloff
|
|
30
|
+
* exponent, 0 for the frame's, in a fourth row of its own (`PointLightSource.falloffExponent`).
|
|
30
31
|
*/
|
|
31
|
-
export declare const BOUNCE_LIGHT_FLOATS =
|
|
32
|
+
export declare const BOUNCE_LIGHT_FLOATS = 16;
|
|
32
33
|
export declare function createBounceLights(): Float32Array;
|
|
33
34
|
/** Pack the lights of `lights` the bounce reads into `out`, and answer how many. */
|
|
34
35
|
export declare function resolveBounceLights(lights: PointLightSet | null, out: Float32Array): number;
|
|
@@ -21,14 +21,15 @@
|
|
|
21
21
|
* What it gives up against the frame: a photometric profile and a cookie, which the bounce reads as
|
|
22
22
|
* a plain cone, and a light's specular, which a diffuse bounce has no use for.
|
|
23
23
|
*/
|
|
24
|
-
import { POINT_LIGHT_COS_INNER, POINT_LIGHT_COS_OUTER } from '../clusteredLights.js';
|
|
24
|
+
import { POINT_LIGHT_COS_INNER, POINT_LIGHT_COS_OUTER, lightFalloffExponentOf, } from '../clusteredLights.js';
|
|
25
25
|
/** Lights the probe bake shades a hit with. */
|
|
26
26
|
export const MAX_BOUNCE_LIGHTS = 32;
|
|
27
27
|
/**
|
|
28
28
|
* Floats a light takes: where it is and how far it reaches; its colour at its weight and the cosine
|
|
29
|
-
* of its inner cone; the way it points and the cosine of its outer cone
|
|
29
|
+
* of its inner cone; the way it points and the cosine of its outer cone; and its own falloff
|
|
30
|
+
* exponent, 0 for the frame's, in a fourth row of its own (`PointLightSource.falloffExponent`).
|
|
30
31
|
*/
|
|
31
|
-
export const BOUNCE_LIGHT_FLOATS =
|
|
32
|
+
export const BOUNCE_LIGHT_FLOATS = 16;
|
|
32
33
|
export function createBounceLights() {
|
|
33
34
|
return new Float32Array(MAX_BOUNCE_LIGHTS * BOUNCE_LIGHT_FLOATS);
|
|
34
35
|
}
|
|
@@ -60,6 +61,10 @@ export function resolveBounceLights(lights, out) {
|
|
|
60
61
|
out[at + 9] = lights.lightDirections?.[i * 3 + 1] ?? 0;
|
|
61
62
|
out[at + 10] = lights.lightDirections?.[i * 3 + 2] ?? 0;
|
|
62
63
|
out[at + 11] = lights.lightConeCos?.[i * 2 + 1] ?? POINT_LIGHT_COS_OUTER;
|
|
64
|
+
out[at + 12] = lightFalloffExponentOf(lights.lightFalloffExponents?.[i]);
|
|
65
|
+
out[at + 13] = 0;
|
|
66
|
+
out[at + 14] = 0;
|
|
67
|
+
out[at + 15] = 0;
|
|
63
68
|
count++;
|
|
64
69
|
}
|
|
65
70
|
return count;
|
|
@@ -44,6 +44,18 @@ export function cullInstances(data, local, frustum, out) {
|
|
|
44
44
|
out.tints[kept * 3] = tints[i * 3];
|
|
45
45
|
out.tints[kept * 3 + 1] = tints[i * 3 + 1];
|
|
46
46
|
out.tints[kept * 3 + 2] = tints[i * 3 + 2];
|
|
47
|
+
if (out.alphas !== undefined)
|
|
48
|
+
out.alphas[kept] = data.alphas?.[i] ?? 1;
|
|
49
|
+
const regions = data.lightmapRegions;
|
|
50
|
+
if (regions !== undefined && out.lightmapRegions !== undefined) {
|
|
51
|
+
for (let k = 0; k < 4; k++)
|
|
52
|
+
out.lightmapRegions[kept * 4 + k] = regions[i * 4 + k];
|
|
53
|
+
}
|
|
54
|
+
const cells = data.uvRegions;
|
|
55
|
+
if (cells !== undefined && out.uvRegions !== undefined) {
|
|
56
|
+
for (let k = 0; k < 4; k++)
|
|
57
|
+
out.uvRegions[kept * 4 + k] = cells[i * 4 + k];
|
|
58
|
+
}
|
|
47
59
|
kept++;
|
|
48
60
|
}
|
|
49
61
|
out.count = kept;
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* plant. This one carries a full transform, because a vehicle pitches and rolls on its suspension
|
|
11
11
|
* and a yaw cannot say so.
|
|
12
12
|
*
|
|
13
|
-
* **What it gives up** is a matrix per instance where a scatter spends five floats:
|
|
13
|
+
* **What it gives up** is a matrix per instance where a scatter spends five floats: 80 bytes
|
|
14
14
|
* against 44, and no wind. **What would make it wrong** is a caller wanting per-instance
|
|
15
|
-
* anything
|
|
16
|
-
* WebGL2 guarantees are already spent.
|
|
15
|
+
* anything beyond an opacity and a texture cell — a morph weight, say — which wants another
|
|
16
|
+
* attribute, and the sixteen WebGL2 guarantees are already spent. The opacity rides the float the
|
|
17
|
+
* stride was padded with and the cell the matrix's bottom row, which is why neither costs a byte.
|
|
17
18
|
*/
|
|
18
19
|
export interface MeshInstances {
|
|
19
20
|
/**
|
|
@@ -30,6 +31,40 @@ export interface MeshInstances {
|
|
|
30
31
|
* — the alternative is a second batch per colour, which is the thing this exists to avoid.
|
|
31
32
|
*/
|
|
32
33
|
readonly tints: Float32Array;
|
|
34
|
+
/**
|
|
35
|
+
* One float each, the instance's opacity from 0 to 1, which a blended draw
|
|
36
|
+
* (`drawTranslucentInstanced`) multiplies into the batch's own. **Absent, or an instance past its
|
|
37
|
+
* end, is 1** — wholly opaque, which is what every instance was before 4.8.6.
|
|
38
|
+
*
|
|
39
|
+
* What lets one draw carry particles fading at different rates: without it a consumer drew a
|
|
40
|
+
* batch per opacity level, each a draw and a material of its own. It is the instance's twin of
|
|
41
|
+
* the per-vertex alpha lane (`MeshData.channel`), multiplied in where that lane is.
|
|
42
|
+
*/
|
|
43
|
+
readonly alphas?: Float32Array;
|
|
44
|
+
/**
|
|
45
|
+
* Four floats each, the instance's region of its lightmap page: `[scaleU, scaleV, biasU, biasV]`,
|
|
46
|
+
* read by a draw whose material is a `lightmapModel` — instances of one mesh in one material sit
|
|
47
|
+
* in different regions of one page, each applied before the material's own region. **They ride
|
|
48
|
+
* the tint and the opacity**, which a lightmapped instance gives up (see `lightmap.ts`): where
|
|
49
|
+
* these are given, those are not read. **A lightmapped batch needs them**, since a bake is
|
|
50
|
+
* different light at each instance; one drawn without them reads its tints as its regions, and
|
|
51
|
+
* either mismatch is said once on the console.
|
|
52
|
+
*/
|
|
53
|
+
readonly lightmapRegions?: Float32Array;
|
|
54
|
+
/**
|
|
55
|
+
* Four floats each, the instance's cell of its texture: `[scaleU, scaleV, offsetU, offsetV]`,
|
|
56
|
+
* applied to the mesh's coordinates before the material's own scale and offset, in every pass
|
|
57
|
+
* that reads them — so one draw carries particles in different cells of a flipbook, or props
|
|
58
|
+
* wearing different tiles of one atlas. **Absent, or past `count`, is the whole texture**,
|
|
59
|
+
* `[1, 1, 0, 0]`, which is what every instance read before 4.8.7.
|
|
60
|
+
*
|
|
61
|
+
* **It rides the matrix's bottom row**, which an instance's placement — a turn, a scale and a
|
|
62
|
+
* move — leaves at `[0, 0, 0, 1]`, and which every stage drawing an instance rebuilds as that.
|
|
63
|
+
* So a placement must be affine, as every placement of a solid object is; a projective one is
|
|
64
|
+
* drawn as its affine part. A culled batch (`cullInstances`) carries the cells only into a
|
|
65
|
+
* target that has the array.
|
|
66
|
+
*/
|
|
67
|
+
readonly uvRegions?: Float32Array;
|
|
33
68
|
/** How many instances the arrays hold. Fixed at creation; the buffers are sized from it. */
|
|
34
69
|
readonly capacity: number;
|
|
35
70
|
/** How many are live. The rest of the buffer is neither uploaded nor drawn. */
|
|
@@ -44,20 +79,21 @@ export interface MeshInstances {
|
|
|
44
79
|
*/
|
|
45
80
|
export declare function createMeshInstances(capacity: number): MeshInstances;
|
|
46
81
|
/**
|
|
47
|
-
* Floats one instance occupies in the interleaved vertex buffer: sixteen of matrix,
|
|
48
|
-
* tint, and
|
|
82
|
+
* Floats one instance occupies in the interleaved vertex buffer: sixteen of matrix, the bottom row
|
|
83
|
+
* of which carries the texture cell, three of tint, and the opacity.
|
|
49
84
|
*
|
|
50
|
-
* **
|
|
51
|
-
*
|
|
52
|
-
* offsets: the tint sits at byte 64 and a three-float attribute ending at 76 leaves the
|
|
53
|
-
* instance's first column starting there, which is legal but puts every second instance on an
|
|
54
|
-
* offset no driver aligns well.
|
|
85
|
+
* **Twenty so the stride is 80 bytes rather than 76.** A vertex buffer's stride must be a multiple
|
|
86
|
+
* of four on both backends, which 76 already is — the twentieth float was padding for the
|
|
87
|
+
* *attribute* offsets: the tint sits at byte 64 and a three-float attribute ending at 76 leaves the
|
|
88
|
+
* next instance's first column starting there, which is legal but puts every second instance on an
|
|
89
|
+
* offset no driver aligns well. Since 4.8.6 it carries the instance's opacity, read with the tint
|
|
90
|
+
* as one four-float attribute, so the opacity cost no attribute and no byte.
|
|
55
91
|
*/
|
|
56
92
|
export declare const INSTANCE_FLOATS = 20;
|
|
57
93
|
/** Bytes one instance occupies. See `INSTANCE_FLOATS`. */
|
|
58
94
|
export declare const INSTANCE_STRIDE: number;
|
|
59
95
|
/**
|
|
60
|
-
* Interleave `count` instances into `out`, matrix
|
|
96
|
+
* Interleave `count` instances into `out`, matrix, tint and opacity, at `INSTANCE_FLOATS` apart.
|
|
61
97
|
*
|
|
62
98
|
* **Interleaved rather than two buffers** so a batch's upload is one `writeBuffer` and an
|
|
63
99
|
* instance's colour sits beside the matrix that places it. Shared by both backends, because
|
package/dist/render/instances.js
CHANGED
|
@@ -9,25 +9,27 @@ export function createMeshInstances(capacity) {
|
|
|
9
9
|
return {
|
|
10
10
|
models: new Float32Array(capacity * 16),
|
|
11
11
|
tints: new Float32Array(capacity * 3),
|
|
12
|
+
alphas: new Float32Array(capacity).fill(1),
|
|
12
13
|
capacity,
|
|
13
14
|
count: 0,
|
|
14
15
|
};
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
|
-
* Floats one instance occupies in the interleaved vertex buffer: sixteen of matrix,
|
|
18
|
-
* tint, and
|
|
18
|
+
* Floats one instance occupies in the interleaved vertex buffer: sixteen of matrix, the bottom row
|
|
19
|
+
* of which carries the texture cell, three of tint, and the opacity.
|
|
19
20
|
*
|
|
20
|
-
* **
|
|
21
|
-
*
|
|
22
|
-
* offsets: the tint sits at byte 64 and a three-float attribute ending at 76 leaves the
|
|
23
|
-
* instance's first column starting there, which is legal but puts every second instance on an
|
|
24
|
-
* offset no driver aligns well.
|
|
21
|
+
* **Twenty so the stride is 80 bytes rather than 76.** A vertex buffer's stride must be a multiple
|
|
22
|
+
* of four on both backends, which 76 already is — the twentieth float was padding for the
|
|
23
|
+
* *attribute* offsets: the tint sits at byte 64 and a three-float attribute ending at 76 leaves the
|
|
24
|
+
* next instance's first column starting there, which is legal but puts every second instance on an
|
|
25
|
+
* offset no driver aligns well. Since 4.8.6 it carries the instance's opacity, read with the tint
|
|
26
|
+
* as one four-float attribute, so the opacity cost no attribute and no byte.
|
|
25
27
|
*/
|
|
26
28
|
export const INSTANCE_FLOATS = 20;
|
|
27
29
|
/** Bytes one instance occupies. See `INSTANCE_FLOATS`. */
|
|
28
30
|
export const INSTANCE_STRIDE = INSTANCE_FLOATS * 4;
|
|
29
31
|
/**
|
|
30
|
-
* Interleave `count` instances into `out`, matrix
|
|
32
|
+
* Interleave `count` instances into `out`, matrix, tint and opacity, at `INSTANCE_FLOATS` apart.
|
|
31
33
|
*
|
|
32
34
|
* **Interleaved rather than two buffers** so a batch's upload is one `writeBuffer` and an
|
|
33
35
|
* instance's colour sits beside the matrix that places it. Shared by both backends, because
|
|
@@ -37,16 +39,29 @@ export const INSTANCE_STRIDE = INSTANCE_FLOATS * 4;
|
|
|
37
39
|
* Writes nothing beyond `count`, and allocates nothing: `out` is the caller's staging array.
|
|
38
40
|
*/
|
|
39
41
|
export function packInstances(instances, out) {
|
|
40
|
-
const { models, tints } = instances;
|
|
42
|
+
const { models, tints, alphas, lightmapRegions, uvRegions } = instances;
|
|
41
43
|
const count = Math.min(instances.count, instances.capacity);
|
|
42
44
|
for (let i = 0; i < count; i += 1) {
|
|
43
45
|
const at = i * INSTANCE_FLOATS;
|
|
44
46
|
const m = i * 16;
|
|
45
47
|
for (let c = 0; c < 16; c += 1)
|
|
46
48
|
out[at + c] = models[m + c];
|
|
49
|
+
/* The texture cell in the bottom row an affine placement leaves free. See `uvRegions`. */
|
|
50
|
+
const cell = i * 4;
|
|
51
|
+
out[at + 3] = uvRegions?.[cell] ?? 1;
|
|
52
|
+
out[at + 7] = uvRegions?.[cell + 1] ?? 1;
|
|
53
|
+
out[at + 11] = uvRegions?.[cell + 2] ?? 0;
|
|
54
|
+
out[at + 15] = uvRegions?.[cell + 3] ?? 0;
|
|
55
|
+
if (lightmapRegions !== undefined) {
|
|
56
|
+
/* The region in the tint's and the opacity's place. See `lightmapRegions`. */
|
|
57
|
+
for (let c = 0; c < 4; c += 1)
|
|
58
|
+
out[at + 16 + c] = lightmapRegions[i * 4 + c];
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
47
61
|
const t = i * 3;
|
|
48
62
|
out[at + 16] = tints[t];
|
|
49
63
|
out[at + 17] = tints[t + 1];
|
|
50
64
|
out[at + 18] = tints[t + 2];
|
|
65
|
+
out[at + 19] = alphas?.[i] ?? 1;
|
|
51
66
|
}
|
|
52
67
|
}
|