@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
|
@@ -57,6 +57,50 @@ export declare function bloomProfileWarning(quality: {
|
|
|
57
57
|
readonly hdrScene: boolean;
|
|
58
58
|
readonly bloomThreshold: number;
|
|
59
59
|
}): string | null;
|
|
60
|
+
/**
|
|
61
|
+
* How a frame's bloom answers the light it is given, beside the strength and threshold `setBloom`
|
|
62
|
+
* already takes. Absent, or a field absent, is the response every frame had before 4.8.6.
|
|
63
|
+
*
|
|
64
|
+
* **Why it exists: one band over the colour less the threshold washes a stage built for another
|
|
65
|
+
* engine.** Unreal's standard bloom keeps `saturate((L − threshold) / 2)` of a colour rather than
|
|
66
|
+
* the colour less the threshold, and spreads it over six bands, each in a tint of its own. A
|
|
67
|
+
* stage authored against that — a threshold of 0.1 and tints summing below one — came out white
|
|
68
|
+
* here, because subtracting so low a threshold keeps nearly every light in the frame at full
|
|
69
|
+
* strength. These are the two things that differ, as numbers.
|
|
70
|
+
*/
|
|
71
|
+
export interface BloomResponse {
|
|
72
|
+
/**
|
|
73
|
+
* Scene units over which a colour comes in, from none of it at the threshold to all of it this
|
|
74
|
+
* far past. **0, the default, subtracts instead**: a colour keeps what it has past the threshold,
|
|
75
|
+
* the response since bloom shipped. Unreal's is 2 after exposure, so a frame exposed by `e` that
|
|
76
|
+
* wants it passes `2 / e`, as it passes the threshold over the exposure.
|
|
77
|
+
*
|
|
78
|
+
* Brightness is still the largest channel rather than luminance, for the reason
|
|
79
|
+
* `shaders/bloom.ts` gives; on a neutral colour the two are equal.
|
|
80
|
+
*/
|
|
81
|
+
readonly ramp?: number;
|
|
82
|
+
/**
|
|
83
|
+
* Each level's colour, three numbers a level, finest first: the halo at a few pixels first and
|
|
84
|
+
* at a twentieth of the frame last. **White, the default, is the identity**; a level not named
|
|
85
|
+
* takes white. Not clamped above one, which is how a tint carries a strength past the 0..1 the
|
|
86
|
+
* dial gives — a stage asking for twice the light names tints twice as bright.
|
|
87
|
+
*
|
|
88
|
+
* At most `BLOOM_LEVELS` levels. A frame too small for that many uses the first it has room for.
|
|
89
|
+
*/
|
|
90
|
+
readonly tints?: ArrayLike<number>;
|
|
91
|
+
}
|
|
92
|
+
/** Floats `resolveBloomResponse` writes: the ramp, then a tint a level. */
|
|
93
|
+
export declare const BLOOM_RESPONSE_FLOATS: number;
|
|
94
|
+
/**
|
|
95
|
+
* A response as both backends bind it, into a caller-owned `out` of `BLOOM_RESPONSE_FLOATS`: the
|
|
96
|
+
* ramp, then `BLOOM_LEVELS` tints, white where none was named. Refuses a tint list that is not
|
|
97
|
+
* whole colours or names more levels than the pyramid has, naming the length — a caller error with
|
|
98
|
+
* one correct outcome. A ramp that is not a positive number is the subtraction; a tint channel
|
|
99
|
+
* that is not a number or is below zero is zero.
|
|
100
|
+
*/
|
|
101
|
+
export declare function resolveBloomResponse(response: BloomResponse | null | undefined, out: Float32Array): void;
|
|
102
|
+
/** The response every frame had before 4.8.6: the subtraction, every level white. */
|
|
103
|
+
export declare function defaultBloomResponse(): Float32Array;
|
|
60
104
|
/** One level of the pyramid: level 0 is half the frame and each one after it half again. */
|
|
61
105
|
export interface BloomLevelSize {
|
|
62
106
|
readonly width: number;
|
|
@@ -67,6 +67,34 @@ export function bloomProfileWarning(quality) {
|
|
|
67
67
|
}
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
70
|
+
/** Floats `resolveBloomResponse` writes: the ramp, then a tint a level. */
|
|
71
|
+
export const BLOOM_RESPONSE_FLOATS = 1 + BLOOM_LEVELS * 3;
|
|
72
|
+
/**
|
|
73
|
+
* A response as both backends bind it, into a caller-owned `out` of `BLOOM_RESPONSE_FLOATS`: the
|
|
74
|
+
* ramp, then `BLOOM_LEVELS` tints, white where none was named. Refuses a tint list that is not
|
|
75
|
+
* whole colours or names more levels than the pyramid has, naming the length — a caller error with
|
|
76
|
+
* one correct outcome. A ramp that is not a positive number is the subtraction; a tint channel
|
|
77
|
+
* that is not a number or is below zero is zero.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveBloomResponse(response, out) {
|
|
80
|
+
const ramp = response?.ramp ?? 0;
|
|
81
|
+
out[0] = Number.isFinite(ramp) && ramp > 0 ? ramp : 0;
|
|
82
|
+
const tints = response?.tints;
|
|
83
|
+
if (tints !== undefined && (tints.length % 3 !== 0 || tints.length > BLOOM_LEVELS * 3)) {
|
|
84
|
+
throw new Error(`setBloom: ${tints.length} tint values is not up to ${BLOOM_LEVELS} whole colours, three ` +
|
|
85
|
+
'numbers a level, finest first');
|
|
86
|
+
}
|
|
87
|
+
for (let i = 0; i < BLOOM_LEVELS * 3; i++) {
|
|
88
|
+
const value = tints !== undefined && i < tints.length ? tints[i] : 1;
|
|
89
|
+
out[1 + i] = Number.isFinite(value) && value > 0 ? value : 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** The response every frame had before 4.8.6: the subtraction, every level white. */
|
|
93
|
+
export function defaultBloomResponse() {
|
|
94
|
+
const out = new Float32Array(BLOOM_RESPONSE_FLOATS);
|
|
95
|
+
resolveBloomResponse(null, out);
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
70
98
|
/**
|
|
71
99
|
* The pyramid for a frame of this size, largest first, or an empty list where none would fit.
|
|
72
100
|
*
|
|
@@ -46,7 +46,9 @@ export declare class BloomPass {
|
|
|
46
46
|
* `threshold` is in scene units, so it only separates a light from white paint on a target
|
|
47
47
|
* that kept the range. The caller owns that decision and the warning that goes with it.
|
|
48
48
|
*/
|
|
49
|
-
run(scene: WebGLTexture, width: number, height: number, threshold: number
|
|
49
|
+
run(scene: WebGLTexture, width: number, height: number, threshold: number,
|
|
50
|
+
/** The ramp, then each level's tint: `resolveBloomResponse`'s floats. */
|
|
51
|
+
response: Float32Array): WebGLTexture | null;
|
|
50
52
|
/** One source texel in UV, which is what the thirteen taps are laid out in. */
|
|
51
53
|
private uploadTexel;
|
|
52
54
|
private release;
|
package/dist/render/bloomPass.js
CHANGED
|
@@ -130,7 +130,9 @@ export class BloomPass {
|
|
|
130
130
|
* `threshold` is in scene units, so it only separates a light from white paint on a target
|
|
131
131
|
* that kept the range. The caller owns that decision and the warning that goes with it.
|
|
132
132
|
*/
|
|
133
|
-
run(scene, width, height, threshold
|
|
133
|
+
run(scene, width, height, threshold,
|
|
134
|
+
/** The ramp, then each level's tint: `resolveBloomResponse`'s floats. */
|
|
135
|
+
response) {
|
|
134
136
|
if (!this.ensureSize(width, height))
|
|
135
137
|
return null;
|
|
136
138
|
const { gl } = this;
|
|
@@ -153,6 +155,7 @@ export class BloomPass {
|
|
|
153
155
|
gl.bindTexture(gl.TEXTURE_2D, scene);
|
|
154
156
|
gl.uniform1i(this.prefilterUniforms['uSource'] ?? null, 0);
|
|
155
157
|
gl.uniform1f(this.prefilterUniforms['uThreshold'] ?? null, threshold);
|
|
158
|
+
gl.uniform1f(this.prefilterUniforms['uRamp'] ?? null, response[0] ?? 0);
|
|
156
159
|
this.uploadTexel(this.prefilterUniforms, width, height);
|
|
157
160
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
158
161
|
/* Down the chain. Each level reads the one above it, which was written a draw ago. */
|
|
@@ -174,8 +177,13 @@ export class BloomPass {
|
|
|
174
177
|
* every octave at once. Additive blending rather than a second target to ping-pong through:
|
|
175
178
|
* a level is only ever read while a *different* level is bound, so there is no feedback.
|
|
176
179
|
*/
|
|
180
|
+
/*
|
|
181
|
+
* Each level's tint rides the blend: the level being added onto is multiplied by its own as the
|
|
182
|
+
* octave below is added, and the deepest by `uScale` on the first draw, the only one reading it
|
|
183
|
+
* before anything has been added to it. White everywhere is `ONE, ONE` exactly.
|
|
184
|
+
*/
|
|
177
185
|
gl.enable(gl.BLEND);
|
|
178
|
-
gl.blendFunc(gl.ONE, gl.
|
|
186
|
+
gl.blendFunc(gl.ONE, gl.CONSTANT_COLOR);
|
|
179
187
|
gl.useProgram(this.upsampleProgram);
|
|
180
188
|
gl.uniform1i(this.upsampleUniforms['uSource'] ?? null, 0);
|
|
181
189
|
gl.uniform1f(this.upsampleUniforms['uRadius'] ?? null, BLOOM_FILTER_RADIUS_UV);
|
|
@@ -184,11 +192,17 @@ export class BloomPass {
|
|
|
184
192
|
const source = levels[index];
|
|
185
193
|
if (target === undefined || source === undefined)
|
|
186
194
|
break;
|
|
195
|
+
const deepest = index === levels.length - 1;
|
|
196
|
+
const into = 1 + (index - 1) * 3;
|
|
197
|
+
const from = 1 + index * 3;
|
|
198
|
+
gl.blendColor(response[into] ?? 1, response[into + 1] ?? 1, response[into + 2] ?? 1, 1);
|
|
199
|
+
gl.uniform3f(this.upsampleUniforms['uScale'] ?? null, deepest ? (response[from] ?? 1) : 1, deepest ? (response[from + 1] ?? 1) : 1, deepest ? (response[from + 2] ?? 1) : 1);
|
|
187
200
|
gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
|
|
188
201
|
gl.viewport(0, 0, target.width, target.height);
|
|
189
202
|
gl.bindTexture(gl.TEXTURE_2D, source.texture);
|
|
190
203
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
191
204
|
}
|
|
205
|
+
gl.blendColor(0, 0, 0, 0);
|
|
192
206
|
gl.disable(gl.BLEND);
|
|
193
207
|
gl.bindVertexArray(null);
|
|
194
208
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AreaLightBuffer } from './areaLights.ts';
|
|
1
2
|
/**
|
|
2
3
|
* The froxel table: what it looks like in memory, and the CPU binner that fills it.
|
|
3
4
|
*
|
|
@@ -139,7 +140,21 @@ export declare const LIGHT_RECORD: {
|
|
|
139
140
|
readonly iesAxisX: 16;
|
|
140
141
|
readonly iesAxisY: 17;
|
|
141
142
|
readonly iesAxisZ: 18;
|
|
143
|
+
/**
|
|
144
|
+
* The light's lighting channels, a mask (`PointLightSet.lightChannels`), in the slot that was
|
|
145
|
+
* spare. A light naming channels other than the default is read as carrying a fixture, so its
|
|
146
|
+
* last texels are fetched; a plain light's are not, and the shader takes the default for it.
|
|
147
|
+
*/
|
|
148
|
+
readonly channels: 19;
|
|
142
149
|
};
|
|
150
|
+
/** The lighting channel every light and every surface is on unless it says otherwise. */
|
|
151
|
+
export declare const DEFAULT_LIGHT_CHANNELS = 1;
|
|
152
|
+
/**
|
|
153
|
+
* A light's channels as the shader compares them: a whole mask from 1 to 255, or the default where
|
|
154
|
+
* none was named or what was named is not one. The froxel record and the uniform path both write
|
|
155
|
+
* this, so a light named badly is on channel 1 in both rather than on two different guesses.
|
|
156
|
+
*/
|
|
157
|
+
export declare function lightChannelsOf(value: number | undefined): number;
|
|
143
158
|
/** Texels the lit pass reads for a light whose record carries no fixture. */
|
|
144
159
|
export declare const PLAIN_LIGHT_TEXELS = 2;
|
|
145
160
|
/**
|
|
@@ -270,6 +285,16 @@ export interface ClusterLightSet {
|
|
|
270
285
|
readonly cookies?: Float32Array;
|
|
271
286
|
/** One per light: an index into the IES atlas, or −1. */
|
|
272
287
|
readonly iesProfiles?: Float32Array;
|
|
288
|
+
/** One per light: its lighting channels. See `PointLightSet.lightChannels`. */
|
|
289
|
+
readonly channels?: Float32Array;
|
|
290
|
+
/**
|
|
291
|
+
* The rectangles shaded through the table, after the `count` point lights: those of `areas` from
|
|
292
|
+
* `areaFrom` on, `areaCount` of them. The ones before `areaFrom` are the fixed arm's, which is
|
|
293
|
+
* where a rectangle's shadow lives. Absent, there are none.
|
|
294
|
+
*/
|
|
295
|
+
readonly areas?: AreaLightBuffer | null;
|
|
296
|
+
readonly areaFrom?: number;
|
|
297
|
+
readonly areaCount?: number;
|
|
273
298
|
}
|
|
274
299
|
/** Where `fillClusterLightSet` reads a frame's lights from: the arrays `Environment` carries. */
|
|
275
300
|
export interface ClusterLightArrays {
|
|
@@ -284,6 +309,8 @@ export interface ClusterLightArrays {
|
|
|
284
309
|
readonly lightIesProfiles?: Float32Array;
|
|
285
310
|
readonly lightIesAxes?: Float32Array;
|
|
286
311
|
readonly lightCookies?: Float32Array;
|
|
312
|
+
readonly lightChannels?: Float32Array;
|
|
313
|
+
readonly areaLights?: AreaLightBuffer | null;
|
|
287
314
|
}
|
|
288
315
|
/** A `ClusterLightSet` a renderer refills every frame. */
|
|
289
316
|
export type ClusterLightScratch = {
|
|
@@ -305,7 +332,9 @@ export declare function createClusterLightSet(): ClusterLightScratch;
|
|
|
305
332
|
* radius in its place, an emitter as wide as the light reaches, which widened every lamp's
|
|
306
333
|
* highlight on a specular surface on the clustered arm alone.
|
|
307
334
|
*/
|
|
308
|
-
export declare function fillClusterLightSet(env: ClusterLightArrays, out: ClusterLightScratch
|
|
335
|
+
export declare function fillClusterLightSet(env: ClusterLightArrays, out: ClusterLightScratch,
|
|
336
|
+
/** How many rectangles the fixed arm shades; the rest of the buffer is clustered. */
|
|
337
|
+
areaFrom?: number): ClusterLightSet;
|
|
309
338
|
/**
|
|
310
339
|
* Whether a light's record needs its last three texels: it holds a shadow slot, a cone, a profile
|
|
311
340
|
* or a cookie. Everything else is a plain light, read in `PLAIN_LIGHT_TEXELS`.
|
|
@@ -343,6 +372,8 @@ export declare function clusteredMode(lights: ClusterLightSet, count: number, sh
|
|
|
343
372
|
* is a light without a map, which is what a scene with two hundred lamps has anyway.
|
|
344
373
|
*/
|
|
345
374
|
export declare function writeLightRecord(lights: ClusterLightSet, light: number, shadowSlots: number, out: Uint32Array, at: number): void;
|
|
375
|
+
/** How many lights the table carries: the point lights and the clustered rectangles after them. */
|
|
376
|
+
export declare function clusteredLightTotal(lights: ClusterLightSet): number;
|
|
346
377
|
/**
|
|
347
378
|
* Fill the table with this frame's light assignment, and answer how many lights it carried.
|
|
348
379
|
*
|
|
@@ -389,3 +420,26 @@ export declare function writeLightRecord(lights: ClusterLightSet, light: number,
|
|
|
389
420
|
* Fills a caller-owned table and allocates nothing.
|
|
390
421
|
*/
|
|
391
422
|
export declare function buildLightClusters(lights: ClusterLightSet, view: Float32Array, near: number, far: number, tanHalfFovY: number, aspect: number, table: Uint32Array, shadowSlots: number): number;
|
|
423
|
+
/**
|
|
424
|
+
* How far a froxel lies outside a spot's cone, in metres, or a negative number where the cone
|
|
425
|
+
* reaches it: positive is a froxel the light cannot shade. Everything in view space with depth
|
|
426
|
+
* positive, as the binner holds it; `axis` is unit length and `cosA`, `sinA` are the outer cone's.
|
|
427
|
+
*
|
|
428
|
+
* **The froxel's bounding sphere against the infinite cone**, after Wronski's cone test for
|
|
429
|
+
* clustered spots. With `V` from the apex to the sphere's centre at an angle `θ` from the axis and
|
|
430
|
+
* `A` the cone's half-angle, the centre's distance from the cone is `|V| sin(θ − A)` — which is
|
|
431
|
+
* `cosA |V⊥| − (V·axis) sinA` — while `θ − A` is under a right angle, and `|V|` past it, where the
|
|
432
|
+
* nearest point of the cone is its apex. Inside the cone it is negative. So the gap is exact for
|
|
433
|
+
* the sphere, and the sphere holds the froxel: the test can only keep a light, never drop one the
|
|
434
|
+
* shader would draw. The apex case is what drops a froxel behind a spot whose sphere reaches it,
|
|
435
|
+
* which the formula alone underestimates.
|
|
436
|
+
*
|
|
437
|
+
* **What it gives up** is tightness at the near end of the frustum, where a froxel is a long thin
|
|
438
|
+
* box whose sphere is much larger than it: a spot grazing such a froxel is kept. **What would make
|
|
439
|
+
* it wrong** is a profile showing those near froxels hold the cost; testing the box's corners is
|
|
440
|
+
* the tight version, at eight dot products a pair.
|
|
441
|
+
*
|
|
442
|
+
* Exported so the conformance page can tell a disagreement at the cone's edge from a real one; the
|
|
443
|
+
* GPU binner writes the same arithmetic in WGSL.
|
|
444
|
+
*/
|
|
445
|
+
export declare function coneBoxGap(px: number, py: number, pz: number, axisX: number, axisY: number, axisZ: number, cosA: number, sinA: number, box: Float32Array): number;
|
|
@@ -139,7 +139,26 @@ export const LIGHT_RECORD = {
|
|
|
139
139
|
iesAxisX: 16,
|
|
140
140
|
iesAxisY: 17,
|
|
141
141
|
iesAxisZ: 18,
|
|
142
|
+
/**
|
|
143
|
+
* The light's lighting channels, a mask (`PointLightSet.lightChannels`), in the slot that was
|
|
144
|
+
* spare. A light naming channels other than the default is read as carrying a fixture, so its
|
|
145
|
+
* last texels are fetched; a plain light's are not, and the shader takes the default for it.
|
|
146
|
+
*/
|
|
147
|
+
channels: 19,
|
|
142
148
|
};
|
|
149
|
+
/** The lighting channel every light and every surface is on unless it says otherwise. */
|
|
150
|
+
export const DEFAULT_LIGHT_CHANNELS = 1;
|
|
151
|
+
/**
|
|
152
|
+
* A light's channels as the shader compares them: a whole mask from 1 to 255, or the default where
|
|
153
|
+
* none was named or what was named is not one. The froxel record and the uniform path both write
|
|
154
|
+
* this, so a light named badly is on channel 1 in both rather than on two different guesses.
|
|
155
|
+
*/
|
|
156
|
+
export function lightChannelsOf(value) {
|
|
157
|
+
if (value === undefined || !Number.isInteger(value) || value < 1 || value > 255) {
|
|
158
|
+
return DEFAULT_LIGHT_CHANNELS;
|
|
159
|
+
}
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
143
162
|
/** Texels the lit pass reads for a light whose record carries no fixture. */
|
|
144
163
|
export const PLAIN_LIGHT_TEXELS = 2;
|
|
145
164
|
/**
|
|
@@ -354,8 +373,14 @@ export function createClusterLightSet() {
|
|
|
354
373
|
* radius in its place, an emitter as wide as the light reaches, which widened every lamp's
|
|
355
374
|
* highlight on a specular surface on the clustered arm alone.
|
|
356
375
|
*/
|
|
357
|
-
export function fillClusterLightSet(env, out
|
|
376
|
+
export function fillClusterLightSet(env, out,
|
|
377
|
+
/** How many rectangles the fixed arm shades; the rest of the buffer is clustered. */
|
|
378
|
+
areaFrom = 0) {
|
|
358
379
|
out.count = Math.min(env.lightCount ?? 0, MAX_CLUSTERED_LIGHTS);
|
|
380
|
+
const areas = env.areaLights ?? null;
|
|
381
|
+
out.areas = areas;
|
|
382
|
+
out.areaFrom = areaFrom;
|
|
383
|
+
out.areaCount = Math.max(0, Math.min((areas?.count ?? 0) - areaFrom, MAX_CLUSTERED_LIGHTS - out.count));
|
|
359
384
|
out.positions = env.lightPositions;
|
|
360
385
|
out.colors = env.lightColors;
|
|
361
386
|
out.radii = env.lightRadii;
|
|
@@ -366,6 +391,7 @@ export function fillClusterLightSet(env, out) {
|
|
|
366
391
|
out.iesProfiles = env.lightIesProfiles;
|
|
367
392
|
out.iesAxes = env.lightIesAxes;
|
|
368
393
|
out.cookies = env.lightCookies;
|
|
394
|
+
out.channels = env.lightChannels;
|
|
369
395
|
return out;
|
|
370
396
|
}
|
|
371
397
|
/*
|
|
@@ -418,12 +444,16 @@ function distanceSqToBounds(x, y, z, b) {
|
|
|
418
444
|
* them was paying for three texels the shader then ignored.
|
|
419
445
|
*/
|
|
420
446
|
export function lightHasFixture(lights, light, shadowSlots) {
|
|
447
|
+
/* A rectangle's record always carries its axes and extents past the second texel. */
|
|
448
|
+
if (light >= lights.count)
|
|
449
|
+
return true;
|
|
421
450
|
const cone = lights.coneCos;
|
|
422
451
|
return (light < shadowSlots ||
|
|
423
452
|
(cone?.[light * 2] ?? POINT_LIGHT_COS_INNER) !== POINT_LIGHT_COS_INNER ||
|
|
424
453
|
(cone?.[light * 2 + 1] ?? POINT_LIGHT_COS_OUTER) !== POINT_LIGHT_COS_OUTER ||
|
|
425
454
|
(lights.iesProfiles?.[light] ?? NO_IES_PROFILE) >= 0 ||
|
|
426
|
-
(lights.cookies?.[light] ?? NO_IES_PROFILE) >= 0
|
|
455
|
+
(lights.cookies?.[light] ?? NO_IES_PROFILE) >= 0 ||
|
|
456
|
+
lightChannelsOf(lights.channels?.[light]) !== DEFAULT_LIGHT_CHANNELS);
|
|
427
457
|
}
|
|
428
458
|
/**
|
|
429
459
|
* The value of `uClustered` for a frame: 0 with clustering off, 1 when every light is plain, 2 when
|
|
@@ -459,6 +489,10 @@ export function clusteredMode(lights, count, shadowSlots) {
|
|
|
459
489
|
* is a light without a map, which is what a scene with two hundred lamps has anyway.
|
|
460
490
|
*/
|
|
461
491
|
export function writeLightRecord(lights, light, shadowSlots, out, at) {
|
|
492
|
+
if (light >= lights.count) {
|
|
493
|
+
writeAreaRecord(lights, light, out, at);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
462
496
|
const directions = lights.directions;
|
|
463
497
|
const cone = lights.coneCos;
|
|
464
498
|
const axes = lights.iesAxes;
|
|
@@ -490,7 +524,64 @@ export function writeLightRecord(lights, light, shadowSlots, out, at) {
|
|
|
490
524
|
out[at + LIGHT_RECORD.iesAxisX] = floatBits(axes?.[light * 3] ?? 0);
|
|
491
525
|
out[at + LIGHT_RECORD.iesAxisY] = floatBits(axes?.[light * 3 + 1] ?? 0);
|
|
492
526
|
out[at + LIGHT_RECORD.iesAxisZ] = floatBits(axes?.[light * 3 + 2] ?? 0);
|
|
493
|
-
out[at +
|
|
527
|
+
out[at + LIGHT_RECORD.channels] = floatBits(lightChannelsOf(lights.channels?.[light]));
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Which rectangle of the area buffer clustered light `light` is, for a light past the point
|
|
531
|
+
* lights. See `ClusterLightSet.areas`.
|
|
532
|
+
*/
|
|
533
|
+
function areaSlot(lights, light) {
|
|
534
|
+
return (lights.areaFrom ?? 0) + light - lights.count;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* A rectangle's record, which the shader tells from a lamp's by its **negative radius** — its
|
|
538
|
+
* reach, negated — and reads to its fifth texel: sidedness where a lamp keeps its shadow slot,
|
|
539
|
+
* the right axis where a spot keeps its direction, the half extents where its cone is, the up
|
|
540
|
+
* axis where the azimuth is, and the channels last. Never shadowed: a rectangle's occlusion is
|
|
541
|
+
* the fixed arm's, which is why the first `maxAreaLights` stay there.
|
|
542
|
+
*/
|
|
543
|
+
function writeAreaRecord(lights, light, out, at) {
|
|
544
|
+
const areas = lights.areas;
|
|
545
|
+
const a = areaSlot(lights, light);
|
|
546
|
+
const pos = areas?.positions;
|
|
547
|
+
const color = areas?.colors;
|
|
548
|
+
const right = areas?.right;
|
|
549
|
+
const up = areas?.up;
|
|
550
|
+
const sizes = areas?.sizes;
|
|
551
|
+
out[at + LIGHT_RECORD.positionX] = floatBits(pos?.[a * 3] ?? 0);
|
|
552
|
+
out[at + LIGHT_RECORD.positionY] = floatBits(pos?.[a * 3 + 1] ?? 0);
|
|
553
|
+
out[at + LIGHT_RECORD.positionZ] = floatBits(pos?.[a * 3 + 2] ?? 0);
|
|
554
|
+
out[at + LIGHT_RECORD.radius] = floatBits(-areaReach(lights, light));
|
|
555
|
+
out[at + LIGHT_RECORD.colorR] = floatBits(color?.[a * 3] ?? 0);
|
|
556
|
+
out[at + LIGHT_RECORD.colorG] = floatBits(color?.[a * 3 + 1] ?? 0);
|
|
557
|
+
out[at + LIGHT_RECORD.colorB] = floatBits(color?.[a * 3 + 2] ?? 0);
|
|
558
|
+
out[at + LIGHT_RECORD.sizeAndWeight] = packSizeAndWeight(0, 1, true);
|
|
559
|
+
out[at + LIGHT_RECORD.shadowSlot] = floatBits(areas?.twoSided[a] ?? 0);
|
|
560
|
+
out[at + LIGHT_RECORD.directionX] = floatBits(right?.[a * 3] ?? 1);
|
|
561
|
+
out[at + LIGHT_RECORD.directionY] = floatBits(right?.[a * 3 + 1] ?? 0);
|
|
562
|
+
out[at + LIGHT_RECORD.directionZ] = floatBits(right?.[a * 3 + 2] ?? 0);
|
|
563
|
+
out[at + LIGHT_RECORD.cosInner] = floatBits(sizes?.[a * 2] ?? 0);
|
|
564
|
+
out[at + LIGHT_RECORD.cosOuter] = floatBits(sizes?.[a * 2 + 1] ?? 0);
|
|
565
|
+
out[at + LIGHT_RECORD.iesProfile] = floatBits(NO_IES_PROFILE);
|
|
566
|
+
out[at + LIGHT_RECORD.cookie] = floatBits(NO_IES_PROFILE);
|
|
567
|
+
out[at + LIGHT_RECORD.iesAxisX] = floatBits(up?.[a * 3] ?? 0);
|
|
568
|
+
out[at + LIGHT_RECORD.iesAxisY] = floatBits(up?.[a * 3 + 1] ?? 1);
|
|
569
|
+
out[at + LIGHT_RECORD.iesAxisZ] = floatBits(up?.[a * 3 + 2] ?? 0);
|
|
570
|
+
out[at + LIGHT_RECORD.channels] = floatBits(DEFAULT_LIGHT_CHANNELS);
|
|
571
|
+
}
|
|
572
|
+
/** A clustered rectangle's reach, which its record carries negated. See `AreaLightSource.range`. */
|
|
573
|
+
function areaReach(lights, light) {
|
|
574
|
+
const areas = lights.areas;
|
|
575
|
+
const a = areaSlot(lights, light);
|
|
576
|
+
const named = areas?.ranges?.[a] ?? 0;
|
|
577
|
+
if (named > 0)
|
|
578
|
+
return named;
|
|
579
|
+
const sizes = areas?.sizes;
|
|
580
|
+
return Math.hypot(sizes?.[a * 2] ?? 0, sizes?.[a * 2 + 1] ?? 0);
|
|
581
|
+
}
|
|
582
|
+
/** How many lights the table carries: the point lights and the clustered rectangles after them. */
|
|
583
|
+
export function clusteredLightTotal(lights) {
|
|
584
|
+
return Math.min(lights.count + (lights.areaCount ?? 0), MAX_CLUSTERED_LIGHTS);
|
|
494
585
|
}
|
|
495
586
|
/**
|
|
496
587
|
* Fill the table with this frame's light assignment, and answer how many lights it carried.
|
|
@@ -538,7 +629,7 @@ export function writeLightRecord(lights, light, shadowSlots, out, at) {
|
|
|
538
629
|
* Fills a caller-owned table and allocates nothing.
|
|
539
630
|
*/
|
|
540
631
|
export function buildLightClusters(lights, view, near, far, tanHalfFovY, aspect, table, shadowSlots) {
|
|
541
|
-
const count =
|
|
632
|
+
const count = clusteredLightTotal(lights);
|
|
542
633
|
/*
|
|
543
634
|
* The whole cluster region, not just the counts.
|
|
544
635
|
*
|
|
@@ -553,10 +644,14 @@ export function buildLightClusters(lights, view, near, far, tanHalfFovY, aspect,
|
|
|
553
644
|
for (let k = 0; k <= CLUSTER_Z; k++)
|
|
554
645
|
sliceDepths[k] = sliceNearDepth(k, near, far);
|
|
555
646
|
for (let light = 0; light < count; light++) {
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
const
|
|
647
|
+
/* A rectangle past the lamps is placed by its centre and binned by its reach. */
|
|
648
|
+
const area = light >= lights.count;
|
|
649
|
+
const from = area ? (lights.areas?.positions ?? lights.positions) : lights.positions;
|
|
650
|
+
const slot = area ? areaSlot(lights, light) : light;
|
|
651
|
+
const x = from[slot * 3] ?? 0;
|
|
652
|
+
const y = from[slot * 3 + 1] ?? 0;
|
|
653
|
+
const z = from[slot * 3 + 2] ?? 0;
|
|
654
|
+
const radius = area ? areaReach(lights, light) : (lights.radii[light] ?? 0);
|
|
560
655
|
/* The record, as the shader reads it. World space; see the note above. */
|
|
561
656
|
writeLightRecord(lights, light, shadowSlots, table, lightBase(light));
|
|
562
657
|
/* Column-major, as gl-matrix builds it. The view looks down -z, so depth is -vz. */
|
|
@@ -570,6 +665,39 @@ export function buildLightClusters(lights, view, near, far, tanHalfFovY, aspect,
|
|
|
570
665
|
/* Entirely behind the camera, so no cluster can reach it. */
|
|
571
666
|
if (depth + radius <= 0)
|
|
572
667
|
continue;
|
|
668
|
+
/*
|
|
669
|
+
* **A spot is binned by its cone as well as its sphere.** A 10° spot reaching thirty metres
|
|
670
|
+
* touches every froxel its sphere does and lights a sliver of them, and a stage rigged with
|
|
671
|
+
* hundreds of such fixtures put every one of them in nearly every froxel near the floor: 313
|
|
672
|
+
* lights cost 11.7 ms of a 1600x900 frame. The cone is tested against each froxel's bounding
|
|
673
|
+
* sphere in view space, which is conservative, by the construction `coneBoxGap` describes.
|
|
674
|
+
*/
|
|
675
|
+
const cosOuter = lights.coneCos?.[light * 2 + 1] ?? POINT_LIGHT_COS_OUTER;
|
|
676
|
+
const directions = lights.directions;
|
|
677
|
+
let coneCos = POINT_LIGHT_COS_OUTER;
|
|
678
|
+
let coneSin = 0;
|
|
679
|
+
let axisX = 0;
|
|
680
|
+
let axisY = 0;
|
|
681
|
+
let axisZ = 0;
|
|
682
|
+
if (!area && directions !== undefined && cosOuter > -1) {
|
|
683
|
+
const dx = directions[light * 3] ?? 0;
|
|
684
|
+
const dy = directions[light * 3 + 1] ?? 0;
|
|
685
|
+
const dz = directions[light * 3 + 2] ?? 0;
|
|
686
|
+
const ax = (view[0] ?? 0) * dx + (view[4] ?? 0) * dy + (view[8] ?? 0) * dz;
|
|
687
|
+
const ay = (view[1] ?? 0) * dx + (view[5] ?? 0) * dy + (view[9] ?? 0) * dz;
|
|
688
|
+
const az = -((view[2] ?? 0) * dx + (view[6] ?? 0) * dy + (view[10] ?? 0) * dz);
|
|
689
|
+
const length = Math.sqrt(ax * ax + ay * ay + az * az);
|
|
690
|
+
/* The shader measures the cone against the direction as given, so a long one is a wider
|
|
691
|
+
cone: `dot(L, d) >= c` is `dot(L, d / |d|) >= c / |d|`. */
|
|
692
|
+
if (length > 1e-6 && cosOuter / length > -1) {
|
|
693
|
+
coneCos = cosOuter / length;
|
|
694
|
+
coneSin = Math.sqrt(Math.max(0, 1 - coneCos * coneCos));
|
|
695
|
+
axisX = ax / length;
|
|
696
|
+
axisY = ay / length;
|
|
697
|
+
axisZ = az / length;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const spot = coneCos > -1;
|
|
573
701
|
/*
|
|
574
702
|
* The slices the sphere could possibly touch. The tiles are derived per slice, below.
|
|
575
703
|
*
|
|
@@ -617,6 +745,10 @@ export function buildLightClusters(lights, view, near, far, tanHalfFovY, aspect,
|
|
|
617
745
|
sliceBounds(i, j, zNear, zFar, halfWNear, halfWFar, halfHNear, halfHFar, bounds);
|
|
618
746
|
if (distanceSqToBounds(vx, vy, depth, bounds) > radiusSq)
|
|
619
747
|
continue;
|
|
748
|
+
if (spot &&
|
|
749
|
+
coneBoxGap(vx, vy, depth, axisX, axisY, axisZ, coneCos, coneSin, bounds) > 0) {
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
620
752
|
insert(table, i + j * CLUSTER_X + k * CLUSTER_X * CLUSTER_Y, light, bounds);
|
|
621
753
|
}
|
|
622
754
|
}
|
|
@@ -624,6 +756,48 @@ export function buildLightClusters(lights, view, near, far, tanHalfFovY, aspect,
|
|
|
624
756
|
}
|
|
625
757
|
return count;
|
|
626
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* How far a froxel lies outside a spot's cone, in metres, or a negative number where the cone
|
|
761
|
+
* reaches it: positive is a froxel the light cannot shade. Everything in view space with depth
|
|
762
|
+
* positive, as the binner holds it; `axis` is unit length and `cosA`, `sinA` are the outer cone's.
|
|
763
|
+
*
|
|
764
|
+
* **The froxel's bounding sphere against the infinite cone**, after Wronski's cone test for
|
|
765
|
+
* clustered spots. With `V` from the apex to the sphere's centre at an angle `θ` from the axis and
|
|
766
|
+
* `A` the cone's half-angle, the centre's distance from the cone is `|V| sin(θ − A)` — which is
|
|
767
|
+
* `cosA |V⊥| − (V·axis) sinA` — while `θ − A` is under a right angle, and `|V|` past it, where the
|
|
768
|
+
* nearest point of the cone is its apex. Inside the cone it is negative. So the gap is exact for
|
|
769
|
+
* the sphere, and the sphere holds the froxel: the test can only keep a light, never drop one the
|
|
770
|
+
* shader would draw. The apex case is what drops a froxel behind a spot whose sphere reaches it,
|
|
771
|
+
* which the formula alone underestimates.
|
|
772
|
+
*
|
|
773
|
+
* **What it gives up** is tightness at the near end of the frustum, where a froxel is a long thin
|
|
774
|
+
* box whose sphere is much larger than it: a spot grazing such a froxel is kept. **What would make
|
|
775
|
+
* it wrong** is a profile showing those near froxels hold the cost; testing the box's corners is
|
|
776
|
+
* the tight version, at eight dot products a pair.
|
|
777
|
+
*
|
|
778
|
+
* Exported so the conformance page can tell a disagreement at the cone's edge from a real one; the
|
|
779
|
+
* GPU binner writes the same arithmetic in WGSL.
|
|
780
|
+
*/
|
|
781
|
+
export function coneBoxGap(px, py, pz, axisX, axisY, axisZ, cosA, sinA, box) {
|
|
782
|
+
const b0 = box[0] ?? 0;
|
|
783
|
+
const b1 = box[1] ?? 0;
|
|
784
|
+
const b2 = box[2] ?? 0;
|
|
785
|
+
const b3 = box[3] ?? 0;
|
|
786
|
+
const b4 = box[4] ?? 0;
|
|
787
|
+
const b5 = box[5] ?? 0;
|
|
788
|
+
const ex = (b3 - b0) * 0.5;
|
|
789
|
+
const ey = (b4 - b1) * 0.5;
|
|
790
|
+
const ez = (b5 - b2) * 0.5;
|
|
791
|
+
const vx = (b0 + b3) * 0.5 - px;
|
|
792
|
+
const vy = (b1 + b4) * 0.5 - py;
|
|
793
|
+
const vz = (b2 + b5) * 0.5 - pz;
|
|
794
|
+
const along = vx * axisX + vy * axisY + vz * axisZ;
|
|
795
|
+
const lengthSq = vx * vx + vy * vy + vz * vz;
|
|
796
|
+
const across = Math.sqrt(Math.max(lengthSq - along * along, 0));
|
|
797
|
+
const reach = Math.sqrt(lengthSq);
|
|
798
|
+
const distance = along < -sinA * reach ? reach : cosA * across - along * sinA;
|
|
799
|
+
return distance - Math.sqrt(ex * ex + ey * ey + ez * ez);
|
|
800
|
+
}
|
|
627
801
|
/** An NDC coordinate to a tile index, clamped. */
|
|
628
802
|
function tileOf(ndc, tiles) {
|
|
629
803
|
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.
|
|
@@ -44,6 +44,13 @@ 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
|
+
}
|
|
47
54
|
kept++;
|
|
48
55
|
}
|
|
49
56
|
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 — 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. */
|
|
@@ -45,19 +66,20 @@ export interface MeshInstances {
|
|
|
45
66
|
export declare function createMeshInstances(capacity: number): MeshInstances;
|
|
46
67
|
/**
|
|
47
68
|
* Floats one instance occupies in the interleaved vertex buffer: sixteen of matrix, three of
|
|
48
|
-
* tint, and
|
|
69
|
+
* tint, and the opacity.
|
|
49
70
|
*
|
|
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.
|
|
71
|
+
* **Twenty so the stride is 80 bytes rather than 76.** A vertex buffer's stride must be a multiple
|
|
72
|
+
* of four on both backends, which 76 already is — the twentieth float was padding for the
|
|
73
|
+
* *attribute* offsets: the tint sits at byte 64 and a three-float attribute ending at 76 leaves the
|
|
74
|
+
* next instance's first column starting there, which is legal but puts every second instance on an
|
|
75
|
+
* offset no driver aligns well. Since 4.8.6 it carries the instance's opacity, read with the tint
|
|
76
|
+
* as one four-float attribute, so the opacity cost no attribute and no byte.
|
|
55
77
|
*/
|
|
56
78
|
export declare const INSTANCE_FLOATS = 20;
|
|
57
79
|
/** Bytes one instance occupies. See `INSTANCE_FLOATS`. */
|
|
58
80
|
export declare const INSTANCE_STRIDE: number;
|
|
59
81
|
/**
|
|
60
|
-
* Interleave `count` instances into `out`, matrix
|
|
82
|
+
* Interleave `count` instances into `out`, matrix, tint and opacity, at `INSTANCE_FLOATS` apart.
|
|
61
83
|
*
|
|
62
84
|
* **Interleaved rather than two buffers** so a batch's upload is one `writeBuffer` and an
|
|
63
85
|
* instance's colour sits beside the matrix that places it. Shared by both backends, because
|