@codexo/exojs 2.1.0 → 2.1.1
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.md +20 -0
- package/README.md +1 -3
- package/dist/esm/particles/emitters/ParticleOptions.d.ts +11 -0
- package/dist/esm/particles/emitters/ParticleOptions.js +11 -0
- package/dist/esm/particles/emitters/ParticleOptions.js.map +1 -1
- package/dist/esm/rendering/Container.d.ts +1 -1
- package/dist/esm/rendering/Container.js +5 -2
- package/dist/esm/rendering/Container.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2ShaderRuntime.js +7 -0
- package/dist/esm/rendering/webgl2/WebGl2ShaderRuntime.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js +66 -43
- package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.d.ts +2 -6
- package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.js +160 -93
- package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuRenderManager.js +50 -39
- package/dist/esm/rendering/webgpu/WebGpuRenderManager.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js +75 -32
- package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js.map +1 -1
- package/dist/exo.d.ts +14 -7
- package/dist/exo.esm.js +374 -209
- package/dist/exo.esm.js.map +1 -1
- package/dist/exo.esm.min.js +1 -1
- package/dist/exo.esm.min.js.map +1 -1
- package/dist/exo.global.js +374 -209
- package/dist/exo.global.js.map +1 -1
- package/dist/exo.global.min.js +1 -1
- package/dist/exo.global.min.js.map +1 -1
- package/package.json +3 -4
package/dist/exo.esm.js
CHANGED
|
@@ -5291,6 +5291,13 @@ function createWebGl2ShaderRuntime(gl) {
|
|
|
5291
5291
|
gl.useProgram(null);
|
|
5292
5292
|
},
|
|
5293
5293
|
sync: () => {
|
|
5294
|
+
// Bind the program before syncing uniforms. WebGl2RenderManager
|
|
5295
|
+
// does not call bindShader() on the active renderer's shader
|
|
5296
|
+
// during normal draw flow, so sync() is the first entry point
|
|
5297
|
+
// that must establish program binding — otherwise uniform*
|
|
5298
|
+
// targets the wrong (or no) program and the subsequent draw
|
|
5299
|
+
// call fails with "no valid shader program in use".
|
|
5300
|
+
gl.useProgram(program);
|
|
5294
5301
|
syncUniforms();
|
|
5295
5302
|
},
|
|
5296
5303
|
destroy: (shader) => {
|
|
@@ -7423,15 +7430,8 @@ function getWebGpuBlendState(blendMode) {
|
|
|
7423
7430
|
|
|
7424
7431
|
/// <reference types="@webgpu/types" />
|
|
7425
7432
|
const primitiveShaderSource = `
|
|
7426
|
-
struct TransformUniforms {
|
|
7427
|
-
matrix: mat4x4<f32>,
|
|
7428
|
-
};
|
|
7429
|
-
|
|
7430
|
-
@group(0) @binding(0)
|
|
7431
|
-
var<uniform> uniforms: TransformUniforms;
|
|
7432
|
-
|
|
7433
7433
|
struct VertexInput {
|
|
7434
|
-
@location(0) position:
|
|
7434
|
+
@location(0) position: vec4<f32>,
|
|
7435
7435
|
@location(1) color: vec4<f32>,
|
|
7436
7436
|
};
|
|
7437
7437
|
|
|
@@ -7444,7 +7444,7 @@ struct VertexOutput {
|
|
|
7444
7444
|
fn vertexMain(input: VertexInput) -> VertexOutput {
|
|
7445
7445
|
var output: VertexOutput;
|
|
7446
7446
|
|
|
7447
|
-
output.position =
|
|
7447
|
+
output.position = input.position;
|
|
7448
7448
|
output.color = vec4<f32>(input.color.rgb * input.color.a, input.color.a);
|
|
7449
7449
|
|
|
7450
7450
|
return output;
|
|
@@ -7455,23 +7455,24 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
|
7455
7455
|
return input.color;
|
|
7456
7456
|
}
|
|
7457
7457
|
`;
|
|
7458
|
-
|
|
7459
|
-
|
|
7458
|
+
// 4 floats (pre-transformed clip-space position) + 1 u32 (color) = 20 bytes.
|
|
7459
|
+
// The CPU applies (view * shape.globalTransform) to each vertex before writing
|
|
7460
|
+
// it into the vertex buffer, so the shader outputs the position as-is. This
|
|
7461
|
+
// matches the sprite renderer's approach and eliminates the need for a per-
|
|
7462
|
+
// drawcall uniform binding.
|
|
7463
|
+
const vertexStrideBytes$1 = 20;
|
|
7464
|
+
const wordsPerVertex$1 = vertexStrideBytes$1 / Uint32Array.BYTES_PER_ELEMENT;
|
|
7460
7465
|
class WebGpuPrimitiveRenderer extends AbstractWebGpuRenderer {
|
|
7461
7466
|
constructor() {
|
|
7462
7467
|
super(...arguments);
|
|
7463
7468
|
this._combinedTransform = new Matrix();
|
|
7464
7469
|
this._drawCalls = [];
|
|
7465
7470
|
this._drawCallCount = 0;
|
|
7466
|
-
this._transformData = new Float32Array(transformByteLength / Float32Array.BYTES_PER_ELEMENT);
|
|
7467
7471
|
this._pipelines = new Map();
|
|
7468
7472
|
this._renderManager = null;
|
|
7469
7473
|
this._device = null;
|
|
7470
7474
|
this._shaderModule = null;
|
|
7471
|
-
this._bindGroupLayout = null;
|
|
7472
7475
|
this._pipelineLayout = null;
|
|
7473
|
-
this._uniformBuffer = null;
|
|
7474
|
-
this._bindGroup = null;
|
|
7475
7476
|
this._vertexBuffer = null;
|
|
7476
7477
|
this._indexBuffer = null;
|
|
7477
7478
|
this._vertexBufferCapacity = 0;
|
|
@@ -7479,6 +7480,7 @@ class WebGpuPrimitiveRenderer extends AbstractWebGpuRenderer {
|
|
|
7479
7480
|
this._vertexData = new ArrayBuffer(0);
|
|
7480
7481
|
this._float32View = new Float32Array(this._vertexData);
|
|
7481
7482
|
this._uint32View = new Uint32Array(this._vertexData);
|
|
7483
|
+
this._packedIndexData = new Uint16Array(0);
|
|
7482
7484
|
this._generatedIndexData = new Uint16Array(0);
|
|
7483
7485
|
this._sequentialIndexData = new Uint16Array(0);
|
|
7484
7486
|
}
|
|
@@ -7516,60 +7518,121 @@ class WebGpuPrimitiveRenderer extends AbstractWebGpuRenderer {
|
|
|
7516
7518
|
flush() {
|
|
7517
7519
|
const runtime = this._renderManager;
|
|
7518
7520
|
const device = this._device;
|
|
7519
|
-
|
|
7520
|
-
const uniformBuffer = this._uniformBuffer;
|
|
7521
|
-
if (!runtime || !device || !bindGroup || !uniformBuffer) {
|
|
7521
|
+
if (!runtime || !device) {
|
|
7522
7522
|
return;
|
|
7523
7523
|
}
|
|
7524
7524
|
if (this._drawCallCount === 0 && !runtime.clearRequested) {
|
|
7525
7525
|
return;
|
|
7526
7526
|
}
|
|
7527
|
-
const encoder = device.createCommandEncoder();
|
|
7528
|
-
const pass = encoder.beginRenderPass({
|
|
7529
|
-
colorAttachments: [runtime.createColorAttachment()],
|
|
7530
|
-
});
|
|
7531
|
-
runtime.stats.renderPasses++;
|
|
7532
7527
|
const scissor = runtime.getScissorRect();
|
|
7533
7528
|
const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7529
|
+
// Phase 1: resolve drawcalls and record each one's offsets into the
|
|
7530
|
+
// shared packed buffers. Transform gets baked into the vertex data
|
|
7531
|
+
// during phase 2 so no per-drawcall uniform binding is needed.
|
|
7532
|
+
const plan = [];
|
|
7533
|
+
const resolvedDrawCalls = [];
|
|
7534
|
+
let totalVertices = 0;
|
|
7535
|
+
let totalIndices = 0;
|
|
7536
|
+
if (this._drawCallCount > 0 && !maskClipsAll) {
|
|
7538
7537
|
for (let drawCallIndex = 0; drawCallIndex < this._drawCallCount; drawCallIndex++) {
|
|
7539
7538
|
const drawCall = this._drawCalls[drawCallIndex];
|
|
7540
7539
|
const shape = drawCall.shape;
|
|
7541
|
-
const
|
|
7542
|
-
|
|
7543
|
-
if (
|
|
7540
|
+
const resolved = this._resolveDrawCall(shape);
|
|
7541
|
+
resolvedDrawCalls.push(resolved);
|
|
7542
|
+
if (resolved === null) {
|
|
7544
7543
|
continue;
|
|
7545
7544
|
}
|
|
7546
7545
|
const pipeline = this._getPipeline({
|
|
7547
|
-
topology:
|
|
7548
|
-
usesStripIndex:
|
|
7546
|
+
topology: resolved.topology,
|
|
7547
|
+
usesStripIndex: resolved.usesStripIndex,
|
|
7549
7548
|
blendMode: drawCall.blendMode,
|
|
7550
7549
|
format: runtime.renderTargetFormat,
|
|
7551
7550
|
});
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7551
|
+
plan.push({
|
|
7552
|
+
pipeline,
|
|
7553
|
+
vertexByteOffset: totalVertices * vertexStrideBytes$1,
|
|
7554
|
+
vertexCount: resolved.vertexCount,
|
|
7555
|
+
indexByteOffset: totalIndices * Uint16Array.BYTES_PER_ELEMENT,
|
|
7556
|
+
indexCount: resolved.indexCount,
|
|
7557
|
+
});
|
|
7558
|
+
totalVertices += resolved.vertexCount;
|
|
7559
|
+
totalIndices += resolved.indexCount;
|
|
7560
|
+
}
|
|
7561
|
+
}
|
|
7562
|
+
// If nothing will actually render, still honor a pending clear with
|
|
7563
|
+
// a single empty pass so createColorAttachment consumes the clear
|
|
7564
|
+
// state exactly once.
|
|
7565
|
+
if (plan.length === 0) {
|
|
7566
|
+
if (runtime.clearRequested) {
|
|
7567
|
+
const encoder = device.createCommandEncoder();
|
|
7568
|
+
const pass = encoder.beginRenderPass({
|
|
7569
|
+
colorAttachments: [runtime.createColorAttachment()],
|
|
7570
|
+
});
|
|
7571
|
+
runtime.stats.renderPasses++;
|
|
7572
|
+
pass.end();
|
|
7573
|
+
runtime.submit(encoder.finish());
|
|
7574
|
+
}
|
|
7575
|
+
this._drawCallCount = 0;
|
|
7576
|
+
return;
|
|
7577
|
+
}
|
|
7578
|
+
// Phase 2: size GPU buffers for the whole-frame totals, then pack
|
|
7579
|
+
// every drawcall's CPU-side data. _writeShapeVertices applies
|
|
7580
|
+
// (view * shape.globalTransform) per-vertex so the shader simply
|
|
7581
|
+
// outputs input.position unchanged.
|
|
7582
|
+
this._ensureVertexCapacity(totalVertices);
|
|
7583
|
+
if (totalIndices > 0) {
|
|
7584
|
+
this._ensureIndexCapacity(totalIndices);
|
|
7585
|
+
if (this._packedIndexData.length < totalIndices) {
|
|
7586
|
+
this._packedIndexData = new Uint16Array(Math.max(totalIndices, this._packedIndexData.length === 0 ? 1 : this._packedIndexData.length * 2));
|
|
7587
|
+
}
|
|
7588
|
+
}
|
|
7589
|
+
{
|
|
7590
|
+
let vOffset = 0;
|
|
7591
|
+
let iOffset = 0;
|
|
7592
|
+
for (let i = 0; i < this._drawCallCount; i++) {
|
|
7593
|
+
const resolved = resolvedDrawCalls[i];
|
|
7594
|
+
if (resolved === null) {
|
|
7595
|
+
continue;
|
|
7565
7596
|
}
|
|
7566
|
-
|
|
7567
|
-
|
|
7597
|
+
const drawCall = this._drawCalls[i];
|
|
7598
|
+
const shape = drawCall.shape;
|
|
7599
|
+
this._writeShapeVertices(runtime, shape, vOffset);
|
|
7600
|
+
if (resolved.indices !== null && resolved.indexCount > 0) {
|
|
7601
|
+
this._packedIndexData.set(resolved.indices.subarray(0, resolved.indexCount), iOffset);
|
|
7602
|
+
iOffset += resolved.indexCount;
|
|
7568
7603
|
}
|
|
7569
|
-
|
|
7570
|
-
runtime.stats.drawCalls++;
|
|
7604
|
+
vOffset += resolved.vertexCount;
|
|
7571
7605
|
}
|
|
7572
7606
|
}
|
|
7607
|
+
// Phase 3: single writeBuffer per GPU buffer covers the whole frame.
|
|
7608
|
+
device.queue.writeBuffer(this._vertexBuffer, 0, this._vertexData, 0, totalVertices * vertexStrideBytes$1);
|
|
7609
|
+
if (totalIndices > 0) {
|
|
7610
|
+
device.queue.writeBuffer(this._indexBuffer, 0, this._packedIndexData.buffer, this._packedIndexData.byteOffset, totalIndices * Uint16Array.BYTES_PER_ELEMENT);
|
|
7611
|
+
}
|
|
7612
|
+
// Phase 4: single render pass. Per-draw state is just pipeline and
|
|
7613
|
+
// vertex/index subrange offsets — the transform has already been
|
|
7614
|
+
// baked into the vertex data.
|
|
7615
|
+
const encoder = device.createCommandEncoder();
|
|
7616
|
+
const pass = encoder.beginRenderPass({
|
|
7617
|
+
colorAttachments: [runtime.createColorAttachment()],
|
|
7618
|
+
});
|
|
7619
|
+
runtime.stats.renderPasses++;
|
|
7620
|
+
if (scissor !== null) {
|
|
7621
|
+
pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
|
|
7622
|
+
}
|
|
7623
|
+
for (const planned of plan) {
|
|
7624
|
+
pass.setPipeline(planned.pipeline);
|
|
7625
|
+
pass.setVertexBuffer(0, this._vertexBuffer, planned.vertexByteOffset);
|
|
7626
|
+
if (planned.indexCount > 0) {
|
|
7627
|
+
pass.setIndexBuffer(this._indexBuffer, 'uint16', planned.indexByteOffset);
|
|
7628
|
+
pass.drawIndexed(planned.indexCount);
|
|
7629
|
+
}
|
|
7630
|
+
else {
|
|
7631
|
+
pass.draw(planned.vertexCount);
|
|
7632
|
+
}
|
|
7633
|
+
runtime.stats.batches++;
|
|
7634
|
+
runtime.stats.drawCalls++;
|
|
7635
|
+
}
|
|
7573
7636
|
pass.end();
|
|
7574
7637
|
runtime.submit(encoder.finish());
|
|
7575
7638
|
this._drawCallCount = 0;
|
|
@@ -7582,65 +7645,76 @@ class WebGpuPrimitiveRenderer extends AbstractWebGpuRenderer {
|
|
|
7582
7645
|
this._renderManager = runtime;
|
|
7583
7646
|
this._device = this._renderManager.device;
|
|
7584
7647
|
this._shaderModule = this._device.createShaderModule({ code: primitiveShaderSource });
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
binding: 0,
|
|
7588
|
-
visibility: GPUShaderStage.VERTEX,
|
|
7589
|
-
buffer: {
|
|
7590
|
-
type: 'uniform',
|
|
7591
|
-
},
|
|
7592
|
-
}],
|
|
7593
|
-
});
|
|
7648
|
+
// Transform is applied per-vertex on the CPU, so no uniform binding
|
|
7649
|
+
// is needed — the shader outputs input.position directly.
|
|
7594
7650
|
this._pipelineLayout = this._device.createPipelineLayout({
|
|
7595
|
-
bindGroupLayouts: [
|
|
7596
|
-
});
|
|
7597
|
-
this._uniformBuffer = this._device.createBuffer({
|
|
7598
|
-
size: transformByteLength,
|
|
7599
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
7600
|
-
});
|
|
7601
|
-
this._bindGroup = this._device.createBindGroup({
|
|
7602
|
-
layout: this._bindGroupLayout,
|
|
7603
|
-
entries: [{
|
|
7604
|
-
binding: 0,
|
|
7605
|
-
resource: {
|
|
7606
|
-
buffer: this._uniformBuffer,
|
|
7607
|
-
},
|
|
7608
|
-
}],
|
|
7651
|
+
bindGroupLayouts: [],
|
|
7609
7652
|
});
|
|
7610
7653
|
}
|
|
7611
7654
|
onDisconnect() {
|
|
7612
7655
|
this.flush();
|
|
7613
7656
|
this._destroyBuffers();
|
|
7614
7657
|
this._pipelines.clear();
|
|
7615
|
-
this._uniformBuffer?.destroy();
|
|
7616
|
-
this._uniformBuffer = null;
|
|
7617
|
-
this._bindGroup = null;
|
|
7618
|
-
this._bindGroupLayout = null;
|
|
7619
7658
|
this._pipelineLayout = null;
|
|
7620
7659
|
this._shaderModule = null;
|
|
7621
7660
|
this._device = null;
|
|
7622
7661
|
this._renderManager = null;
|
|
7623
7662
|
this._drawCallCount = 0;
|
|
7624
7663
|
}
|
|
7625
|
-
|
|
7664
|
+
_writeShapeVertices(runtime, shape, vertexStart) {
|
|
7665
|
+
// Matrix.combine is `other * this` (see Matrix.rotate and
|
|
7666
|
+
// SceneNode.getGlobalTransform, both of which chain via
|
|
7667
|
+
// local.combine(parent.global) to yield parent.global * local).
|
|
7668
|
+
//
|
|
7669
|
+
// We need view * global applied to a local vertex, so start with
|
|
7670
|
+
// global and combine with view — that gives
|
|
7671
|
+
// _combinedTransform = view * global.
|
|
7626
7672
|
const matrix = this._combinedTransform
|
|
7627
|
-
.copy(
|
|
7628
|
-
.combine(
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
]
|
|
7635
|
-
|
|
7636
|
-
|
|
7673
|
+
.copy(shape.getGlobalTransform())
|
|
7674
|
+
.combine(runtime.view.getTransform());
|
|
7675
|
+
// Match the original uniform-based WGSL layout exactly.
|
|
7676
|
+
//
|
|
7677
|
+
// The shader packs the Matrix's 9 fields into a 4x4 mat (column-major
|
|
7678
|
+
// in WGSL):
|
|
7679
|
+
// col 0 = [a, c, 0, 0]
|
|
7680
|
+
// col 1 = [b, d, 0, 0]
|
|
7681
|
+
// col 2 = [0, 0, 1, 0]
|
|
7682
|
+
// col 3 = [x, y, 0, z]
|
|
7683
|
+
//
|
|
7684
|
+
// Multiplied by vec4(px, py, 0, 1):
|
|
7685
|
+
// out = col0*px + col1*py + col2*0 + col3*1
|
|
7686
|
+
// out.x = a*px + b*py + x
|
|
7687
|
+
// out.y = c*px + d*py + y
|
|
7688
|
+
// out.z = 0
|
|
7689
|
+
// out.w = z
|
|
7690
|
+
//
|
|
7691
|
+
// The Matrix class represents the affine matrix in the order
|
|
7692
|
+
// [a b x]
|
|
7693
|
+
// [c d y]
|
|
7694
|
+
// [e f z]
|
|
7695
|
+
// so a/b/c/d are rotation+scale (note: b on the TOP row, c on the
|
|
7696
|
+
// LEFT column, not the other way around) and x/y/z the translation /
|
|
7697
|
+
// w component. Matrix.toArray(false) confirms this layout.
|
|
7698
|
+
const a = matrix.a;
|
|
7699
|
+
const b = matrix.b;
|
|
7700
|
+
const c = matrix.c;
|
|
7701
|
+
const d = matrix.d;
|
|
7702
|
+
const tx = matrix.x;
|
|
7703
|
+
const ty = matrix.y;
|
|
7704
|
+
const tw = matrix.z;
|
|
7705
|
+
const color = shape.color.toRgba();
|
|
7706
|
+
const vertices = shape.geometry.vertices;
|
|
7637
7707
|
const vertexCount = vertices.length / 2;
|
|
7638
7708
|
for (let i = 0; i < vertexCount; i++) {
|
|
7639
7709
|
const sourceIndex = i * 2;
|
|
7640
|
-
const targetIndex = i *
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
this.
|
|
7710
|
+
const targetIndex = (vertexStart + i) * wordsPerVertex$1;
|
|
7711
|
+
const px = vertices[sourceIndex];
|
|
7712
|
+
const py = vertices[sourceIndex + 1];
|
|
7713
|
+
this._float32View[targetIndex + 0] = a * px + b * py + tx;
|
|
7714
|
+
this._float32View[targetIndex + 1] = c * px + d * py + ty;
|
|
7715
|
+
this._float32View[targetIndex + 2] = 0;
|
|
7716
|
+
this._float32View[targetIndex + 3] = tw;
|
|
7717
|
+
this._uint32View[targetIndex + 4] = color;
|
|
7644
7718
|
}
|
|
7645
7719
|
}
|
|
7646
7720
|
_ensureVertexCapacity(vertexCount) {
|
|
@@ -7687,10 +7761,10 @@ class WebGpuPrimitiveRenderer extends AbstractWebGpuRenderer {
|
|
|
7687
7761
|
attributes: [{
|
|
7688
7762
|
shaderLocation: 0,
|
|
7689
7763
|
offset: 0,
|
|
7690
|
-
format: '
|
|
7764
|
+
format: 'float32x4',
|
|
7691
7765
|
}, {
|
|
7692
7766
|
shaderLocation: 1,
|
|
7693
|
-
offset:
|
|
7767
|
+
offset: 16,
|
|
7694
7768
|
format: 'unorm8x4',
|
|
7695
7769
|
}],
|
|
7696
7770
|
}],
|
|
@@ -7844,7 +7918,7 @@ const spriteShaderSource = `
|
|
|
7844
7918
|
struct ProjectionUniforms {
|
|
7845
7919
|
matrix: mat4x4<f32>,
|
|
7846
7920
|
};
|
|
7847
|
-
|
|
7921
|
+
|
|
7848
7922
|
@group(0) @binding(0)
|
|
7849
7923
|
var<uniform> projection: ProjectionUniforms;
|
|
7850
7924
|
|
|
@@ -7881,7 +7955,7 @@ var spriteSampler5: sampler;
|
|
|
7881
7955
|
var spriteSampler6: sampler;
|
|
7882
7956
|
@group(1) @binding(15)
|
|
7883
7957
|
var spriteSampler7: sampler;
|
|
7884
|
-
|
|
7958
|
+
|
|
7885
7959
|
struct VertexInput {
|
|
7886
7960
|
@location(0) position: vec2<f32>,
|
|
7887
7961
|
@location(1) texcoord: vec2<f32>,
|
|
@@ -7889,7 +7963,7 @@ struct VertexInput {
|
|
|
7889
7963
|
@location(3) premultiplySample: u32,
|
|
7890
7964
|
@location(4) textureSlot: u32,
|
|
7891
7965
|
};
|
|
7892
|
-
|
|
7966
|
+
|
|
7893
7967
|
struct VertexOutput {
|
|
7894
7968
|
@builtin(position) position: vec4<f32>,
|
|
7895
7969
|
@location(0) texcoord: vec2<f32>,
|
|
@@ -7897,12 +7971,12 @@ struct VertexOutput {
|
|
|
7897
7971
|
@location(2) @interpolate(flat) premultiplySample: u32,
|
|
7898
7972
|
@location(3) @interpolate(flat) textureSlot: u32,
|
|
7899
7973
|
};
|
|
7900
|
-
|
|
7901
|
-
@vertex
|
|
7902
|
-
fn vertexMain(input: VertexInput) -> VertexOutput {
|
|
7903
|
-
var output: VertexOutput;
|
|
7904
|
-
|
|
7905
|
-
output.position = projection.matrix * vec4<f32>(input.position, 0.0, 1.0);
|
|
7974
|
+
|
|
7975
|
+
@vertex
|
|
7976
|
+
fn vertexMain(input: VertexInput) -> VertexOutput {
|
|
7977
|
+
var output: VertexOutput;
|
|
7978
|
+
|
|
7979
|
+
output.position = projection.matrix * vec4<f32>(input.position, 0.0, 1.0);
|
|
7906
7980
|
output.texcoord = input.texcoord;
|
|
7907
7981
|
output.color = vec4(input.color.rgb * input.color.a, input.color.a);
|
|
7908
7982
|
output.premultiplySample = input.premultiplySample;
|
|
@@ -7911,38 +7985,46 @@ fn vertexMain(input: VertexInput) -> VertexOutput {
|
|
|
7911
7985
|
return output;
|
|
7912
7986
|
}
|
|
7913
7987
|
|
|
7914
|
-
fn sampleTexture(slot: u32, uv: vec2<f32>) -> vec4<f32> {
|
|
7988
|
+
fn sampleTexture(slot: u32, uv: vec2<f32>, ddx: vec2<f32>, ddy: vec2<f32>) -> vec4<f32> {
|
|
7915
7989
|
switch slot {
|
|
7916
7990
|
case 0u: {
|
|
7917
|
-
return
|
|
7991
|
+
return textureSampleGrad(spriteTexture0, spriteSampler0, uv, ddx, ddy);
|
|
7918
7992
|
}
|
|
7919
7993
|
case 1u: {
|
|
7920
|
-
return
|
|
7994
|
+
return textureSampleGrad(spriteTexture1, spriteSampler1, uv, ddx, ddy);
|
|
7921
7995
|
}
|
|
7922
7996
|
case 2u: {
|
|
7923
|
-
return
|
|
7997
|
+
return textureSampleGrad(spriteTexture2, spriteSampler2, uv, ddx, ddy);
|
|
7924
7998
|
}
|
|
7925
7999
|
case 3u: {
|
|
7926
|
-
return
|
|
8000
|
+
return textureSampleGrad(spriteTexture3, spriteSampler3, uv, ddx, ddy);
|
|
7927
8001
|
}
|
|
7928
8002
|
case 4u: {
|
|
7929
|
-
return
|
|
8003
|
+
return textureSampleGrad(spriteTexture4, spriteSampler4, uv, ddx, ddy);
|
|
7930
8004
|
}
|
|
7931
8005
|
case 5u: {
|
|
7932
|
-
return
|
|
8006
|
+
return textureSampleGrad(spriteTexture5, spriteSampler5, uv, ddx, ddy);
|
|
7933
8007
|
}
|
|
7934
8008
|
case 6u: {
|
|
7935
|
-
return
|
|
8009
|
+
return textureSampleGrad(spriteTexture6, spriteSampler6, uv, ddx, ddy);
|
|
7936
8010
|
}
|
|
7937
8011
|
default: {
|
|
7938
|
-
return
|
|
8012
|
+
return textureSampleGrad(spriteTexture7, spriteSampler7, uv, ddx, ddy);
|
|
7939
8013
|
}
|
|
7940
8014
|
}
|
|
7941
8015
|
}
|
|
7942
8016
|
|
|
7943
8017
|
@fragment
|
|
7944
8018
|
fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
7945
|
-
|
|
8019
|
+
// Compute screen-space derivatives in uniform control flow before the
|
|
8020
|
+
// per-slot switch. WGSL requires textureSample (implicit LOD) to run in
|
|
8021
|
+
// uniform control flow, which multi-texture batching breaks because the
|
|
8022
|
+
// slot varies per fragment. textureSampleGrad takes explicit derivatives
|
|
8023
|
+
// and is valid regardless of control-flow uniformity, while preserving
|
|
8024
|
+
// mipmap-correct LOD when sprites use mipmapped textures.
|
|
8025
|
+
let ddx = dpdx(input.texcoord);
|
|
8026
|
+
let ddy = dpdy(input.texcoord);
|
|
8027
|
+
let sample = sampleTexture(input.textureSlot, input.texcoord, ddx, ddy);
|
|
7946
8028
|
let resolvedSample = select(sample, vec4(sample.rgb * sample.a, sample.a), input.premultiplySample == 1u);
|
|
7947
8029
|
|
|
7948
8030
|
return resolvedSample * input.color;
|
|
@@ -8092,6 +8174,44 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
8092
8174
|
if (this._drawCallCount === 0 && !renderManager.clearRequested) {
|
|
8093
8175
|
return;
|
|
8094
8176
|
}
|
|
8177
|
+
// Grow vertex/index buffers up front for the TOTAL sprite count. Two
|
|
8178
|
+
// reasons this must happen before the render pass begins:
|
|
8179
|
+
// 1. _ensureBatchCapacity destroys old buffers and creates new ones
|
|
8180
|
+
// when capacity grows, so running it after setVertexBuffer /
|
|
8181
|
+
// setIndexBuffer would leave the pass bound to destroyed buffers.
|
|
8182
|
+
// 2. All batches are packed into the vertex buffer at distinct
|
|
8183
|
+
// sprite offsets, so the buffer must hold every sprite in the
|
|
8184
|
+
// flush, not just one batch worth.
|
|
8185
|
+
if (this._drawCallCount > 0) {
|
|
8186
|
+
this._ensureBatchCapacity(this._drawCallCount);
|
|
8187
|
+
}
|
|
8188
|
+
// Walk the batches once, packing each batch's vertex data into the
|
|
8189
|
+
// CPU-side buffer at its own sprite-aligned offset. Each batch's
|
|
8190
|
+
// metadata is recorded for the draw loop below.
|
|
8191
|
+
//
|
|
8192
|
+
// This replaces an earlier per-batch queue.writeBuffer(..., offset: 0)
|
|
8193
|
+
// pattern where every writeBuffer targeted the same GPU offset. All
|
|
8194
|
+
// writeBuffers in a frame execute before queue.submit(commandBuffer),
|
|
8195
|
+
// so only the last batch's vertex data survived — which meant any
|
|
8196
|
+
// flush containing more than one batch rendered every batch using
|
|
8197
|
+
// the LAST batch's vertices (background vanished, sprites duplicated
|
|
8198
|
+
// at wrong sizes, etc. whenever blend mode / texture slot / pipeline
|
|
8199
|
+
// caused a split into multiple batches).
|
|
8200
|
+
const batchPlan = [];
|
|
8201
|
+
let packedSpriteCount = 0;
|
|
8202
|
+
for (let start = 0; start < this._drawCallCount;) {
|
|
8203
|
+
const batch = this._getBatchRange(start);
|
|
8204
|
+
const spriteCount = batch.end - batch.start;
|
|
8205
|
+
this._writeBatchVertexData(batch, packedSpriteCount);
|
|
8206
|
+
batchPlan.push({
|
|
8207
|
+
firstSprite: packedSpriteCount,
|
|
8208
|
+
spriteCount,
|
|
8209
|
+
blendMode: batch.blendMode,
|
|
8210
|
+
textures: batch.textures,
|
|
8211
|
+
});
|
|
8212
|
+
packedSpriteCount += spriteCount;
|
|
8213
|
+
start = batch.end;
|
|
8214
|
+
}
|
|
8095
8215
|
const viewMatrix = renderManager.view.getTransform();
|
|
8096
8216
|
this._projectionData.set([
|
|
8097
8217
|
viewMatrix.a, viewMatrix.c, 0, 0,
|
|
@@ -8111,23 +8231,20 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
8111
8231
|
pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
|
|
8112
8232
|
}
|
|
8113
8233
|
if (this._drawCallCount > 0 && !maskClipsAll) {
|
|
8234
|
+
// Single upload for the whole packed vertex buffer — every batch
|
|
8235
|
+
// reads from its own sprite range via drawIndexed's firstIndex.
|
|
8236
|
+
device.queue.writeBuffer(this._vertexBuffer, 0, this._vertexData, 0, packedSpriteCount * spriteVertexCount * vertexStrideBytes);
|
|
8114
8237
|
pass.setBindGroup(0, uniformBindGroup);
|
|
8115
8238
|
pass.setVertexBuffer(0, this._vertexBuffer);
|
|
8116
8239
|
pass.setIndexBuffer(this._indexBuffer, 'uint32');
|
|
8117
|
-
for (
|
|
8118
|
-
const
|
|
8119
|
-
const
|
|
8120
|
-
const spriteCount = batch.end - batch.start;
|
|
8121
|
-
this._ensureBatchCapacity(spriteCount);
|
|
8122
|
-
this._writeBatchVertexData(batch);
|
|
8123
|
-
device.queue.writeBuffer(this._vertexBuffer, 0, this._vertexData, 0, spriteCount * spriteVertexCount * vertexStrideBytes);
|
|
8124
|
-
const textureBindGroup = this._createTextureBindGroup(device, renderManager, batch.textures);
|
|
8240
|
+
for (const plan of batchPlan) {
|
|
8241
|
+
const pipeline = this._getPipeline(plan.blendMode, renderManager.renderTargetFormat);
|
|
8242
|
+
const textureBindGroup = this._createTextureBindGroup(device, renderManager, plan.textures);
|
|
8125
8243
|
pass.setPipeline(pipeline);
|
|
8126
8244
|
pass.setBindGroup(1, textureBindGroup);
|
|
8127
|
-
pass.drawIndexed(
|
|
8245
|
+
pass.drawIndexed(plan.spriteCount * spriteIndexCount, 1, plan.firstSprite * spriteIndexCount, 0, 0);
|
|
8128
8246
|
renderManager.stats.batches++;
|
|
8129
8247
|
renderManager.stats.drawCalls++;
|
|
8130
|
-
start = batch.end;
|
|
8131
8248
|
}
|
|
8132
8249
|
}
|
|
8133
8250
|
pass.end();
|
|
@@ -8152,7 +8269,7 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
8152
8269
|
});
|
|
8153
8270
|
const indexData = new Uint32Array(nextCapacity * spriteIndexCount);
|
|
8154
8271
|
const indexBuffer = this._device.createBuffer({
|
|
8155
|
-
size: indexData.byteLength
|
|
8272
|
+
size: indexData.byteLength,
|
|
8156
8273
|
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
8157
8274
|
});
|
|
8158
8275
|
for (let spriteIndex = 0; spriteIndex < nextCapacity; spriteIndex++) {
|
|
@@ -8175,12 +8292,12 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
|
|
|
8175
8292
|
this._vertexBuffer = vertexBuffer;
|
|
8176
8293
|
this._indexBuffer = indexBuffer;
|
|
8177
8294
|
}
|
|
8178
|
-
_writeBatchVertexData(batch) {
|
|
8295
|
+
_writeBatchVertexData(batch, firstSprite) {
|
|
8179
8296
|
const renderManager = this._renderManager;
|
|
8180
8297
|
if (!renderManager) {
|
|
8181
8298
|
return;
|
|
8182
8299
|
}
|
|
8183
|
-
let vertexOffset =
|
|
8300
|
+
let vertexOffset = firstSprite * spriteVertexCount * wordsPerVertex;
|
|
8184
8301
|
for (let drawCallIndex = batch.start; drawCallIndex < batch.end; drawCallIndex++) {
|
|
8185
8302
|
const drawCall = this._drawCalls[drawCallIndex];
|
|
8186
8303
|
const textureSlot = batch.textureSlots.get(drawCall.texture) ?? 0;
|
|
@@ -8462,54 +8579,77 @@ class WebGpuParticleRenderer extends AbstractWebGpuRenderer {
|
|
|
8462
8579
|
if (this._drawCallCount === 0 && !runtime.clearRequested) {
|
|
8463
8580
|
return;
|
|
8464
8581
|
}
|
|
8465
|
-
const encoder = device.createCommandEncoder();
|
|
8466
|
-
const pass = encoder.beginRenderPass({
|
|
8467
|
-
colorAttachments: [runtime.createColorAttachment()],
|
|
8468
|
-
});
|
|
8469
|
-
runtime.stats.renderPasses++;
|
|
8470
8582
|
const scissor = runtime.getScissorRect();
|
|
8471
8583
|
const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
if (
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
const
|
|
8479
|
-
|
|
8480
|
-
const particleCount = system.particles.length;
|
|
8481
|
-
if (particleCount === 0) {
|
|
8482
|
-
continue;
|
|
8483
|
-
}
|
|
8484
|
-
const pipeline = this._getPipeline(drawCall.blendMode, runtime.renderTargetFormat);
|
|
8485
|
-
const textureBinding = runtime.getTextureBinding(drawCall.texture);
|
|
8486
|
-
const textureBindGroup = device.createBindGroup({
|
|
8487
|
-
layout: this._textureBindGroupLayout,
|
|
8488
|
-
entries: [{
|
|
8489
|
-
binding: 0,
|
|
8490
|
-
resource: textureBinding.view,
|
|
8491
|
-
}, {
|
|
8492
|
-
binding: 1,
|
|
8493
|
-
resource: textureBinding.sampler,
|
|
8494
|
-
}],
|
|
8584
|
+
// If no drawcalls will actually render (none queued, or the scissor
|
|
8585
|
+
// clips everything), but a clear is pending, open a single empty
|
|
8586
|
+
// pass so createColorAttachment consumes the clear state.
|
|
8587
|
+
if (this._drawCallCount === 0 || maskClipsAll) {
|
|
8588
|
+
if (runtime.clearRequested) {
|
|
8589
|
+
const encoder = device.createCommandEncoder();
|
|
8590
|
+
const pass = encoder.beginRenderPass({
|
|
8591
|
+
colorAttachments: [runtime.createColorAttachment()],
|
|
8495
8592
|
});
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
device.queue.writeBuffer(this._instanceBuffer, 0, this._instanceData, 0, particleCount * instanceStrideBytes);
|
|
8500
|
-
device.queue.writeBuffer(uniformBuffer, 0, this._uniformData.buffer, this._uniformData.byteOffset, this._uniformData.byteLength);
|
|
8501
|
-
pass.setPipeline(pipeline);
|
|
8502
|
-
pass.setBindGroup(1, textureBindGroup);
|
|
8503
|
-
pass.setVertexBuffer(0, staticVertexBuffer);
|
|
8504
|
-
pass.setVertexBuffer(1, this._instanceBuffer);
|
|
8505
|
-
pass.setIndexBuffer(indexBuffer, 'uint16');
|
|
8506
|
-
pass.drawIndexed(indicesPerParticle, particleCount, 0, 0, 0);
|
|
8507
|
-
runtime.stats.batches++;
|
|
8508
|
-
runtime.stats.drawCalls++;
|
|
8593
|
+
runtime.stats.renderPasses++;
|
|
8594
|
+
pass.end();
|
|
8595
|
+
runtime.submit(encoder.finish());
|
|
8509
8596
|
}
|
|
8597
|
+
this._drawCallCount = 0;
|
|
8598
|
+
return;
|
|
8599
|
+
}
|
|
8600
|
+
// One command encoder / pass per drawcall. Each particle system's
|
|
8601
|
+
// queue.writeBuffer calls target offset 0 of the instance and uniform
|
|
8602
|
+
// buffers — a single pass with multiple systems would see all
|
|
8603
|
+
// writeBuffers serialize before submit, leaving only the last
|
|
8604
|
+
// system's data in those buffers and making every earlier draw read
|
|
8605
|
+
// the wrong data. Also: _ensureCapacity may destroy and recreate the
|
|
8606
|
+
// instance buffer on growth; keeping one drawcall per pass means
|
|
8607
|
+
// that destroy happens strictly between submits, so no pass holds a
|
|
8608
|
+
// reference to a buffer that has since been destroyed.
|
|
8609
|
+
for (let drawCallIndex = 0; drawCallIndex < this._drawCallCount; drawCallIndex++) {
|
|
8610
|
+
const drawCall = this._drawCalls[drawCallIndex];
|
|
8611
|
+
const system = drawCall.system;
|
|
8612
|
+
const particleCount = system.particles.length;
|
|
8613
|
+
if (particleCount === 0) {
|
|
8614
|
+
continue;
|
|
8615
|
+
}
|
|
8616
|
+
const pipeline = this._getPipeline(drawCall.blendMode, runtime.renderTargetFormat);
|
|
8617
|
+
const textureBinding = runtime.getTextureBinding(drawCall.texture);
|
|
8618
|
+
const textureBindGroup = device.createBindGroup({
|
|
8619
|
+
layout: this._textureBindGroupLayout,
|
|
8620
|
+
entries: [{
|
|
8621
|
+
binding: 0,
|
|
8622
|
+
resource: textureBinding.view,
|
|
8623
|
+
}, {
|
|
8624
|
+
binding: 1,
|
|
8625
|
+
resource: textureBinding.sampler,
|
|
8626
|
+
}],
|
|
8627
|
+
});
|
|
8628
|
+
this._ensureCapacity(particleCount);
|
|
8629
|
+
this._writeInstanceData(system.vertices, system.texCoords, system.particles);
|
|
8630
|
+
this._writeUniformData(runtime, system, drawCall.texture);
|
|
8631
|
+
device.queue.writeBuffer(this._instanceBuffer, 0, this._instanceData, 0, particleCount * instanceStrideBytes);
|
|
8632
|
+
device.queue.writeBuffer(uniformBuffer, 0, this._uniformData.buffer, this._uniformData.byteOffset, this._uniformData.byteLength);
|
|
8633
|
+
const encoder = device.createCommandEncoder();
|
|
8634
|
+
const pass = encoder.beginRenderPass({
|
|
8635
|
+
colorAttachments: [runtime.createColorAttachment()],
|
|
8636
|
+
});
|
|
8637
|
+
runtime.stats.renderPasses++;
|
|
8638
|
+
if (scissor !== null) {
|
|
8639
|
+
pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
|
|
8640
|
+
}
|
|
8641
|
+
pass.setBindGroup(0, uniformBindGroup);
|
|
8642
|
+
pass.setPipeline(pipeline);
|
|
8643
|
+
pass.setBindGroup(1, textureBindGroup);
|
|
8644
|
+
pass.setVertexBuffer(0, staticVertexBuffer);
|
|
8645
|
+
pass.setVertexBuffer(1, this._instanceBuffer);
|
|
8646
|
+
pass.setIndexBuffer(indexBuffer, 'uint16');
|
|
8647
|
+
pass.drawIndexed(indicesPerParticle, particleCount, 0, 0, 0);
|
|
8648
|
+
runtime.stats.batches++;
|
|
8649
|
+
runtime.stats.drawCalls++;
|
|
8650
|
+
pass.end();
|
|
8651
|
+
runtime.submit(encoder.finish());
|
|
8510
8652
|
}
|
|
8511
|
-
pass.end();
|
|
8512
|
-
runtime.submit(encoder.finish());
|
|
8513
8653
|
this._drawCallCount = 0;
|
|
8514
8654
|
}
|
|
8515
8655
|
destroy() {
|
|
@@ -9092,10 +9232,11 @@ class WebGpuRenderManager {
|
|
|
9092
9232
|
if (typeof gpuNavigator.gpu.getPreferredCanvasFormat !== 'function') {
|
|
9093
9233
|
throw new Error('WebGPU is available, but navigator.gpu.getPreferredCanvasFormat is not implemented.');
|
|
9094
9234
|
}
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9235
|
+
// Request the adapter before acquiring a WebGPU canvas context.
|
|
9236
|
+
// getContext('webgpu') is exclusive per canvas — once it succeeds, the
|
|
9237
|
+
// same canvas can no longer produce a WebGL2 context. Doing it the
|
|
9238
|
+
// other way round means an unavailable adapter still locks the canvas
|
|
9239
|
+
// and breaks the automatic WebGL2 fallback in Application.
|
|
9099
9240
|
let adapter = null;
|
|
9100
9241
|
try {
|
|
9101
9242
|
adapter = await gpuNavigator.gpu.requestAdapter();
|
|
@@ -9106,6 +9247,10 @@ class WebGpuRenderManager {
|
|
|
9106
9247
|
if (adapter === null) {
|
|
9107
9248
|
throw new Error('Could not acquire a WebGPU adapter.');
|
|
9108
9249
|
}
|
|
9250
|
+
const context = this._canvas.getContext('webgpu');
|
|
9251
|
+
if (context === null) {
|
|
9252
|
+
throw new Error('Could not create WebGPU canvas context.');
|
|
9253
|
+
}
|
|
9109
9254
|
if (typeof adapter.requestDevice !== 'function') {
|
|
9110
9255
|
throw new Error('WebGPU adapter does not expose requestDevice().');
|
|
9111
9256
|
}
|
|
@@ -9396,41 +9541,47 @@ class WebGpuRenderManager {
|
|
|
9396
9541
|
_getMipmapResources() {
|
|
9397
9542
|
if (this._mipmapShaderModule === null || this._mipmapBindGroupLayout === null || this._mipmapPipelineLayout === null || this._mipmapPipeline === null || this._mipmapSampler === null) {
|
|
9398
9543
|
this._mipmapShaderModule = this.device.createShaderModule({
|
|
9399
|
-
code: `
|
|
9400
|
-
struct VertexOutput {
|
|
9401
|
-
@builtin(position) position: vec4<f32>,
|
|
9402
|
-
@location(0) texcoord: vec2<f32>,
|
|
9403
|
-
};
|
|
9404
|
-
|
|
9405
|
-
@group(0) @binding(0)
|
|
9406
|
-
var sourceTexture: texture_2d<f32>;
|
|
9407
|
-
@group(0) @binding(1)
|
|
9408
|
-
var sourceSampler: sampler;
|
|
9409
|
-
|
|
9410
|
-
@vertex
|
|
9411
|
-
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
9412
|
-
var positions = array<vec2<f32>, 3>(
|
|
9413
|
-
vec2<f32>(-1.0, -1.0),
|
|
9414
|
-
vec2<f32>(3.0, -1.0),
|
|
9415
|
-
vec2<f32>(-1.0, 3.0)
|
|
9416
|
-
);
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9544
|
+
code: `
|
|
9545
|
+
struct VertexOutput {
|
|
9546
|
+
@builtin(position) position: vec4<f32>,
|
|
9547
|
+
@location(0) texcoord: vec2<f32>,
|
|
9548
|
+
};
|
|
9549
|
+
|
|
9550
|
+
@group(0) @binding(0)
|
|
9551
|
+
var sourceTexture: texture_2d<f32>;
|
|
9552
|
+
@group(0) @binding(1)
|
|
9553
|
+
var sourceSampler: sampler;
|
|
9554
|
+
|
|
9555
|
+
@vertex
|
|
9556
|
+
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
9557
|
+
var positions = array<vec2<f32>, 3>(
|
|
9558
|
+
vec2<f32>(-1.0, -1.0),
|
|
9559
|
+
vec2<f32>(3.0, -1.0),
|
|
9560
|
+
vec2<f32>(-1.0, 3.0)
|
|
9561
|
+
);
|
|
9562
|
+
// Y is flipped vs the position array: NDC Y points up, but texture UV
|
|
9563
|
+
// Y points down (UV (0,0) is the top-left of the source). Matching the
|
|
9564
|
+
// two ensures that the output texture's top-left pixel samples from the
|
|
9565
|
+
// source's top-left, so every mip level has the same orientation as the
|
|
9566
|
+
// level above it. Prior to this, odd mip levels were rendered upside
|
|
9567
|
+
// down, producing visible texture flips at view-size doublings.
|
|
9568
|
+
var texcoords = array<vec2<f32>, 3>(
|
|
9569
|
+
vec2<f32>(0.0, 1.0),
|
|
9570
|
+
vec2<f32>(2.0, 1.0),
|
|
9571
|
+
vec2<f32>(0.0, -1.0)
|
|
9572
|
+
);
|
|
9573
|
+
var output: VertexOutput;
|
|
9574
|
+
|
|
9575
|
+
output.position = vec4<f32>(positions[vertexIndex], 0.0, 1.0);
|
|
9576
|
+
output.texcoord = texcoords[vertexIndex];
|
|
9577
|
+
|
|
9578
|
+
return output;
|
|
9579
|
+
}
|
|
9580
|
+
|
|
9581
|
+
@fragment
|
|
9582
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
9583
|
+
return textureSample(sourceTexture, sourceSampler, input.texcoord);
|
|
9584
|
+
}
|
|
9434
9585
|
`,
|
|
9435
9586
|
});
|
|
9436
9587
|
this._mipmapBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -13037,8 +13188,11 @@ class Container extends RenderNode {
|
|
|
13037
13188
|
get bottom() {
|
|
13038
13189
|
return (this.y + this.height - this.origin.y);
|
|
13039
13190
|
}
|
|
13040
|
-
addChild(
|
|
13041
|
-
|
|
13191
|
+
addChild(...children) {
|
|
13192
|
+
for (const child of children) {
|
|
13193
|
+
this.addChildAt(child, this._children.length);
|
|
13194
|
+
}
|
|
13195
|
+
return this;
|
|
13042
13196
|
}
|
|
13043
13197
|
addChildAt(child, index) {
|
|
13044
13198
|
if (index < 0 || index > this._children.length) {
|
|
@@ -15155,6 +15309,17 @@ class ParticleOptions {
|
|
|
15155
15309
|
set elapsedLifetime(elapsedLifetime) {
|
|
15156
15310
|
this._elapsedLifetime.copy(elapsedLifetime);
|
|
15157
15311
|
}
|
|
15312
|
+
/**
|
|
15313
|
+
* Spawn position for particles emitted with these options, expressed in
|
|
15314
|
+
* the owning ParticleSystem's LOCAL coordinate space — the system's own
|
|
15315
|
+
* `getGlobalTransform()` is applied on top during rendering (both the
|
|
15316
|
+
* WebGL2 and WebGPU shaders do `projection * translation * rotated`).
|
|
15317
|
+
*
|
|
15318
|
+
* Setting a world-space value here (e.g. `system.x + offset`) will
|
|
15319
|
+
* double-translate the emitter because the shader will translate again.
|
|
15320
|
+
* For an emitter anchored at the system origin, use small offsets around
|
|
15321
|
+
* `(0, 0)` and position the system itself via `system.setPosition(...)`.
|
|
15322
|
+
*/
|
|
15158
15323
|
get position() {
|
|
15159
15324
|
return this._position;
|
|
15160
15325
|
}
|