@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
|
@@ -102,6 +102,41 @@ fn centreDistanceSq(light: u32, centre: vec3<f32>) -> f32 {
|
|
|
102
102
|
return dot(d, d);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Whether a spot's cone misses this froxel: \`coneBoxGap\` in \`clusteredLights.ts\`, the same
|
|
107
|
+
* arithmetic in the same order — the froxel's bounding sphere against the infinite cone, its apex
|
|
108
|
+
* included, which can keep a light the shader would not draw and never drop one it would. A light
|
|
109
|
+
* with no cone carries a cosine of -2 and never reaches the test.
|
|
110
|
+
*/
|
|
111
|
+
fn outsideCone(light: u32, apex: vec3<f32>, lo: vec3<f32>, hi: vec3<f32>) -> bool {
|
|
112
|
+
let cosOuter = bitcast<f32>(lights[light * LIGHT_TEXELS + 3u].y);
|
|
113
|
+
if (cosOuter <= -1.0) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
let world = bitcast<vec3<f32>>(lights[light * LIGHT_TEXELS + 2u].yzw);
|
|
117
|
+
let a = (params.view * vec4<f32>(world, 0.0)).xyz;
|
|
118
|
+
let axis = vec3<f32>(a.x, a.y, -a.z);
|
|
119
|
+
let len = length(axis);
|
|
120
|
+
/* The shader measures the cone against the direction as given, so a long one is a wider cone. */
|
|
121
|
+
if (len <= 1e-6 || cosOuter / len <= -1.0) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
let cosA = cosOuter / len;
|
|
125
|
+
let sinA = sqrt(max(0.0, 1.0 - cosA * cosA));
|
|
126
|
+
let unit = axis / len;
|
|
127
|
+
let extent = (hi - lo) * 0.5;
|
|
128
|
+
let toCentre = (lo + hi) * 0.5 - apex;
|
|
129
|
+
let along = dot(toCentre, unit);
|
|
130
|
+
let lengthSq = dot(toCentre, toCentre);
|
|
131
|
+
let across = sqrt(max(lengthSq - along * along, 0.0));
|
|
132
|
+
let reach = sqrt(lengthSq);
|
|
133
|
+
var distance = cosA * across - along * sinA;
|
|
134
|
+
if (along < -sinA * reach) {
|
|
135
|
+
distance = reach;
|
|
136
|
+
}
|
|
137
|
+
return distance - length(extent) > 0.0;
|
|
138
|
+
}
|
|
139
|
+
|
|
105
140
|
@compute @workgroup_size(64)
|
|
106
141
|
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
107
142
|
let id = gid.x;
|
|
@@ -161,7 +196,9 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
161
196
|
|
|
162
197
|
/* Increasing light index, which is what makes the list sorted without a sort. */
|
|
163
198
|
for (var light: u32 = 0u; light < params.lightCount; light = light + 1u) {
|
|
164
|
-
|
|
199
|
+
/* A rectangle carries its reach negated, which is how the lit pass tells it from a lamp. */
|
|
200
|
+
let signedRadius = bitcast<f32>(lights[light * LIGHT_TEXELS].w);
|
|
201
|
+
let radius = abs(signedRadius);
|
|
165
202
|
let v = viewOf(light);
|
|
166
203
|
if (v.z + radius <= 0.0) {
|
|
167
204
|
continue;
|
|
@@ -169,6 +206,9 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
169
206
|
if (distanceSqToBounds(v, lo, hi) > radius * radius) {
|
|
170
207
|
continue;
|
|
171
208
|
}
|
|
209
|
+
if (signedRadius >= 0.0 && outsideCone(light, v, lo, hi)) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
172
212
|
|
|
173
213
|
if (count < MAX_LIGHTS_PER_CLUSTER) {
|
|
174
214
|
indices[count] = light;
|
|
@@ -1,27 +1,5 @@
|
|
|
1
1
|
import { SkinPaletteTexture } from './skinPaletteTexture.ts';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* How many palettes one frame may set before the ring is full.
|
|
5
|
-
*
|
|
6
|
-
* The arithmetic rather than a round number: a character's palette is uploaded once per *pass*
|
|
7
|
-
* that draws it, and a full profile draws the same character five times — the main pass, a planar
|
|
8
|
-
* reflection, and the static, peel and dynamic shadow layers. So this is twenty-five characters at
|
|
9
|
-
* once, which is a crowd rather than a cast.
|
|
10
|
-
*
|
|
11
|
-
* **What it costs is nothing until it is used.** A slot allocates its texture on the first frame
|
|
12
|
-
* that reaches it and keeps it, so a scene with two characters holds two: a 24-joint rig is 96
|
|
13
|
-
* texels of `rgba32float`, which is 1.5 KB, and a 96-joint humanoid is 6 KB.
|
|
14
|
-
*
|
|
15
|
-
* **What would make it wrong** is a genuine crowd — a stadium, a swarm of rigged birds — and the
|
|
16
|
-
* fix is then the one `skinPalette.ts` names: one texture holding every palette with a per-draw
|
|
17
|
-
* row offset, which spends a uniform and a shader change to stop spending textures.
|
|
18
|
-
*
|
|
19
|
-
* **A reconstructing frame spends one more per skinned mover**: last frame's pose is uploaded into
|
|
20
|
-
* a slot of its own for the motion pass. So the same budget is about twelve such characters with a
|
|
21
|
-
* reflection and three shadow layers each.
|
|
22
|
-
*/
|
|
23
|
-
export const MAX_SKIN_PALETTES = 128;
|
|
24
|
-
|
|
25
3
|
/**
|
|
26
4
|
* Every joint palette a frame sets, each in its own texture, reused frame after frame.
|
|
27
5
|
*
|
|
@@ -42,20 +20,53 @@ export const MAX_SKIN_PALETTES = 128;
|
|
|
42
20
|
* draws separates them there by construction. The decision — a palette belongs to a draw — is one;
|
|
43
21
|
* only what it takes to honour it differs, which is what `skinPalette.ts` says a binder is for.
|
|
44
22
|
*
|
|
45
|
-
* **A
|
|
46
|
-
*
|
|
47
|
-
*
|
|
23
|
+
* **A palette set again with the same numbers is the slot it already has.** A rig is drawn in
|
|
24
|
+
* every pass of a frame — the main pass, a reflection, three shadow layers — and each one used to
|
|
25
|
+
* upload the same matrices into a slot of its own, so a crowd ran out five times sooner than it
|
|
26
|
+
* had characters. The ring now remembers the slot each array last went into and compares what it
|
|
27
|
+
* holds, so a rig posed once a frame spends one slot however many passes draw it. Identity alone
|
|
28
|
+
* would be wrong: a caller may pose every rig into one scratch array, and that array then holds a
|
|
29
|
+
* different rig at every set — the comparison is what tells the two apart.
|
|
30
|
+
*
|
|
31
|
+
* **It was capped at 128 until 4.8.6, and the cap declined draws.** A stage of three dozen idling
|
|
32
|
+
* extras beside two fighters filled it in one frame and every skinned draw past it was refused, so
|
|
33
|
+
* the consumer stood its crowd in the bind pose. The ring now holds as many palettes as a frame
|
|
34
|
+
* holds draws (`RenderQuality.drawsPerFrame`), since no frame can bind more palettes than it draws;
|
|
35
|
+
* a slot still allocates its texture only the first time a frame reaches it, so a scene with two
|
|
36
|
+
* characters holds two. **What it costs** is a texture and a cached bind group a slot, 1.5 KB for a
|
|
37
|
+
* 24-joint rig and 6 KB for a 96-joint humanoid. **What would make it wrong** is a crowd of
|
|
38
|
+
* thousands, where a texture a palette is the expensive half; the fix is then one texture holding
|
|
39
|
+
* every palette with a row offset per draw, which spends a per-draw uniform and a vertex-stage
|
|
40
|
+
* change to stop spending textures.
|
|
41
|
+
*
|
|
42
|
+
* **A slot is written at most once per frame**, since a palette that differs from what a slot
|
|
43
|
+
* holds always takes a fresh one. That is what makes the reallocation inside `SkinPaletteTexture`
|
|
44
|
+
* safe: a joint count can only change a slot's texture between frames, never while a recorded draw
|
|
45
|
+
* is still pointing at it.
|
|
48
46
|
*/
|
|
49
47
|
export class SkinPaletteRing {
|
|
50
48
|
private readonly slots: SkinPaletteTexture[] = [];
|
|
49
|
+
/** What each slot was last written with, for the comparison that lets a pass reuse it. */
|
|
50
|
+
private readonly held: Float32Array[] = [];
|
|
51
|
+
/** The slot each palette array last went into, by identity. Weak, so a caller's array can go. */
|
|
52
|
+
private readonly lastSlot = new WeakMap<Float32Array, number>();
|
|
51
53
|
private used = 0;
|
|
54
|
+
private uploads = 0;
|
|
52
55
|
private warnedFull = false;
|
|
53
56
|
|
|
57
|
+
/** `capacity` is how many palettes one frame may hold: one per draw at most. */
|
|
58
|
+
constructor(private readonly capacity: number) {}
|
|
59
|
+
|
|
54
60
|
/** How many slots this frame has taken. */
|
|
55
61
|
get count(): number {
|
|
56
62
|
return this.used;
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
/** How many palettes have been written to the device since the ring was made. */
|
|
66
|
+
get uploaded(): number {
|
|
67
|
+
return this.uploads;
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
/**
|
|
60
71
|
* Start a frame: every slot is free again.
|
|
61
72
|
*
|
|
@@ -78,20 +89,23 @@ export class SkinPaletteRing {
|
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
/**
|
|
81
|
-
*
|
|
92
|
+
* The slot this frame holds for these numbers, uploading them into a fresh one where none does,
|
|
93
|
+
* or null when the ring is full.
|
|
82
94
|
*
|
|
83
|
-
* **Null rather than reusing a slot**, because
|
|
84
|
-
* after: the caller's character would silently take another character's pose. A
|
|
85
|
-
* out declines the draw and says so once.
|
|
95
|
+
* **Null rather than reusing a slot that holds something else**, because that is the defect this
|
|
96
|
+
* class is named after: the caller's character would silently take another character's pose. A
|
|
97
|
+
* caller that runs out declines the draw and says so once.
|
|
86
98
|
*/
|
|
87
99
|
take(device: GPUDevice, palette: Float32Array): number | null {
|
|
88
|
-
|
|
100
|
+
const known = this.lastSlot.get(palette);
|
|
101
|
+
if (known !== undefined && known < this.used && this.holds(known, palette)) return known;
|
|
102
|
+
if (this.used >= this.capacity) {
|
|
89
103
|
if (!this.warnedFull) {
|
|
90
104
|
this.warnedFull = true;
|
|
91
105
|
console.warn(
|
|
92
|
-
`WebGPU: more than ${
|
|
93
|
-
'skinned draws are declined this frame
|
|
94
|
-
'
|
|
106
|
+
`WebGPU: more than ${this.capacity} distinct skin palettes in one frame, which is ` +
|
|
107
|
+
'RenderQuality.drawsPerFrame; the rest of the skinned draws are declined this frame ' +
|
|
108
|
+
'rather than drawn in another rig’s pose.',
|
|
95
109
|
);
|
|
96
110
|
}
|
|
97
111
|
return null;
|
|
@@ -103,6 +117,15 @@ export class SkinPaletteRing {
|
|
|
103
117
|
this.slots[at] = slot;
|
|
104
118
|
}
|
|
105
119
|
slot.update(device, palette);
|
|
120
|
+
this.uploads += 1;
|
|
121
|
+
let copy = this.held[at];
|
|
122
|
+
if (copy === undefined || copy.length !== palette.length) {
|
|
123
|
+
/* Once per slot and joint count, the way the slot's own texture is: never per frame. */
|
|
124
|
+
copy = new Float32Array(palette.length);
|
|
125
|
+
this.held[at] = copy;
|
|
126
|
+
}
|
|
127
|
+
copy.set(palette);
|
|
128
|
+
this.lastSlot.set(palette, at);
|
|
106
129
|
return at;
|
|
107
130
|
}
|
|
108
131
|
|
|
@@ -114,6 +137,15 @@ export class SkinPaletteRing {
|
|
|
114
137
|
dispose(): void {
|
|
115
138
|
for (const slot of this.slots) slot.dispose();
|
|
116
139
|
this.slots.length = 0;
|
|
140
|
+
this.held.length = 0;
|
|
117
141
|
this.used = 0;
|
|
118
142
|
}
|
|
143
|
+
|
|
144
|
+
/** Whether a slot was last written with exactly these numbers. */
|
|
145
|
+
private holds(slot: number, palette: Float32Array): boolean {
|
|
146
|
+
const copy = this.held[slot];
|
|
147
|
+
if (copy === undefined || copy.length !== palette.length) return false;
|
|
148
|
+
for (let i = 0; i < copy.length; i++) if (copy[i] !== palette[i]) return false;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
119
151
|
}
|
|
@@ -3,19 +3,21 @@
|
|
|
3
3
|
* that spread them. `skinBlur.ts` is what the blur computes and why; this is where it runs.
|
|
4
4
|
*
|
|
5
5
|
* **What the renderer hands it.** Each skin draw, recorded into the frame with its diffuse taken
|
|
6
|
-
* out, is recorded
|
|
7
|
-
* blended — or ends, if nothing is — the renderer ends its
|
|
8
|
-
* replays them into
|
|
9
|
-
* calls `spread`, which blurs across into a
|
|
6
|
+
* out, is recorded twice more here: with the diffuse's light alone, and with its colour alone.
|
|
7
|
+
* When the frame first draws something blended — or ends, if nothing is — the renderer ends its
|
|
8
|
+
* pass and calls `drawDiffuse`, which replays them into two half-float targets against the frame's
|
|
9
|
+
* own depth; resolves the depth; and calls `spread`, which blurs the light across into a third
|
|
10
|
+
* target and then down, multiplied by the colour, adding into the frame.
|
|
10
11
|
*
|
|
11
12
|
* **The draws are commands, not calls**, for the reason `LatePass` keeps its own pool: the frame
|
|
12
13
|
* graph's is spent at every flush, and these are replayed after the flush that ends the frame's
|
|
13
14
|
* pass. Each holds the per-draw ring slot its frame half used, and those stay valid until the
|
|
14
15
|
* encoder is replaced, which is after this has run.
|
|
15
16
|
*
|
|
16
|
-
* **What it costs**: every skin draw
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* **What it costs**: every skin draw three times — the frame's half, the light alone, and the
|
|
18
|
+
* colour alone, which the spread light is multiplied by; three half-float targets the size of the
|
|
19
|
+
* frame, and two multisampled ones under multisampling; a depth resolve; and two full-screen passes
|
|
20
|
+
* that leave at once wherever no skin was drawn.
|
|
19
21
|
*/
|
|
20
22
|
import { SKIN_PROFILES } from '../../skinBlur.ts';
|
|
21
23
|
import { SKINBLUR_BINDINGS, SKIN_BLUR_FRAG_WGSL } from '../../shaders/generated/skinBlur.wgsl.ts';
|
|
@@ -42,14 +44,20 @@ export class SkinScatterPass {
|
|
|
42
44
|
private readonly linear: GPUSampler;
|
|
43
45
|
private readonly nearest: GPUSampler;
|
|
44
46
|
private readonly commands: CommandPool = createCommandPool(16);
|
|
47
|
+
/** The same draws again, through the pipelines that write the skin's colour alone. */
|
|
48
|
+
private readonly albedoCommands: CommandPool = createCommandPool(16);
|
|
45
49
|
/** Each profile's scatter distance per channel, in metres: four floats a profile. */
|
|
46
50
|
private readonly profiles = new Float32Array(SKIN_PROFILES * 4);
|
|
47
51
|
private diffuse: GPUTexture | null = null;
|
|
48
52
|
private diffuseMsaa: GPUTexture | null = null;
|
|
49
53
|
private across: GPUTexture | null = null;
|
|
54
|
+
private albedo: GPUTexture | null = null;
|
|
55
|
+
private albedoMsaa: GPUTexture | null = null;
|
|
50
56
|
private diffuseView: GPUTextureView | null = null;
|
|
51
57
|
private diffuseMsaaView: GPUTextureView | null = null;
|
|
52
58
|
private acrossView: GPUTextureView | null = null;
|
|
59
|
+
private albedoView: GPUTextureView | null = null;
|
|
60
|
+
private albedoMsaaView: GPUTextureView | null = null;
|
|
53
61
|
private groupAcross: GPUBindGroup | null = null;
|
|
54
62
|
private groupDown: GPUBindGroup | null = null;
|
|
55
63
|
private groupsDepth: GPUTextureView | null = null;
|
|
@@ -59,6 +67,7 @@ export class SkinScatterPass {
|
|
|
59
67
|
this.layout = createPostStageLayout(device, BINDINGS.uniforms, BINDINGS.uniformSize, [
|
|
60
68
|
{ binding: BINDINGS.textures.uSkin },
|
|
61
69
|
{ binding: BINDINGS.textures.uDepth, filterable: false },
|
|
70
|
+
{ binding: BINDINGS.textures.uAlbedo },
|
|
62
71
|
]);
|
|
63
72
|
this.uniforms = device.createBuffer({
|
|
64
73
|
label: 'skin.blurUniforms',
|
|
@@ -89,6 +98,11 @@ export class SkinScatterPass {
|
|
|
89
98
|
return this.commands.commands[takeCommand(this.commands)] as DrawCommand;
|
|
90
99
|
}
|
|
91
100
|
|
|
101
|
+
/** A command for the same draw's colour half, filled by the caller. */
|
|
102
|
+
takeAlbedo(): DrawCommand {
|
|
103
|
+
return this.albedoCommands.commands[takeCommand(this.albedoCommands)] as DrawCommand;
|
|
104
|
+
}
|
|
105
|
+
|
|
92
106
|
/** What one profile scatters, per channel, in metres: the last material to name it says. */
|
|
93
107
|
setProfile(index: number, r: number, g: number, b: number): void {
|
|
94
108
|
const at = Math.min(Math.max(Math.round(index), 0), SKIN_PROFILES - 1) * 4;
|
|
@@ -119,17 +133,21 @@ export class SkinScatterPass {
|
|
|
119
133
|
});
|
|
120
134
|
this.diffuse = make('skin.diffuse', 1);
|
|
121
135
|
this.across = make('skin.across', 1);
|
|
136
|
+
this.albedo = make('skin.albedo', 1);
|
|
122
137
|
this.diffuseMsaa = samples > 1 ? make('skin.diffuseMsaa', samples) : null;
|
|
138
|
+
this.albedoMsaa = samples > 1 ? make('skin.albedoMsaa', samples) : null;
|
|
123
139
|
this.diffuseView = this.diffuse.createView();
|
|
124
140
|
this.acrossView = this.across.createView();
|
|
141
|
+
this.albedoView = this.albedo.createView();
|
|
125
142
|
this.diffuseMsaaView = this.diffuseMsaa?.createView() ?? null;
|
|
143
|
+
this.albedoMsaaView = this.albedoMsaa?.createView() ?? null;
|
|
126
144
|
this.groupAcross = null;
|
|
127
145
|
this.groupDown = null;
|
|
128
146
|
}
|
|
129
147
|
|
|
130
148
|
/**
|
|
131
|
-
* Every kept draw's diffuse into the target,
|
|
132
|
-
*
|
|
149
|
+
* Every kept draw's diffuse into the target, and its colour into the other, against the frame's
|
|
150
|
+
* own depth, which they read and do not write. Called with no pass open on `encoder`.
|
|
133
151
|
*/
|
|
134
152
|
drawDiffuse(
|
|
135
153
|
encoder: GPUCommandEncoder,
|
|
@@ -157,6 +175,26 @@ export class SkinScatterPass {
|
|
|
157
175
|
issue(pass, this.commands.commands[i] as DrawCommand);
|
|
158
176
|
}
|
|
159
177
|
pass.end();
|
|
178
|
+
|
|
179
|
+
const colour = this.albedoView;
|
|
180
|
+
if (colour === null) return;
|
|
181
|
+
const albedoPass = encoder.beginRenderPass({
|
|
182
|
+
label: 'skin.albedo',
|
|
183
|
+
colorAttachments: [
|
|
184
|
+
{
|
|
185
|
+
view: this.albedoMsaaView ?? colour,
|
|
186
|
+
resolveTarget: this.albedoMsaaView === null ? undefined : colour,
|
|
187
|
+
clearValue: [0, 0, 0, 0],
|
|
188
|
+
loadOp: 'clear',
|
|
189
|
+
storeOp: this.albedoMsaaView === null ? 'store' : 'discard',
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
depthStencilAttachment: { view: depth, depthLoadOp: 'load', depthStoreOp: 'store' },
|
|
193
|
+
});
|
|
194
|
+
for (let i = 0; i < this.albedoCommands.taken; i += 1) {
|
|
195
|
+
issue(albedoPass, this.albedoCommands.commands[i] as DrawCommand);
|
|
196
|
+
}
|
|
197
|
+
albedoPass.end();
|
|
160
198
|
}
|
|
161
199
|
|
|
162
200
|
/**
|
|
@@ -177,6 +215,7 @@ export class SkinScatterPass {
|
|
|
177
215
|
): void {
|
|
178
216
|
const diffuse = this.diffuse;
|
|
179
217
|
if (diffuse === null || this.diffuseView === null || this.acrossView === null) return;
|
|
218
|
+
if (this.albedoView === null) return;
|
|
180
219
|
if (this.groupAcross === null || this.groupDown === null || this.groupsDepth !== depth) {
|
|
181
220
|
this.groupAcross = this.group('skin.blurAcross', this.diffuseView, depth);
|
|
182
221
|
this.groupDown = this.group('skin.blurDown', this.acrossView, depth);
|
|
@@ -244,6 +283,7 @@ export class SkinScatterPass {
|
|
|
244
283
|
/** Spent at the frame's start: the kept draws were this frame's. */
|
|
245
284
|
reset(): void {
|
|
246
285
|
resetPool(this.commands);
|
|
286
|
+
resetPool(this.albedoCommands);
|
|
247
287
|
}
|
|
248
288
|
|
|
249
289
|
dispose(): void {
|
|
@@ -264,6 +304,8 @@ export class SkinScatterPass {
|
|
|
264
304
|
{ binding: BINDINGS.textures.uSkin.sampler, resource: this.linear },
|
|
265
305
|
{ binding: BINDINGS.textures.uDepth.texture, resource: depth },
|
|
266
306
|
{ binding: BINDINGS.textures.uDepth.sampler, resource: this.nearest },
|
|
307
|
+
{ binding: BINDINGS.textures.uAlbedo.texture, resource: this.albedoView ?? skin },
|
|
308
|
+
{ binding: BINDINGS.textures.uAlbedo.sampler, resource: this.nearest },
|
|
267
309
|
],
|
|
268
310
|
});
|
|
269
311
|
}
|
|
@@ -291,6 +333,8 @@ export class SkinScatterPass {
|
|
|
291
333
|
too wherever a pixel is square. */
|
|
292
334
|
f[base + fields.uFocal.offset / 4] = ((projection[5] as number) * height) / 2;
|
|
293
335
|
f.set(this.profiles, base + fields.uProfiles.offset / 4);
|
|
336
|
+
/* The colour is applied once, on the axis that adds into the frame. */
|
|
337
|
+
f[base + fields.uApplyAlbedo.offset / 4] = axis;
|
|
294
338
|
}
|
|
295
339
|
this.device.queue.writeBuffer(this.uniforms, 0, f);
|
|
296
340
|
}
|
|
@@ -299,11 +343,17 @@ export class SkinScatterPass {
|
|
|
299
343
|
this.diffuse?.destroy();
|
|
300
344
|
this.diffuseMsaa?.destroy();
|
|
301
345
|
this.across?.destroy();
|
|
346
|
+
this.albedo?.destroy();
|
|
347
|
+
this.albedoMsaa?.destroy();
|
|
302
348
|
this.diffuse = null;
|
|
303
349
|
this.diffuseMsaa = null;
|
|
304
350
|
this.across = null;
|
|
351
|
+
this.albedo = null;
|
|
352
|
+
this.albedoMsaa = null;
|
|
305
353
|
this.diffuseView = null;
|
|
306
354
|
this.diffuseMsaaView = null;
|
|
307
355
|
this.acrossView = null;
|
|
356
|
+
this.albedoView = null;
|
|
357
|
+
this.albedoMsaaView = null;
|
|
308
358
|
}
|
|
309
359
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { SurfaceTextureOptions } from '../../surfaceTexture.ts';
|
|
2
2
|
import { SURFACE_EFFECT_TEXELS, packSurfaceEffects } from '../../surfaceEffects.ts';
|
|
3
3
|
import { toHalfFloats } from '../../halfFloat.ts';
|
|
4
|
+
import { isLightmapTexels } from '../../lightmap.ts';
|
|
5
|
+
import type { LightmapTexels } from '../../lightmap.ts';
|
|
4
6
|
import { layerSize, refuseArrayUpdate, sourceSize } from '../../textureSource.ts';
|
|
5
7
|
import { compressedLayers, planBlocks, refuseBlockUpdate } from '../../compressedSource.ts';
|
|
6
8
|
import type {
|
|
@@ -193,17 +195,36 @@ export class GpuSurfaceTexture {
|
|
|
193
195
|
constructor(
|
|
194
196
|
private readonly device: GPUDevice,
|
|
195
197
|
private readonly pipelines: PipelineCache,
|
|
196
|
-
source: SurfaceSource | readonly SurfaceSource[],
|
|
198
|
+
source: SurfaceSource | readonly SurfaceSource[] | LightmapTexels,
|
|
197
199
|
options: SurfaceTextureOptions = {},
|
|
198
200
|
compressed: readonly CompressedTextureFormat[] = [],
|
|
199
201
|
) {
|
|
200
|
-
|
|
202
|
+
/* A lightmap's page: half floats, uploaded as they are. See `lightmap.ts`. */
|
|
203
|
+
const baked = isLightmapTexels(source) ? source : null;
|
|
204
|
+
const listed = (
|
|
205
|
+
baked !== null ? [] : Array.isArray(source) ? source : [source]
|
|
206
|
+
) as readonly SurfaceSource[];
|
|
201
207
|
const blocks = compressedLayers(listed);
|
|
202
208
|
const srgb = (options.colorSpace ?? 'linear') === 'srgb';
|
|
203
209
|
/* `SRGB8_ALPHA8`'s equivalent. Decoded in the sampler, before filtering, which is the only
|
|
204
210
|
place it is correct — `surfaceTexture.ts` makes the argument in full. */
|
|
205
|
-
this.format = srgb ? 'rgba8unorm-srgb' : 'rgba8unorm';
|
|
206
|
-
if (
|
|
211
|
+
this.format = baked !== null ? 'rgba16float' : srgb ? 'rgba8unorm-srgb' : 'rgba8unorm';
|
|
212
|
+
if (baked !== null) {
|
|
213
|
+
/* No chain: a page is read at its own resolution, and a level of it would blend regions. */
|
|
214
|
+
this.layers = baked.layers;
|
|
215
|
+
this.blockFormat = null;
|
|
216
|
+
this.mipmapped = false;
|
|
217
|
+
this.width = baked.width;
|
|
218
|
+
this.height = baked.height;
|
|
219
|
+
this.levels = 1;
|
|
220
|
+
this.texture = this.allocate();
|
|
221
|
+
device.queue.writeTexture(
|
|
222
|
+
{ texture: this.texture },
|
|
223
|
+
baked.texels,
|
|
224
|
+
{ bytesPerRow: baked.width * 8, rowsPerImage: baked.height },
|
|
225
|
+
{ width: baked.width, height: baked.height, depthOrArrayLayers: baked.layers },
|
|
226
|
+
);
|
|
227
|
+
} else if (blocks !== null) {
|
|
207
228
|
/* Refused before anything is allocated; the stored chain, or level 0 alone where no chain
|
|
208
229
|
was asked for. See `compressedUpload.ts`. */
|
|
209
230
|
const plan = planBlocks(blocks, srgb, compressed);
|
|
@@ -261,7 +282,7 @@ export class GpuSurfaceTexture {
|
|
|
261
282
|
: 1,
|
|
262
283
|
});
|
|
263
284
|
|
|
264
|
-
if (blocks === null) this.upload(listed as readonly TexImageSource[]);
|
|
285
|
+
if (blocks === null && baked === null) this.upload(listed as readonly TexImageSource[]);
|
|
265
286
|
|
|
266
287
|
/* Half floats, as `renderer.ts` uploads it: filterable, so the ordinary float layout takes it,
|
|
267
288
|
and read by `textureLoad` alone, so the filter never runs. */
|
package/src/render/bloomChain.ts
CHANGED
|
@@ -81,6 +81,75 @@ export function bloomProfileWarning(quality: {
|
|
|
81
81
|
return null;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* How a frame's bloom answers the light it is given, beside the strength and threshold `setBloom`
|
|
86
|
+
* already takes. Absent, or a field absent, is the response every frame had before 4.8.6.
|
|
87
|
+
*
|
|
88
|
+
* **Why it exists: one band over the colour less the threshold washes a stage built for another
|
|
89
|
+
* engine.** Unreal's standard bloom keeps `saturate((L − threshold) / 2)` of a colour rather than
|
|
90
|
+
* the colour less the threshold, and spreads it over six bands, each in a tint of its own. A
|
|
91
|
+
* stage authored against that — a threshold of 0.1 and tints summing below one — came out white
|
|
92
|
+
* here, because subtracting so low a threshold keeps nearly every light in the frame at full
|
|
93
|
+
* strength. These are the two things that differ, as numbers.
|
|
94
|
+
*/
|
|
95
|
+
export interface BloomResponse {
|
|
96
|
+
/**
|
|
97
|
+
* Scene units over which a colour comes in, from none of it at the threshold to all of it this
|
|
98
|
+
* far past. **0, the default, subtracts instead**: a colour keeps what it has past the threshold,
|
|
99
|
+
* the response since bloom shipped. Unreal's is 2 after exposure, so a frame exposed by `e` that
|
|
100
|
+
* wants it passes `2 / e`, as it passes the threshold over the exposure.
|
|
101
|
+
*
|
|
102
|
+
* Brightness is still the largest channel rather than luminance, for the reason
|
|
103
|
+
* `shaders/bloom.ts` gives; on a neutral colour the two are equal.
|
|
104
|
+
*/
|
|
105
|
+
readonly ramp?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Each level's colour, three numbers a level, finest first: the halo at a few pixels first and
|
|
108
|
+
* at a twentieth of the frame last. **White, the default, is the identity**; a level not named
|
|
109
|
+
* takes white. Not clamped above one, which is how a tint carries a strength past the 0..1 the
|
|
110
|
+
* dial gives — a stage asking for twice the light names tints twice as bright.
|
|
111
|
+
*
|
|
112
|
+
* At most `BLOOM_LEVELS` levels. A frame too small for that many uses the first it has room for.
|
|
113
|
+
*/
|
|
114
|
+
readonly tints?: ArrayLike<number>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Floats `resolveBloomResponse` writes: the ramp, then a tint a level. */
|
|
118
|
+
export const BLOOM_RESPONSE_FLOATS = 1 + BLOOM_LEVELS * 3;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A response as both backends bind it, into a caller-owned `out` of `BLOOM_RESPONSE_FLOATS`: the
|
|
122
|
+
* ramp, then `BLOOM_LEVELS` tints, white where none was named. Refuses a tint list that is not
|
|
123
|
+
* whole colours or names more levels than the pyramid has, naming the length — a caller error with
|
|
124
|
+
* one correct outcome. A ramp that is not a positive number is the subtraction; a tint channel
|
|
125
|
+
* that is not a number or is below zero is zero.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveBloomResponse(
|
|
128
|
+
response: BloomResponse | null | undefined,
|
|
129
|
+
out: Float32Array,
|
|
130
|
+
): void {
|
|
131
|
+
const ramp = response?.ramp ?? 0;
|
|
132
|
+
out[0] = Number.isFinite(ramp) && ramp > 0 ? ramp : 0;
|
|
133
|
+
const tints = response?.tints;
|
|
134
|
+
if (tints !== undefined && (tints.length % 3 !== 0 || tints.length > BLOOM_LEVELS * 3)) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`setBloom: ${tints.length} tint values is not up to ${BLOOM_LEVELS} whole colours, three ` +
|
|
137
|
+
'numbers a level, finest first',
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
for (let i = 0; i < BLOOM_LEVELS * 3; i++) {
|
|
141
|
+
const value = tints !== undefined && i < tints.length ? (tints[i] as number) : 1;
|
|
142
|
+
out[1 + i] = Number.isFinite(value) && value > 0 ? value : 0;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The response every frame had before 4.8.6: the subtraction, every level white. */
|
|
147
|
+
export function defaultBloomResponse(): Float32Array {
|
|
148
|
+
const out = new Float32Array(BLOOM_RESPONSE_FLOATS);
|
|
149
|
+
resolveBloomResponse(null, out);
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
84
153
|
/** One level of the pyramid: level 0 is half the frame and each one after it half again. */
|
|
85
154
|
export interface BloomLevelSize {
|
|
86
155
|
readonly width: number;
|
package/src/render/bloomPass.ts
CHANGED
|
@@ -159,7 +159,14 @@ export class BloomPass {
|
|
|
159
159
|
* `threshold` is in scene units, so it only separates a light from white paint on a target
|
|
160
160
|
* that kept the range. The caller owns that decision and the warning that goes with it.
|
|
161
161
|
*/
|
|
162
|
-
run(
|
|
162
|
+
run(
|
|
163
|
+
scene: WebGLTexture,
|
|
164
|
+
width: number,
|
|
165
|
+
height: number,
|
|
166
|
+
threshold: number,
|
|
167
|
+
/** The ramp, then each level's tint: `resolveBloomResponse`'s floats. */
|
|
168
|
+
response: Float32Array,
|
|
169
|
+
): WebGLTexture | null {
|
|
163
170
|
if (!this.ensureSize(width, height)) return null;
|
|
164
171
|
const { gl } = this;
|
|
165
172
|
const levels = this.levels;
|
|
@@ -182,6 +189,7 @@ export class BloomPass {
|
|
|
182
189
|
gl.bindTexture(gl.TEXTURE_2D, scene);
|
|
183
190
|
gl.uniform1i(this.prefilterUniforms['uSource'] ?? null, 0);
|
|
184
191
|
gl.uniform1f(this.prefilterUniforms['uThreshold'] ?? null, threshold);
|
|
192
|
+
gl.uniform1f(this.prefilterUniforms['uRamp'] ?? null, response[0] ?? 0);
|
|
185
193
|
this.uploadTexel(this.prefilterUniforms, width, height);
|
|
186
194
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
187
195
|
|
|
@@ -204,8 +212,13 @@ export class BloomPass {
|
|
|
204
212
|
* every octave at once. Additive blending rather than a second target to ping-pong through:
|
|
205
213
|
* a level is only ever read while a *different* level is bound, so there is no feedback.
|
|
206
214
|
*/
|
|
215
|
+
/*
|
|
216
|
+
* Each level's tint rides the blend: the level being added onto is multiplied by its own as the
|
|
217
|
+
* octave below is added, and the deepest by `uScale` on the first draw, the only one reading it
|
|
218
|
+
* before anything has been added to it. White everywhere is `ONE, ONE` exactly.
|
|
219
|
+
*/
|
|
207
220
|
gl.enable(gl.BLEND);
|
|
208
|
-
gl.blendFunc(gl.ONE, gl.
|
|
221
|
+
gl.blendFunc(gl.ONE, gl.CONSTANT_COLOR);
|
|
209
222
|
gl.useProgram(this.upsampleProgram);
|
|
210
223
|
gl.uniform1i(this.upsampleUniforms['uSource'] ?? null, 0);
|
|
211
224
|
gl.uniform1f(this.upsampleUniforms['uRadius'] ?? null, BLOOM_FILTER_RADIUS_UV);
|
|
@@ -213,12 +226,23 @@ export class BloomPass {
|
|
|
213
226
|
const target = levels[index - 1];
|
|
214
227
|
const source = levels[index];
|
|
215
228
|
if (target === undefined || source === undefined) break;
|
|
229
|
+
const deepest = index === levels.length - 1;
|
|
230
|
+
const into = 1 + (index - 1) * 3;
|
|
231
|
+
const from = 1 + index * 3;
|
|
232
|
+
gl.blendColor(response[into] ?? 1, response[into + 1] ?? 1, response[into + 2] ?? 1, 1);
|
|
233
|
+
gl.uniform3f(
|
|
234
|
+
this.upsampleUniforms['uScale'] ?? null,
|
|
235
|
+
deepest ? (response[from] ?? 1) : 1,
|
|
236
|
+
deepest ? (response[from + 1] ?? 1) : 1,
|
|
237
|
+
deepest ? (response[from + 2] ?? 1) : 1,
|
|
238
|
+
);
|
|
216
239
|
gl.bindFramebuffer(gl.FRAMEBUFFER, target.framebuffer);
|
|
217
240
|
gl.viewport(0, 0, target.width, target.height);
|
|
218
241
|
gl.bindTexture(gl.TEXTURE_2D, source.texture);
|
|
219
242
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
220
243
|
}
|
|
221
244
|
|
|
245
|
+
gl.blendColor(0, 0, 0, 0);
|
|
222
246
|
gl.disable(gl.BLEND);
|
|
223
247
|
gl.bindVertexArray(null);
|
|
224
248
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|