@codexo/exojs-particles 0.15.3 → 0.16.0
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/README.md +21 -43
- package/dist/esm/ParticleStorage.d.ts +188 -0
- package/dist/esm/ParticleStorage.d.ts.map +1 -0
- package/dist/esm/ParticleStorage.js +208 -0
- package/dist/esm/ParticleStorage.js.map +1 -0
- package/dist/esm/ParticleSystem.d.ts +189 -60
- package/dist/esm/ParticleSystem.d.ts.map +1 -0
- package/dist/esm/ParticleSystem.js +660 -560
- package/dist/esm/ParticleSystem.js.map +1 -1
- package/dist/esm/distributions/BoxArea.d.ts +1 -0
- package/dist/esm/distributions/BoxArea.d.ts.map +1 -0
- package/dist/esm/distributions/BoxArea.js +38 -44
- package/dist/esm/distributions/BoxArea.js.map +1 -1
- package/dist/esm/distributions/CircleArea.d.ts +1 -0
- package/dist/esm/distributions/CircleArea.d.ts.map +1 -0
- package/dist/esm/distributions/CircleArea.js +30 -28
- package/dist/esm/distributions/CircleArea.js.map +1 -1
- package/dist/esm/distributions/ColorGradient.d.ts +3 -2
- package/dist/esm/distributions/ColorGradient.d.ts.map +1 -0
- package/dist/esm/distributions/ColorGradient.js +70 -75
- package/dist/esm/distributions/ColorGradient.js.map +1 -1
- package/dist/esm/distributions/ConeDirection.d.ts +2 -1
- package/dist/esm/distributions/ConeDirection.d.ts.map +1 -0
- package/dist/esm/distributions/ConeDirection.js +41 -39
- package/dist/esm/distributions/ConeDirection.js.map +1 -1
- package/dist/esm/distributions/Constant.d.ts +1 -0
- package/dist/esm/distributions/Constant.d.ts.map +1 -0
- package/dist/esm/distributions/Constant.js +32 -32
- package/dist/esm/distributions/Constant.js.map +1 -1
- package/dist/esm/distributions/Curve.d.ts +2 -1
- package/dist/esm/distributions/Curve.d.ts.map +1 -0
- package/dist/esm/distributions/Curve.js +46 -55
- package/dist/esm/distributions/Curve.js.map +1 -1
- package/dist/esm/distributions/Distribution.d.ts +4 -3
- package/dist/esm/distributions/Distribution.d.ts.map +1 -0
- package/dist/esm/distributions/LineSegment.d.ts +1 -0
- package/dist/esm/distributions/LineSegment.d.ts.map +1 -0
- package/dist/esm/distributions/LineSegment.js +25 -23
- package/dist/esm/distributions/LineSegment.js.map +1 -1
- package/dist/esm/distributions/Range.d.ts +1 -0
- package/dist/esm/distributions/Range.d.ts.map +1 -0
- package/dist/esm/distributions/Range.js +18 -16
- package/dist/esm/distributions/Range.js.map +1 -1
- package/dist/esm/distributions/VectorRange.d.ts +1 -0
- package/dist/esm/distributions/VectorRange.d.ts.map +1 -0
- package/dist/esm/distributions/VectorRange.js +29 -27
- package/dist/esm/distributions/VectorRange.js.map +1 -1
- package/dist/esm/distributions/index.d.ts +1 -0
- package/dist/esm/distributions/index.d.ts.map +1 -0
- package/dist/esm/gpu/ParticleGpuState.d.ts +143 -18
- package/dist/esm/gpu/ParticleGpuState.d.ts.map +1 -0
- package/dist/esm/gpu/ParticleGpuState.js +655 -497
- package/dist/esm/gpu/ParticleGpuState.js.map +1 -1
- package/dist/esm/gpu/shaders/particle-simulate.wgsl.js +6 -0
- package/dist/esm/gpu/shaders/particle-simulate.wgsl.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +39 -32
- package/dist/esm/modules/AlphaFadeOverLifetime.d.ts +4 -3
- package/dist/esm/modules/AlphaFadeOverLifetime.d.ts.map +1 -0
- package/dist/esm/modules/AlphaFadeOverLifetime.js +68 -55
- package/dist/esm/modules/AlphaFadeOverLifetime.js.map +1 -1
- package/dist/esm/modules/ApplyForce.d.ts +4 -3
- package/dist/esm/modules/ApplyForce.d.ts.map +1 -0
- package/dist/esm/modules/ApplyForce.js +49 -43
- package/dist/esm/modules/ApplyForce.js.map +1 -1
- package/dist/esm/modules/AttractToPoint.d.ts +4 -3
- package/dist/esm/modules/AttractToPoint.d.ts.map +1 -0
- package/dist/esm/modules/AttractToPoint.js +73 -62
- package/dist/esm/modules/AttractToPoint.js.map +1 -1
- package/dist/esm/modules/BurstSpawn.d.ts +6 -18
- package/dist/esm/modules/BurstSpawn.d.ts.map +1 -0
- package/dist/esm/modules/BurstSpawn.js +56 -91
- package/dist/esm/modules/BurstSpawn.js.map +1 -1
- package/dist/esm/modules/ColorOverLifetime.d.ts +4 -3
- package/dist/esm/modules/ColorOverLifetime.d.ts.map +1 -0
- package/dist/esm/modules/ColorOverLifetime.js +65 -53
- package/dist/esm/modules/ColorOverLifetime.js.map +1 -1
- package/dist/esm/modules/ColorOverSpeed.d.ts +5 -4
- package/dist/esm/modules/ColorOverSpeed.d.ts.map +1 -0
- package/dist/esm/modules/ColorOverSpeed.js +90 -75
- package/dist/esm/modules/ColorOverSpeed.js.map +1 -1
- package/dist/esm/modules/DeathModule.d.ts +26 -10
- package/dist/esm/modules/DeathModule.d.ts.map +1 -0
- package/dist/esm/modules/DeathModule.js +38 -22
- package/dist/esm/modules/DeathModule.js.map +1 -1
- package/dist/esm/modules/Drag.d.ts +3 -2
- package/dist/esm/modules/Drag.d.ts.map +1 -0
- package/dist/esm/modules/Drag.js +43 -37
- package/dist/esm/modules/Drag.js.map +1 -1
- package/dist/esm/modules/OrbitalForce.d.ts +4 -3
- package/dist/esm/modules/OrbitalForce.d.ts.map +1 -0
- package/dist/esm/modules/OrbitalForce.js +70 -58
- package/dist/esm/modules/OrbitalForce.js.map +1 -1
- package/dist/esm/modules/RateSpawn.d.ts +7 -22
- package/dist/esm/modules/RateSpawn.d.ts.map +1 -0
- package/dist/esm/modules/RateSpawn.js +39 -71
- package/dist/esm/modules/RateSpawn.js.map +1 -1
- package/dist/esm/modules/RepelFromPoint.d.ts +4 -3
- package/dist/esm/modules/RepelFromPoint.d.ts.map +1 -0
- package/dist/esm/modules/RepelFromPoint.js +72 -62
- package/dist/esm/modules/RepelFromPoint.js.map +1 -1
- package/dist/esm/modules/RotateOverLifetime.d.ts +3 -2
- package/dist/esm/modules/RotateOverLifetime.d.ts.map +1 -0
- package/dist/esm/modules/RotateOverLifetime.js +40 -36
- package/dist/esm/modules/RotateOverLifetime.js.map +1 -1
- package/dist/esm/modules/ScaleOverLifetime.d.ts +5 -4
- package/dist/esm/modules/ScaleOverLifetime.d.ts.map +1 -0
- package/dist/esm/modules/ScaleOverLifetime.js +60 -50
- package/dist/esm/modules/ScaleOverLifetime.js.map +1 -1
- package/dist/esm/modules/SpawnModule.d.ts +15 -12
- package/dist/esm/modules/SpawnModule.d.ts.map +1 -0
- package/dist/esm/modules/SpawnModule.js +32 -28
- package/dist/esm/modules/SpawnModule.js.map +1 -1
- package/dist/esm/modules/SpawnOnDeath.d.ts +13 -9
- package/dist/esm/modules/SpawnOnDeath.d.ts.map +1 -0
- package/dist/esm/modules/SpawnOnDeath.js +41 -43
- package/dist/esm/modules/SpawnOnDeath.js.map +1 -1
- package/dist/esm/modules/Turbulence.d.ts +4 -3
- package/dist/esm/modules/Turbulence.d.ts.map +1 -0
- package/dist/esm/modules/Turbulence.js +101 -89
- package/dist/esm/modules/Turbulence.js.map +1 -1
- package/dist/esm/modules/UpdateModule.d.ts +21 -13
- package/dist/esm/modules/UpdateModule.d.ts.map +1 -0
- package/dist/esm/modules/UpdateModule.js +65 -63
- package/dist/esm/modules/UpdateModule.js.map +1 -1
- package/dist/esm/modules/VelocityOverLifetime.d.ts +7 -6
- package/dist/esm/modules/VelocityOverLifetime.d.ts.map +1 -0
- package/dist/esm/modules/VelocityOverLifetime.js +75 -73
- package/dist/esm/modules/VelocityOverLifetime.js.map +1 -1
- package/dist/esm/modules/WgslContribution.d.ts +16 -15
- package/dist/esm/modules/WgslContribution.d.ts.map +1 -0
- package/dist/esm/modules/WgslContribution.js +37 -29
- package/dist/esm/modules/WgslContribution.js.map +1 -1
- package/dist/esm/modules/index.d.ts +3 -1
- package/dist/esm/modules/index.d.ts.map +1 -0
- package/dist/esm/modules/spawnFields.d.ts +32 -0
- package/dist/esm/modules/spawnFields.d.ts.map +1 -0
- package/dist/esm/modules/spawnFields.js +31 -0
- package/dist/esm/modules/spawnFields.js.map +1 -0
- package/dist/esm/particlesBuildInfo.d.ts +1 -0
- package/dist/esm/particlesBuildInfo.d.ts.map +1 -0
- package/dist/esm/particlesBuildInfo.js +6 -4
- package/dist/esm/particlesBuildInfo.js.map +1 -1
- package/dist/esm/particlesExtension.d.ts +13 -5
- package/dist/esm/particlesExtension.d.ts.map +1 -0
- package/dist/esm/particlesExtension.js +38 -42
- package/dist/esm/particlesExtension.js.map +1 -1
- package/dist/esm/public.d.ts +11 -0
- package/dist/esm/public.d.ts.map +1 -0
- package/dist/esm/renderModes/MeshParticles.d.ts +128 -0
- package/dist/esm/renderModes/MeshParticles.d.ts.map +1 -0
- package/dist/esm/renderModes/MeshParticles.js +263 -0
- package/dist/esm/renderModes/MeshParticles.js.map +1 -0
- package/dist/esm/renderModes/ParticleBufferLayout.d.ts +59 -0
- package/dist/esm/renderModes/ParticleBufferLayout.d.ts.map +1 -0
- package/dist/esm/renderModes/ParticleBufferLayout.js +99 -0
- package/dist/esm/renderModes/ParticleBufferLayout.js.map +1 -0
- package/dist/esm/renderModes/ParticleInstanceWriter.d.ts +58 -0
- package/dist/esm/renderModes/ParticleInstanceWriter.d.ts.map +1 -0
- package/dist/esm/renderModes/ParticleInstanceWriter.js +173 -0
- package/dist/esm/renderModes/ParticleInstanceWriter.js.map +1 -0
- package/dist/esm/renderModes/ParticleMaterial.d.ts +18 -0
- package/dist/esm/renderModes/ParticleMaterial.d.ts.map +1 -0
- package/dist/esm/renderModes/ParticleMaterial.js +24 -0
- package/dist/esm/renderModes/ParticleMaterial.js.map +1 -0
- package/dist/esm/renderModes/ParticleRenderMode.d.ts +114 -0
- package/dist/esm/renderModes/ParticleRenderMode.d.ts.map +1 -0
- package/dist/esm/renderModes/ParticleRenderMode.js +99 -0
- package/dist/esm/renderModes/ParticleRenderMode.js.map +1 -0
- package/dist/esm/renderModes/QuadParticles.d.ts +55 -0
- package/dist/esm/renderModes/QuadParticles.d.ts.map +1 -0
- package/dist/esm/renderModes/QuadParticles.js +95 -0
- package/dist/esm/renderModes/QuadParticles.js.map +1 -0
- package/dist/esm/renderModes/RibbonParticles.d.ts +96 -0
- package/dist/esm/renderModes/RibbonParticles.d.ts.map +1 -0
- package/dist/esm/renderModes/RibbonParticles.js +202 -0
- package/dist/esm/renderModes/RibbonParticles.js.map +1 -0
- package/dist/esm/renderModes/shaders/mesh-particles.wgsl.js +6 -0
- package/dist/esm/renderModes/shaders/mesh-particles.wgsl.js.map +1 -0
- package/dist/esm/renderModes/shaders/mesh.vert.js +6 -0
- package/dist/esm/renderModes/shaders/mesh.vert.js.map +1 -0
- package/dist/esm/renderModes/shaders/quad-particles.wgsl.js +6 -0
- package/dist/esm/renderModes/shaders/quad-particles.wgsl.js.map +1 -0
- package/dist/esm/renderModes/shaders/ribbon-particles.wgsl.js +6 -0
- package/dist/esm/renderModes/shaders/ribbon-particles.wgsl.js.map +1 -0
- package/dist/esm/renderModes/shaders/ribbon.frag.js +6 -0
- package/dist/esm/renderModes/shaders/ribbon.frag.js.map +1 -0
- package/dist/esm/renderModes/shaders/ribbon.vert.js +6 -0
- package/dist/esm/renderModes/shaders/ribbon.vert.js.map +1 -0
- package/dist/esm/renderers/WebGl2ParticleRenderer.d.ts +48 -20
- package/dist/esm/renderers/WebGl2ParticleRenderer.d.ts.map +1 -0
- package/dist/esm/renderers/WebGl2ParticleRenderer.js +317 -321
- package/dist/esm/renderers/WebGl2ParticleRenderer.js.map +1 -1
- package/dist/esm/renderers/WebGpuParticleRenderer.d.ts +99 -18
- package/dist/esm/renderers/WebGpuParticleRenderer.d.ts.map +1 -0
- package/dist/esm/renderers/WebGpuParticleRenderer.js +542 -551
- package/dist/esm/renderers/WebGpuParticleRenderer.js.map +1 -1
- package/dist/esm/renderers/shaders/particle.frag.js +6 -0
- package/dist/esm/renderers/shaders/particle.frag.js.map +1 -0
- package/dist/esm/renderers/shaders/particle.vert.js +6 -0
- package/dist/esm/renderers/shaders/particle.vert.js.map +1 -0
- package/package.json +8 -15
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/register.d.ts +0 -1
- package/dist/esm/register.js +0 -43
- package/dist/esm/register.js.map +0 -1
- package/dist/esm/renderers/glsl/particle.frag.js +0 -4
- package/dist/esm/renderers/glsl/particle.frag.js.map +0 -1
- package/dist/esm/renderers/glsl/particle.vert.js +0 -4
- package/dist/esm/renderers/glsl/particle.vert.js.map +0 -1
|
@@ -1,557 +1,548 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertVertexGeometryCompatible } from "../renderModes/ParticleBufferLayout.js";
|
|
2
|
+
import { AbstractWebGpuRenderer, DataTexture, Texture, getWebGpuBlendState, stencilContentDepthStencilState } from "@codexo/exojs/renderer-sdk";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
//#region src/renderers/WebGpuParticleRenderer.ts
|
|
5
|
+
const uniformByteLength = 176;
|
|
6
|
+
/**
|
|
7
|
+
* Stride of one draw call's slot in the uniform ring. `setBindGroup`'s dynamic
|
|
8
|
+
* offset must be a multiple of the device's `minUniformBufferOffsetAlignment`,
|
|
9
|
+
* whose spec-mandated maximum is 256 - so a fixed 256 is valid on every device
|
|
10
|
+
* and needs no limit query.
|
|
11
|
+
*/
|
|
12
|
+
const uniformSlotStride = 256;
|
|
13
|
+
/**
|
|
14
|
+
* WebGPU vertex formats by `<n?><type>x<size>` key, where the leading `n`
|
|
15
|
+
* marks a normalised integer attribute. WebGPU has no 1- or 3-component 8/16-bit
|
|
16
|
+
* formats, so those combinations are deliberately absent and reported as an
|
|
17
|
+
* error rather than silently widened.
|
|
18
|
+
*/
|
|
19
|
+
const vertexFormatsByKey = {
|
|
20
|
+
f32x1: "float32",
|
|
21
|
+
f32x2: "float32x2",
|
|
22
|
+
f32x3: "float32x3",
|
|
23
|
+
f32x4: "float32x4",
|
|
24
|
+
u8x2: "uint8x2",
|
|
25
|
+
u8x4: "uint8x4",
|
|
26
|
+
nu8x2: "unorm8x2",
|
|
27
|
+
nu8x4: "unorm8x4",
|
|
28
|
+
u16x2: "uint16x2",
|
|
29
|
+
u16x4: "uint16x4",
|
|
30
|
+
nu16x2: "unorm16x2",
|
|
31
|
+
nu16x4: "unorm16x4",
|
|
32
|
+
u32x1: "uint32",
|
|
33
|
+
u32x2: "uint32x2",
|
|
34
|
+
u32x3: "uint32x3",
|
|
35
|
+
u32x4: "uint32x4",
|
|
36
|
+
i32x1: "sint32",
|
|
37
|
+
i32x2: "sint32x2",
|
|
38
|
+
i32x3: "sint32x3",
|
|
39
|
+
i32x4: "sint32x4"
|
|
11
40
|
};
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
var particleTexture: texture_2d<f32>;
|
|
18
|
-
|
|
19
|
-
@group(1) @binding(1)
|
|
20
|
-
var particleSampler: sampler;
|
|
21
|
-
|
|
22
|
-
// Per-instance attributes (one entry per particle, 40 bytes total).
|
|
23
|
-
struct VertexInput {
|
|
24
|
-
@location(0) unitPosition: vec2<f32>, // per-vertex (static unit quad)
|
|
25
|
-
@location(1) translation: vec2<f32>,
|
|
26
|
-
@location(2) scale: vec2<f32>,
|
|
27
|
-
@location(3) rotation: f32,
|
|
28
|
-
@location(4) color: vec4<f32>,
|
|
29
|
-
@location(5) uvMin: vec2<f32>, // pre-resolved frame UV (top-left)
|
|
30
|
-
@location(6) uvMax: vec2<f32>, // pre-resolved frame UV (bottom-right)
|
|
41
|
+
const resolveVertexFormat = (attribute) => {
|
|
42
|
+
const normalized = attribute.normalized && attribute.type !== "f32";
|
|
43
|
+
const format = vertexFormatsByKey[`${normalized ? "n" : ""}${attribute.type}x${attribute.size}`];
|
|
44
|
+
if (format === void 0) throw new Error(`WebGpuParticleRenderer: attribute "${attribute.name}" (${attribute.type} x${attribute.size}) has no WebGPU vertex format.`);
|
|
45
|
+
return format;
|
|
31
46
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Particle renderer for WebGPU.
|
|
49
|
+
*
|
|
50
|
+
* One ParticleSystem = one draw call. The system's {@link ParticleRenderMode}
|
|
51
|
+
* owns the *how* - vertex layout, shader, draw model and the loop that fills
|
|
52
|
+
* the buffer - and this renderer is the executor: it holds the system-level
|
|
53
|
+
* uniforms (projection, transform, local bounds, texture flags), uploads what
|
|
54
|
+
* the mode built and issues the draw the mode declares.
|
|
55
|
+
*
|
|
56
|
+
* Everything mode-specific is read off the mode's `dataLayout`/`Material`
|
|
57
|
+
* rather than hard-coded: the render pipeline's vertex layout comes from the
|
|
58
|
+
* layout's attributes and stride, its shader from the material's WGSL, and the
|
|
59
|
+
* draw is instanced or plain per `ParticleRenderMode.instanced`. A mode
|
|
60
|
+
* declaring a `vertexGeometry` gets a second buffer stepping per vertex, and
|
|
61
|
+
* that geometry supplies the topology and indices instead.
|
|
62
|
+
*
|
|
63
|
+
* WGSL binds vertex inputs by number rather than by name, and `GeometryAttribute`
|
|
64
|
+
* carries no location, so the binding rule is positional: `@location(i)` is
|
|
65
|
+
* `dataLayout.attributes[i]`, with any per-vertex attributes taking the
|
|
66
|
+
* locations after them. This is the WebGPU counterpart of the WebGL2 renderer's
|
|
67
|
+
* name lookup through the compiled program.
|
|
68
|
+
*
|
|
69
|
+
* A system running in GPU compute mode bypasses the mode's builder entirely:
|
|
70
|
+
* its compute pipeline has already written the interleaved instance data
|
|
71
|
+
* GPU-side, so that buffer is bound directly.
|
|
72
|
+
*/
|
|
73
|
+
var WebGpuParticleRenderer = class extends AbstractWebGpuRenderer {
|
|
74
|
+
/**
|
|
75
|
+
* The particle system's transform is bound as a uniform and each particle is
|
|
76
|
+
* positioned system-locally, so this renderer never reads the shared transform
|
|
77
|
+
* storage; the plan player skips writing transform records for particle draws.
|
|
78
|
+
* @internal
|
|
79
|
+
*/
|
|
80
|
+
_consumesSharedTransform = false;
|
|
81
|
+
_drawCalls = [];
|
|
82
|
+
_drawCallCount = 0;
|
|
83
|
+
_uniformData = new Float32Array(uniformByteLength / Float32Array.BYTES_PER_ELEMENT);
|
|
84
|
+
_device = null;
|
|
85
|
+
_uniformBindGroupLayout = null;
|
|
86
|
+
_textureBindGroupLayout = null;
|
|
87
|
+
_pipelineLayout = null;
|
|
88
|
+
_uniformBuffer = null;
|
|
89
|
+
_uniformBindGroup = null;
|
|
90
|
+
_uniformBufferCapacity = 0;
|
|
91
|
+
/**
|
|
92
|
+
* Slots of the uniform ring the open pass has consumed, and the pass they are
|
|
93
|
+
* scoped to. One draw call takes one slot; the base is added as the bind
|
|
94
|
+
* group's dynamic offset, so consecutive flushes append instead of all
|
|
95
|
+
* rewriting slot 0. Same identity-comparison rule as
|
|
96
|
+
* {@link ParticleModeResources.passRef}, one level up: the ring is shared by
|
|
97
|
+
* every mode this renderer draws.
|
|
98
|
+
*/
|
|
99
|
+
_uniformPassSlots = 0;
|
|
100
|
+
_ownDrawsPass = null;
|
|
101
|
+
_resources = /* @__PURE__ */ new Map();
|
|
102
|
+
/**
|
|
103
|
+
* Materials this renderer already registered a dispose listener on.
|
|
104
|
+
*
|
|
105
|
+
* `Material` has no unsubscribe, and a disconnect clears {@link _resources}
|
|
106
|
+
* without clearing the material's callback set - so registering on every
|
|
107
|
+
* resource creation would leave one dead closure per material behind on
|
|
108
|
+
* every device-loss/reconnect cycle. The listener stays valid across those
|
|
109
|
+
* cycles (it resolves the entry through the map when it fires), so it is
|
|
110
|
+
* registered once and this set remembers that. Weak, so a material dropped
|
|
111
|
+
* without `destroy()` stays collectable.
|
|
112
|
+
*/
|
|
113
|
+
_disposeListenerRegistered = /* @__PURE__ */ new WeakSet();
|
|
114
|
+
render(system) {
|
|
115
|
+
const backend = this._backend;
|
|
116
|
+
const texture = system.texture;
|
|
117
|
+
const awaitingImage = texture instanceof Texture && !(texture instanceof DataTexture) && texture.source === null;
|
|
118
|
+
if (backend === null || !(texture instanceof Texture) || awaitingImage || texture.width === 0 || texture.height === 0 || system.liveCount === 0) return;
|
|
119
|
+
backend.setBlendMode(system.blendMode);
|
|
120
|
+
const drawCallIndex = this._drawCallCount++;
|
|
121
|
+
const drawCall = this._drawCalls[drawCallIndex];
|
|
122
|
+
if (drawCall) {
|
|
123
|
+
drawCall.system = system;
|
|
124
|
+
drawCall.texture = texture;
|
|
125
|
+
drawCall.blendMode = system.blendMode;
|
|
126
|
+
} else this._drawCalls.push({
|
|
127
|
+
system,
|
|
128
|
+
texture,
|
|
129
|
+
blendMode: system.blendMode
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
flush() {
|
|
133
|
+
const backend = this._backend;
|
|
134
|
+
const device = this._device;
|
|
135
|
+
if (!backend || !device || this._uniformBuffer === null || this._uniformBindGroup === null) return;
|
|
136
|
+
if (this._drawCallCount === 0 && !backend.clearRequested) return;
|
|
137
|
+
const scissor = backend.getScissorRect();
|
|
138
|
+
const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
|
|
139
|
+
if (this._drawCallCount === 0 || maskClipsAll) {
|
|
140
|
+
if (backend.clearRequested) {
|
|
141
|
+
backend._passCoordinator.acquirePass();
|
|
142
|
+
backend._passCoordinator.endPass();
|
|
143
|
+
}
|
|
144
|
+
this._drawCallCount = 0;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
for (let drawCallIndex = 0; drawCallIndex < this._drawCallCount; drawCallIndex++) {
|
|
148
|
+
const drawCall = this._drawCalls[drawCallIndex];
|
|
149
|
+
if (drawCall.system.liveCount === 0) continue;
|
|
150
|
+
this._drawSystem(backend, device, drawCall);
|
|
151
|
+
}
|
|
152
|
+
this._drawCallCount = 0;
|
|
153
|
+
}
|
|
154
|
+
destroy() {
|
|
155
|
+
this.disconnect();
|
|
156
|
+
}
|
|
157
|
+
onConnect(backend) {
|
|
158
|
+
this._backend = backend;
|
|
159
|
+
this._device = this._backend.device;
|
|
160
|
+
this._uniformBindGroupLayout = this._device.createBindGroupLayout({ entries: [{
|
|
161
|
+
binding: 0,
|
|
162
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
163
|
+
buffer: {
|
|
164
|
+
type: "uniform",
|
|
165
|
+
hasDynamicOffset: true
|
|
166
|
+
}
|
|
167
|
+
}] });
|
|
168
|
+
this._textureBindGroupLayout = this._device.createBindGroupLayout({ entries: [{
|
|
169
|
+
binding: 0,
|
|
170
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
171
|
+
texture: { sampleType: "float" }
|
|
172
|
+
}, {
|
|
173
|
+
binding: 1,
|
|
174
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
175
|
+
sampler: { type: "filtering" }
|
|
176
|
+
}] });
|
|
177
|
+
this._pipelineLayout = this._device.createPipelineLayout({ bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout] });
|
|
178
|
+
this._createUniformResources(this._device, uniformSlotStride);
|
|
179
|
+
}
|
|
180
|
+
onDisconnect() {
|
|
181
|
+
this.flush();
|
|
182
|
+
for (const resources of this._resources.values()) this._destroyResources(resources);
|
|
183
|
+
this._resources.clear();
|
|
184
|
+
this._uniformBuffer?.destroy();
|
|
185
|
+
this._uniformBindGroup = null;
|
|
186
|
+
this._uniformBuffer = null;
|
|
187
|
+
this._uniformBufferCapacity = 0;
|
|
188
|
+
this._uniformPassSlots = 0;
|
|
189
|
+
this._ownDrawsPass = null;
|
|
190
|
+
this._pipelineLayout = null;
|
|
191
|
+
this._textureBindGroupLayout = null;
|
|
192
|
+
this._uniformBindGroupLayout = null;
|
|
193
|
+
this._device = null;
|
|
194
|
+
this._backend = null;
|
|
195
|
+
this._drawCallCount = 0;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Append one system's draw at the open pass's cursors and record it, leaving
|
|
199
|
+
* the pass open. Ends (submits) the pass first only where appending cannot
|
|
200
|
+
* cover the hazard - see {@link _appendWouldAlias}.
|
|
201
|
+
*/
|
|
202
|
+
_drawSystem(backend, device, drawCall) {
|
|
203
|
+
const system = drawCall.system;
|
|
204
|
+
const mode = system.renderMode;
|
|
205
|
+
const resources = this._getOrCreateResources(mode, device);
|
|
206
|
+
const coordinator = backend._passCoordinator;
|
|
207
|
+
const gpuState = system.gpuMode ? system.gpuState : null;
|
|
208
|
+
let drawCount = system.liveCount;
|
|
209
|
+
let appendBytes = 0;
|
|
210
|
+
if (gpuState === null) {
|
|
211
|
+
mode.build(system, system._storage);
|
|
212
|
+
drawCount = mode.count;
|
|
213
|
+
appendBytes = Math.max(drawCount, 1) * resources.stride;
|
|
214
|
+
}
|
|
215
|
+
const meshGeometry = mode.vertexGeometry;
|
|
216
|
+
const meshSyncRequired = resources.meshBuffer !== null && meshGeometry !== null && resources.meshVersion !== meshGeometry.version;
|
|
217
|
+
const targetVertexBytes = this._modePassBytes(coordinator.activePass, resources) + appendBytes;
|
|
218
|
+
const targetUniformSlots = this._uniformPassBase(coordinator.activePass) + 1;
|
|
219
|
+
if (this._appendWouldAlias(backend, drawCall, resources, meshSyncRequired, targetVertexBytes, targetUniformSlots)) coordinator.endPass();
|
|
220
|
+
const vertexByteOffset = this._modePassBytes(coordinator.activePass, resources);
|
|
221
|
+
const uniformSlot = this._uniformPassBase(coordinator.activePass);
|
|
222
|
+
this._ensureUniformCapacity(device, targetUniformSlots);
|
|
223
|
+
const pipeline = this._getPipeline(resources, drawCall.blendMode, backend.renderTargetFormat, coordinator.stencilActive);
|
|
224
|
+
const textureBinding = backend.getTextureBinding(drawCall.texture, mode.material.sampler);
|
|
225
|
+
const textureBindGroup = device.createBindGroup({
|
|
226
|
+
layout: this._textureBindGroupLayout,
|
|
227
|
+
entries: [{
|
|
228
|
+
binding: 0,
|
|
229
|
+
resource: textureBinding.view
|
|
230
|
+
}, {
|
|
231
|
+
binding: 1,
|
|
232
|
+
resource: textureBinding.sampler
|
|
233
|
+
}]
|
|
234
|
+
});
|
|
235
|
+
this._writeUniformData(backend, system, drawCall.texture);
|
|
236
|
+
const vertexBuffer = gpuState !== null ? gpuState.instanceBuffer : this._uploadModeData(device, resources, mode, targetVertexBytes, vertexByteOffset, drawCount);
|
|
237
|
+
device.queue.writeBuffer(this._uniformBuffer, uniformSlot * uniformSlotStride, this._uniformData.buffer, this._uniformData.byteOffset, this._uniformData.byteLength);
|
|
238
|
+
if (meshSyncRequired) this._syncMeshBuffer(device, resources, mode);
|
|
239
|
+
const active = coordinator.acquirePass();
|
|
240
|
+
const pass = active.pass;
|
|
241
|
+
pass.setBindGroup(0, this._uniformBindGroup, [uniformSlot * uniformSlotStride]);
|
|
242
|
+
pass.setPipeline(pipeline);
|
|
243
|
+
pass.setBindGroup(1, textureBindGroup);
|
|
244
|
+
pass.setVertexBuffer(0, vertexBuffer, gpuState !== null ? 0 : vertexByteOffset);
|
|
245
|
+
if (resources.meshBuffer !== null) pass.setVertexBuffer(1, resources.meshBuffer);
|
|
246
|
+
const instanceCount = resources.instanced ? drawCount : 1;
|
|
247
|
+
if (resources.indexBuffer !== null) {
|
|
248
|
+
pass.setIndexBuffer(resources.indexBuffer, resources.indexFormat);
|
|
249
|
+
pass.drawIndexed(resources.indexCount, instanceCount, 0, 0, 0);
|
|
250
|
+
} else pass.draw(resources.instanced ? resources.indexCount : drawCount, instanceCount, 0, 0);
|
|
251
|
+
coordinator.markPassDraws();
|
|
252
|
+
backend.stats.batches++;
|
|
253
|
+
backend.stats.drawCalls++;
|
|
254
|
+
this._ownDrawsPass = active;
|
|
255
|
+
this._uniformPassSlots = uniformSlot + 1;
|
|
256
|
+
resources.passRef = active;
|
|
257
|
+
resources.vertexPassBytes = vertexByteOffset + appendBytes;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Whether appending this draw would retroactively change what a draw already
|
|
261
|
+
* recorded into the open pass reads - in which case the caller ends (submits)
|
|
262
|
+
* that pass first, which restarts every cursor at 0.
|
|
263
|
+
*/
|
|
264
|
+
_appendWouldAlias(backend, drawCall, resources, meshSyncRequired, targetVertexBytes, targetUniformSlots) {
|
|
265
|
+
const coordinator = backend._passCoordinator;
|
|
266
|
+
const active = coordinator.activePass;
|
|
267
|
+
if (coordinator.passHasDraws && backend._textureUploadWouldMutate(drawCall.texture)) return true;
|
|
268
|
+
if (active === null) return false;
|
|
269
|
+
if (active.viewUpdateId !== backend.view.updateId) return true;
|
|
270
|
+
if (resources.passRef === active && (meshSyncRequired || targetVertexBytes > resources.vertexBufferByteLength)) return true;
|
|
271
|
+
return this._ownDrawsPass === active && targetUniformSlots * uniformSlotStride > this._uniformBufferCapacity;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Bytes of the mode's vertex buffer `active` has consumed, or 0 when it holds
|
|
275
|
+
* none of that mode's draws (a different pass, or none open).
|
|
276
|
+
*/
|
|
277
|
+
_modePassBytes(active, resources) {
|
|
278
|
+
return active !== null && resources.passRef === active ? resources.vertexPassBytes : 0;
|
|
279
|
+
}
|
|
280
|
+
/** Uniform ring slots `active` has consumed, or 0 when it holds no draws of ours. */
|
|
281
|
+
_uniformPassBase(active) {
|
|
282
|
+
return active !== null && this._ownDrawsPass === active ? this._uniformPassSlots : 0;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Upload what the mode just built into this draw's sub-range of the mode's
|
|
286
|
+
* vertex buffer, growing it to the pass total first. Returns the buffer to
|
|
287
|
+
* bind, since growth replaces it.
|
|
288
|
+
*/
|
|
289
|
+
_uploadModeData(device, resources, mode, targetByteLength, byteOffset, elementCount) {
|
|
290
|
+
const buffer = this._ensureCapacity(device, resources, targetByteLength);
|
|
291
|
+
device.queue.writeBuffer(buffer, byteOffset, mode.data, 0, elementCount * resources.stride);
|
|
292
|
+
return buffer;
|
|
293
|
+
}
|
|
294
|
+
_getOrCreateResources(mode, device) {
|
|
295
|
+
const material = mode.material;
|
|
296
|
+
const cached = this._resources.get(material);
|
|
297
|
+
if (cached !== void 0) return cached;
|
|
298
|
+
const created = this._createResources(mode, material, device);
|
|
299
|
+
this._resources.set(material, created);
|
|
300
|
+
if (!this._disposeListenerRegistered.has(material)) {
|
|
301
|
+
this._disposeListenerRegistered.add(material);
|
|
302
|
+
material._onDispose(() => {
|
|
303
|
+
this._disposeListenerRegistered.delete(material);
|
|
304
|
+
const stored = this._resources.get(material);
|
|
305
|
+
if (stored === void 0) return;
|
|
306
|
+
this._destroyResources(stored);
|
|
307
|
+
this._resources.delete(material);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return created;
|
|
311
|
+
}
|
|
312
|
+
_createResources(mode, material, device) {
|
|
313
|
+
const wgsl = material.shader.wgsl;
|
|
314
|
+
if (wgsl === null) throw new Error("Particle material shader has no `wgsl` source; cannot render through the WebGPU backend.");
|
|
315
|
+
const layout = mode.dataLayout;
|
|
316
|
+
const meshGeometry = mode.vertexGeometry;
|
|
317
|
+
assertVertexGeometryCompatible(layout, meshGeometry, mode.instanced, mode.constructor.name);
|
|
318
|
+
const indices = meshGeometry !== null ? meshGeometry.indices : layout.indices;
|
|
319
|
+
let indexBuffer = null;
|
|
320
|
+
if (indices !== null) {
|
|
321
|
+
const indexData = new Uint8Array(Math.ceil(indices.byteLength / 4) * 4);
|
|
322
|
+
indexData.set(new Uint8Array(indices.buffer, indices.byteOffset, indices.byteLength));
|
|
323
|
+
indexBuffer = device.createBuffer({
|
|
324
|
+
size: indexData.byteLength,
|
|
325
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST
|
|
326
|
+
});
|
|
327
|
+
device.queue.writeBuffer(indexBuffer, 0, indexData.buffer, indexData.byteOffset, indexData.byteLength);
|
|
328
|
+
}
|
|
329
|
+
const topology = meshGeometry !== null ? meshGeometry.topology : layout.topology;
|
|
330
|
+
const indexFormat = indices instanceof Uint32Array ? "uint32" : "uint16";
|
|
331
|
+
const indexedStrip = topology === "triangle-strip" && indexBuffer !== null;
|
|
332
|
+
let meshBuffer = null;
|
|
333
|
+
let meshLayout = null;
|
|
334
|
+
if (meshGeometry !== null) {
|
|
335
|
+
const meshData = meshGeometry.vertexData;
|
|
336
|
+
meshLayout = {
|
|
337
|
+
arrayStride: meshGeometry.stride,
|
|
338
|
+
stepMode: "vertex",
|
|
339
|
+
attributes: meshGeometry.attributes.map((attribute, index) => ({
|
|
340
|
+
shaderLocation: layout.attributes.length + index,
|
|
341
|
+
offset: attribute.offset,
|
|
342
|
+
format: resolveVertexFormat(attribute)
|
|
343
|
+
}))
|
|
344
|
+
};
|
|
345
|
+
meshBuffer = device.createBuffer({
|
|
346
|
+
size: Math.ceil(meshData.byteLength / 4) * 4,
|
|
347
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
|
|
348
|
+
});
|
|
349
|
+
device.queue.writeBuffer(meshBuffer, 0, meshData instanceof Float32Array ? meshData.buffer : meshData, 0, meshData.byteLength);
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
shaderModule: device.createShaderModule({ code: wgsl }),
|
|
353
|
+
vertexLayout: {
|
|
354
|
+
arrayStride: layout.stride,
|
|
355
|
+
stepMode: mode.instanced ? "instance" : "vertex",
|
|
356
|
+
attributes: layout.attributes.map((attribute, location) => ({
|
|
357
|
+
shaderLocation: location,
|
|
358
|
+
offset: attribute.offset,
|
|
359
|
+
format: resolveVertexFormat(attribute)
|
|
360
|
+
}))
|
|
361
|
+
},
|
|
362
|
+
meshLayout,
|
|
363
|
+
meshBuffer,
|
|
364
|
+
meshVersion: meshGeometry?.version ?? -1,
|
|
365
|
+
stride: layout.stride,
|
|
366
|
+
topology,
|
|
367
|
+
stripIndexFormat: indexedStrip ? indexFormat : void 0,
|
|
368
|
+
indexBuffer,
|
|
369
|
+
indexFormat,
|
|
370
|
+
indexCount: meshGeometry !== null ? meshGeometry.indexCount : layout.indexCount,
|
|
371
|
+
instanced: mode.instanced,
|
|
372
|
+
pipelines: /* @__PURE__ */ new Map(),
|
|
373
|
+
vertexBuffer: null,
|
|
374
|
+
vertexBufferByteLength: 0,
|
|
375
|
+
passRef: null,
|
|
376
|
+
vertexPassBytes: 0
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Re-write the mode's own geometry only when it was mutated since the last
|
|
381
|
+
* draw. One integer comparison keeps an unchanging mesh off the bus.
|
|
382
|
+
*/
|
|
383
|
+
_syncMeshBuffer(device, resources, mode) {
|
|
384
|
+
const meshGeometry = mode.vertexGeometry;
|
|
385
|
+
if (meshGeometry === null || resources.meshBuffer === null || resources.meshVersion === meshGeometry.version) return;
|
|
386
|
+
const meshData = meshGeometry.vertexData;
|
|
387
|
+
resources.meshVersion = meshGeometry.version;
|
|
388
|
+
device.queue.writeBuffer(resources.meshBuffer, 0, meshData instanceof Float32Array ? meshData.buffer : meshData, 0, meshData.byteLength);
|
|
389
|
+
}
|
|
390
|
+
_destroyResources(resources) {
|
|
391
|
+
resources.vertexBuffer?.destroy();
|
|
392
|
+
resources.meshBuffer?.destroy();
|
|
393
|
+
resources.indexBuffer?.destroy();
|
|
394
|
+
resources.pipelines.clear();
|
|
395
|
+
resources.vertexBuffer = null;
|
|
396
|
+
resources.meshBuffer = null;
|
|
397
|
+
resources.vertexBufferByteLength = 0;
|
|
398
|
+
resources.passRef = null;
|
|
399
|
+
resources.vertexPassBytes = 0;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Grow the mode's vertex buffer to `requiredByteLength`, which is the whole
|
|
403
|
+
* open pass's consumption of it (this draw's append plus everything already
|
|
404
|
+
* written for the draws it holds), not just this draw's own bytes. Grow-only
|
|
405
|
+
* and doubling, matching the mode's own scratch-buffer policy; the returned
|
|
406
|
+
* buffer is the one this draw must bind, since growth replaces it.
|
|
407
|
+
*/
|
|
408
|
+
_ensureCapacity(device, resources, requiredByteLength) {
|
|
409
|
+
const stride = resources.stride;
|
|
410
|
+
const required = Math.max(requiredByteLength, stride);
|
|
411
|
+
if (resources.vertexBuffer !== null && required <= resources.vertexBufferByteLength) return resources.vertexBuffer;
|
|
412
|
+
let byteLength = resources.vertexBufferByteLength || stride;
|
|
413
|
+
while (byteLength < required) byteLength *= 2;
|
|
414
|
+
resources.vertexBuffer?.destroy();
|
|
415
|
+
resources.vertexBuffer = device.createBuffer({
|
|
416
|
+
size: byteLength,
|
|
417
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
|
|
418
|
+
});
|
|
419
|
+
resources.vertexBufferByteLength = byteLength;
|
|
420
|
+
return resources.vertexBuffer;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Grow the uniform ring to `slots` draw-call slots - again the open pass's
|
|
424
|
+
* total, not this draw's one slot. Doubling and grow-only; growth frees the
|
|
425
|
+
* current buffer, so the caller must have ended any pass holding draws of
|
|
426
|
+
* ours that read it.
|
|
427
|
+
*/
|
|
428
|
+
_ensureUniformCapacity(device, slots) {
|
|
429
|
+
const requiredBytes = slots * uniformSlotStride;
|
|
430
|
+
if (requiredBytes <= this._uniformBufferCapacity) return;
|
|
431
|
+
this._uniformBuffer?.destroy();
|
|
432
|
+
this._createUniformResources(device, Math.max(requiredBytes, this._uniformBufferCapacity * 2));
|
|
433
|
+
}
|
|
434
|
+
/** (Re-)create the uniform ring and its bind group at `capacityBytes`. */
|
|
435
|
+
_createUniformResources(device, capacityBytes) {
|
|
436
|
+
this._uniformBufferCapacity = capacityBytes;
|
|
437
|
+
this._uniformBuffer = device.createBuffer({
|
|
438
|
+
label: "particles:uniform-ring",
|
|
439
|
+
size: capacityBytes,
|
|
440
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
441
|
+
});
|
|
442
|
+
this._uniformBindGroup = device.createBindGroup({
|
|
443
|
+
label: "particles:uniform-bind-group",
|
|
444
|
+
layout: this._uniformBindGroupLayout,
|
|
445
|
+
entries: [{
|
|
446
|
+
binding: 0,
|
|
447
|
+
resource: {
|
|
448
|
+
buffer: this._uniformBuffer,
|
|
449
|
+
size: uniformByteLength
|
|
450
|
+
}
|
|
451
|
+
}]
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
_writeUniformData(backend, system, texture) {
|
|
455
|
+
const projection = backend.view.getTransform().toArray(false);
|
|
456
|
+
const transform = system.getGlobalTransform().toArray(false);
|
|
457
|
+
const shouldPremultiplySample = backend.shouldPremultiplyTextureSample(texture);
|
|
458
|
+
const vertices = system.vertices;
|
|
459
|
+
const texCoords = system.texCoords;
|
|
460
|
+
const quadMinX = vertices[0];
|
|
461
|
+
const quadMinY = vertices[1];
|
|
462
|
+
const quadSizeX = vertices[2] - vertices[0];
|
|
463
|
+
const quadSizeY = vertices[3] - vertices[1];
|
|
464
|
+
const uvMinX = (texCoords[0] & 65535) / 65535;
|
|
465
|
+
const uvMinY = (texCoords[0] >>> 16 & 65535) / 65535;
|
|
466
|
+
const uvMaxX = (texCoords[2] & 65535) / 65535;
|
|
467
|
+
const uvMaxY = (texCoords[2] >>> 16 & 65535) / 65535;
|
|
468
|
+
const u = this._uniformData;
|
|
469
|
+
u[0] = projection[0];
|
|
470
|
+
u[1] = projection[1];
|
|
471
|
+
u[2] = 0;
|
|
472
|
+
u[3] = 0;
|
|
473
|
+
u[4] = projection[3];
|
|
474
|
+
u[5] = projection[4];
|
|
475
|
+
u[6] = 0;
|
|
476
|
+
u[7] = 0;
|
|
477
|
+
u[8] = 0;
|
|
478
|
+
u[9] = 0;
|
|
479
|
+
u[10] = 1;
|
|
480
|
+
u[11] = 0;
|
|
481
|
+
u[12] = projection[6];
|
|
482
|
+
u[13] = projection[7];
|
|
483
|
+
u[14] = 0;
|
|
484
|
+
u[15] = projection[8];
|
|
485
|
+
u[16] = transform[0];
|
|
486
|
+
u[17] = transform[1];
|
|
487
|
+
u[18] = 0;
|
|
488
|
+
u[19] = 0;
|
|
489
|
+
u[20] = transform[3];
|
|
490
|
+
u[21] = transform[4];
|
|
491
|
+
u[22] = 0;
|
|
492
|
+
u[23] = 0;
|
|
493
|
+
u[24] = 0;
|
|
494
|
+
u[25] = 0;
|
|
495
|
+
u[26] = 1;
|
|
496
|
+
u[27] = 0;
|
|
497
|
+
u[28] = transform[6];
|
|
498
|
+
u[29] = transform[7];
|
|
499
|
+
u[30] = 0;
|
|
500
|
+
u[31] = transform[8];
|
|
501
|
+
u[32] = shouldPremultiplySample ? 1 : 0;
|
|
502
|
+
u[33] = 0;
|
|
503
|
+
u[34] = 0;
|
|
504
|
+
u[35] = 0;
|
|
505
|
+
u[36] = quadMinX;
|
|
506
|
+
u[37] = quadMinY;
|
|
507
|
+
u[38] = quadSizeX;
|
|
508
|
+
u[39] = quadSizeY;
|
|
509
|
+
u[40] = uvMinX;
|
|
510
|
+
u[41] = uvMinY;
|
|
511
|
+
u[42] = uvMaxX;
|
|
512
|
+
u[43] = uvMaxY;
|
|
513
|
+
}
|
|
514
|
+
_getPipeline(resources, blendMode, format, stencil) {
|
|
515
|
+
const pipelineKey = `${blendMode}:${format}:${stencil ? "s" : "n"}`;
|
|
516
|
+
const existingPipeline = resources.pipelines.get(pipelineKey);
|
|
517
|
+
if (existingPipeline) return existingPipeline;
|
|
518
|
+
const descriptor = {
|
|
519
|
+
layout: this._pipelineLayout,
|
|
520
|
+
vertex: {
|
|
521
|
+
module: resources.shaderModule,
|
|
522
|
+
entryPoint: "vertexMain",
|
|
523
|
+
buffers: resources.meshLayout !== null ? [resources.vertexLayout, resources.meshLayout] : [resources.vertexLayout]
|
|
524
|
+
},
|
|
525
|
+
fragment: {
|
|
526
|
+
module: resources.shaderModule,
|
|
527
|
+
entryPoint: "fragmentMain",
|
|
528
|
+
targets: [{
|
|
529
|
+
format,
|
|
530
|
+
blend: getWebGpuBlendState(blendMode),
|
|
531
|
+
writeMask: GPUColorWrite.ALL
|
|
532
|
+
}]
|
|
533
|
+
},
|
|
534
|
+
primitive: resources.stripIndexFormat === void 0 ? { topology: resources.topology } : {
|
|
535
|
+
topology: resources.topology,
|
|
536
|
+
stripIndexFormat: resources.stripIndexFormat
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
if (stencil) descriptor.depthStencil = stencilContentDepthStencilState();
|
|
540
|
+
const pipeline = this._device.createRenderPipeline(descriptor);
|
|
541
|
+
resources.pipelines.set(pipelineKey, pipeline);
|
|
542
|
+
return pipeline;
|
|
543
|
+
}
|
|
37
544
|
};
|
|
38
545
|
|
|
39
|
-
|
|
40
|
-
fn vertexMain(input: VertexInput) -> VertexOutput {
|
|
41
|
-
let quadMin = uniforms.localBounds.xy;
|
|
42
|
-
let quadSize = uniforms.localBounds.zw;
|
|
43
|
-
|
|
44
|
-
let localPosition = quadMin + (input.unitPosition * quadSize);
|
|
45
|
-
let radians = radians(input.rotation);
|
|
46
|
-
let sinValue = sin(radians);
|
|
47
|
-
let cosValue = cos(radians);
|
|
48
|
-
let rotated = vec2<f32>(
|
|
49
|
-
(localPosition.x * (input.scale.x * cosValue)) + (localPosition.y * (input.scale.y * sinValue)) + input.translation.x,
|
|
50
|
-
(localPosition.x * (input.scale.x * -sinValue)) + (localPosition.y * (input.scale.y * cosValue)) + input.translation.y
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
var output: VertexOutput;
|
|
54
|
-
|
|
55
|
-
output.position = uniforms.projection * uniforms.translation * vec4<f32>(rotated, 0.0, 1.0);
|
|
56
|
-
output.texcoord = input.uvMin + ((input.uvMax - input.uvMin) * input.unitPosition);
|
|
57
|
-
output.color = vec4(input.color.rgb * input.color.a, input.color.a);
|
|
58
|
-
|
|
59
|
-
return output;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
@fragment
|
|
63
|
-
fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
64
|
-
let sample = textureSample(particleTexture, particleSampler, input.texcoord);
|
|
65
|
-
let premultipliedSample = select(sample, vec4(sample.rgb * sample.a, sample.a), uniforms.flags.x > 0.5);
|
|
66
|
-
|
|
67
|
-
return premultipliedSample * input.color;
|
|
68
|
-
}
|
|
69
|
-
`;
|
|
70
|
-
const staticVertexStrideBytes = 8;
|
|
71
|
-
const instanceWords = 10;
|
|
72
|
-
const instanceStrideBytes = 40;
|
|
73
|
-
const indicesPerParticle = 6;
|
|
74
|
-
const uniformByteLength = 176;
|
|
75
|
-
const initialParticleCapacity = 1;
|
|
76
|
-
const staticVertexData = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
|
|
77
|
-
const staticIndexData = new Uint16Array([0, 1, 2, 0, 2, 3]);
|
|
78
|
-
class WebGpuParticleRenderer extends AbstractWebGpuRenderer {
|
|
79
|
-
/**
|
|
80
|
-
* The particle system's transform is bound as a uniform and each particle is
|
|
81
|
-
* positioned system-locally, so this renderer never reads the shared transform
|
|
82
|
-
* storage; the plan player skips writing transform records for particle draws.
|
|
83
|
-
* @internal
|
|
84
|
-
*/
|
|
85
|
-
_consumesSharedTransform = false;
|
|
86
|
-
_drawCalls = [];
|
|
87
|
-
_drawCallCount = 0;
|
|
88
|
-
_uniformData = new Float32Array(uniformByteLength / Float32Array.BYTES_PER_ELEMENT);
|
|
89
|
-
_device = null;
|
|
90
|
-
_shaderModule = null;
|
|
91
|
-
_uniformBindGroupLayout = null;
|
|
92
|
-
_textureBindGroupLayout = null;
|
|
93
|
-
_pipelineLayout = null;
|
|
94
|
-
_uniformBuffer = null;
|
|
95
|
-
_uniformBindGroup = null;
|
|
96
|
-
_staticVertexBuffer = null;
|
|
97
|
-
_instanceBuffer = null;
|
|
98
|
-
_indexBuffer = null;
|
|
99
|
-
_instanceBufferByteLength = 0;
|
|
100
|
-
_instanceData = new ArrayBuffer(instanceStrideBytes * initialParticleCapacity);
|
|
101
|
-
_float32View = new Float32Array(this._instanceData);
|
|
102
|
-
_uint32View = new Uint32Array(this._instanceData);
|
|
103
|
-
_pipelines = new Map();
|
|
104
|
-
render(system) {
|
|
105
|
-
const backend = this._backend;
|
|
106
|
-
const texture = system.texture;
|
|
107
|
-
if (backend === null || !(texture instanceof Texture) || texture.source === null || texture.width === 0 || texture.height === 0 || system.liveCount === 0) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
backend.setBlendMode(system.blendMode);
|
|
111
|
-
const drawCallIndex = this._drawCallCount++;
|
|
112
|
-
const drawCall = this._drawCalls[drawCallIndex];
|
|
113
|
-
if (drawCall) {
|
|
114
|
-
drawCall.system = system;
|
|
115
|
-
drawCall.texture = texture;
|
|
116
|
-
drawCall.blendMode = system.blendMode;
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
this._drawCalls.push({
|
|
120
|
-
system,
|
|
121
|
-
texture,
|
|
122
|
-
blendMode: system.blendMode,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
flush() {
|
|
127
|
-
const backend = this._backend;
|
|
128
|
-
const device = this._device;
|
|
129
|
-
const uniformBuffer = this._uniformBuffer;
|
|
130
|
-
const uniformBindGroup = this._uniformBindGroup;
|
|
131
|
-
const staticVertexBuffer = this._staticVertexBuffer;
|
|
132
|
-
const indexBuffer = this._indexBuffer;
|
|
133
|
-
if (!backend || !device || !uniformBuffer || !uniformBindGroup || !staticVertexBuffer || !this._instanceBuffer || !indexBuffer) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (this._drawCallCount === 0 && !backend.clearRequested) {
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
const scissor = backend.getScissorRect();
|
|
140
|
-
const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
|
|
141
|
-
// If no drawcalls will actually render (none queued, or the scissor
|
|
142
|
-
// clips everything), but a clear is pending, open a single empty
|
|
143
|
-
// pass so createColorAttachment consumes the clear state.
|
|
144
|
-
if (this._drawCallCount === 0 || maskClipsAll) {
|
|
145
|
-
if (backend.clearRequested) {
|
|
146
|
-
backend._passCoordinator.acquirePass();
|
|
147
|
-
backend._passCoordinator.endPass();
|
|
148
|
-
}
|
|
149
|
-
this._drawCallCount = 0;
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
// One command encoder / pass per drawcall. Each particle system's
|
|
153
|
-
// queue.writeBuffer calls target offset 0 of the instance and uniform
|
|
154
|
-
// buffers — a single pass with multiple systems would see all
|
|
155
|
-
// writeBuffers serialize before submit, leaving only the last
|
|
156
|
-
// system's data in those buffers and making every earlier draw read
|
|
157
|
-
// the wrong data. Also: _ensureCapacity may destroy and recreate the
|
|
158
|
-
// instance buffer on growth; keeping one drawcall per pass means
|
|
159
|
-
// that destroy happens strictly between submits, so no pass holds a
|
|
160
|
-
// reference to a buffer that has since been destroyed.
|
|
161
|
-
for (let drawCallIndex = 0; drawCallIndex < this._drawCallCount; drawCallIndex++) {
|
|
162
|
-
const drawCall = this._drawCalls[drawCallIndex];
|
|
163
|
-
const system = drawCall.system;
|
|
164
|
-
const particleCount = system.liveCount;
|
|
165
|
-
if (particleCount === 0) {
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
const pipeline = this._getPipeline(drawCall.blendMode, backend.renderTargetFormat, backend._passCoordinator.stencilActive);
|
|
169
|
-
const textureBinding = backend.getTextureBinding(drawCall.texture);
|
|
170
|
-
const textureBindGroup = device.createBindGroup({
|
|
171
|
-
layout: this._textureBindGroupLayout,
|
|
172
|
-
entries: [
|
|
173
|
-
{
|
|
174
|
-
binding: 0,
|
|
175
|
-
resource: textureBinding.view,
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
binding: 1,
|
|
179
|
-
resource: textureBinding.sampler,
|
|
180
|
-
},
|
|
181
|
-
],
|
|
182
|
-
});
|
|
183
|
-
this._writeUniformData(backend, system, drawCall.texture);
|
|
184
|
-
// GPU mode: the system's compute pipeline already wrote the
|
|
185
|
-
// interleaved instance data into its own buffer. Bind it
|
|
186
|
-
// directly — no CPU pack, no writeBuffer for instance data.
|
|
187
|
-
// CPU mode: pack from CPU SoA into our owned instance buffer.
|
|
188
|
-
let drawInstanceCount = particleCount;
|
|
189
|
-
const instanceBuffer = (() => {
|
|
190
|
-
if (system.gpuMode && system.gpuState !== null) {
|
|
191
|
-
return system.gpuState.instanceBuffer;
|
|
192
|
-
}
|
|
193
|
-
this._ensureCapacity(particleCount);
|
|
194
|
-
drawInstanceCount = this._writeInstanceData(system);
|
|
195
|
-
device.queue.writeBuffer(this._instanceBuffer, 0, this._instanceData, 0, drawInstanceCount * instanceStrideBytes);
|
|
196
|
-
return this._instanceBuffer;
|
|
197
|
-
})();
|
|
198
|
-
device.queue.writeBuffer(uniformBuffer, 0, this._uniformData.buffer, this._uniformData.byteOffset, this._uniformData.byteLength);
|
|
199
|
-
// One coordinator-owned pass per drawcall: each system's writeBuffers
|
|
200
|
-
// target offset 0, so the pass must be submitted before the next system
|
|
201
|
-
// overwrites those buffers. acquirePass/endPass preserve that 1:1 ratio.
|
|
202
|
-
const pass = backend._passCoordinator.acquirePass().pass;
|
|
203
|
-
pass.setBindGroup(0, uniformBindGroup);
|
|
204
|
-
pass.setPipeline(pipeline);
|
|
205
|
-
pass.setBindGroup(1, textureBindGroup);
|
|
206
|
-
pass.setVertexBuffer(0, staticVertexBuffer);
|
|
207
|
-
pass.setVertexBuffer(1, instanceBuffer);
|
|
208
|
-
pass.setIndexBuffer(indexBuffer, 'uint16');
|
|
209
|
-
pass.drawIndexed(indicesPerParticle, drawInstanceCount, 0, 0, 0);
|
|
210
|
-
backend.stats.batches++;
|
|
211
|
-
backend.stats.drawCalls++;
|
|
212
|
-
backend._passCoordinator.endPass();
|
|
213
|
-
}
|
|
214
|
-
this._drawCallCount = 0;
|
|
215
|
-
}
|
|
216
|
-
destroy() {
|
|
217
|
-
this.disconnect();
|
|
218
|
-
}
|
|
219
|
-
onConnect(backend) {
|
|
220
|
-
this._backend = backend;
|
|
221
|
-
this._device = this._backend.device;
|
|
222
|
-
this._shaderModule = this._device.createShaderModule({ code: particleShaderSource });
|
|
223
|
-
this._uniformBindGroupLayout = this._device.createBindGroupLayout({
|
|
224
|
-
entries: [
|
|
225
|
-
{
|
|
226
|
-
binding: 0,
|
|
227
|
-
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
228
|
-
buffer: {
|
|
229
|
-
type: 'uniform',
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
],
|
|
233
|
-
});
|
|
234
|
-
this._textureBindGroupLayout = this._device.createBindGroupLayout({
|
|
235
|
-
entries: [
|
|
236
|
-
{
|
|
237
|
-
binding: 0,
|
|
238
|
-
visibility: GPUShaderStage.FRAGMENT,
|
|
239
|
-
texture: {
|
|
240
|
-
sampleType: 'float',
|
|
241
|
-
},
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
binding: 1,
|
|
245
|
-
visibility: GPUShaderStage.FRAGMENT,
|
|
246
|
-
sampler: {
|
|
247
|
-
type: 'filtering',
|
|
248
|
-
},
|
|
249
|
-
},
|
|
250
|
-
],
|
|
251
|
-
});
|
|
252
|
-
this._pipelineLayout = this._device.createPipelineLayout({
|
|
253
|
-
bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
|
|
254
|
-
});
|
|
255
|
-
this._uniformBuffer = this._device.createBuffer({
|
|
256
|
-
size: uniformByteLength,
|
|
257
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
258
|
-
});
|
|
259
|
-
this._uniformBindGroup = this._device.createBindGroup({
|
|
260
|
-
layout: this._uniformBindGroupLayout,
|
|
261
|
-
entries: [
|
|
262
|
-
{
|
|
263
|
-
binding: 0,
|
|
264
|
-
resource: {
|
|
265
|
-
buffer: this._uniformBuffer,
|
|
266
|
-
},
|
|
267
|
-
},
|
|
268
|
-
],
|
|
269
|
-
});
|
|
270
|
-
this._staticVertexBuffer = this._device.createBuffer({
|
|
271
|
-
size: staticVertexData.byteLength,
|
|
272
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
273
|
-
});
|
|
274
|
-
this._device.queue.writeBuffer(this._staticVertexBuffer, 0, staticVertexData.buffer, staticVertexData.byteOffset, staticVertexData.byteLength);
|
|
275
|
-
this._indexBuffer = this._device.createBuffer({
|
|
276
|
-
size: staticIndexData.byteLength,
|
|
277
|
-
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
278
|
-
});
|
|
279
|
-
this._device.queue.writeBuffer(this._indexBuffer, 0, staticIndexData.buffer, staticIndexData.byteOffset, staticIndexData.byteLength);
|
|
280
|
-
this._ensureCapacity(initialParticleCapacity);
|
|
281
|
-
}
|
|
282
|
-
onDisconnect() {
|
|
283
|
-
this.flush();
|
|
284
|
-
this._staticVertexBuffer?.destroy();
|
|
285
|
-
this._instanceBuffer?.destroy();
|
|
286
|
-
this._indexBuffer?.destroy();
|
|
287
|
-
this._uniformBuffer?.destroy();
|
|
288
|
-
this._pipelines.clear();
|
|
289
|
-
this._indexBuffer = null;
|
|
290
|
-
this._staticVertexBuffer = null;
|
|
291
|
-
this._instanceBuffer = null;
|
|
292
|
-
this._uniformBindGroup = null;
|
|
293
|
-
this._uniformBuffer = null;
|
|
294
|
-
this._pipelineLayout = null;
|
|
295
|
-
this._textureBindGroupLayout = null;
|
|
296
|
-
this._uniformBindGroupLayout = null;
|
|
297
|
-
this._shaderModule = null;
|
|
298
|
-
this._device = null;
|
|
299
|
-
this._backend = null;
|
|
300
|
-
this._instanceBufferByteLength = 0;
|
|
301
|
-
this._instanceData = new ArrayBuffer(instanceStrideBytes * initialParticleCapacity);
|
|
302
|
-
this._float32View = new Float32Array(this._instanceData);
|
|
303
|
-
this._uint32View = new Uint32Array(this._instanceData);
|
|
304
|
-
this._drawCallCount = 0;
|
|
305
|
-
}
|
|
306
|
-
_ensureCapacity(particleCount) {
|
|
307
|
-
const requiredInstanceBytes = particleCount * instanceStrideBytes;
|
|
308
|
-
if (requiredInstanceBytes > this._instanceData.byteLength) {
|
|
309
|
-
let byteLength = this._instanceData.byteLength;
|
|
310
|
-
while (byteLength < requiredInstanceBytes) {
|
|
311
|
-
byteLength *= 2;
|
|
312
|
-
}
|
|
313
|
-
this._instanceData = new ArrayBuffer(byteLength);
|
|
314
|
-
this._float32View = new Float32Array(this._instanceData);
|
|
315
|
-
this._uint32View = new Uint32Array(this._instanceData);
|
|
316
|
-
}
|
|
317
|
-
if (requiredInstanceBytes > this._instanceBufferByteLength) {
|
|
318
|
-
let byteLength = this._instanceBufferByteLength || instanceStrideBytes;
|
|
319
|
-
while (byteLength < requiredInstanceBytes) {
|
|
320
|
-
byteLength *= 2;
|
|
321
|
-
}
|
|
322
|
-
this._instanceBuffer?.destroy();
|
|
323
|
-
this._instanceBuffer = this._device.createBuffer({
|
|
324
|
-
size: byteLength,
|
|
325
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
326
|
-
});
|
|
327
|
-
this._instanceBufferByteLength = byteLength;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
_writeUniformData(backend, system, texture) {
|
|
331
|
-
const projection = backend.view.getTransform().toArray(false);
|
|
332
|
-
const transform = system.getGlobalTransform().toArray(false);
|
|
333
|
-
const shouldPremultiplySample = backend.shouldPremultiplyTextureSample(texture);
|
|
334
|
-
const vertices = system.vertices;
|
|
335
|
-
const texCoords = system.texCoords;
|
|
336
|
-
const quadMinX = vertices[0];
|
|
337
|
-
const quadMinY = vertices[1];
|
|
338
|
-
const quadSizeX = vertices[2] - vertices[0];
|
|
339
|
-
const quadSizeY = vertices[3] - vertices[1];
|
|
340
|
-
const uvMinX = (texCoords[0] & 0xffff) / 0xffff;
|
|
341
|
-
const uvMinY = ((texCoords[0] >>> 16) & 0xffff) / 0xffff;
|
|
342
|
-
const uvMaxX = (texCoords[2] & 0xffff) / 0xffff;
|
|
343
|
-
const uvMaxY = ((texCoords[2] >>> 16) & 0xffff) / 0xffff;
|
|
344
|
-
const u = this._uniformData;
|
|
345
|
-
// projection mat4 (col-major, padded to 4×4)
|
|
346
|
-
u[0] = projection[0];
|
|
347
|
-
u[1] = projection[1];
|
|
348
|
-
u[2] = 0;
|
|
349
|
-
u[3] = 0;
|
|
350
|
-
u[4] = projection[3];
|
|
351
|
-
u[5] = projection[4];
|
|
352
|
-
u[6] = 0;
|
|
353
|
-
u[7] = 0;
|
|
354
|
-
u[8] = 0;
|
|
355
|
-
u[9] = 0;
|
|
356
|
-
u[10] = 1;
|
|
357
|
-
u[11] = 0;
|
|
358
|
-
u[12] = projection[6];
|
|
359
|
-
u[13] = projection[7];
|
|
360
|
-
u[14] = 0;
|
|
361
|
-
u[15] = projection[8];
|
|
362
|
-
// transform mat4 (col-major, padded to 4×4)
|
|
363
|
-
u[16] = transform[0];
|
|
364
|
-
u[17] = transform[1];
|
|
365
|
-
u[18] = 0;
|
|
366
|
-
u[19] = 0;
|
|
367
|
-
u[20] = transform[3];
|
|
368
|
-
u[21] = transform[4];
|
|
369
|
-
u[22] = 0;
|
|
370
|
-
u[23] = 0;
|
|
371
|
-
u[24] = 0;
|
|
372
|
-
u[25] = 0;
|
|
373
|
-
u[26] = 1;
|
|
374
|
-
u[27] = 0;
|
|
375
|
-
u[28] = transform[6];
|
|
376
|
-
u[29] = transform[7];
|
|
377
|
-
u[30] = 0;
|
|
378
|
-
u[31] = transform[8];
|
|
379
|
-
// flags vec4
|
|
380
|
-
u[32] = shouldPremultiplySample ? 1 : 0;
|
|
381
|
-
u[33] = 0;
|
|
382
|
-
u[34] = 0;
|
|
383
|
-
u[35] = 0;
|
|
384
|
-
// localBounds vec4
|
|
385
|
-
u[36] = quadMinX;
|
|
386
|
-
u[37] = quadMinY;
|
|
387
|
-
u[38] = quadSizeX;
|
|
388
|
-
u[39] = quadSizeY;
|
|
389
|
-
// uvBounds vec4
|
|
390
|
-
u[40] = uvMinX;
|
|
391
|
-
u[41] = uvMinY;
|
|
392
|
-
u[42] = uvMaxX;
|
|
393
|
-
u[43] = uvMaxY;
|
|
394
|
-
}
|
|
395
|
-
_writeInstanceData(system) {
|
|
396
|
-
const { posX, posY, scaleX, scaleY, rotations, color, textureIndex, alive, liveCount } = system;
|
|
397
|
-
const f32 = this._float32View;
|
|
398
|
-
const u32 = this._uint32View;
|
|
399
|
-
const { uvMins, uvMaxs } = this._computeFrameUvs(system);
|
|
400
|
-
const frameCount = uvMins.length / 2;
|
|
401
|
-
let writeIndex = 0;
|
|
402
|
-
for (let particleIndex = 0; particleIndex < liveCount; particleIndex++) {
|
|
403
|
-
// Skip dead slots — present in GPU-mode systems where the live
|
|
404
|
-
// range can carry holes filled in on next spawn.
|
|
405
|
-
if (alive[particleIndex] === 0) {
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
const targetIndex = writeIndex * instanceWords;
|
|
409
|
-
const frame = textureIndex[particleIndex] < frameCount ? textureIndex[particleIndex] : 0;
|
|
410
|
-
const uvBase = frame * 2;
|
|
411
|
-
f32[targetIndex + 0] = posX[particleIndex];
|
|
412
|
-
f32[targetIndex + 1] = posY[particleIndex];
|
|
413
|
-
f32[targetIndex + 2] = scaleX[particleIndex];
|
|
414
|
-
f32[targetIndex + 3] = scaleY[particleIndex];
|
|
415
|
-
f32[targetIndex + 4] = rotations[particleIndex];
|
|
416
|
-
u32[targetIndex + 5] = color[particleIndex];
|
|
417
|
-
f32[targetIndex + 6] = uvMins[uvBase + 0];
|
|
418
|
-
f32[targetIndex + 7] = uvMins[uvBase + 1];
|
|
419
|
-
f32[targetIndex + 8] = uvMaxs[uvBase + 0];
|
|
420
|
-
f32[targetIndex + 9] = uvMaxs[uvBase + 1];
|
|
421
|
-
writeIndex++;
|
|
422
|
-
}
|
|
423
|
-
return writeIndex;
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Same atlas/UV-resolution as the WebGL2 path. Returns the per-frame
|
|
427
|
-
* (uvMin, uvMax) pairs derived from `system.frames` (or fallback to
|
|
428
|
-
* `system.textureFrame` when no atlas is declared), already flipY-
|
|
429
|
-
* adjusted for the current texture.
|
|
430
|
-
*/
|
|
431
|
-
_uvMinsScratch = new Float32Array(2);
|
|
432
|
-
_uvMaxsScratch = new Float32Array(2);
|
|
433
|
-
_computeFrameUvs(system) {
|
|
434
|
-
const frames = system.frames;
|
|
435
|
-
const tex = system.texture;
|
|
436
|
-
const texW = tex.width;
|
|
437
|
-
const texH = tex.height;
|
|
438
|
-
const flipY = tex.flipY;
|
|
439
|
-
const count = frames.length === 0 ? 1 : frames.length;
|
|
440
|
-
if (this._uvMinsScratch.length < count * 2) {
|
|
441
|
-
this._uvMinsScratch = new Float32Array(count * 2);
|
|
442
|
-
this._uvMaxsScratch = new Float32Array(count * 2);
|
|
443
|
-
}
|
|
444
|
-
const mins = this._uvMinsScratch;
|
|
445
|
-
const maxs = this._uvMaxsScratch;
|
|
446
|
-
if (frames.length === 0) {
|
|
447
|
-
const f = system.textureFrame;
|
|
448
|
-
const minU = f.left / texW;
|
|
449
|
-
const maxU = f.right / texW;
|
|
450
|
-
const topV = f.top / texH;
|
|
451
|
-
const bottomV = f.bottom / texH;
|
|
452
|
-
mins[0] = minU;
|
|
453
|
-
mins[1] = flipY ? bottomV : topV;
|
|
454
|
-
maxs[0] = maxU;
|
|
455
|
-
maxs[1] = flipY ? topV : bottomV;
|
|
456
|
-
return { uvMins: mins, uvMaxs: maxs };
|
|
457
|
-
}
|
|
458
|
-
for (let i = 0; i < frames.length; i++) {
|
|
459
|
-
const f = frames[i];
|
|
460
|
-
const o = i * 2;
|
|
461
|
-
const minU = f.left / texW;
|
|
462
|
-
const maxU = f.right / texW;
|
|
463
|
-
const topV = f.top / texH;
|
|
464
|
-
const bottomV = f.bottom / texH;
|
|
465
|
-
mins[o + 0] = minU;
|
|
466
|
-
mins[o + 1] = flipY ? bottomV : topV;
|
|
467
|
-
maxs[o + 0] = maxU;
|
|
468
|
-
maxs[o + 1] = flipY ? topV : bottomV;
|
|
469
|
-
}
|
|
470
|
-
return { uvMins: mins, uvMaxs: maxs };
|
|
471
|
-
}
|
|
472
|
-
_getPipeline(blendMode, format, stencil) {
|
|
473
|
-
const pipelineKey = `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
|
|
474
|
-
const existingPipeline = this._pipelines.get(pipelineKey);
|
|
475
|
-
if (existingPipeline) {
|
|
476
|
-
return existingPipeline;
|
|
477
|
-
}
|
|
478
|
-
const descriptor = {
|
|
479
|
-
layout: this._pipelineLayout,
|
|
480
|
-
vertex: {
|
|
481
|
-
module: this._shaderModule,
|
|
482
|
-
entryPoint: 'vertexMain',
|
|
483
|
-
buffers: [
|
|
484
|
-
{
|
|
485
|
-
arrayStride: staticVertexStrideBytes,
|
|
486
|
-
attributes: [
|
|
487
|
-
{
|
|
488
|
-
shaderLocation: 0,
|
|
489
|
-
offset: 0,
|
|
490
|
-
format: 'float32x2',
|
|
491
|
-
},
|
|
492
|
-
],
|
|
493
|
-
},
|
|
494
|
-
{
|
|
495
|
-
arrayStride: instanceStrideBytes,
|
|
496
|
-
stepMode: 'instance',
|
|
497
|
-
attributes: [
|
|
498
|
-
{
|
|
499
|
-
shaderLocation: 1,
|
|
500
|
-
offset: 0,
|
|
501
|
-
format: 'float32x2',
|
|
502
|
-
},
|
|
503
|
-
{
|
|
504
|
-
shaderLocation: 2,
|
|
505
|
-
offset: 8,
|
|
506
|
-
format: 'float32x2',
|
|
507
|
-
},
|
|
508
|
-
{
|
|
509
|
-
shaderLocation: 3,
|
|
510
|
-
offset: 16,
|
|
511
|
-
format: 'float32',
|
|
512
|
-
},
|
|
513
|
-
{
|
|
514
|
-
shaderLocation: 4,
|
|
515
|
-
offset: 20,
|
|
516
|
-
format: 'unorm8x4',
|
|
517
|
-
},
|
|
518
|
-
{
|
|
519
|
-
shaderLocation: 5,
|
|
520
|
-
offset: 24,
|
|
521
|
-
format: 'float32x2',
|
|
522
|
-
},
|
|
523
|
-
{
|
|
524
|
-
shaderLocation: 6,
|
|
525
|
-
offset: 32,
|
|
526
|
-
format: 'float32x2',
|
|
527
|
-
},
|
|
528
|
-
],
|
|
529
|
-
},
|
|
530
|
-
],
|
|
531
|
-
},
|
|
532
|
-
fragment: {
|
|
533
|
-
module: this._shaderModule,
|
|
534
|
-
entryPoint: 'fragmentMain',
|
|
535
|
-
targets: [
|
|
536
|
-
{
|
|
537
|
-
format,
|
|
538
|
-
blend: getWebGpuBlendState(blendMode),
|
|
539
|
-
writeMask: GPUColorWrite.ALL,
|
|
540
|
-
},
|
|
541
|
-
],
|
|
542
|
-
},
|
|
543
|
-
primitive: {
|
|
544
|
-
topology: 'triangle-list',
|
|
545
|
-
},
|
|
546
|
-
};
|
|
547
|
-
if (stencil) {
|
|
548
|
-
descriptor.depthStencil = stencilContentDepthStencilState();
|
|
549
|
-
}
|
|
550
|
-
const pipeline = this._device.createRenderPipeline(descriptor);
|
|
551
|
-
this._pipelines.set(pipelineKey, pipeline);
|
|
552
|
-
return pipeline;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
546
|
+
//#endregion
|
|
556
547
|
export { WebGpuParticleRenderer };
|
|
557
|
-
//# sourceMappingURL=WebGpuParticleRenderer.js.map
|
|
548
|
+
//# sourceMappingURL=WebGpuParticleRenderer.js.map
|