@driftengine/core 4.8.1 → 4.8.2

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.
Files changed (62) hide show
  1. package/CHANGELOG.json +86 -0
  2. package/dist/index.d.ts +2 -1
  3. package/dist/index.js +2 -0
  4. package/dist/input/actionMap.d.ts +6 -1
  5. package/dist/input/actionMap.js +20 -7
  6. package/dist/input/input.d.ts +28 -0
  7. package/dist/input/input.js +71 -9
  8. package/dist/input/inputHarness.d.ts +9 -0
  9. package/dist/input/inputHarness.js +34 -2
  10. package/dist/render/backend/webgl2/renderer.d.ts +52 -3
  11. package/dist/render/backend/webgl2/renderer.js +151 -5
  12. package/dist/render/backend/webgpu/buffers.d.ts +2 -0
  13. package/dist/render/backend/webgpu/buffers.js +2 -0
  14. package/dist/render/backend/webgpu/flatPass.js +16 -7
  15. package/dist/render/backend/webgpu/pipelineCache.d.ts +40 -8
  16. package/dist/render/backend/webgpu/pipelineCache.js +76 -9
  17. package/dist/render/backend/webgpu/renderer.d.ts +56 -1
  18. package/dist/render/backend/webgpu/renderer.js +151 -26
  19. package/dist/render/mapsWithoutUvs.d.ts +24 -0
  20. package/dist/render/mapsWithoutUvs.js +24 -0
  21. package/dist/render/mesh.d.ts +2 -0
  22. package/dist/render/mesh.js +3 -0
  23. package/dist/render/particlePool.d.ts +15 -0
  24. package/dist/render/particlePool.js +72 -37
  25. package/dist/render/pass.d.ts +12 -0
  26. package/dist/render/renderQuality.d.ts +7 -6
  27. package/dist/render/shaders/flat/glassTint.d.ts +28 -0
  28. package/dist/render/shaders/flat/glassTint.js +31 -0
  29. package/dist/render/shaders/flat/index.d.ts +12 -0
  30. package/dist/render/shaders/flat/index.js +7 -1
  31. package/dist/render/shaders/flat/main.d.ts +1 -1
  32. package/dist/render/shaders/flat/main.js +60 -25
  33. package/dist/render/shaders/flat/surfaceEffects.d.ts +1 -1
  34. package/dist/render/shaders/flat/surfaceEffects.js +6 -2
  35. package/dist/render/shaders/generated/flat.wgsl.d.ts +64 -0
  36. package/dist/render/shaders/generated/flat.wgsl.js +102 -38
  37. package/dist/render/surfaceTexture.d.ts +2 -0
  38. package/dist/render/surfaceTexture.js +4 -0
  39. package/dist/render/viewModel.d.ts +29 -0
  40. package/dist/render/viewModel.js +35 -0
  41. package/package.json +6 -6
  42. package/src/index.ts +9 -1
  43. package/src/input/actionMap.ts +23 -8
  44. package/src/input/input.ts +79 -9
  45. package/src/input/inputHarness.ts +27 -2
  46. package/src/render/backend/webgl2/renderer.ts +166 -13
  47. package/src/render/backend/webgpu/buffers.ts +5 -0
  48. package/src/render/backend/webgpu/flatPass.ts +41 -25
  49. package/src/render/backend/webgpu/pipelineCache.ts +82 -8
  50. package/src/render/backend/webgpu/renderer.ts +151 -26
  51. package/src/render/mapsWithoutUvs.ts +43 -0
  52. package/src/render/mesh.ts +3 -0
  53. package/src/render/particlePool.ts +101 -54
  54. package/src/render/pass.ts +12 -0
  55. package/src/render/renderQuality.ts +7 -6
  56. package/src/render/shaders/flat/glassTint.ts +48 -0
  57. package/src/render/shaders/flat/index.ts +19 -1
  58. package/src/render/shaders/flat/main.ts +60 -25
  59. package/src/render/shaders/flat/surfaceEffects.ts +6 -2
  60. package/src/render/shaders/generated/flat.wgsl.ts +102 -38
  61. package/src/render/surfaceTexture.ts +5 -0
  62. package/src/render/viewModel.ts +41 -0
@@ -38,6 +38,15 @@ export class ParticlePool {
38
38
  * together.
39
39
  */
40
40
  seedOf;
41
+ /**
42
+ * Where each slot's particle sits in the drawn streams, or -1 where it sits nowhere.
43
+ *
44
+ * **What lets `emit` draw a particle the frame it is born.** The streams are compacted, so a
45
+ * slot's entry is wherever `update` last put it; an emit into a slot that has one overwrites
46
+ * that entry, and an emit into one that has none appends. Without it a new particle waited for
47
+ * the next `update` to be drawn at all.
48
+ */
49
+ drawnAt;
41
50
  next = 0;
42
51
  constructor(options) {
43
52
  this.options = options;
@@ -60,6 +69,7 @@ export class ParticlePool {
60
69
  this.spin = new Float32Array(capacity);
61
70
  this.scale = new Float32Array(capacity).fill(1);
62
71
  this.seedOf = new Float32Array(capacity);
72
+ this.drawnAt = new Int32Array(capacity).fill(-1);
63
73
  }
64
74
  /** How many particles are alive. */
65
75
  get live() {
@@ -95,6 +105,18 @@ export class ParticlePool {
95
105
  this.spin[i] = (hash - Math.floor(hash)) * Math.PI * 2;
96
106
  this.scale[i] = sizeScale;
97
107
  this.seedOf[i] = seed;
108
+ /*
109
+ * Drawn now, at birth, rather than at the next `update`: into the entry this slot already
110
+ * has when it is taking over a particle that was drawn, and onto the end otherwise.
111
+ */
112
+ const held = this.drawnAt[i];
113
+ const at = held >= 0 ? held : this.particles.count;
114
+ this.writeDrawn(i, at, 0, x, y, z, vx, vy, vz);
115
+ if (held < 0) {
116
+ this.drawnAt[i] = at;
117
+ this.instances.count = at + 1;
118
+ this.particles.count = at + 1;
119
+ }
98
120
  }
99
121
  /**
100
122
  * Advance every particle and rebuild the instance buffer.
@@ -108,12 +130,15 @@ export class ParticlePool {
108
130
  let count = 0;
109
131
  for (let i = 0; i < this.age.length; i++) {
110
132
  const age = this.age[i];
111
- if (age === Infinity)
133
+ if (age === Infinity) {
134
+ this.drawnAt[i] = -1;
112
135
  continue;
136
+ }
113
137
  const life = this.life[i];
114
138
  const next = age + dt;
115
139
  if (next >= life) {
116
140
  this.age[i] = Infinity;
141
+ this.drawnAt[i] = -1;
117
142
  continue;
118
143
  }
119
144
  this.age[i] = next;
@@ -131,50 +156,60 @@ export class ParticlePool {
131
156
  this.x[i] = px;
132
157
  this.y[i] = py;
133
158
  this.z[i] = pz;
134
- const t = next / life;
135
- const size = (o.sizeStart + (o.sizeEnd - o.sizeStart) * t) * this.scale[i];
136
- const r = o.colorStart[0] + (o.colorEnd[0] - o.colorStart[0]) * t;
137
- const g = o.colorStart[1] + (o.colorEnd[1] - o.colorStart[1]) * t;
138
- const b = o.colorStart[2] + (o.colorEnd[2] - o.colorStart[2]) * t;
139
- /*
140
- * Colour carries the fade rather than an alpha channel: the instanced
141
- * path multiplies a tint into the base mesh and has no per-instance
142
- * opacity, and for an additive puff darkening to nothing *is* fading out.
143
- * It also means the effect needs no blend-state change.
144
- */
145
- const fade = 1 - t * t;
146
- writeInstance(this.instances, count, px, py, pz, size, this.spin[i], r * fade, g * fade, b * fade, 0, 0, 0);
147
- /*
148
- * The same particle for a material that has a real opacity channel. Colour
149
- * is written *unfaded* here, because dimming it is the scatter path's
150
- * workaround and doing both would fade twice — a puff that vanished at half
151
- * its life while still occluding what was behind it.
152
- */
153
- const p = this.particles;
154
- const alphaStart = o.alphaStart ?? 1;
155
- const alphaEnd = o.alphaEnd ?? 0;
156
- p.positions[count * 3] = px;
157
- p.positions[count * 3 + 1] = py;
158
- p.positions[count * 3 + 2] = pz;
159
- p.sizes[count] = size;
160
- p.spins[count] = this.spin[i];
161
- p.colors[count * 3] = r;
162
- p.colors[count * 3 + 1] = g;
163
- p.colors[count * 3 + 2] = b;
164
- p.alphas[count] = alphaStart + (alphaEnd - alphaStart) * t;
165
- p.ages[count] = t;
166
- p.seeds[count] = this.seedOf[i];
167
- p.velocities[count * 3] = vx;
168
- p.velocities[count * 3 + 1] = vy;
169
- p.velocities[count * 3 + 2] = vz;
159
+ this.drawnAt[i] = count;
160
+ this.writeDrawn(i, count, next / life, px, py, pz, vx, vy, vz);
170
161
  count++;
171
162
  }
172
163
  this.instances.count = count;
173
164
  this.particles.count = count;
174
165
  }
166
+ /**
167
+ * One particle's entry in both drawn streams, at `t` of its life: what `update` writes for every
168
+ * live particle and `emit` for a new one, so the two cannot disagree about what a particle looks
169
+ * like.
170
+ */
171
+ writeDrawn(i, count, t, px, py, pz, vx, vy, vz) {
172
+ const o = this.options;
173
+ const size = (o.sizeStart + (o.sizeEnd - o.sizeStart) * t) * this.scale[i];
174
+ const r = o.colorStart[0] + (o.colorEnd[0] - o.colorStart[0]) * t;
175
+ const g = o.colorStart[1] + (o.colorEnd[1] - o.colorStart[1]) * t;
176
+ const b = o.colorStart[2] + (o.colorEnd[2] - o.colorStart[2]) * t;
177
+ /*
178
+ * Colour carries the fade rather than an alpha channel: the instanced
179
+ * path multiplies a tint into the base mesh and has no per-instance
180
+ * opacity, and for an additive puff darkening to nothing *is* fading out.
181
+ * It also means the effect needs no blend-state change.
182
+ */
183
+ const fade = 1 - t * t;
184
+ writeInstance(this.instances, count, px, py, pz, size, this.spin[i], r * fade, g * fade, b * fade, 0, 0, 0);
185
+ /*
186
+ * The same particle for a material that has a real opacity channel. Colour
187
+ * is written *unfaded* here, because dimming it is the scatter path's
188
+ * workaround and doing both would fade twice — a puff that vanished at half
189
+ * its life while still occluding what was behind it.
190
+ */
191
+ const p = this.particles;
192
+ const alphaStart = o.alphaStart ?? 1;
193
+ const alphaEnd = o.alphaEnd ?? 0;
194
+ p.positions[count * 3] = px;
195
+ p.positions[count * 3 + 1] = py;
196
+ p.positions[count * 3 + 2] = pz;
197
+ p.sizes[count] = size;
198
+ p.spins[count] = this.spin[i];
199
+ p.colors[count * 3] = r;
200
+ p.colors[count * 3 + 1] = g;
201
+ p.colors[count * 3 + 2] = b;
202
+ p.alphas[count] = alphaStart + (alphaEnd - alphaStart) * t;
203
+ p.ages[count] = t;
204
+ p.seeds[count] = this.seedOf[i];
205
+ p.velocities[count * 3] = vx;
206
+ p.velocities[count * 3 + 1] = vy;
207
+ p.velocities[count * 3 + 2] = vz;
208
+ }
175
209
  /** Drop everything, for a respawn or a scene change. */
176
210
  clear() {
177
211
  this.age.fill(Infinity);
212
+ this.drawnAt.fill(-1);
178
213
  this.instances.count = 0;
179
214
  this.particles.count = 0;
180
215
  this.next = 0;
@@ -122,6 +122,18 @@ export type PassContext = {
122
122
  readonly pass: GPURenderPassEncoder;
123
123
  /** The frame's jitter, the same array `PrepareContext.jitter` held. */
124
124
  readonly jitter: Float32Array;
125
+ /**
126
+ * The colour format, depth format and sample count of the pass this draw lands in, which a
127
+ * pipeline set on it must match exactly.
128
+ *
129
+ * **Not always the device's.** `PassDevice` says what the frame is; a pass drawn after
130
+ * `endFrame` lands on the canvas, at the canvas's format and one sample, where the frame is
131
+ * the scene target at the scene's samples. A pass that is drawn in both places keeps a
132
+ * pipeline for each and chooses here; one built only from the device is refused by the other.
133
+ */
134
+ readonly format: GPUTextureFormat;
135
+ readonly depthFormat: GPUTextureFormat;
136
+ readonly samples: number;
125
137
  });
126
138
  /**
127
139
  * What a pass is handed once a frame, before the frame's own target exists.
@@ -185,12 +185,13 @@ export interface RenderQuality {
185
185
  * existed: a pane casts nothing at all.
186
186
  *
187
187
  * Nothing is allocated until a glass caster first reaches a shadow, so a scene with no glass pays
188
- * no memory and no pass whatever this says. **The lit shader is the exception**: `'full'` and
189
- * `'half'` compile the glass lookup into it, and a lookup that finds no glass and returns at its
190
- * first line still costs the lit pass its registers — measured on the courtyard at night, 0.27 ms
191
- * at 720p on WebGPU, 0.42 ms on WebGL2 and 1.2 ms at 4K. `'off'` compiles none of it, so a world
192
- * that never casts through glass is that much faster for saying so. What would make `'full'` wrong
193
- * is a phone budget with a lot of glass; that is what `'half'` is for. See `glassShadow.ts`.
188
+ * no memory and no pass whatever this says. **Nor any shader, since 4.8.2**: the lit shader is
189
+ * built without the glass lookup and rebuilt with it when the first glass caster reaches a shadow.
190
+ * A lookup that finds no glass still cost the lit pass its registers — measured on the courtyard at
191
+ * night, 0.27 ms at 720p on WebGPU, 0.42 ms on WebGL2 and 1.2 ms at 4K — and from 4.5.0 to 4.8.1
192
+ * every scene paid it, glass or none: about 30% of the lit shader's instructions, which on a phone
193
+ * was the lit pass. `'off'` never rebuilds. What would make `'full'` wrong is a phone budget with a
194
+ * lot of glass; that is what `'half'` is for. See `glassShadow.ts`.
194
195
  */
195
196
  readonly glassShadows: GlassShadows;
196
197
  /** Horizontal world-space reach over which a directional shadow dissolves. */
@@ -4,3 +4,31 @@
4
4
  * outside every conditional, so each lookup can call it whichever shadows a permutation compiles.
5
5
  */
6
6
  export declare function glassTintGlsl(glassShadows: boolean): string;
7
+ /**
8
+ * The lit stage's switches, by the name of the constant each is: the decision both backends make
9
+ * about which optional code a lit program carries. See `litSwitchesGlsl`.
10
+ */
11
+ export type LitSwitch = 'GLASS_SHADOWS' | 'CLUSTERED_LIGHTS' | 'LIGHT_FIXTURES' | 'SURFACE_EFFECTS' | 'DRIFT_LIGHT';
12
+ /** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
13
+ export declare function noLitSwitches(): Record<LitSwitch, boolean>;
14
+ /** Which of the lit stage's optional features a build reads. See `litSwitchesGlsl`. */
15
+ export interface LitSwitches {
16
+ readonly clusteredLights: boolean;
17
+ readonly lightFixtures: boolean;
18
+ readonly surfaceEffects: boolean;
19
+ readonly driftLight: boolean;
20
+ }
21
+ /**
22
+ * The lit stage's other switches, declared after `GLASS_SHADOWS` so that one keeps the id 0 the
23
+ * pipelines and their tests already name, each a pipeline-overridable constant on the generated
24
+ * WGSL and a written-in value on WebGL2 — the arrangement `GLASS_SHADOWS` has, for its reason:
25
+ * code a branch never runs still costs a shader its registers, and the device compiles it away
26
+ * only where the switch is a constant.
27
+ *
28
+ * **Clustering is the profile's and fixed.** The other three are content, off until a consumer
29
+ * first uses them — a measured profile or a cookie, a material with an effects table, a DriftLight
30
+ * volume — and then on for good; see `PipelineCache.enable` and the WebGL2 renderer's rebuild.
31
+ * Measured on RADV against the lit shader with glass already out: 6,852 instructions and 120
32
+ * registers, 4,641 and 96 with all four out.
33
+ */
34
+ export declare function litSwitchesGlsl(switches: LitSwitches): string;
@@ -23,3 +23,34 @@ vec4 glassUnmix(vec4 mip, float centreClarity) {
23
23
  }
24
24
  `;
25
25
  }
26
+ /** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
27
+ export function noLitSwitches() {
28
+ return {
29
+ GLASS_SHADOWS: false,
30
+ CLUSTERED_LIGHTS: false,
31
+ LIGHT_FIXTURES: false,
32
+ SURFACE_EFFECTS: false,
33
+ DRIFT_LIGHT: false,
34
+ };
35
+ }
36
+ /**
37
+ * The lit stage's other switches, declared after `GLASS_SHADOWS` so that one keeps the id 0 the
38
+ * pipelines and their tests already name, each a pipeline-overridable constant on the generated
39
+ * WGSL and a written-in value on WebGL2 — the arrangement `GLASS_SHADOWS` has, for its reason:
40
+ * code a branch never runs still costs a shader its registers, and the device compiles it away
41
+ * only where the switch is a constant.
42
+ *
43
+ * **Clustering is the profile's and fixed.** The other three are content, off until a consumer
44
+ * first uses them — a measured profile or a cookie, a material with an effects table, a DriftLight
45
+ * volume — and then on for good; see `PipelineCache.enable` and the WebGL2 renderer's rebuild.
46
+ * Measured on RADV against the lit shader with glass already out: 6,852 instructions and 120
47
+ * registers, 4,641 and 96 with all four out.
48
+ */
49
+ export function litSwitchesGlsl(switches) {
50
+ return /* glsl */ `
51
+ const bool CLUSTERED_LIGHTS = ${switches.clusteredLights ? 'true' : 'false'}; // wgsl:override
52
+ const bool LIGHT_FIXTURES = ${switches.lightFixtures ? 'true' : 'false'}; // wgsl:override
53
+ const bool SURFACE_EFFECTS = ${switches.surfaceEffects ? 'true' : 'false'}; // wgsl:override
54
+ const bool DRIFT_LIGHT = ${switches.driftLight ? 'true' : 'false'}; // wgsl:override
55
+ `;
56
+ }
@@ -92,6 +92,18 @@ export interface FlatShaderOptions {
92
92
  * courtyard at night, measured, and nothing once compiled out.
93
93
  */
94
94
  readonly glassShadows?: boolean;
95
+ /**
96
+ * Whether this build has the clustered arm of the light loop: `clusteredLights`, fixed per
97
+ * renderer. True when absent, which is what the generated WGSL is built with. See
98
+ * `litSwitchesGlsl`.
99
+ */
100
+ readonly clusteredLights?: boolean;
101
+ /** Whether it reads measured profiles and cookies. True when absent. See `litSwitchesGlsl`. */
102
+ readonly lightFixtures?: boolean;
103
+ /** Whether it reads a material's effects table. True when absent. See `litSwitchesGlsl`. */
104
+ readonly surfaceEffects?: boolean;
105
+ /** Whether it reads a DriftLight volume. True when absent. See `litSwitchesGlsl`. */
106
+ readonly driftLight?: boolean;
95
107
  /**
96
108
  * How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
97
109
  *
@@ -7,7 +7,7 @@ import { preambleGlsl } from './preamble.js';
7
7
  import { LOBES_GLSL } from './lobes.js';
8
8
  import { POINTSHADOW_GLSL } from './pointShadow.js';
9
9
  import { DIRECTIONALSHADOW_GLSL } from './directionalShadow.js';
10
- import { glassTintGlsl } from './glassTint.js';
10
+ import { glassTintGlsl, litSwitchesGlsl } from './glassTint.js';
11
11
  import { MORPH_GLSL } from '../morph.js';
12
12
  import { SKINNING_GLSL } from '../skinning.js';
13
13
  import { SURFACE_GLSL } from './surface.js';
@@ -359,6 +359,12 @@ export function flatFrag(options) {
359
359
  /* Depth zero as well: both glass lookups, sun and lamps, call it, and every permutation
360
360
  declares its switch, which the pipelines set whether the permutation reads it or not. */
361
361
  glassTintGlsl(options.glassShadows ?? true),
362
+ litSwitchesGlsl({
363
+ clusteredLights: options.clusteredLights ?? true,
364
+ lightFixtures: options.lightFixtures ?? true,
365
+ surfaceEffects: options.surfaceEffects ?? true,
366
+ driftLight: options.driftLight ?? true,
367
+ }),
362
368
  LOBES_GLSL,
363
369
  POINTSHADOW_GLSL,
364
370
  DIRECTIONALSHADOW_GLSL,