@driftengine/core 4.8.4 → 4.8.6
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 +4332 -4250
- package/dist/build/shaderComments.js +0 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +11 -1
- package/dist/render/ambientHarmonics.d.ts +46 -0
- package/dist/render/ambientHarmonics.js +80 -0
- package/dist/render/areaLights.d.ts +19 -0
- package/dist/render/areaLights.js +20 -0
- package/dist/render/backend/api.d.ts +13 -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 +74 -1
- package/dist/render/backend/webgl2/renderer.js +229 -37
- 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 +80 -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 +58 -7
- package/dist/render/backend/webgpu/renderer.js +329 -78
- 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 +2 -1
- package/dist/render/backend/webgpu/surfaceTexturePass.js +18 -4
- 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 +55 -1
- package/dist/render/clusteredLights.js +182 -8
- package/dist/render/fixtureAtlas.js +1 -1
- package/dist/render/instanceCull.js +7 -0
- package/dist/render/instances.d.ts +32 -10
- package/dist/render/instances.js +17 -8
- package/dist/render/lightBudget.d.ts +26 -1
- package/dist/render/lightBudget.js +26 -3
- package/dist/render/lightmap.d.ts +102 -0
- package/dist/render/lightmap.js +118 -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 +11 -0
- package/dist/render/particlePool.js +2 -0
- package/dist/render/particleSort.d.ts +34 -0
- package/dist/render/particleSort.js +96 -0
- package/dist/render/sceneTarget.d.ts +5 -0
- package/dist/render/sceneTarget.js +8 -1
- package/dist/render/shaders/bloom.d.ts +8 -4
- package/dist/render/shaders/bloom.js +20 -3
- package/dist/render/shaders/flat/areaLight.d.ts +10 -0
- package/dist/render/shaders/flat/areaLight.js +96 -0
- package/dist/render/shaders/flat/index.js +30 -4
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +107 -105
- package/dist/render/shaders/flat/models.d.ts +16 -4
- package/dist/render/shaders/flat/models.js +64 -4
- 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 +74 -37
- 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/flat.wgsl.d.ts +1631 -2044
- package/dist/render/shaders/generated/flat.wgsl.js +1669 -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/particle.d.ts +17 -1
- package/dist/render/shaders/particle.js +133 -1
- package/dist/render/shaders/skinBlur.js +25 -3
- package/dist/render/skinBlur.d.ts +8 -5
- package/dist/render/skinBlur.js +8 -5
- package/dist/render/surfaceModel.d.ts +21 -1
- package/dist/render/surfaceModel.js +22 -0
- package/dist/render/surfaceTexture.d.ts +14 -1
- package/dist/render/surfaceTexture.js +15 -2
- package/package.json +6 -6
- package/src/build/shaderComments.ts +0 -0
- package/src/index.ts +20 -1
- package/src/render/ambientHarmonics.ts +92 -0
- package/src/render/areaLights.ts +39 -0
- package/src/render/backend/api.ts +14 -1
- package/src/render/backend/webgl2/instanced.ts +12 -2
- package/src/render/backend/webgl2/renderer.ts +292 -35
- 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 +118 -12
- package/src/render/backend/webgpu/postPass.ts +7 -2
- package/src/render/backend/webgpu/renderer.ts +396 -95
- 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 +26 -5
- package/src/render/bloomChain.ts +69 -0
- package/src/render/bloomPass.ts +26 -2
- package/src/render/clusteredLights.ts +221 -7
- package/src/render/fixtureAtlas.ts +1 -1
- package/src/render/instanceCull.ts +5 -0
- package/src/render/instances.ts +40 -11
- package/src/render/lightBudget.ts +48 -2
- package/src/render/lightmap.ts +202 -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 +13 -0
- package/src/render/particleSort.ts +133 -0
- package/src/render/sceneTarget.ts +18 -1
- package/src/render/shaders/bloom.ts +20 -3
- package/src/render/shaders/flat/areaLight.ts +96 -0
- package/src/render/shaders/flat/index.ts +30 -4
- package/src/render/shaders/flat/main.ts +107 -105
- package/src/render/shaders/flat/models.ts +73 -8
- package/src/render/shaders/flat/normalMap.ts +25 -0
- package/src/render/shaders/flat/preamble.ts +74 -37
- package/src/render/shaders/flat/skin.ts +6 -5
- package/src/render/shaders/generated/bloom.wgsl.ts +13 -3
- package/src/render/shaders/generated/flat.wgsl.ts +1669 -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/particle.ts +134 -1
- package/src/render/shaders/skinBlur.ts +25 -3
- package/src/render/skinBlur.ts +8 -5
- package/src/render/surfaceModel.ts +36 -1
- package/src/render/surfaceTexture.ts +41 -3
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AreaLightBuffer } from './areaLights.ts';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The froxel table: what it looks like in memory, and the CPU binner that fills it.
|
|
3
5
|
*
|
|
@@ -146,8 +148,29 @@ export const LIGHT_RECORD = {
|
|
|
146
148
|
iesAxisX: 16,
|
|
147
149
|
iesAxisY: 17,
|
|
148
150
|
iesAxisZ: 18,
|
|
151
|
+
/**
|
|
152
|
+
* The light's lighting channels, a mask (`PointLightSet.lightChannels`), in the slot that was
|
|
153
|
+
* spare. A light naming channels other than the default is read as carrying a fixture, so its
|
|
154
|
+
* last texels are fetched; a plain light's are not, and the shader takes the default for it.
|
|
155
|
+
*/
|
|
156
|
+
channels: 19,
|
|
149
157
|
} as const;
|
|
150
158
|
|
|
159
|
+
/** The lighting channel every light and every surface is on unless it says otherwise. */
|
|
160
|
+
export const DEFAULT_LIGHT_CHANNELS = 1;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A light's channels as the shader compares them: a whole mask from 1 to 255, or the default where
|
|
164
|
+
* none was named or what was named is not one. The froxel record and the uniform path both write
|
|
165
|
+
* this, so a light named badly is on channel 1 in both rather than on two different guesses.
|
|
166
|
+
*/
|
|
167
|
+
export function lightChannelsOf(value: number | undefined): number {
|
|
168
|
+
if (value === undefined || !Number.isInteger(value) || value < 1 || value > 255) {
|
|
169
|
+
return DEFAULT_LIGHT_CHANNELS;
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
|
|
151
174
|
/** Texels the lit pass reads for a light whose record carries no fixture. */
|
|
152
175
|
export const PLAIN_LIGHT_TEXELS = 2;
|
|
153
176
|
|
|
@@ -401,6 +424,16 @@ export interface ClusterLightSet {
|
|
|
401
424
|
readonly cookies?: Float32Array;
|
|
402
425
|
/** One per light: an index into the IES atlas, or −1. */
|
|
403
426
|
readonly iesProfiles?: Float32Array;
|
|
427
|
+
/** One per light: its lighting channels. See `PointLightSet.lightChannels`. */
|
|
428
|
+
readonly channels?: Float32Array;
|
|
429
|
+
/**
|
|
430
|
+
* The rectangles shaded through the table, after the `count` point lights: those of `areas` from
|
|
431
|
+
* `areaFrom` on, `areaCount` of them. The ones before `areaFrom` are the fixed arm's, which is
|
|
432
|
+
* where a rectangle's shadow lives. Absent, there are none.
|
|
433
|
+
*/
|
|
434
|
+
readonly areas?: AreaLightBuffer | null;
|
|
435
|
+
readonly areaFrom?: number;
|
|
436
|
+
readonly areaCount?: number;
|
|
404
437
|
}
|
|
405
438
|
|
|
406
439
|
/** Where `fillClusterLightSet` reads a frame's lights from: the arrays `Environment` carries. */
|
|
@@ -416,6 +449,8 @@ export interface ClusterLightArrays {
|
|
|
416
449
|
readonly lightIesProfiles?: Float32Array;
|
|
417
450
|
readonly lightIesAxes?: Float32Array;
|
|
418
451
|
readonly lightCookies?: Float32Array;
|
|
452
|
+
readonly lightChannels?: Float32Array;
|
|
453
|
+
readonly areaLights?: AreaLightBuffer | null;
|
|
419
454
|
}
|
|
420
455
|
|
|
421
456
|
/** A `ClusterLightSet` a renderer refills every frame. */
|
|
@@ -453,8 +488,17 @@ export function createClusterLightSet(): ClusterLightScratch {
|
|
|
453
488
|
export function fillClusterLightSet(
|
|
454
489
|
env: ClusterLightArrays,
|
|
455
490
|
out: ClusterLightScratch,
|
|
491
|
+
/** How many rectangles the fixed arm shades; the rest of the buffer is clustered. */
|
|
492
|
+
areaFrom = 0,
|
|
456
493
|
): ClusterLightSet {
|
|
457
494
|
out.count = Math.min(env.lightCount ?? 0, MAX_CLUSTERED_LIGHTS);
|
|
495
|
+
const areas = env.areaLights ?? null;
|
|
496
|
+
out.areas = areas;
|
|
497
|
+
out.areaFrom = areaFrom;
|
|
498
|
+
out.areaCount = Math.max(
|
|
499
|
+
0,
|
|
500
|
+
Math.min((areas?.count ?? 0) - areaFrom, MAX_CLUSTERED_LIGHTS - out.count),
|
|
501
|
+
);
|
|
458
502
|
out.positions = env.lightPositions;
|
|
459
503
|
out.colors = env.lightColors;
|
|
460
504
|
out.radii = env.lightRadii;
|
|
@@ -465,6 +509,7 @@ export function fillClusterLightSet(
|
|
|
465
509
|
out.iesProfiles = env.lightIesProfiles;
|
|
466
510
|
out.iesAxes = env.lightIesAxes;
|
|
467
511
|
out.cookies = env.lightCookies;
|
|
512
|
+
out.channels = env.lightChannels;
|
|
468
513
|
return out;
|
|
469
514
|
}
|
|
470
515
|
|
|
@@ -526,13 +571,16 @@ export function lightHasFixture(
|
|
|
526
571
|
light: number,
|
|
527
572
|
shadowSlots: number,
|
|
528
573
|
): boolean {
|
|
574
|
+
/* A rectangle's record always carries its axes and extents past the second texel. */
|
|
575
|
+
if (light >= lights.count) return true;
|
|
529
576
|
const cone = lights.coneCos;
|
|
530
577
|
return (
|
|
531
578
|
light < shadowSlots ||
|
|
532
579
|
(cone?.[light * 2] ?? POINT_LIGHT_COS_INNER) !== POINT_LIGHT_COS_INNER ||
|
|
533
580
|
(cone?.[light * 2 + 1] ?? POINT_LIGHT_COS_OUTER) !== POINT_LIGHT_COS_OUTER ||
|
|
534
581
|
(lights.iesProfiles?.[light] ?? NO_IES_PROFILE) >= 0 ||
|
|
535
|
-
(lights.cookies?.[light] ?? NO_IES_PROFILE) >= 0
|
|
582
|
+
(lights.cookies?.[light] ?? NO_IES_PROFILE) >= 0 ||
|
|
583
|
+
lightChannelsOf(lights.channels?.[light]) !== DEFAULT_LIGHT_CHANNELS
|
|
536
584
|
);
|
|
537
585
|
}
|
|
538
586
|
|
|
@@ -576,6 +624,10 @@ export function writeLightRecord(
|
|
|
576
624
|
out: Uint32Array,
|
|
577
625
|
at: number,
|
|
578
626
|
): void {
|
|
627
|
+
if (light >= lights.count) {
|
|
628
|
+
writeAreaRecord(lights, light, out, at);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
579
631
|
const directions = lights.directions;
|
|
580
632
|
const cone = lights.coneCos;
|
|
581
633
|
const axes = lights.iesAxes;
|
|
@@ -612,7 +664,72 @@ export function writeLightRecord(
|
|
|
612
664
|
out[at + LIGHT_RECORD.iesAxisX] = floatBits(axes?.[light * 3] ?? 0);
|
|
613
665
|
out[at + LIGHT_RECORD.iesAxisY] = floatBits(axes?.[light * 3 + 1] ?? 0);
|
|
614
666
|
out[at + LIGHT_RECORD.iesAxisZ] = floatBits(axes?.[light * 3 + 2] ?? 0);
|
|
615
|
-
out[at +
|
|
667
|
+
out[at + LIGHT_RECORD.channels] = floatBits(lightChannelsOf(lights.channels?.[light]));
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Which rectangle of the area buffer clustered light `light` is, for a light past the point
|
|
672
|
+
* lights. See `ClusterLightSet.areas`.
|
|
673
|
+
*/
|
|
674
|
+
function areaSlot(lights: ClusterLightSet, light: number): number {
|
|
675
|
+
return (lights.areaFrom ?? 0) + light - lights.count;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* A rectangle's record, which the shader tells from a lamp's by its **negative radius** — its
|
|
680
|
+
* reach, negated — and reads to its fifth texel: sidedness where a lamp keeps its shadow slot,
|
|
681
|
+
* the right axis where a spot keeps its direction, the half extents where its cone is, the up
|
|
682
|
+
* axis where the azimuth is, and the channels last. Never shadowed: a rectangle's occlusion is
|
|
683
|
+
* the fixed arm's, which is why the first `maxAreaLights` stay there.
|
|
684
|
+
*/
|
|
685
|
+
function writeAreaRecord(
|
|
686
|
+
lights: ClusterLightSet,
|
|
687
|
+
light: number,
|
|
688
|
+
out: Uint32Array,
|
|
689
|
+
at: number,
|
|
690
|
+
): void {
|
|
691
|
+
const areas = lights.areas;
|
|
692
|
+
const a = areaSlot(lights, light);
|
|
693
|
+
const pos = areas?.positions;
|
|
694
|
+
const color = areas?.colors;
|
|
695
|
+
const right = areas?.right;
|
|
696
|
+
const up = areas?.up;
|
|
697
|
+
const sizes = areas?.sizes;
|
|
698
|
+
out[at + LIGHT_RECORD.positionX] = floatBits(pos?.[a * 3] ?? 0);
|
|
699
|
+
out[at + LIGHT_RECORD.positionY] = floatBits(pos?.[a * 3 + 1] ?? 0);
|
|
700
|
+
out[at + LIGHT_RECORD.positionZ] = floatBits(pos?.[a * 3 + 2] ?? 0);
|
|
701
|
+
out[at + LIGHT_RECORD.radius] = floatBits(-areaReach(lights, light));
|
|
702
|
+
out[at + LIGHT_RECORD.colorR] = floatBits(color?.[a * 3] ?? 0);
|
|
703
|
+
out[at + LIGHT_RECORD.colorG] = floatBits(color?.[a * 3 + 1] ?? 0);
|
|
704
|
+
out[at + LIGHT_RECORD.colorB] = floatBits(color?.[a * 3 + 2] ?? 0);
|
|
705
|
+
out[at + LIGHT_RECORD.sizeAndWeight] = packSizeAndWeight(0, 1, true);
|
|
706
|
+
out[at + LIGHT_RECORD.shadowSlot] = floatBits(areas?.twoSided[a] ?? 0);
|
|
707
|
+
out[at + LIGHT_RECORD.directionX] = floatBits(right?.[a * 3] ?? 1);
|
|
708
|
+
out[at + LIGHT_RECORD.directionY] = floatBits(right?.[a * 3 + 1] ?? 0);
|
|
709
|
+
out[at + LIGHT_RECORD.directionZ] = floatBits(right?.[a * 3 + 2] ?? 0);
|
|
710
|
+
out[at + LIGHT_RECORD.cosInner] = floatBits(sizes?.[a * 2] ?? 0);
|
|
711
|
+
out[at + LIGHT_RECORD.cosOuter] = floatBits(sizes?.[a * 2 + 1] ?? 0);
|
|
712
|
+
out[at + LIGHT_RECORD.iesProfile] = floatBits(NO_IES_PROFILE);
|
|
713
|
+
out[at + LIGHT_RECORD.cookie] = floatBits(NO_IES_PROFILE);
|
|
714
|
+
out[at + LIGHT_RECORD.iesAxisX] = floatBits(up?.[a * 3] ?? 0);
|
|
715
|
+
out[at + LIGHT_RECORD.iesAxisY] = floatBits(up?.[a * 3 + 1] ?? 1);
|
|
716
|
+
out[at + LIGHT_RECORD.iesAxisZ] = floatBits(up?.[a * 3 + 2] ?? 0);
|
|
717
|
+
out[at + LIGHT_RECORD.channels] = floatBits(DEFAULT_LIGHT_CHANNELS);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** A clustered rectangle's reach, which its record carries negated. See `AreaLightSource.range`. */
|
|
721
|
+
function areaReach(lights: ClusterLightSet, light: number): number {
|
|
722
|
+
const areas = lights.areas;
|
|
723
|
+
const a = areaSlot(lights, light);
|
|
724
|
+
const named = areas?.ranges?.[a] ?? 0;
|
|
725
|
+
if (named > 0) return named;
|
|
726
|
+
const sizes = areas?.sizes;
|
|
727
|
+
return Math.hypot(sizes?.[a * 2] ?? 0, sizes?.[a * 2 + 1] ?? 0);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** How many lights the table carries: the point lights and the clustered rectangles after them. */
|
|
731
|
+
export function clusteredLightTotal(lights: ClusterLightSet): number {
|
|
732
|
+
return Math.min(lights.count + (lights.areaCount ?? 0), MAX_CLUSTERED_LIGHTS);
|
|
616
733
|
}
|
|
617
734
|
|
|
618
735
|
/**
|
|
@@ -670,7 +787,7 @@ export function buildLightClusters(
|
|
|
670
787
|
table: Uint32Array,
|
|
671
788
|
shadowSlots: number,
|
|
672
789
|
): number {
|
|
673
|
-
const count =
|
|
790
|
+
const count = clusteredLightTotal(lights);
|
|
674
791
|
|
|
675
792
|
/*
|
|
676
793
|
* The whole cluster region, not just the counts.
|
|
@@ -686,10 +803,14 @@ export function buildLightClusters(
|
|
|
686
803
|
for (let k = 0; k <= CLUSTER_Z; k++) sliceDepths[k] = sliceNearDepth(k, near, far);
|
|
687
804
|
|
|
688
805
|
for (let light = 0; light < count; light++) {
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
const
|
|
692
|
-
const
|
|
806
|
+
/* A rectangle past the lamps is placed by its centre and binned by its reach. */
|
|
807
|
+
const area = light >= lights.count;
|
|
808
|
+
const from = area ? (lights.areas?.positions ?? lights.positions) : lights.positions;
|
|
809
|
+
const slot = area ? areaSlot(lights, light) : light;
|
|
810
|
+
const x = from[slot * 3] ?? 0;
|
|
811
|
+
const y = from[slot * 3 + 1] ?? 0;
|
|
812
|
+
const z = from[slot * 3 + 2] ?? 0;
|
|
813
|
+
const radius = area ? areaReach(lights, light) : (lights.radii[light] ?? 0);
|
|
693
814
|
|
|
694
815
|
/* The record, as the shader reads it. World space; see the note above. */
|
|
695
816
|
writeLightRecord(lights, light, shadowSlots, table, lightBase(light));
|
|
@@ -706,6 +827,40 @@ export function buildLightClusters(
|
|
|
706
827
|
/* Entirely behind the camera, so no cluster can reach it. */
|
|
707
828
|
if (depth + radius <= 0) continue;
|
|
708
829
|
|
|
830
|
+
/*
|
|
831
|
+
* **A spot is binned by its cone as well as its sphere.** A 10° spot reaching thirty metres
|
|
832
|
+
* touches every froxel its sphere does and lights a sliver of them, and a stage rigged with
|
|
833
|
+
* hundreds of such fixtures put every one of them in nearly every froxel near the floor: 313
|
|
834
|
+
* lights cost 11.7 ms of a 1600x900 frame. The cone is tested against each froxel's bounding
|
|
835
|
+
* sphere in view space, which is conservative, by the construction `coneBoxGap` describes.
|
|
836
|
+
*/
|
|
837
|
+
const cosOuter = lights.coneCos?.[light * 2 + 1] ?? POINT_LIGHT_COS_OUTER;
|
|
838
|
+
const directions = lights.directions;
|
|
839
|
+
let coneCos = POINT_LIGHT_COS_OUTER;
|
|
840
|
+
let coneSin = 0;
|
|
841
|
+
let axisX = 0;
|
|
842
|
+
let axisY = 0;
|
|
843
|
+
let axisZ = 0;
|
|
844
|
+
if (!area && directions !== undefined && cosOuter > -1) {
|
|
845
|
+
const dx = directions[light * 3] ?? 0;
|
|
846
|
+
const dy = directions[light * 3 + 1] ?? 0;
|
|
847
|
+
const dz = directions[light * 3 + 2] ?? 0;
|
|
848
|
+
const ax = (view[0] ?? 0) * dx + (view[4] ?? 0) * dy + (view[8] ?? 0) * dz;
|
|
849
|
+
const ay = (view[1] ?? 0) * dx + (view[5] ?? 0) * dy + (view[9] ?? 0) * dz;
|
|
850
|
+
const az = -((view[2] ?? 0) * dx + (view[6] ?? 0) * dy + (view[10] ?? 0) * dz);
|
|
851
|
+
const length = Math.sqrt(ax * ax + ay * ay + az * az);
|
|
852
|
+
/* The shader measures the cone against the direction as given, so a long one is a wider
|
|
853
|
+
cone: `dot(L, d) >= c` is `dot(L, d / |d|) >= c / |d|`. */
|
|
854
|
+
if (length > 1e-6 && cosOuter / length > -1) {
|
|
855
|
+
coneCos = cosOuter / length;
|
|
856
|
+
coneSin = Math.sqrt(Math.max(0, 1 - coneCos * coneCos));
|
|
857
|
+
axisX = ax / length;
|
|
858
|
+
axisY = ay / length;
|
|
859
|
+
axisZ = az / length;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
const spot = coneCos > -1;
|
|
863
|
+
|
|
709
864
|
/*
|
|
710
865
|
* The slices the sphere could possibly touch. The tiles are derived per slice, below.
|
|
711
866
|
*
|
|
@@ -765,6 +920,12 @@ export function buildLightClusters(
|
|
|
765
920
|
for (let i = iLo; i <= iHi; i++) {
|
|
766
921
|
sliceBounds(i, j, zNear, zFar, halfWNear, halfWFar, halfHNear, halfHFar, bounds);
|
|
767
922
|
if (distanceSqToBounds(vx, vy, depth, bounds) > radiusSq) continue;
|
|
923
|
+
if (
|
|
924
|
+
spot &&
|
|
925
|
+
coneBoxGap(vx, vy, depth, axisX, axisY, axisZ, coneCos, coneSin, bounds) > 0
|
|
926
|
+
) {
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
768
929
|
insert(table, i + j * CLUSTER_X + k * CLUSTER_X * CLUSTER_Y, light, bounds);
|
|
769
930
|
}
|
|
770
931
|
}
|
|
@@ -773,6 +934,59 @@ export function buildLightClusters(
|
|
|
773
934
|
return count;
|
|
774
935
|
}
|
|
775
936
|
|
|
937
|
+
/**
|
|
938
|
+
* How far a froxel lies outside a spot's cone, in metres, or a negative number where the cone
|
|
939
|
+
* reaches it: positive is a froxel the light cannot shade. Everything in view space with depth
|
|
940
|
+
* positive, as the binner holds it; `axis` is unit length and `cosA`, `sinA` are the outer cone's.
|
|
941
|
+
*
|
|
942
|
+
* **The froxel's bounding sphere against the infinite cone**, after Wronski's cone test for
|
|
943
|
+
* clustered spots. With `V` from the apex to the sphere's centre at an angle `θ` from the axis and
|
|
944
|
+
* `A` the cone's half-angle, the centre's distance from the cone is `|V| sin(θ − A)` — which is
|
|
945
|
+
* `cosA |V⊥| − (V·axis) sinA` — while `θ − A` is under a right angle, and `|V|` past it, where the
|
|
946
|
+
* nearest point of the cone is its apex. Inside the cone it is negative. So the gap is exact for
|
|
947
|
+
* the sphere, and the sphere holds the froxel: the test can only keep a light, never drop one the
|
|
948
|
+
* shader would draw. The apex case is what drops a froxel behind a spot whose sphere reaches it,
|
|
949
|
+
* which the formula alone underestimates.
|
|
950
|
+
*
|
|
951
|
+
* **What it gives up** is tightness at the near end of the frustum, where a froxel is a long thin
|
|
952
|
+
* box whose sphere is much larger than it: a spot grazing such a froxel is kept. **What would make
|
|
953
|
+
* it wrong** is a profile showing those near froxels hold the cost; testing the box's corners is
|
|
954
|
+
* the tight version, at eight dot products a pair.
|
|
955
|
+
*
|
|
956
|
+
* Exported so the conformance page can tell a disagreement at the cone's edge from a real one; the
|
|
957
|
+
* GPU binner writes the same arithmetic in WGSL.
|
|
958
|
+
*/
|
|
959
|
+
export function coneBoxGap(
|
|
960
|
+
px: number,
|
|
961
|
+
py: number,
|
|
962
|
+
pz: number,
|
|
963
|
+
axisX: number,
|
|
964
|
+
axisY: number,
|
|
965
|
+
axisZ: number,
|
|
966
|
+
cosA: number,
|
|
967
|
+
sinA: number,
|
|
968
|
+
box: Float32Array,
|
|
969
|
+
): number {
|
|
970
|
+
const b0 = box[0] ?? 0;
|
|
971
|
+
const b1 = box[1] ?? 0;
|
|
972
|
+
const b2 = box[2] ?? 0;
|
|
973
|
+
const b3 = box[3] ?? 0;
|
|
974
|
+
const b4 = box[4] ?? 0;
|
|
975
|
+
const b5 = box[5] ?? 0;
|
|
976
|
+
const ex = (b3 - b0) * 0.5;
|
|
977
|
+
const ey = (b4 - b1) * 0.5;
|
|
978
|
+
const ez = (b5 - b2) * 0.5;
|
|
979
|
+
const vx = (b0 + b3) * 0.5 - px;
|
|
980
|
+
const vy = (b1 + b4) * 0.5 - py;
|
|
981
|
+
const vz = (b2 + b5) * 0.5 - pz;
|
|
982
|
+
const along = vx * axisX + vy * axisY + vz * axisZ;
|
|
983
|
+
const lengthSq = vx * vx + vy * vy + vz * vz;
|
|
984
|
+
const across = Math.sqrt(Math.max(lengthSq - along * along, 0));
|
|
985
|
+
const reach = Math.sqrt(lengthSq);
|
|
986
|
+
const distance = along < -sinA * reach ? reach : cosA * across - along * sinA;
|
|
987
|
+
return distance - Math.sqrt(ex * ex + ey * ey + ez * ez);
|
|
988
|
+
}
|
|
989
|
+
|
|
776
990
|
/** An NDC coordinate to a tile index, clamped. */
|
|
777
991
|
function tileOf(ndc: number, tiles: number): number {
|
|
778
992
|
const tile = Math.floor((ndc + 1) * 0.5 * tiles);
|
|
@@ -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.
|
|
@@ -71,6 +71,11 @@ export function cullInstances(
|
|
|
71
71
|
out.tints[kept * 3] = tints[i * 3] as number;
|
|
72
72
|
out.tints[kept * 3 + 1] = tints[i * 3 + 1] as number;
|
|
73
73
|
out.tints[kept * 3 + 2] = tints[i * 3 + 2] as number;
|
|
74
|
+
if (out.alphas !== undefined) out.alphas[kept] = data.alphas?.[i] ?? 1;
|
|
75
|
+
const regions = data.lightmapRegions;
|
|
76
|
+
if (regions !== undefined && out.lightmapRegions !== undefined) {
|
|
77
|
+
for (let k = 0; k < 4; k++) out.lightmapRegions[kept * 4 + k] = regions[i * 4 + k] as number;
|
|
78
|
+
}
|
|
74
79
|
kept++;
|
|
75
80
|
}
|
|
76
81
|
out.count = kept;
|
package/src/render/instances.ts
CHANGED
|
@@ -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 — a morph weight, a texture cell — which wants another attribute,
|
|
16
|
+
* and the sixteen WebGL2 guarantees are already spent. The opacity rides the float the stride was
|
|
17
|
+
* padded with, which is why it costs nothing.
|
|
17
18
|
*/
|
|
18
19
|
export interface MeshInstances {
|
|
19
20
|
/**
|
|
@@ -30,6 +31,26 @@ 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;
|
|
33
54
|
/** How many instances the arrays hold. Fixed at creation; the buffers are sized from it. */
|
|
34
55
|
readonly capacity: number;
|
|
35
56
|
/** How many are live. The rest of the buffer is neither uploaded nor drawn. */
|
|
@@ -47,6 +68,7 @@ export function createMeshInstances(capacity: number): MeshInstances {
|
|
|
47
68
|
return {
|
|
48
69
|
models: new Float32Array(capacity * 16),
|
|
49
70
|
tints: new Float32Array(capacity * 3),
|
|
71
|
+
alphas: new Float32Array(capacity).fill(1),
|
|
50
72
|
capacity,
|
|
51
73
|
count: 0,
|
|
52
74
|
};
|
|
@@ -54,13 +76,14 @@ export function createMeshInstances(capacity: number): MeshInstances {
|
|
|
54
76
|
|
|
55
77
|
/**
|
|
56
78
|
* Floats one instance occupies in the interleaved vertex buffer: sixteen of matrix, three of
|
|
57
|
-
* tint, and
|
|
79
|
+
* tint, and the opacity.
|
|
58
80
|
*
|
|
59
|
-
* **
|
|
60
|
-
*
|
|
61
|
-
* offsets: the tint sits at byte 64 and a three-float attribute ending at 76 leaves the
|
|
62
|
-
* instance's first column starting there, which is legal but puts every second instance on an
|
|
63
|
-
* offset no driver aligns well.
|
|
81
|
+
* **Twenty so the stride is 80 bytes rather than 76.** A vertex buffer's stride must be a multiple
|
|
82
|
+
* of four on both backends, which 76 already is — the twentieth float was padding for the
|
|
83
|
+
* *attribute* offsets: the tint sits at byte 64 and a three-float attribute ending at 76 leaves the
|
|
84
|
+
* next instance's first column starting there, which is legal but puts every second instance on an
|
|
85
|
+
* offset no driver aligns well. Since 4.8.6 it carries the instance's opacity, read with the tint
|
|
86
|
+
* as one four-float attribute, so the opacity cost no attribute and no byte.
|
|
64
87
|
*/
|
|
65
88
|
export const INSTANCE_FLOATS = 20;
|
|
66
89
|
|
|
@@ -68,7 +91,7 @@ export const INSTANCE_FLOATS = 20;
|
|
|
68
91
|
export const INSTANCE_STRIDE = INSTANCE_FLOATS * 4;
|
|
69
92
|
|
|
70
93
|
/**
|
|
71
|
-
* Interleave `count` instances into `out`, matrix
|
|
94
|
+
* Interleave `count` instances into `out`, matrix, tint and opacity, at `INSTANCE_FLOATS` apart.
|
|
72
95
|
*
|
|
73
96
|
* **Interleaved rather than two buffers** so a batch's upload is one `writeBuffer` and an
|
|
74
97
|
* instance's colour sits beside the matrix that places it. Shared by both backends, because
|
|
@@ -78,16 +101,22 @@ export const INSTANCE_STRIDE = INSTANCE_FLOATS * 4;
|
|
|
78
101
|
* Writes nothing beyond `count`, and allocates nothing: `out` is the caller's staging array.
|
|
79
102
|
*/
|
|
80
103
|
export function packInstances(instances: MeshInstances, out: Float32Array): void {
|
|
81
|
-
const { models, tints } = instances;
|
|
104
|
+
const { models, tints, alphas, lightmapRegions } = instances;
|
|
82
105
|
const count = Math.min(instances.count, instances.capacity);
|
|
83
106
|
for (let i = 0; i < count; i += 1) {
|
|
84
107
|
const at = i * INSTANCE_FLOATS;
|
|
85
108
|
const m = i * 16;
|
|
86
109
|
for (let c = 0; c < 16; c += 1) out[at + c] = models[m + c] as number;
|
|
110
|
+
if (lightmapRegions !== undefined) {
|
|
111
|
+
/* The region in the tint's and the opacity's place. See `lightmapRegions`. */
|
|
112
|
+
for (let c = 0; c < 4; c += 1) out[at + 16 + c] = lightmapRegions[i * 4 + c] as number;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
87
115
|
const t = i * 3;
|
|
88
116
|
out[at + 16] = tints[t] as number;
|
|
89
117
|
out[at + 17] = tints[t + 1] as number;
|
|
90
118
|
out[at + 18] = tints[t + 2] as number;
|
|
119
|
+
out[at + 19] = alphas?.[i] ?? 1;
|
|
91
120
|
}
|
|
92
121
|
}
|
|
93
122
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
POINT_LIGHT_COS_OUTER,
|
|
7
7
|
lampSourceRadius,
|
|
8
8
|
lampWeight,
|
|
9
|
+
lightChannelsOf,
|
|
9
10
|
} from './clusteredLights.ts';
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -256,12 +257,22 @@ export const CLOTH_REST_TEXTURE_UNIT = CLOTH_PARTICLES_TEXTURE_UNIT + 1;
|
|
|
256
257
|
export const MODEL_MAP_TEXTURE_UNIT = CLOTH_REST_TEXTURE_UNIT + 1;
|
|
257
258
|
|
|
258
259
|
/**
|
|
259
|
-
* The
|
|
260
|
+
* The units skin's screen-space blur reads on WebGL2 — its target and the frame's depth — past
|
|
260
261
|
* every unit a lit program binds, so spreading skin in the middle of a frame unbinds nothing the
|
|
261
262
|
* draws after it sample. WebGL2 guarantees thirty-two.
|
|
262
263
|
*/
|
|
263
264
|
export const SKIN_BLUR_TEXTURE_UNIT = MODEL_MAP_TEXTURE_UNIT + 1;
|
|
264
265
|
export const SKIN_BLUR_DEPTH_UNIT = SKIN_BLUR_TEXTURE_UNIT + 1;
|
|
266
|
+
/** And the skin's colour, which the blur's second axis multiplies the spread light by. */
|
|
267
|
+
export const SKIN_BLUR_ALBEDO_UNIT = SKIN_BLUR_DEPTH_UNIT + 1;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* A sprite particle's image and the frame's depth copy its soft edge reads, on WebGL2: past every
|
|
271
|
+
* unit a lit program binds, for the skin blur's reason — a particle is drawn in the middle of a
|
|
272
|
+
* frame, and the draws after it must find their textures where they left them.
|
|
273
|
+
*/
|
|
274
|
+
export const PARTICLE_SPRITE_UNIT = SKIN_BLUR_ALBEDO_UNIT + 1;
|
|
275
|
+
export const PARTICLE_DEPTH_UNIT = PARTICLE_SPRITE_UNIT + 1;
|
|
265
276
|
|
|
266
277
|
/**
|
|
267
278
|
* A cookie's tile, in texels a side.
|
|
@@ -380,6 +391,16 @@ export interface PointLightSet {
|
|
|
380
391
|
* drawn for one spot works in another with a different angle.
|
|
381
392
|
*/
|
|
382
393
|
readonly lightCookies?: Float32Array;
|
|
394
|
+
/**
|
|
395
|
+
* One per light: the lighting channels it lights, as a bitmask in a float — 1, 2, 3 … 255.
|
|
396
|
+
* **Absent, or a short array, is channel 1 for every light**, which is every surface's default
|
|
397
|
+
* too, so a scene that never names a channel lights everything with everything as it did.
|
|
398
|
+
*
|
|
399
|
+
* A light shades a surface only where its mask and the surface's `SurfaceMaterial.lightChannels`
|
|
400
|
+
* share a bit. What it is for: a rig of lights that belongs to some draws and not others — a
|
|
401
|
+
* character's own key and rim lights, which the floor around the character must not take.
|
|
402
|
+
*/
|
|
403
|
+
readonly lightChannels?: Float32Array;
|
|
383
404
|
}
|
|
384
405
|
|
|
385
406
|
/** Weights are optional per consumer; a short array means every light is fully present. */
|
|
@@ -485,7 +506,8 @@ export function bindPointLights(
|
|
|
485
506
|
gl.uniform3fv(uniforms['uLightDir[0]'] ?? null, r.directions);
|
|
486
507
|
gl.uniform2fv(uniforms['uLightCone[0]'] ?? null, r.coneCos);
|
|
487
508
|
gl.uniform1fv(uniforms['uLightIesProfile[0]'] ?? null, r.iesProfiles);
|
|
488
|
-
|
|
509
|
+
/* The azimuth and, in w, the channels. See `axesAndChannels`. */
|
|
510
|
+
gl.uniform4fv(uniforms['uLightIesAxis[0]'] ?? null, r.axesAndChannels);
|
|
489
511
|
gl.uniform1fv(uniforms['uLightCookie[0]'] ?? null, r.cookies);
|
|
490
512
|
}
|
|
491
513
|
|
|
@@ -561,6 +583,12 @@ export interface ResolvedPointLights {
|
|
|
561
583
|
iesProfiles: Float32Array;
|
|
562
584
|
iesAxes: Float32Array;
|
|
563
585
|
cookies: Float32Array;
|
|
586
|
+
/**
|
|
587
|
+
* Owned, four a light: the photometric azimuth in xyz and the light's channels in w, which is how
|
|
588
|
+
* `uLightIesAxis` carries them — the channels in a lane the axis left, rather than in a uniform
|
|
589
|
+
* array of their own, which would cost a row a light of a budget that has none to give.
|
|
590
|
+
*/
|
|
591
|
+
readonly axesAndChannels: Float32Array;
|
|
564
592
|
/**
|
|
565
593
|
* Owned, `MAX_POINT_LIGHTS` long: the emitter sizes and weights as the froxel record carries
|
|
566
594
|
* them, at half precision. `sourceRadii` and `weights` point here once a light set resolves, so
|
|
@@ -585,6 +613,7 @@ const glLights: ResolvedPointLights = {
|
|
|
585
613
|
iesProfiles: NO_PROFILES,
|
|
586
614
|
iesAxes: NO_IES_AXES,
|
|
587
615
|
cookies: NO_COOKIES,
|
|
616
|
+
axesAndChannels: new Float32Array(MAX_POINT_LIGHTS * 4),
|
|
588
617
|
heldSourceRadii: new Float32Array(MAX_POINT_LIGHTS),
|
|
589
618
|
heldWeights: new Float32Array(MAX_POINT_LIGHTS),
|
|
590
619
|
};
|
|
@@ -619,6 +648,7 @@ export function resolvePointLights(
|
|
|
619
648
|
out.iesProfiles = NO_PROFILES;
|
|
620
649
|
out.iesAxes = NO_IES_AXES;
|
|
621
650
|
out.cookies = NO_COOKIES;
|
|
651
|
+
packAxesAndChannels(NO_IES_AXES, undefined, out.axesAndChannels);
|
|
622
652
|
return out;
|
|
623
653
|
}
|
|
624
654
|
/*
|
|
@@ -711,5 +741,21 @@ export function resolvePointLights(
|
|
|
711
741
|
out.iesProfiles = iesProfiles;
|
|
712
742
|
out.iesAxes = iesAxes;
|
|
713
743
|
out.cookies = cookies;
|
|
744
|
+
packAxesAndChannels(iesAxes, lights.lightChannels, out.axesAndChannels);
|
|
714
745
|
return out;
|
|
715
746
|
}
|
|
747
|
+
|
|
748
|
+
/** The azimuth and the channels, four a light, as `uLightIesAxis` holds them. */
|
|
749
|
+
function packAxesAndChannels(
|
|
750
|
+
axes: Float32Array,
|
|
751
|
+
channels: Float32Array | undefined,
|
|
752
|
+
out: Float32Array,
|
|
753
|
+
): void {
|
|
754
|
+
const named = (channels?.length ?? 0) >= MAX_POINT_LIGHTS ? channels : undefined;
|
|
755
|
+
for (let light = 0; light < MAX_POINT_LIGHTS; light++) {
|
|
756
|
+
out[light * 4] = axes[light * 3] ?? 0;
|
|
757
|
+
out[light * 4 + 1] = axes[light * 3 + 1] ?? 0;
|
|
758
|
+
out[light * 4 + 2] = axes[light * 3 + 2] ?? 0;
|
|
759
|
+
out[light * 4 + 3] = lightChannelsOf(named?.[light]);
|
|
760
|
+
}
|
|
761
|
+
}
|