@opentuah/core 0.1.77
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/3d/SpriteResourceManager.d.ts +74 -0
- package/3d/SpriteUtils.d.ts +13 -0
- package/3d/TextureUtils.d.ts +24 -0
- package/3d/ThreeRenderable.d.ts +40 -0
- package/3d/WGPURenderer.d.ts +61 -0
- package/3d/animation/ExplodingSpriteEffect.d.ts +71 -0
- package/3d/animation/PhysicsExplodingSpriteEffect.d.ts +76 -0
- package/3d/animation/SpriteAnimator.d.ts +124 -0
- package/3d/animation/SpriteParticleGenerator.d.ts +62 -0
- package/3d/canvas.d.ts +44 -0
- package/3d/index.d.ts +12 -0
- package/3d/physics/PlanckPhysicsAdapter.d.ts +19 -0
- package/3d/physics/RapierPhysicsAdapter.d.ts +19 -0
- package/3d/physics/physics-interface.d.ts +27 -0
- package/3d.d.ts +2 -0
- package/3d.js +2805 -0
- package/3d.js.map +22 -0
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/Renderable.d.ts +334 -0
- package/animation/Timeline.d.ts +126 -0
- package/ansi.d.ts +13 -0
- package/assets/javascript/highlights.scm +205 -0
- package/assets/javascript/tree-sitter-javascript.wasm +0 -0
- package/assets/markdown/highlights.scm +150 -0
- package/assets/markdown/injections.scm +27 -0
- package/assets/markdown/tree-sitter-markdown.wasm +0 -0
- package/assets/markdown_inline/highlights.scm +115 -0
- package/assets/markdown_inline/tree-sitter-markdown_inline.wasm +0 -0
- package/assets/typescript/highlights.scm +604 -0
- package/assets/typescript/tree-sitter-typescript.wasm +0 -0
- package/assets/zig/highlights.scm +284 -0
- package/assets/zig/tree-sitter-zig.wasm +0 -0
- package/buffer.d.ts +98 -0
- package/console.d.ts +140 -0
- package/edit-buffer.d.ts +98 -0
- package/editor-view.d.ts +73 -0
- package/index-cgvb25mm.js +14921 -0
- package/index-cgvb25mm.js.map +56 -0
- package/index.d.ts +17 -0
- package/index.js +9331 -0
- package/index.js.map +37 -0
- package/lib/KeyHandler.d.ts +61 -0
- package/lib/RGBA.d.ts +27 -0
- package/lib/ascii.font.d.ts +508 -0
- package/lib/border.d.ts +49 -0
- package/lib/bunfs.d.ts +7 -0
- package/lib/clipboard.d.ts +17 -0
- package/lib/data-paths.d.ts +26 -0
- package/lib/debounce.d.ts +42 -0
- package/lib/env.d.ts +42 -0
- package/lib/extmarks-history.d.ts +17 -0
- package/lib/extmarks.d.ts +89 -0
- package/lib/hast-styled-text.d.ts +17 -0
- package/lib/index.d.ts +18 -0
- package/lib/keymapping.d.ts +25 -0
- package/lib/objects-in-viewport.d.ts +24 -0
- package/lib/output.capture.d.ts +24 -0
- package/lib/parse.keypress-kitty.d.ts +2 -0
- package/lib/parse.keypress.d.ts +26 -0
- package/lib/parse.mouse.d.ts +23 -0
- package/lib/queue.d.ts +15 -0
- package/lib/renderable.validations.d.ts +12 -0
- package/lib/scroll-acceleration.d.ts +43 -0
- package/lib/selection.d.ts +63 -0
- package/lib/singleton.d.ts +7 -0
- package/lib/stdin-buffer.d.ts +44 -0
- package/lib/styled-text.d.ts +63 -0
- package/lib/terminal-capability-detection.d.ts +30 -0
- package/lib/terminal-palette.d.ts +43 -0
- package/lib/tree-sitter/assets/update.d.ts +11 -0
- package/lib/tree-sitter/client.d.ts +47 -0
- package/lib/tree-sitter/default-parsers.d.ts +2 -0
- package/lib/tree-sitter/download-utils.d.ts +21 -0
- package/lib/tree-sitter/index.d.ts +8 -0
- package/lib/tree-sitter/parser.worker.d.ts +1 -0
- package/lib/tree-sitter/parsers-config.d.ts +38 -0
- package/lib/tree-sitter/resolve-ft.d.ts +2 -0
- package/lib/tree-sitter/types.d.ts +81 -0
- package/lib/tree-sitter-styled-text.d.ts +14 -0
- package/lib/validate-dir-name.d.ts +1 -0
- package/lib/yoga.options.d.ts +32 -0
- package/package.json +67 -0
- package/parser.worker.js +855 -0
- package/parser.worker.js.map +12 -0
- package/post/filters.d.ts +105 -0
- package/renderables/ASCIIFont.d.ts +52 -0
- package/renderables/Box.d.ts +72 -0
- package/renderables/Code.d.ts +66 -0
- package/renderables/Diff.d.ts +185 -0
- package/renderables/EditBufferRenderable.d.ts +162 -0
- package/renderables/FrameBuffer.d.ts +16 -0
- package/renderables/Input.d.ts +60 -0
- package/renderables/LineNumberRenderable.d.ts +111 -0
- package/renderables/Markdown.d.ts +98 -0
- package/renderables/ScrollBar.d.ts +77 -0
- package/renderables/ScrollBox.d.ts +116 -0
- package/renderables/Select.d.ts +115 -0
- package/renderables/Slider.d.ts +44 -0
- package/renderables/TabSelect.d.ts +96 -0
- package/renderables/Text.d.ts +36 -0
- package/renderables/TextBufferRenderable.d.ts +103 -0
- package/renderables/TextNode.d.ts +91 -0
- package/renderables/Textarea.d.ts +114 -0
- package/renderables/__tests__/renderable-test-utils.d.ts +7 -0
- package/renderables/composition/VRenderable.d.ts +16 -0
- package/renderables/composition/constructs.d.ts +35 -0
- package/renderables/composition/vnode.d.ts +46 -0
- package/renderables/index.d.ts +20 -0
- package/renderables/markdown-parser.d.ts +10 -0
- package/renderer.d.ts +370 -0
- package/syntax-style.d.ts +54 -0
- package/testing/mock-keys.d.ts +80 -0
- package/testing/mock-mouse.d.ts +38 -0
- package/testing/mock-tree-sitter-client.d.ts +23 -0
- package/testing/spy.d.ts +7 -0
- package/testing/test-recorder.d.ts +61 -0
- package/testing/test-renderer.d.ts +23 -0
- package/testing.d.ts +6 -0
- package/testing.js +670 -0
- package/testing.js.map +15 -0
- package/text-buffer-view.d.ts +42 -0
- package/text-buffer.d.ts +67 -0
- package/types.d.ts +120 -0
- package/utils.d.ts +14 -0
- package/zig-structs.d.ts +42 -0
- package/zig.d.ts +326 -0
package/3d.js
ADDED
|
@@ -0,0 +1,2805 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
RGBA,
|
|
4
|
+
Renderable
|
|
5
|
+
} from "./index-cgvb25mm.js";
|
|
6
|
+
|
|
7
|
+
// src/3d/WGPURenderer.ts
|
|
8
|
+
import { PerspectiveCamera, Color, NoToneMapping, LinearSRGBColorSpace } from "three";
|
|
9
|
+
import { WebGPURenderer } from "three/webgpu";
|
|
10
|
+
import { createWebGPUDevice, setupGlobals } from "bun-webgpu";
|
|
11
|
+
|
|
12
|
+
// src/3d/canvas.ts
|
|
13
|
+
import { GPUCanvasContextMock } from "bun-webgpu";
|
|
14
|
+
import { toArrayBuffer } from "bun:ffi";
|
|
15
|
+
import { Jimp } from "jimp";
|
|
16
|
+
|
|
17
|
+
// src/3d/shaders/supersampling.wgsl
|
|
18
|
+
var supersampling_default = `struct CellResult {
|
|
19
|
+
bg: vec4<f32>, // Background RGBA (16 bytes)
|
|
20
|
+
fg: vec4<f32>, // Foreground RGBA (16 bytes)
|
|
21
|
+
char: u32, // Unicode character code (4 bytes)
|
|
22
|
+
_padding1: u32, // Padding (4 bytes)
|
|
23
|
+
_padding2: u32, // Extra padding (4 bytes)
|
|
24
|
+
_padding3: u32, // Extra padding (4 bytes) - total now 48 bytes (16-byte aligned)
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
struct CellBuffer {
|
|
28
|
+
cells: array<CellResult>
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
struct SuperSamplingParams {
|
|
32
|
+
width: u32, // Canvas width in pixels
|
|
33
|
+
height: u32, // Canvas height in pixels
|
|
34
|
+
sampleAlgo: u32, // 0 = standard 2x2, 1 = pre-squeezed horizontal blend
|
|
35
|
+
_padding: u32, // Padding for 16-byte alignment
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
39
|
+
@group(0) @binding(1) var<storage, read_write> output: CellBuffer;
|
|
40
|
+
@group(0) @binding(2) var<uniform> params: SuperSamplingParams;
|
|
41
|
+
|
|
42
|
+
// Quadrant character lookup table (same as Zig implementation)
|
|
43
|
+
const quadrantChars = array<u32, 16>(
|
|
44
|
+
32u, // ' ' - 0000
|
|
45
|
+
0x2597u, // \u2597 - 0001 BR
|
|
46
|
+
0x2596u, // \u2596 - 0010 BL
|
|
47
|
+
0x2584u, // \u2584 - 0011 Lower Half Block
|
|
48
|
+
0x259Du, // \u259D - 0100 TR
|
|
49
|
+
0x2590u, // \u2590 - 0101 Right Half Block
|
|
50
|
+
0x259Eu, // \u259E - 0110 TR+BL
|
|
51
|
+
0x259Fu, // \u259F - 0111 TR+BL+BR
|
|
52
|
+
0x2598u, // \u2598 - 1000 TL
|
|
53
|
+
0x259Au, // \u259A - 1001 TL+BR
|
|
54
|
+
0x258Cu, // \u258C - 1010 Left Half Block
|
|
55
|
+
0x2599u, // \u2599 - 1011 TL+BL+BR
|
|
56
|
+
0x2580u, // \u2580 - 1100 Upper Half Block
|
|
57
|
+
0x259Cu, // \u259C - 1101 TL+TR+BR
|
|
58
|
+
0x259Bu, // \u259B - 1110 TL+TR+BL
|
|
59
|
+
0x2588u // \u2588 - 1111 Full Block
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const inv_255: f32 = 1.0 / 255.0;
|
|
63
|
+
|
|
64
|
+
fn getPixelColor(pixelX: u32, pixelY: u32) -> vec4<f32> {
|
|
65
|
+
if (pixelX >= params.width || pixelY >= params.height) {
|
|
66
|
+
return vec4<f32>(0.0, 0.0, 0.0, 1.0); // Black for out-of-bounds
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// textureLoad automatically handles format conversion to RGBA
|
|
70
|
+
return textureLoad(inputTexture, vec2<i32>(i32(pixelX), i32(pixelY)), 0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn colorDistance(a: vec4<f32>, b: vec4<f32>) -> f32 {
|
|
74
|
+
let diff = a.rgb - b.rgb;
|
|
75
|
+
return dot(diff, diff);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn luminance(color: vec4<f32>) -> f32 {
|
|
79
|
+
return 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn closestColorIndex(pixel: vec4<f32>, candA: vec4<f32>, candB: vec4<f32>) -> u32 {
|
|
83
|
+
return select(1u, 0u, colorDistance(pixel, candA) <= colorDistance(pixel, candB));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn averageColor(pixels: array<vec4<f32>, 4>) -> vec4<f32> {
|
|
87
|
+
return (pixels[0] + pixels[1] + pixels[2] + pixels[3]) * 0.25;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn blendColors(color1: vec4<f32>, color2: vec4<f32>) -> vec4<f32> {
|
|
91
|
+
let a1 = color1.a;
|
|
92
|
+
let a2 = color2.a;
|
|
93
|
+
|
|
94
|
+
if (a1 == 0.0 && a2 == 0.0) {
|
|
95
|
+
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let outAlpha = a1 + a2 - a1 * a2;
|
|
99
|
+
if (outAlpha == 0.0) {
|
|
100
|
+
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let rgb = (color1.rgb * a1 + color2.rgb * a2 * (1.0 - a1)) / outAlpha;
|
|
104
|
+
|
|
105
|
+
return vec4<f32>(rgb, outAlpha);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fn averageColorsWithAlpha(pixels: array<vec4<f32>, 4>) -> vec4<f32> {
|
|
109
|
+
let blend1 = blendColors(pixels[0], pixels[1]);
|
|
110
|
+
let blend2 = blendColors(pixels[2], pixels[3]);
|
|
111
|
+
|
|
112
|
+
return blendColors(blend1, blend2);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn renderQuadrantBlock(pixels: array<vec4<f32>, 4>) -> CellResult {
|
|
116
|
+
var maxDist: f32 = colorDistance(pixels[0], pixels[1]);
|
|
117
|
+
var pIdxA: u32 = 0u;
|
|
118
|
+
var pIdxB: u32 = 1u;
|
|
119
|
+
|
|
120
|
+
for (var i: u32 = 0u; i < 4u; i++) {
|
|
121
|
+
for (var j: u32 = i + 1u; j < 4u; j++) {
|
|
122
|
+
let dist = colorDistance(pixels[i], pixels[j]);
|
|
123
|
+
if (dist > maxDist) {
|
|
124
|
+
pIdxA = i;
|
|
125
|
+
pIdxB = j;
|
|
126
|
+
maxDist = dist;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let pCandA = pixels[pIdxA];
|
|
132
|
+
let pCandB = pixels[pIdxB];
|
|
133
|
+
|
|
134
|
+
var chosenDarkColor: vec4<f32>;
|
|
135
|
+
var chosenLightColor: vec4<f32>;
|
|
136
|
+
|
|
137
|
+
if (luminance(pCandA) <= luminance(pCandB)) {
|
|
138
|
+
chosenDarkColor = pCandA;
|
|
139
|
+
chosenLightColor = pCandB;
|
|
140
|
+
} else {
|
|
141
|
+
chosenDarkColor = pCandB;
|
|
142
|
+
chosenLightColor = pCandA;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
var quadrantBits: u32 = 0u;
|
|
146
|
+
let bitValues = array<u32, 4>(8u, 4u, 2u, 1u); // TL, TR, BL, BR
|
|
147
|
+
|
|
148
|
+
for (var i: u32 = 0u; i < 4u; i++) {
|
|
149
|
+
if (closestColorIndex(pixels[i], chosenDarkColor, chosenLightColor) == 0u) {
|
|
150
|
+
quadrantBits |= bitValues[i];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Construct result
|
|
155
|
+
var result: CellResult;
|
|
156
|
+
|
|
157
|
+
if (quadrantBits == 0u) { // All light
|
|
158
|
+
result.char = 32u; // Space character
|
|
159
|
+
result.fg = chosenDarkColor;
|
|
160
|
+
result.bg = averageColorsWithAlpha(pixels);
|
|
161
|
+
} else if (quadrantBits == 15u) { // All dark
|
|
162
|
+
result.char = quadrantChars[15]; // Full block
|
|
163
|
+
result.fg = averageColorsWithAlpha(pixels);
|
|
164
|
+
result.bg = chosenLightColor;
|
|
165
|
+
} else { // Mixed pattern
|
|
166
|
+
result.char = quadrantChars[quadrantBits];
|
|
167
|
+
result.fg = chosenDarkColor;
|
|
168
|
+
result.bg = chosenLightColor;
|
|
169
|
+
}
|
|
170
|
+
result._padding1 = 0u;
|
|
171
|
+
result._padding2 = 0u;
|
|
172
|
+
result._padding3 = 0u;
|
|
173
|
+
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@compute @workgroup_size(\${WORKGROUP_SIZE}, \${WORKGROUP_SIZE}, 1)
|
|
178
|
+
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
179
|
+
let cellX = id.x;
|
|
180
|
+
let cellY = id.y;
|
|
181
|
+
let bufferWidthCells = (params.width + 1u) / 2u;
|
|
182
|
+
let bufferHeightCells = (params.height + 1u) / 2u;
|
|
183
|
+
|
|
184
|
+
if (cellX >= bufferWidthCells || cellY >= bufferHeightCells) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let renderX = cellX * 2u;
|
|
189
|
+
let renderY = cellY * 2u;
|
|
190
|
+
|
|
191
|
+
var pixelsRgba: array<vec4<f32>, 4>;
|
|
192
|
+
|
|
193
|
+
if (params.sampleAlgo == 1u) {
|
|
194
|
+
let topColor = getPixelColor(renderX, renderY);
|
|
195
|
+
let topColor2 = getPixelColor(renderX + 1u, renderY);
|
|
196
|
+
|
|
197
|
+
let blendedTop = blendColors(topColor, topColor2);
|
|
198
|
+
|
|
199
|
+
let bottomColor = getPixelColor(renderX, renderY + 1u);
|
|
200
|
+
let bottomColor2 = getPixelColor(renderX + 1u, renderY + 1u);
|
|
201
|
+
let blendedBottom = blendColors(bottomColor, bottomColor2);
|
|
202
|
+
|
|
203
|
+
pixelsRgba[0] = blendedTop; // TL
|
|
204
|
+
pixelsRgba[1] = blendedTop; // TR
|
|
205
|
+
pixelsRgba[2] = blendedBottom; // BL
|
|
206
|
+
pixelsRgba[3] = blendedBottom; // BR
|
|
207
|
+
} else {
|
|
208
|
+
pixelsRgba[0] = getPixelColor(renderX, renderY); // TL
|
|
209
|
+
pixelsRgba[1] = getPixelColor(renderX + 1u, renderY); // TR
|
|
210
|
+
pixelsRgba[2] = getPixelColor(renderX, renderY + 1u); // BL
|
|
211
|
+
pixelsRgba[3] = getPixelColor(renderX + 1u, renderY + 1u); // BR
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let cellResult = renderQuadrantBlock(pixelsRgba);
|
|
215
|
+
|
|
216
|
+
let outputIndex = cellY * bufferWidthCells + cellX;
|
|
217
|
+
output.cells[outputIndex] = cellResult;
|
|
218
|
+
}`;
|
|
219
|
+
|
|
220
|
+
// src/3d/canvas.ts
|
|
221
|
+
var WORKGROUP_SIZE = 4;
|
|
222
|
+
var SUPERSAMPLING_COMPUTE_SHADER = supersampling_default.replace(/\${WORKGROUP_SIZE}/g, WORKGROUP_SIZE.toString());
|
|
223
|
+
var SuperSampleAlgorithm;
|
|
224
|
+
((SuperSampleAlgorithm2) => {
|
|
225
|
+
SuperSampleAlgorithm2[SuperSampleAlgorithm2["STANDARD"] = 0] = "STANDARD";
|
|
226
|
+
SuperSampleAlgorithm2[SuperSampleAlgorithm2["PRE_SQUEEZED"] = 1] = "PRE_SQUEEZED";
|
|
227
|
+
})(SuperSampleAlgorithm ||= {});
|
|
228
|
+
|
|
229
|
+
class CLICanvas {
|
|
230
|
+
device;
|
|
231
|
+
readbackBuffer = null;
|
|
232
|
+
width;
|
|
233
|
+
height;
|
|
234
|
+
gpuCanvasContext;
|
|
235
|
+
superSampleDrawTimeMs = 0;
|
|
236
|
+
mapAsyncTimeMs = 0;
|
|
237
|
+
superSample = "gpu" /* GPU */;
|
|
238
|
+
computePipeline = null;
|
|
239
|
+
computeBindGroupLayout = null;
|
|
240
|
+
computeOutputBuffer = null;
|
|
241
|
+
computeParamsBuffer = null;
|
|
242
|
+
computeReadbackBuffer = null;
|
|
243
|
+
updateScheduled = false;
|
|
244
|
+
screenshotGPUBuffer = null;
|
|
245
|
+
superSampleAlgorithm = 0 /* STANDARD */;
|
|
246
|
+
destroyed = false;
|
|
247
|
+
constructor(device, width, height, superSample, sampleAlgo = 0 /* STANDARD */) {
|
|
248
|
+
this.device = device;
|
|
249
|
+
this.width = width;
|
|
250
|
+
this.height = height;
|
|
251
|
+
this.superSample = superSample;
|
|
252
|
+
this.gpuCanvasContext = new GPUCanvasContextMock(this, width, height);
|
|
253
|
+
this.superSampleAlgorithm = sampleAlgo;
|
|
254
|
+
}
|
|
255
|
+
destroy() {
|
|
256
|
+
this.destroyed = true;
|
|
257
|
+
}
|
|
258
|
+
setSuperSampleAlgorithm(superSampleAlgorithm) {
|
|
259
|
+
this.superSampleAlgorithm = superSampleAlgorithm;
|
|
260
|
+
this.scheduleUpdateComputeBuffers();
|
|
261
|
+
}
|
|
262
|
+
getSuperSampleAlgorithm() {
|
|
263
|
+
return this.superSampleAlgorithm;
|
|
264
|
+
}
|
|
265
|
+
getContext(type, attrs) {
|
|
266
|
+
if (type === "webgpu") {
|
|
267
|
+
this.updateReadbackBuffer(this.width, this.height);
|
|
268
|
+
this.updateComputeBuffers(this.width, this.height);
|
|
269
|
+
return this.gpuCanvasContext;
|
|
270
|
+
}
|
|
271
|
+
throw new Error(`getContext not implemented: ${type}`);
|
|
272
|
+
}
|
|
273
|
+
setSize(width, height) {
|
|
274
|
+
this.width = width;
|
|
275
|
+
this.height = height;
|
|
276
|
+
this.gpuCanvasContext.setSize(width, height);
|
|
277
|
+
this.updateReadbackBuffer(width, height);
|
|
278
|
+
this.scheduleUpdateComputeBuffers();
|
|
279
|
+
}
|
|
280
|
+
addEventListener(event, listener, options) {
|
|
281
|
+
console.error("addEventListener mockCanvas", event, listener, options);
|
|
282
|
+
}
|
|
283
|
+
removeEventListener(event, listener, options) {
|
|
284
|
+
console.error("removeEventListener mockCanvas", event, listener, options);
|
|
285
|
+
}
|
|
286
|
+
dispatchEvent(event) {
|
|
287
|
+
console.error("dispatchEvent mockCanvas", event);
|
|
288
|
+
}
|
|
289
|
+
setSuperSample(superSample) {
|
|
290
|
+
this.superSample = superSample;
|
|
291
|
+
}
|
|
292
|
+
async saveToFile(filePath) {
|
|
293
|
+
const bytesPerPixel = 4;
|
|
294
|
+
const unalignedBytesPerRow = this.width * bytesPerPixel;
|
|
295
|
+
const alignedBytesPerRow = Math.ceil(unalignedBytesPerRow / 256) * 256;
|
|
296
|
+
const textureBufferSize = alignedBytesPerRow * this.height;
|
|
297
|
+
if (!this.screenshotGPUBuffer || this.screenshotGPUBuffer.size !== textureBufferSize) {
|
|
298
|
+
if (this.screenshotGPUBuffer) {
|
|
299
|
+
this.screenshotGPUBuffer.destroy();
|
|
300
|
+
}
|
|
301
|
+
this.screenshotGPUBuffer = this.device.createBuffer({
|
|
302
|
+
label: "Screenshot GPU Buffer",
|
|
303
|
+
size: textureBufferSize,
|
|
304
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const texture = this.gpuCanvasContext.getCurrentTexture();
|
|
308
|
+
const commandEncoder = this.device.createCommandEncoder({ label: "Screenshot Command Encoder" });
|
|
309
|
+
commandEncoder.copyTextureToBuffer({ texture }, { buffer: this.screenshotGPUBuffer, bytesPerRow: alignedBytesPerRow, rowsPerImage: this.height }, { width: this.width, height: this.height });
|
|
310
|
+
const commandBuffer = commandEncoder.finish();
|
|
311
|
+
this.device.queue.submit([commandBuffer]);
|
|
312
|
+
await this.screenshotGPUBuffer.mapAsync(GPUMapMode.READ);
|
|
313
|
+
const resultBuffer = this.screenshotGPUBuffer.getMappedRange();
|
|
314
|
+
const pixelData = new Uint8Array(resultBuffer);
|
|
315
|
+
const contextFormat = texture.format;
|
|
316
|
+
const isBGRA = contextFormat === "bgra8unorm";
|
|
317
|
+
const imageData = new Uint8Array(this.width * this.height * 4);
|
|
318
|
+
for (let y = 0;y < this.height; y++) {
|
|
319
|
+
const srcOffset = y * alignedBytesPerRow;
|
|
320
|
+
const dstOffset = y * this.width * 4;
|
|
321
|
+
if (isBGRA) {
|
|
322
|
+
for (let x = 0;x < this.width; x++) {
|
|
323
|
+
const srcPixelOffset = srcOffset + x * 4;
|
|
324
|
+
const dstPixelOffset = dstOffset + x * 4;
|
|
325
|
+
imageData[dstPixelOffset] = pixelData[srcPixelOffset + 2];
|
|
326
|
+
imageData[dstPixelOffset + 1] = pixelData[srcPixelOffset + 1];
|
|
327
|
+
imageData[dstPixelOffset + 2] = pixelData[srcPixelOffset];
|
|
328
|
+
imageData[dstPixelOffset + 3] = pixelData[srcPixelOffset + 3];
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
imageData.set(pixelData.subarray(srcOffset, srcOffset + this.width * 4), dstOffset);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const image = new Jimp({
|
|
335
|
+
data: Buffer.from(imageData),
|
|
336
|
+
width: this.width,
|
|
337
|
+
height: this.height
|
|
338
|
+
});
|
|
339
|
+
await image.write(filePath);
|
|
340
|
+
this.screenshotGPUBuffer.unmap();
|
|
341
|
+
}
|
|
342
|
+
async initComputePipeline() {
|
|
343
|
+
if (this.computePipeline)
|
|
344
|
+
return;
|
|
345
|
+
const shaderModule = this.device.createShaderModule({
|
|
346
|
+
label: "SuperSampling Compute Shader",
|
|
347
|
+
code: SUPERSAMPLING_COMPUTE_SHADER
|
|
348
|
+
});
|
|
349
|
+
this.computeBindGroupLayout = this.device.createBindGroupLayout({
|
|
350
|
+
label: "SuperSampling Bind Group Layout",
|
|
351
|
+
entries: [
|
|
352
|
+
{
|
|
353
|
+
binding: 0,
|
|
354
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
355
|
+
texture: { sampleType: "float", viewDimension: "2d" }
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
binding: 1,
|
|
359
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
360
|
+
buffer: { type: "storage" }
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
binding: 2,
|
|
364
|
+
visibility: GPUShaderStage.COMPUTE,
|
|
365
|
+
buffer: { type: "uniform" }
|
|
366
|
+
}
|
|
367
|
+
]
|
|
368
|
+
});
|
|
369
|
+
const pipelineLayout = this.device.createPipelineLayout({
|
|
370
|
+
label: "SuperSampling Pipeline Layout",
|
|
371
|
+
bindGroupLayouts: [this.computeBindGroupLayout]
|
|
372
|
+
});
|
|
373
|
+
this.computePipeline = this.device.createComputePipeline({
|
|
374
|
+
label: "SuperSampling Compute Pipeline",
|
|
375
|
+
layout: pipelineLayout,
|
|
376
|
+
compute: {
|
|
377
|
+
module: shaderModule,
|
|
378
|
+
entryPoint: "main"
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
this.computeParamsBuffer = this.device.createBuffer({
|
|
382
|
+
label: "SuperSampling Params Buffer",
|
|
383
|
+
size: 16,
|
|
384
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
385
|
+
});
|
|
386
|
+
this.updateComputeParams();
|
|
387
|
+
}
|
|
388
|
+
updateComputeParams() {
|
|
389
|
+
if (!this.computeParamsBuffer || this.superSample === "none" /* NONE */)
|
|
390
|
+
return;
|
|
391
|
+
const paramsData = new ArrayBuffer(16);
|
|
392
|
+
const uint32View = new Uint32Array(paramsData);
|
|
393
|
+
uint32View[0] = this.width;
|
|
394
|
+
uint32View[1] = this.height;
|
|
395
|
+
uint32View[2] = this.superSampleAlgorithm;
|
|
396
|
+
this.device.queue.writeBuffer(this.computeParamsBuffer, 0, paramsData);
|
|
397
|
+
}
|
|
398
|
+
scheduleUpdateComputeBuffers() {
|
|
399
|
+
this.updateScheduled = true;
|
|
400
|
+
}
|
|
401
|
+
updateComputeBuffers(width, height) {
|
|
402
|
+
if (this.superSample === "none" /* NONE */)
|
|
403
|
+
return;
|
|
404
|
+
this.updateComputeParams();
|
|
405
|
+
const cellBytesSize = 48;
|
|
406
|
+
const terminalWidthCells = Math.floor((width + 1) / 2);
|
|
407
|
+
const terminalHeightCells = Math.floor((height + 1) / 2);
|
|
408
|
+
const outputBufferSize = terminalWidthCells * terminalHeightCells * cellBytesSize;
|
|
409
|
+
const oldOutputBuffer = this.computeOutputBuffer;
|
|
410
|
+
const oldReadbackBuffer = this.computeReadbackBuffer;
|
|
411
|
+
if (oldOutputBuffer) {
|
|
412
|
+
oldOutputBuffer.destroy();
|
|
413
|
+
}
|
|
414
|
+
if (oldReadbackBuffer) {
|
|
415
|
+
oldReadbackBuffer.destroy();
|
|
416
|
+
}
|
|
417
|
+
this.computeOutputBuffer = this.device.createBuffer({
|
|
418
|
+
label: "SuperSampling Output Buffer",
|
|
419
|
+
size: outputBufferSize,
|
|
420
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
|
|
421
|
+
});
|
|
422
|
+
this.computeReadbackBuffer = this.device.createBuffer({
|
|
423
|
+
label: "SuperSampling Readback Buffer",
|
|
424
|
+
size: outputBufferSize,
|
|
425
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
async runComputeShaderSuperSampling(texture, buffer) {
|
|
429
|
+
if (this.destroyed) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (this.updateScheduled) {
|
|
433
|
+
this.updateScheduled = false;
|
|
434
|
+
await this.device.queue.onSubmittedWorkDone();
|
|
435
|
+
this.updateComputeBuffers(this.width, this.height);
|
|
436
|
+
}
|
|
437
|
+
await this.initComputePipeline();
|
|
438
|
+
if (!this.computePipeline || !this.computeBindGroupLayout || !this.computeOutputBuffer || !this.computeParamsBuffer) {
|
|
439
|
+
throw new Error("Compute pipeline not initialized");
|
|
440
|
+
}
|
|
441
|
+
const mapAsyncStart = performance.now();
|
|
442
|
+
const textureView = texture.createView({
|
|
443
|
+
label: "SuperSampling Input Texture View"
|
|
444
|
+
});
|
|
445
|
+
const bindGroup = this.device.createBindGroup({
|
|
446
|
+
label: "SuperSampling Bind Group",
|
|
447
|
+
layout: this.computeBindGroupLayout,
|
|
448
|
+
entries: [
|
|
449
|
+
{ binding: 0, resource: textureView },
|
|
450
|
+
{ binding: 1, resource: { buffer: this.computeOutputBuffer } },
|
|
451
|
+
{ binding: 2, resource: { buffer: this.computeParamsBuffer } }
|
|
452
|
+
]
|
|
453
|
+
});
|
|
454
|
+
const commandEncoder = this.device.createCommandEncoder({ label: "SuperSampling Command Encoder" });
|
|
455
|
+
const computePass = commandEncoder.beginComputePass({ label: "SuperSampling Compute Pass" });
|
|
456
|
+
computePass.setPipeline(this.computePipeline);
|
|
457
|
+
computePass.setBindGroup(0, bindGroup);
|
|
458
|
+
const terminalWidthCells = Math.floor((this.width + 1) / 2);
|
|
459
|
+
const terminalHeightCells = Math.floor((this.height + 1) / 2);
|
|
460
|
+
const dispatchX = Math.ceil(terminalWidthCells / WORKGROUP_SIZE);
|
|
461
|
+
const dispatchY = Math.ceil(terminalHeightCells / WORKGROUP_SIZE);
|
|
462
|
+
computePass.dispatchWorkgroups(dispatchX, dispatchY, 1);
|
|
463
|
+
computePass.end();
|
|
464
|
+
commandEncoder.copyBufferToBuffer(this.computeOutputBuffer, 0, this.computeReadbackBuffer, 0, this.computeOutputBuffer.size);
|
|
465
|
+
const commandBuffer = commandEncoder.finish();
|
|
466
|
+
this.device.queue.submit([commandBuffer]);
|
|
467
|
+
await this.computeReadbackBuffer.mapAsync(GPUMapMode.READ);
|
|
468
|
+
if (this.destroyed) {
|
|
469
|
+
this.computeReadbackBuffer.unmap();
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const resultsPtr = this.computeReadbackBuffer.getMappedRangePtr();
|
|
473
|
+
const size = this.computeReadbackBuffer.size;
|
|
474
|
+
this.mapAsyncTimeMs = performance.now() - mapAsyncStart;
|
|
475
|
+
const ssStart = performance.now();
|
|
476
|
+
buffer.drawPackedBuffer(resultsPtr, size, 0, 0, terminalWidthCells, terminalHeightCells);
|
|
477
|
+
this.superSampleDrawTimeMs = performance.now() - ssStart;
|
|
478
|
+
this.computeReadbackBuffer.unmap();
|
|
479
|
+
}
|
|
480
|
+
updateReadbackBuffer(renderWidth, renderHeight) {
|
|
481
|
+
if (this.readbackBuffer) {
|
|
482
|
+
this.readbackBuffer.destroy();
|
|
483
|
+
}
|
|
484
|
+
const bytesPerPixel = 4;
|
|
485
|
+
const unalignedBytesPerRow = renderWidth * bytesPerPixel;
|
|
486
|
+
const alignedBytesPerRow = Math.ceil(unalignedBytesPerRow / 256) * 256;
|
|
487
|
+
const textureBufferSize = alignedBytesPerRow * renderHeight;
|
|
488
|
+
this.readbackBuffer = this.device.createBuffer({
|
|
489
|
+
label: "Readback Buffer",
|
|
490
|
+
size: textureBufferSize,
|
|
491
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
async readPixelsIntoBuffer(buffer) {
|
|
495
|
+
if (this.destroyed) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const texture = this.gpuCanvasContext.getCurrentTexture();
|
|
499
|
+
this.gpuCanvasContext.switchTextures();
|
|
500
|
+
if (this.superSample === "gpu" /* GPU */) {
|
|
501
|
+
await this.runComputeShaderSuperSampling(texture, buffer);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const textureBuffer = this.readbackBuffer;
|
|
505
|
+
if (!textureBuffer) {
|
|
506
|
+
throw new Error("Readback buffer not found");
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
const bytesPerPixel = 4;
|
|
510
|
+
const unalignedBytesPerRow = this.width * bytesPerPixel;
|
|
511
|
+
const alignedBytesPerRow = Math.ceil(unalignedBytesPerRow / 256) * 256;
|
|
512
|
+
const contextFormat = texture.format;
|
|
513
|
+
const commandEncoder = this.device.createCommandEncoder({ label: "Readback Command Encoder" });
|
|
514
|
+
commandEncoder.copyTextureToBuffer({ texture }, { buffer: textureBuffer, bytesPerRow: alignedBytesPerRow, rowsPerImage: this.height }, {
|
|
515
|
+
width: this.width,
|
|
516
|
+
height: this.height
|
|
517
|
+
});
|
|
518
|
+
const commandBuffer = commandEncoder.finish();
|
|
519
|
+
this.device.queue.submit([commandBuffer]);
|
|
520
|
+
const mapStart = performance.now();
|
|
521
|
+
await textureBuffer.mapAsync(GPUMapMode.READ, 0, textureBuffer.size);
|
|
522
|
+
this.mapAsyncTimeMs = performance.now() - mapStart;
|
|
523
|
+
if (this.destroyed) {
|
|
524
|
+
textureBuffer.unmap();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const mappedRangePtr = textureBuffer.getMappedRangePtr(0, textureBuffer.size);
|
|
528
|
+
const bufPtr = mappedRangePtr;
|
|
529
|
+
if (this.superSample === "cpu" /* CPU */) {
|
|
530
|
+
const format = contextFormat === "bgra8unorm" ? "bgra8unorm" : "rgba8unorm";
|
|
531
|
+
const ssStart = performance.now();
|
|
532
|
+
buffer.drawSuperSampleBuffer(0, 0, bufPtr, textureBuffer.size, format, alignedBytesPerRow);
|
|
533
|
+
this.superSampleDrawTimeMs = performance.now() - ssStart;
|
|
534
|
+
} else {
|
|
535
|
+
this.superSampleDrawTimeMs = 0;
|
|
536
|
+
const pixelData = new Uint8Array(toArrayBuffer(bufPtr, 0, textureBuffer.size));
|
|
537
|
+
const isBGRA = contextFormat === "bgra8unorm";
|
|
538
|
+
const backgroundColor = RGBA.fromValues(0, 0, 0, 1);
|
|
539
|
+
for (let y = 0;y < this.height; y++) {
|
|
540
|
+
for (let x = 0;x < this.width; x++) {
|
|
541
|
+
const pixelIndexInPaddedRow = y * alignedBytesPerRow + x * bytesPerPixel;
|
|
542
|
+
if (pixelIndexInPaddedRow + 3 >= pixelData.length)
|
|
543
|
+
continue;
|
|
544
|
+
let rByte, gByte, bByte;
|
|
545
|
+
if (isBGRA) {
|
|
546
|
+
bByte = pixelData[pixelIndexInPaddedRow];
|
|
547
|
+
gByte = pixelData[pixelIndexInPaddedRow + 1];
|
|
548
|
+
rByte = pixelData[pixelIndexInPaddedRow + 2];
|
|
549
|
+
} else {
|
|
550
|
+
rByte = pixelData[pixelIndexInPaddedRow];
|
|
551
|
+
gByte = pixelData[pixelIndexInPaddedRow + 1];
|
|
552
|
+
bByte = pixelData[pixelIndexInPaddedRow + 2];
|
|
553
|
+
}
|
|
554
|
+
const r = rByte / 255;
|
|
555
|
+
const g = gByte / 255;
|
|
556
|
+
const b = bByte / 255;
|
|
557
|
+
const cellColor = RGBA.fromValues(r, g, b, 1);
|
|
558
|
+
buffer.setCellWithAlphaBlending(x, y, "\u2588", cellColor, backgroundColor);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
} finally {
|
|
563
|
+
textureBuffer.unmap();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/3d/WGPURenderer.ts
|
|
569
|
+
var SuperSampleType;
|
|
570
|
+
((SuperSampleType2) => {
|
|
571
|
+
SuperSampleType2["NONE"] = "none";
|
|
572
|
+
SuperSampleType2["GPU"] = "gpu";
|
|
573
|
+
SuperSampleType2["CPU"] = "cpu";
|
|
574
|
+
})(SuperSampleType ||= {});
|
|
575
|
+
|
|
576
|
+
class ThreeCliRenderer {
|
|
577
|
+
cliRenderer;
|
|
578
|
+
outputWidth;
|
|
579
|
+
outputHeight;
|
|
580
|
+
renderWidth;
|
|
581
|
+
renderHeight;
|
|
582
|
+
superSample;
|
|
583
|
+
backgroundColor = RGBA.fromValues(0, 0, 0, 1);
|
|
584
|
+
alpha = false;
|
|
585
|
+
threeRenderer;
|
|
586
|
+
canvas;
|
|
587
|
+
device = null;
|
|
588
|
+
activeCamera;
|
|
589
|
+
_aspectRatio = null;
|
|
590
|
+
doRenderStats = false;
|
|
591
|
+
resizeHandler;
|
|
592
|
+
debugToggleHandler;
|
|
593
|
+
destroyHandler;
|
|
594
|
+
renderTimeMs = 0;
|
|
595
|
+
readbackTimeMs = 0;
|
|
596
|
+
totalDrawTimeMs = 0;
|
|
597
|
+
renderMethod = () => Promise.resolve();
|
|
598
|
+
get aspectRatio() {
|
|
599
|
+
if (this._aspectRatio)
|
|
600
|
+
return this._aspectRatio;
|
|
601
|
+
if (this.cliRenderer.resolution) {
|
|
602
|
+
const pixelAspectRatio = this.cliRenderer.resolution.width / this.cliRenderer.resolution.height;
|
|
603
|
+
return pixelAspectRatio;
|
|
604
|
+
}
|
|
605
|
+
const terminalWidth = process.stdout.columns;
|
|
606
|
+
const terminalHeight = process.stdout.rows;
|
|
607
|
+
return terminalWidth / (terminalHeight * 2);
|
|
608
|
+
}
|
|
609
|
+
constructor(cliRenderer, options) {
|
|
610
|
+
this.cliRenderer = cliRenderer;
|
|
611
|
+
this.outputWidth = options.width;
|
|
612
|
+
this.outputHeight = options.height;
|
|
613
|
+
this.superSample = options.superSample ?? "gpu" /* GPU */;
|
|
614
|
+
this.renderWidth = this.outputWidth * (this.superSample !== "none" /* NONE */ ? 2 : 1);
|
|
615
|
+
this.renderHeight = this.outputHeight * (this.superSample !== "none" /* NONE */ ? 2 : 1);
|
|
616
|
+
this.backgroundColor = options.backgroundColor ?? RGBA.fromValues(0, 0, 0, 1);
|
|
617
|
+
this.alpha = options.alpha ?? false;
|
|
618
|
+
if (process.env.CELL_ASPECT_RATIO) {
|
|
619
|
+
this._aspectRatio = parseFloat(process.env.CELL_ASPECT_RATIO);
|
|
620
|
+
}
|
|
621
|
+
const fov = options.focalLength ? 2 * Math.atan(this.outputHeight / (2 * options.focalLength)) * (180 / Math.PI) : 1;
|
|
622
|
+
this.activeCamera = new PerspectiveCamera(fov, this.aspectRatio, 0.1, 1000);
|
|
623
|
+
this.activeCamera.position.set(0, 0, 3);
|
|
624
|
+
this.activeCamera.up.set(0, 1, 0);
|
|
625
|
+
this.activeCamera.lookAt(0, 0, 0);
|
|
626
|
+
this.activeCamera.updateMatrixWorld();
|
|
627
|
+
this.resizeHandler = (width, height) => {
|
|
628
|
+
this.setSize(width, height, true);
|
|
629
|
+
};
|
|
630
|
+
this.debugToggleHandler = (enabled) => {
|
|
631
|
+
this.doRenderStats = enabled;
|
|
632
|
+
};
|
|
633
|
+
this.destroyHandler = () => {
|
|
634
|
+
this.destroy();
|
|
635
|
+
};
|
|
636
|
+
if (options.autoResize !== false) {
|
|
637
|
+
this.cliRenderer.on("resize", this.resizeHandler);
|
|
638
|
+
}
|
|
639
|
+
this.cliRenderer.on("debugOverlay:toggle" /* DEBUG_OVERLAY_TOGGLE */, this.debugToggleHandler);
|
|
640
|
+
this.cliRenderer.on("destroy" /* DESTROY */, this.destroyHandler);
|
|
641
|
+
setupGlobals({ libPath: options.libPath });
|
|
642
|
+
}
|
|
643
|
+
toggleDebugStats() {
|
|
644
|
+
this.doRenderStats = !this.doRenderStats;
|
|
645
|
+
}
|
|
646
|
+
async init() {
|
|
647
|
+
this.device = await createWebGPUDevice();
|
|
648
|
+
this.canvas = new CLICanvas(this.device, this.renderWidth, this.renderHeight, this.superSample);
|
|
649
|
+
try {
|
|
650
|
+
this.threeRenderer = new WebGPURenderer({
|
|
651
|
+
canvas: this.canvas,
|
|
652
|
+
device: this.device,
|
|
653
|
+
alpha: this.alpha
|
|
654
|
+
});
|
|
655
|
+
this.setBackgroundColor(this.backgroundColor);
|
|
656
|
+
this.threeRenderer.toneMapping = NoToneMapping;
|
|
657
|
+
this.threeRenderer.outputColorSpace = LinearSRGBColorSpace;
|
|
658
|
+
this.threeRenderer.setSize(this.renderWidth, this.renderHeight, false);
|
|
659
|
+
} catch (error) {
|
|
660
|
+
console.error("Error creating THREE.WebGPURenderer:", error);
|
|
661
|
+
throw error;
|
|
662
|
+
}
|
|
663
|
+
await this.threeRenderer.init().then(() => {
|
|
664
|
+
this.renderMethod = this.doDrawScene.bind(this);
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
getSuperSampleAlgorithm() {
|
|
668
|
+
return this.canvas.getSuperSampleAlgorithm();
|
|
669
|
+
}
|
|
670
|
+
setSuperSampleAlgorithm(superSampleAlgorithm) {
|
|
671
|
+
this.canvas.setSuperSampleAlgorithm(superSampleAlgorithm);
|
|
672
|
+
}
|
|
673
|
+
saveToFile(filePath) {
|
|
674
|
+
return this.canvas.saveToFile(filePath);
|
|
675
|
+
}
|
|
676
|
+
setActiveCamera(camera) {
|
|
677
|
+
this.activeCamera = camera;
|
|
678
|
+
}
|
|
679
|
+
getActiveCamera() {
|
|
680
|
+
return this.activeCamera;
|
|
681
|
+
}
|
|
682
|
+
setBackgroundColor(color) {
|
|
683
|
+
this.backgroundColor = color;
|
|
684
|
+
const clearColor = new Color(this.backgroundColor.r, this.backgroundColor.g, this.backgroundColor.b);
|
|
685
|
+
const clearAlpha = this.alpha ? this.backgroundColor.a : 1;
|
|
686
|
+
this.threeRenderer.setClearColor(clearColor, clearAlpha);
|
|
687
|
+
}
|
|
688
|
+
setSize(width, height, forceUpdate = false) {
|
|
689
|
+
if (!forceUpdate && this.outputWidth === width && this.outputHeight === height)
|
|
690
|
+
return;
|
|
691
|
+
this.outputWidth = width;
|
|
692
|
+
this.outputHeight = height;
|
|
693
|
+
this.renderWidth = this.outputWidth * (this.superSample !== "none" /* NONE */ ? 2 : 1);
|
|
694
|
+
this.renderHeight = this.outputHeight * (this.superSample !== "none" /* NONE */ ? 2 : 1);
|
|
695
|
+
this.canvas?.setSize(this.renderWidth, this.renderHeight);
|
|
696
|
+
this.threeRenderer?.setSize(this.renderWidth, this.renderHeight, false);
|
|
697
|
+
this.threeRenderer?.setViewport(0, 0, this.renderWidth, this.renderHeight);
|
|
698
|
+
if (this.activeCamera instanceof PerspectiveCamera) {
|
|
699
|
+
this.activeCamera.aspect = this.aspectRatio;
|
|
700
|
+
}
|
|
701
|
+
this.activeCamera.updateProjectionMatrix();
|
|
702
|
+
}
|
|
703
|
+
async drawScene(root, buffer, deltaTime) {
|
|
704
|
+
await this.renderMethod(root, this.activeCamera, buffer, deltaTime);
|
|
705
|
+
if (this.doRenderStats) {
|
|
706
|
+
this.renderStats(buffer);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
rendering = false;
|
|
710
|
+
destroyed = false;
|
|
711
|
+
async doDrawScene(root, camera, buffer, deltaTime) {
|
|
712
|
+
if (this.rendering) {
|
|
713
|
+
console.warn("ThreeCliRenderer.drawScene was called concurrently, which is not supported.");
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (this.destroyed) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
this.rendering = true;
|
|
721
|
+
const totalStart = performance.now();
|
|
722
|
+
const renderStart = performance.now();
|
|
723
|
+
await this.threeRenderer.render(root, camera);
|
|
724
|
+
this.renderTimeMs = performance.now() - renderStart;
|
|
725
|
+
const readbackStart = performance.now();
|
|
726
|
+
await this.canvas.readPixelsIntoBuffer(buffer);
|
|
727
|
+
this.readbackTimeMs = performance.now() - readbackStart;
|
|
728
|
+
this.totalDrawTimeMs = performance.now() - totalStart;
|
|
729
|
+
} finally {
|
|
730
|
+
this.rendering = false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
toggleSuperSampling() {
|
|
734
|
+
if (this.superSample === "none" /* NONE */) {
|
|
735
|
+
this.superSample = "cpu" /* CPU */;
|
|
736
|
+
} else if (this.superSample === "cpu" /* CPU */) {
|
|
737
|
+
this.superSample = "gpu" /* GPU */;
|
|
738
|
+
} else {
|
|
739
|
+
this.superSample = "none" /* NONE */;
|
|
740
|
+
}
|
|
741
|
+
this.canvas.setSuperSample(this.superSample);
|
|
742
|
+
this.setSize(this.outputWidth, this.outputHeight, true);
|
|
743
|
+
}
|
|
744
|
+
renderStats(buffer) {
|
|
745
|
+
const stats = [
|
|
746
|
+
`WebGPU Renderer Stats:`,
|
|
747
|
+
` Render: ${this.renderTimeMs.toFixed(2)}ms`,
|
|
748
|
+
` Readback: ${this.readbackTimeMs.toFixed(2)}ms`,
|
|
749
|
+
` \u251C MapAsync: ${this.canvas.mapAsyncTimeMs.toFixed(2)}ms`,
|
|
750
|
+
` \u2514 SS Draw: ${this.canvas.superSampleDrawTimeMs.toFixed(2)}ms`,
|
|
751
|
+
` Total Draw: ${this.totalDrawTimeMs.toFixed(2)}ms`,
|
|
752
|
+
` SuperSample: ${this.superSample}`,
|
|
753
|
+
` SuperSample Algorithm: ${this.getSuperSampleAlgorithm()}`
|
|
754
|
+
];
|
|
755
|
+
const startY = 4;
|
|
756
|
+
const startX = 2;
|
|
757
|
+
const fg = RGBA.fromValues(0.9, 0.9, 0.9, 1);
|
|
758
|
+
const bg = RGBA.fromValues(0.1, 0.1, 0.1, 1);
|
|
759
|
+
stats.forEach((line, index) => {
|
|
760
|
+
buffer.drawText(line, startX + 1, startY + index, fg, bg);
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
destroy() {
|
|
764
|
+
this.destroyed = true;
|
|
765
|
+
this.cliRenderer.off("resize", this.resizeHandler);
|
|
766
|
+
this.cliRenderer.off("debugOverlay:toggle" /* DEBUG_OVERLAY_TOGGLE */, this.debugToggleHandler);
|
|
767
|
+
if (this.canvas) {
|
|
768
|
+
this.canvas.destroy();
|
|
769
|
+
}
|
|
770
|
+
if (this.threeRenderer) {
|
|
771
|
+
this.threeRenderer.dispose();
|
|
772
|
+
this.threeRenderer = undefined;
|
|
773
|
+
}
|
|
774
|
+
this.canvas = undefined;
|
|
775
|
+
this.device = null;
|
|
776
|
+
this.renderMethod = () => Promise.resolve();
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// src/3d/ThreeRenderable.ts
|
|
780
|
+
import { PerspectiveCamera as PerspectiveCamera2 } from "three";
|
|
781
|
+
class ThreeRenderable extends Renderable {
|
|
782
|
+
engine;
|
|
783
|
+
scene;
|
|
784
|
+
autoAspect;
|
|
785
|
+
initPromise = null;
|
|
786
|
+
initFailed = false;
|
|
787
|
+
drawInFlight = false;
|
|
788
|
+
frameCallback = null;
|
|
789
|
+
frameCallbackRegistered = false;
|
|
790
|
+
cliRenderer;
|
|
791
|
+
clearColor;
|
|
792
|
+
constructor(ctx, options) {
|
|
793
|
+
const { scene = null, camera, renderer, autoAspect = true, ...renderableOptions } = options;
|
|
794
|
+
super(ctx, { ...renderableOptions, buffered: true, live: options.live ?? true });
|
|
795
|
+
const cliRenderer = ctx;
|
|
796
|
+
if (typeof cliRenderer.setFrameCallback !== "function" || typeof cliRenderer.removeFrameCallback !== "function") {
|
|
797
|
+
throw new Error("ThreeRenderable requires a CliRenderer context");
|
|
798
|
+
}
|
|
799
|
+
this.cliRenderer = cliRenderer;
|
|
800
|
+
this.scene = scene;
|
|
801
|
+
this.autoAspect = autoAspect;
|
|
802
|
+
this.clearColor = renderer?.backgroundColor ?? RGBA.fromValues(0, 0, 0, 1);
|
|
803
|
+
const { width, height } = this.getRenderSize();
|
|
804
|
+
this.engine = new ThreeCliRenderer(cliRenderer, {
|
|
805
|
+
width,
|
|
806
|
+
height,
|
|
807
|
+
autoResize: false,
|
|
808
|
+
...renderer
|
|
809
|
+
});
|
|
810
|
+
if (camera) {
|
|
811
|
+
this.engine.setActiveCamera(camera);
|
|
812
|
+
}
|
|
813
|
+
this.updateCameraAspect(width, height);
|
|
814
|
+
this.registerFrameCallback();
|
|
815
|
+
}
|
|
816
|
+
get aspectRatio() {
|
|
817
|
+
return this.getAspectRatio(this.width, this.height);
|
|
818
|
+
}
|
|
819
|
+
get renderer() {
|
|
820
|
+
return this.engine;
|
|
821
|
+
}
|
|
822
|
+
getScene() {
|
|
823
|
+
return this.scene;
|
|
824
|
+
}
|
|
825
|
+
setScene(scene) {
|
|
826
|
+
this.scene = scene;
|
|
827
|
+
this.requestRender();
|
|
828
|
+
}
|
|
829
|
+
getActiveCamera() {
|
|
830
|
+
return this.engine.getActiveCamera();
|
|
831
|
+
}
|
|
832
|
+
setActiveCamera(camera) {
|
|
833
|
+
this.engine.setActiveCamera(camera);
|
|
834
|
+
this.updateCameraAspect(this.width, this.height);
|
|
835
|
+
this.requestRender();
|
|
836
|
+
}
|
|
837
|
+
setAutoAspect(autoAspect) {
|
|
838
|
+
if (this.autoAspect === autoAspect)
|
|
839
|
+
return;
|
|
840
|
+
this.autoAspect = autoAspect;
|
|
841
|
+
if (autoAspect) {
|
|
842
|
+
this.updateCameraAspect(this.width, this.height);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
onResize(width, height) {
|
|
846
|
+
if (width > 0 && height > 0) {
|
|
847
|
+
this.engine.setSize(width, height, true);
|
|
848
|
+
this.updateCameraAspect(width, height);
|
|
849
|
+
}
|
|
850
|
+
super.onResize(width, height);
|
|
851
|
+
}
|
|
852
|
+
renderSelf(buffer, deltaTime) {
|
|
853
|
+
if (!this.visible || this.isDestroyed)
|
|
854
|
+
return;
|
|
855
|
+
if (this.frameCallbackRegistered)
|
|
856
|
+
return;
|
|
857
|
+
if (this.buffered && !this.frameBuffer)
|
|
858
|
+
return;
|
|
859
|
+
this.renderToBuffer(buffer, deltaTime / 1000);
|
|
860
|
+
}
|
|
861
|
+
destroySelf() {
|
|
862
|
+
if (this.frameCallback && this.frameCallbackRegistered) {
|
|
863
|
+
this.cliRenderer.removeFrameCallback(this.frameCallback);
|
|
864
|
+
this.frameCallbackRegistered = false;
|
|
865
|
+
this.frameCallback = null;
|
|
866
|
+
}
|
|
867
|
+
this.engine.destroy();
|
|
868
|
+
super.destroySelf();
|
|
869
|
+
}
|
|
870
|
+
registerFrameCallback() {
|
|
871
|
+
if (this.frameCallbackRegistered)
|
|
872
|
+
return;
|
|
873
|
+
this.frameCallback = async (deltaTime) => {
|
|
874
|
+
if (this.isDestroyed || !this.visible || !this.parent)
|
|
875
|
+
return;
|
|
876
|
+
if (!this.scene || !this.frameBuffer)
|
|
877
|
+
return;
|
|
878
|
+
await this.renderToBuffer(this.frameBuffer, deltaTime / 1000);
|
|
879
|
+
};
|
|
880
|
+
this.cliRenderer.setFrameCallback(this.frameCallback);
|
|
881
|
+
this.frameCallbackRegistered = true;
|
|
882
|
+
}
|
|
883
|
+
async renderToBuffer(buffer, deltaTime) {
|
|
884
|
+
if (!this.scene || this.isDestroyed || this.drawInFlight)
|
|
885
|
+
return;
|
|
886
|
+
this.drawInFlight = true;
|
|
887
|
+
try {
|
|
888
|
+
const initialized = await this.ensureInitialized();
|
|
889
|
+
if (!initialized || !this.scene)
|
|
890
|
+
return;
|
|
891
|
+
if (buffer === this.frameBuffer) {
|
|
892
|
+
buffer.clear(this.clearColor);
|
|
893
|
+
}
|
|
894
|
+
await this.engine.drawScene(this.scene, buffer, deltaTime);
|
|
895
|
+
} finally {
|
|
896
|
+
this.drawInFlight = false;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async ensureInitialized() {
|
|
900
|
+
if (this.initFailed)
|
|
901
|
+
return false;
|
|
902
|
+
if (!this.initPromise) {
|
|
903
|
+
this.initPromise = this.engine.init().then(() => true).catch((error) => {
|
|
904
|
+
this.initFailed = true;
|
|
905
|
+
console.error("ThreeRenderable init failed:", error);
|
|
906
|
+
return false;
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
return this.initPromise;
|
|
910
|
+
}
|
|
911
|
+
updateCameraAspect(width, height) {
|
|
912
|
+
if (!this.autoAspect || width <= 0 || height <= 0)
|
|
913
|
+
return;
|
|
914
|
+
const camera = this.engine.getActiveCamera();
|
|
915
|
+
if (camera instanceof PerspectiveCamera2) {
|
|
916
|
+
camera.aspect = this.getAspectRatio(width, height);
|
|
917
|
+
camera.updateProjectionMatrix();
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
getAspectRatio(width, height) {
|
|
921
|
+
if (width <= 0 || height <= 0)
|
|
922
|
+
return 1;
|
|
923
|
+
const resolution = this.cliRenderer.resolution;
|
|
924
|
+
if (resolution && this.cliRenderer.terminalWidth > 0 && this.cliRenderer.terminalHeight > 0) {
|
|
925
|
+
const cellWidth = resolution.width / this.cliRenderer.terminalWidth;
|
|
926
|
+
const cellHeight = resolution.height / this.cliRenderer.terminalHeight;
|
|
927
|
+
if (cellHeight > 0) {
|
|
928
|
+
return width * cellWidth / (height * cellHeight);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return width / (height * 2);
|
|
932
|
+
}
|
|
933
|
+
getRenderSize() {
|
|
934
|
+
return {
|
|
935
|
+
width: Math.max(1, this.width),
|
|
936
|
+
height: Math.max(1, this.height)
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
// src/3d/TextureUtils.ts
|
|
941
|
+
import { Color as Color2, DataTexture, NearestFilter, ClampToEdgeWrapping, RGBAFormat, UnsignedByteType } from "three";
|
|
942
|
+
import { Jimp as Jimp2 } from "jimp";
|
|
943
|
+
|
|
944
|
+
class TextureUtils {
|
|
945
|
+
static async loadTextureFromFile(path) {
|
|
946
|
+
try {
|
|
947
|
+
const buffer = await Bun.file(path).arrayBuffer();
|
|
948
|
+
const image = await Jimp2.read(buffer);
|
|
949
|
+
image.flip({ horizontal: false, vertical: true });
|
|
950
|
+
const texture = new DataTexture(image.bitmap.data, image.bitmap.width, image.bitmap.height, RGBAFormat, UnsignedByteType);
|
|
951
|
+
texture.needsUpdate = true;
|
|
952
|
+
texture.format = RGBAFormat;
|
|
953
|
+
texture.magFilter = NearestFilter;
|
|
954
|
+
texture.minFilter = NearestFilter;
|
|
955
|
+
texture.wrapS = ClampToEdgeWrapping;
|
|
956
|
+
texture.wrapT = ClampToEdgeWrapping;
|
|
957
|
+
texture.flipY = false;
|
|
958
|
+
return texture;
|
|
959
|
+
} catch (error) {
|
|
960
|
+
console.error(`Failed to load texture from ${path}:`, error);
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
static async fromFile(path) {
|
|
965
|
+
return this.loadTextureFromFile(path);
|
|
966
|
+
}
|
|
967
|
+
static createCheckerboard(size = 256, color1 = new Color2(1, 1, 1), color2 = new Color2(0, 0, 0), checkSize = 32) {
|
|
968
|
+
const data = new Uint8ClampedArray(size * size * 4);
|
|
969
|
+
for (let y = 0;y < size; y++) {
|
|
970
|
+
for (let x = 0;x < size; x++) {
|
|
971
|
+
const isEvenX = Math.floor(x / checkSize) % 2 === 0;
|
|
972
|
+
const isEvenY = Math.floor(y / checkSize) % 2 === 0;
|
|
973
|
+
const color = isEvenX && isEvenY || !isEvenX && !isEvenY ? color1 : color2;
|
|
974
|
+
const index = (y * size + x) * 4;
|
|
975
|
+
data[index] = Math.floor(color.r * 255);
|
|
976
|
+
data[index + 1] = Math.floor(color.g * 255);
|
|
977
|
+
data[index + 2] = Math.floor(color.b * 255);
|
|
978
|
+
data[index + 3] = 255;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
const imageData = { data, width: size, height: size };
|
|
982
|
+
const texture = new DataTexture(data, size, size, RGBAFormat, UnsignedByteType);
|
|
983
|
+
texture.needsUpdate = true;
|
|
984
|
+
texture.format = RGBAFormat;
|
|
985
|
+
texture.magFilter = NearestFilter;
|
|
986
|
+
texture.minFilter = NearestFilter;
|
|
987
|
+
texture.wrapS = ClampToEdgeWrapping;
|
|
988
|
+
texture.wrapT = ClampToEdgeWrapping;
|
|
989
|
+
texture.flipY = false;
|
|
990
|
+
return texture;
|
|
991
|
+
}
|
|
992
|
+
static createGradient(size = 256, startColor = new Color2(1, 0, 0), endColor = new Color2(0, 0, 1), direction = "vertical") {
|
|
993
|
+
const data = new Uint8ClampedArray(size * size * 4);
|
|
994
|
+
for (let y = 0;y < size; y++) {
|
|
995
|
+
for (let x = 0;x < size; x++) {
|
|
996
|
+
let t = 0;
|
|
997
|
+
if (direction === "horizontal") {
|
|
998
|
+
t = x / (size - 1);
|
|
999
|
+
} else if (direction === "vertical") {
|
|
1000
|
+
t = y / (size - 1);
|
|
1001
|
+
} else if (direction === "radial") {
|
|
1002
|
+
const dx = x - size / 2;
|
|
1003
|
+
const dy = y - size / 2;
|
|
1004
|
+
t = Math.min(1, Math.sqrt(dx * dx + dy * dy) / (size / 2));
|
|
1005
|
+
}
|
|
1006
|
+
const r = startColor.r * (1 - t) + endColor.r * t;
|
|
1007
|
+
const g = startColor.g * (1 - t) + endColor.g * t;
|
|
1008
|
+
const b = startColor.b * (1 - t) + endColor.b * t;
|
|
1009
|
+
const index = (y * size + x) * 4;
|
|
1010
|
+
data[index] = Math.floor(r * 255);
|
|
1011
|
+
data[index + 1] = Math.floor(g * 255);
|
|
1012
|
+
data[index + 2] = Math.floor(b * 255);
|
|
1013
|
+
data[index + 3] = 255;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
const imageData = { data, width: size, height: size };
|
|
1017
|
+
const texture = new DataTexture(data, size, size, RGBAFormat, UnsignedByteType);
|
|
1018
|
+
texture.needsUpdate = true;
|
|
1019
|
+
texture.format = RGBAFormat;
|
|
1020
|
+
texture.magFilter = NearestFilter;
|
|
1021
|
+
texture.minFilter = NearestFilter;
|
|
1022
|
+
texture.wrapS = ClampToEdgeWrapping;
|
|
1023
|
+
texture.wrapT = ClampToEdgeWrapping;
|
|
1024
|
+
texture.flipY = false;
|
|
1025
|
+
return texture;
|
|
1026
|
+
}
|
|
1027
|
+
static createNoise(size = 256, scale = 1, octaves = 1, color1 = new Color2(1, 1, 1), color2 = new Color2(0, 0, 0)) {
|
|
1028
|
+
const data = new Uint8ClampedArray(size * size * 4);
|
|
1029
|
+
for (let y = 0;y < size; y++) {
|
|
1030
|
+
for (let x = 0;x < size; x++) {
|
|
1031
|
+
let noise = 0;
|
|
1032
|
+
let amplitude = 1;
|
|
1033
|
+
let frequency = 1;
|
|
1034
|
+
for (let o = 0;o < octaves; o++) {
|
|
1035
|
+
const nx = x * frequency * scale / size;
|
|
1036
|
+
const ny = y * frequency * scale / size;
|
|
1037
|
+
const sampleX = Math.sin(nx * 12.9898) * 43758.5453;
|
|
1038
|
+
const sampleY = Math.cos(ny * 78.233) * 43758.5453;
|
|
1039
|
+
const sample = Math.sin(sampleX + sampleY) * 0.5 + 0.5;
|
|
1040
|
+
noise += sample * amplitude;
|
|
1041
|
+
amplitude *= 0.5;
|
|
1042
|
+
frequency *= 2;
|
|
1043
|
+
}
|
|
1044
|
+
noise = Math.min(1, Math.max(0, noise));
|
|
1045
|
+
const r = color1.r * noise + color2.r * (1 - noise);
|
|
1046
|
+
const g = color1.g * noise + color2.g * (1 - noise);
|
|
1047
|
+
const b = color1.b * noise + color2.b * (1 - noise);
|
|
1048
|
+
const index = (y * size + x) * 4;
|
|
1049
|
+
data[index] = Math.floor(r * 255);
|
|
1050
|
+
data[index + 1] = Math.floor(g * 255);
|
|
1051
|
+
data[index + 2] = Math.floor(b * 255);
|
|
1052
|
+
data[index + 3] = 255;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const imageData = { data, width: size, height: size };
|
|
1056
|
+
const texture = new DataTexture(data, size, size, RGBAFormat, UnsignedByteType);
|
|
1057
|
+
texture.needsUpdate = true;
|
|
1058
|
+
texture.format = RGBAFormat;
|
|
1059
|
+
texture.magFilter = NearestFilter;
|
|
1060
|
+
texture.minFilter = NearestFilter;
|
|
1061
|
+
texture.wrapS = ClampToEdgeWrapping;
|
|
1062
|
+
texture.wrapT = ClampToEdgeWrapping;
|
|
1063
|
+
texture.flipY = false;
|
|
1064
|
+
return texture;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
// src/3d/SpriteUtils.ts
|
|
1068
|
+
import { Sprite, SpriteMaterial } from "three";
|
|
1069
|
+
|
|
1070
|
+
class SheetSprite extends Sprite {
|
|
1071
|
+
_frameIndex = 0;
|
|
1072
|
+
_numFrames = 0;
|
|
1073
|
+
constructor(material, numFrames) {
|
|
1074
|
+
super(material);
|
|
1075
|
+
this._numFrames = numFrames;
|
|
1076
|
+
this.setIndex(0);
|
|
1077
|
+
}
|
|
1078
|
+
setIndex = (index) => {
|
|
1079
|
+
this._frameIndex = index;
|
|
1080
|
+
this.material.map?.repeat.set(1 / this._numFrames, 1);
|
|
1081
|
+
this.material.map?.offset.set(this._frameIndex / this._numFrames, 0);
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
class SpriteUtils {
|
|
1086
|
+
static async fromFile(path, {
|
|
1087
|
+
materialParameters = {
|
|
1088
|
+
alphaTest: 0.1,
|
|
1089
|
+
depthWrite: true
|
|
1090
|
+
}
|
|
1091
|
+
} = {}) {
|
|
1092
|
+
const texture = await TextureUtils.fromFile(path);
|
|
1093
|
+
if (!texture) {
|
|
1094
|
+
throw new Error(`Failed to load sprite texture from ${path}`);
|
|
1095
|
+
}
|
|
1096
|
+
const spriteMaterial = new SpriteMaterial({ map: texture, ...materialParameters });
|
|
1097
|
+
const sprite = new Sprite(spriteMaterial);
|
|
1098
|
+
const textureAspectRatio = texture.image.width / texture.image.height;
|
|
1099
|
+
sprite.updateMatrix = function() {
|
|
1100
|
+
this.matrix.compose(this.position, this.quaternion, this.scale.clone().setX(this.scale.x * textureAspectRatio));
|
|
1101
|
+
};
|
|
1102
|
+
return sprite;
|
|
1103
|
+
}
|
|
1104
|
+
static async sheetFromFile(path, numFrames) {
|
|
1105
|
+
const spriteTexture = await TextureUtils.fromFile(path);
|
|
1106
|
+
if (!spriteTexture) {
|
|
1107
|
+
console.error("Failed to load sprite texture, exiting.");
|
|
1108
|
+
process.exit(1);
|
|
1109
|
+
}
|
|
1110
|
+
const spriteMaterial = new SpriteMaterial({ map: spriteTexture });
|
|
1111
|
+
const sprite = new SheetSprite(spriteMaterial, numFrames);
|
|
1112
|
+
const singleFrameWidth = spriteTexture.image.width / numFrames;
|
|
1113
|
+
const singleFrameHeight = spriteTexture.image.height;
|
|
1114
|
+
const frameAspectRatio = singleFrameWidth / singleFrameHeight;
|
|
1115
|
+
sprite.updateMatrix = function() {
|
|
1116
|
+
this.matrix.compose(this.position, this.quaternion, this.scale.clone().setX(this.scale.x * frameAspectRatio));
|
|
1117
|
+
};
|
|
1118
|
+
return sprite;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
// src/3d/animation/SpriteAnimator.ts
|
|
1122
|
+
import * as THREE from "three";
|
|
1123
|
+
import { uniform, texture as tslTexture, uv, float, vec2, bufferAttribute, mix } from "three/tsl";
|
|
1124
|
+
import { MeshBasicNodeMaterial } from "three/webgpu";
|
|
1125
|
+
var HIDDEN_MATRIX = new THREE.Matrix4().scale(new THREE.Vector3(0, 0, 0));
|
|
1126
|
+
var DEFAULT_FRAME_DURATION = 100;
|
|
1127
|
+
var DEFAULT_INITIAL_FRAME = 0;
|
|
1128
|
+
var DEFAULT_SCALE = 1;
|
|
1129
|
+
var DEFAULT_FLIP_X = false;
|
|
1130
|
+
var DEFAULT_FLIP_Y = false;
|
|
1131
|
+
|
|
1132
|
+
class Animation {
|
|
1133
|
+
name;
|
|
1134
|
+
state;
|
|
1135
|
+
resource;
|
|
1136
|
+
instanceIndex;
|
|
1137
|
+
instanceManager;
|
|
1138
|
+
frameAttribute;
|
|
1139
|
+
flipAttribute;
|
|
1140
|
+
currentLocalFrame;
|
|
1141
|
+
timeAccumulator;
|
|
1142
|
+
isPlaying;
|
|
1143
|
+
_isActive = false;
|
|
1144
|
+
constructor(name, state, resource, instanceIndex, instanceManager, frameAttribute, flipAttribute) {
|
|
1145
|
+
this.name = name;
|
|
1146
|
+
this.state = state;
|
|
1147
|
+
this.resource = resource;
|
|
1148
|
+
this.instanceIndex = instanceIndex;
|
|
1149
|
+
this.instanceManager = instanceManager;
|
|
1150
|
+
this.frameAttribute = frameAttribute;
|
|
1151
|
+
this.flipAttribute = flipAttribute;
|
|
1152
|
+
this.currentLocalFrame = state.initialFrame;
|
|
1153
|
+
this.timeAccumulator = 0;
|
|
1154
|
+
this.isPlaying = true;
|
|
1155
|
+
this.instanceManager.mesh.setMatrixAt(this.instanceIndex, HIDDEN_MATRIX);
|
|
1156
|
+
const absoluteFrame = this.state.animFrameOffset + this.currentLocalFrame;
|
|
1157
|
+
this.frameAttribute.setX(this.instanceIndex, absoluteFrame);
|
|
1158
|
+
this.flipAttribute.setXY(this.instanceIndex, this.state.flipX ? 1 : 0, this.state.flipY ? 1 : 0);
|
|
1159
|
+
}
|
|
1160
|
+
activate(worldTransform) {
|
|
1161
|
+
this._isActive = true;
|
|
1162
|
+
this.isPlaying = true;
|
|
1163
|
+
this.currentLocalFrame = this.state.initialFrame;
|
|
1164
|
+
this.timeAccumulator = 0;
|
|
1165
|
+
this.updateVisuals(worldTransform);
|
|
1166
|
+
const absoluteFrame = this.state.animFrameOffset + this.currentLocalFrame;
|
|
1167
|
+
this.frameAttribute.setX(this.instanceIndex, absoluteFrame);
|
|
1168
|
+
this.frameAttribute.needsUpdate = true;
|
|
1169
|
+
this.flipAttribute.setXY(this.instanceIndex, this.state.flipX ? 1 : 0, this.state.flipY ? 1 : 0);
|
|
1170
|
+
this.flipAttribute.needsUpdate = true;
|
|
1171
|
+
}
|
|
1172
|
+
deactivate() {
|
|
1173
|
+
this._isActive = false;
|
|
1174
|
+
this.isPlaying = false;
|
|
1175
|
+
this.instanceManager.mesh.setMatrixAt(this.instanceIndex, HIDDEN_MATRIX);
|
|
1176
|
+
}
|
|
1177
|
+
updateVisuals(worldTransform) {
|
|
1178
|
+
if (!this._isActive)
|
|
1179
|
+
return;
|
|
1180
|
+
this.instanceManager.mesh.setMatrixAt(this.instanceIndex, worldTransform);
|
|
1181
|
+
}
|
|
1182
|
+
updateTime(deltaTimeMs) {
|
|
1183
|
+
if (!this.isPlaying || !this._isActive)
|
|
1184
|
+
return false;
|
|
1185
|
+
this.timeAccumulator += deltaTimeMs;
|
|
1186
|
+
let needsFrameAttributeUpdate = false;
|
|
1187
|
+
if (this.timeAccumulator >= this.state.frameDuration) {
|
|
1188
|
+
const framesToAdvance = Math.floor(this.timeAccumulator / this.state.frameDuration);
|
|
1189
|
+
this.timeAccumulator %= this.state.frameDuration;
|
|
1190
|
+
const oldLocalFrame = this.currentLocalFrame;
|
|
1191
|
+
let nextLocalFrame = this.currentLocalFrame + framesToAdvance;
|
|
1192
|
+
if (nextLocalFrame >= this.state.animNumFrames) {
|
|
1193
|
+
if (this.state.loop) {
|
|
1194
|
+
this.currentLocalFrame = nextLocalFrame % this.state.animNumFrames;
|
|
1195
|
+
} else {
|
|
1196
|
+
this.currentLocalFrame = this.state.animNumFrames - 1;
|
|
1197
|
+
this.isPlaying = false;
|
|
1198
|
+
}
|
|
1199
|
+
} else {
|
|
1200
|
+
this.currentLocalFrame = nextLocalFrame;
|
|
1201
|
+
}
|
|
1202
|
+
if (this.currentLocalFrame !== oldLocalFrame || !this.isPlaying) {
|
|
1203
|
+
const absoluteFrame = this.state.animFrameOffset + this.currentLocalFrame;
|
|
1204
|
+
this.frameAttribute.setX(this.instanceIndex, absoluteFrame);
|
|
1205
|
+
this.frameAttribute.needsUpdate = true;
|
|
1206
|
+
needsFrameAttributeUpdate = true;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
return needsFrameAttributeUpdate;
|
|
1210
|
+
}
|
|
1211
|
+
play() {
|
|
1212
|
+
if (!this._isActive)
|
|
1213
|
+
return;
|
|
1214
|
+
this.isPlaying = true;
|
|
1215
|
+
}
|
|
1216
|
+
stop() {
|
|
1217
|
+
this.isPlaying = false;
|
|
1218
|
+
}
|
|
1219
|
+
goToFrame(localFrame) {
|
|
1220
|
+
if (!this._isActive)
|
|
1221
|
+
return;
|
|
1222
|
+
const targetLocalFrame = Math.max(0, Math.min(localFrame, this.state.animNumFrames - 1));
|
|
1223
|
+
if (this.currentLocalFrame !== targetLocalFrame) {
|
|
1224
|
+
this.currentLocalFrame = targetLocalFrame;
|
|
1225
|
+
this.timeAccumulator = 0;
|
|
1226
|
+
const absoluteFrame = this.state.animFrameOffset + this.currentLocalFrame;
|
|
1227
|
+
this.frameAttribute.setX(this.instanceIndex, absoluteFrame);
|
|
1228
|
+
this.frameAttribute.needsUpdate = true;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
setFrameDuration(newFrameDuration) {
|
|
1232
|
+
if (newFrameDuration > 0) {
|
|
1233
|
+
this.state = { ...this.state, frameDuration: newFrameDuration };
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
getResource() {
|
|
1237
|
+
return this.resource;
|
|
1238
|
+
}
|
|
1239
|
+
releaseInstanceSlot() {
|
|
1240
|
+
this.instanceManager.releaseInstanceSlot(this.instanceIndex);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
class TiledSprite {
|
|
1245
|
+
id;
|
|
1246
|
+
animator;
|
|
1247
|
+
_animations;
|
|
1248
|
+
_currentAnimation;
|
|
1249
|
+
_transformObject;
|
|
1250
|
+
_reusableMatrix;
|
|
1251
|
+
_reusableAnimGeomScale;
|
|
1252
|
+
_isVisibleState = true;
|
|
1253
|
+
originalDefinition;
|
|
1254
|
+
constructor(id, userSpriteDefinition, animator, animationInstanceParams) {
|
|
1255
|
+
this.id = id;
|
|
1256
|
+
this.originalDefinition = userSpriteDefinition;
|
|
1257
|
+
this.animator = animator;
|
|
1258
|
+
this._transformObject = new THREE.Object3D;
|
|
1259
|
+
this._reusableMatrix = new THREE.Matrix4;
|
|
1260
|
+
this._reusableAnimGeomScale = new THREE.Vector3;
|
|
1261
|
+
const initialScale = userSpriteDefinition.scale ?? DEFAULT_SCALE;
|
|
1262
|
+
this._transformObject.scale.set(initialScale, initialScale, initialScale);
|
|
1263
|
+
this._animations = new Map;
|
|
1264
|
+
for (const params of animationInstanceParams) {
|
|
1265
|
+
const anim = new Animation(params.name, params.state, params.resource, params.index, params.instanceManager, params.frameAttribute, params.flipAttribute);
|
|
1266
|
+
this._animations.set(params.name, anim);
|
|
1267
|
+
}
|
|
1268
|
+
const initialAnim = this._animations.get(userSpriteDefinition.initialAnimation);
|
|
1269
|
+
if (!initialAnim) {
|
|
1270
|
+
throw new Error(`[TiledSprite] Initial animation "${userSpriteDefinition.initialAnimation}" not found for sprite "${this.id}".`);
|
|
1271
|
+
}
|
|
1272
|
+
this._currentAnimation = initialAnim;
|
|
1273
|
+
const initialWorldMatrix = this._calculateAnimationWorldMatrix(this._currentAnimation.state);
|
|
1274
|
+
this._currentAnimation.activate(initialWorldMatrix);
|
|
1275
|
+
this._isVisibleState = true;
|
|
1276
|
+
}
|
|
1277
|
+
_calculateAnimationWorldMatrix(animState) {
|
|
1278
|
+
const matrix = this._reusableMatrix;
|
|
1279
|
+
const animGeomScale = this._reusableAnimGeomScale;
|
|
1280
|
+
const worldHeight = this._transformObject.scale.y;
|
|
1281
|
+
const frameAspectRatio = animState.sheetTilesetWidth / animState.sheetNumFrames / animState.sheetTilesetHeight;
|
|
1282
|
+
const worldWidth = worldHeight * frameAspectRatio;
|
|
1283
|
+
animGeomScale.set(worldWidth, worldHeight, this._transformObject.scale.z);
|
|
1284
|
+
matrix.compose(this._transformObject.position, this._transformObject.quaternion, animGeomScale);
|
|
1285
|
+
return matrix;
|
|
1286
|
+
}
|
|
1287
|
+
get currentAnimation() {
|
|
1288
|
+
return this._currentAnimation;
|
|
1289
|
+
}
|
|
1290
|
+
updateCurrentAnimationVisuals() {
|
|
1291
|
+
if (this._isVisibleState) {
|
|
1292
|
+
const currentAnim = this.currentAnimation;
|
|
1293
|
+
if (currentAnim) {
|
|
1294
|
+
const finalMatrix = this._calculateAnimationWorldMatrix(currentAnim.state);
|
|
1295
|
+
currentAnim.updateVisuals(finalMatrix);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
setPosition(position) {
|
|
1300
|
+
this._transformObject.position.copy(position);
|
|
1301
|
+
this.updateCurrentAnimationVisuals();
|
|
1302
|
+
}
|
|
1303
|
+
setRotation(rotation) {
|
|
1304
|
+
this._transformObject.quaternion.copy(rotation);
|
|
1305
|
+
this.updateCurrentAnimationVisuals();
|
|
1306
|
+
}
|
|
1307
|
+
setScale(scale) {
|
|
1308
|
+
this._transformObject.scale.copy(scale);
|
|
1309
|
+
this.updateCurrentAnimationVisuals();
|
|
1310
|
+
}
|
|
1311
|
+
getScale() {
|
|
1312
|
+
return this._transformObject.scale.clone();
|
|
1313
|
+
}
|
|
1314
|
+
setTransform(position, rotation, newScale) {
|
|
1315
|
+
this._transformObject.position.copy(position);
|
|
1316
|
+
this._transformObject.quaternion.copy(rotation);
|
|
1317
|
+
this._transformObject.scale.copy(newScale);
|
|
1318
|
+
this.updateCurrentAnimationVisuals();
|
|
1319
|
+
}
|
|
1320
|
+
play() {
|
|
1321
|
+
this.currentAnimation.play();
|
|
1322
|
+
}
|
|
1323
|
+
stop() {
|
|
1324
|
+
this.currentAnimation.stop();
|
|
1325
|
+
}
|
|
1326
|
+
goToFrame(frame) {
|
|
1327
|
+
this.currentAnimation.goToFrame(frame);
|
|
1328
|
+
}
|
|
1329
|
+
setFrameDuration(newFrameDuration) {
|
|
1330
|
+
this.currentAnimation.setFrameDuration(newFrameDuration);
|
|
1331
|
+
}
|
|
1332
|
+
isPlaying() {
|
|
1333
|
+
return this.currentAnimation.isPlaying;
|
|
1334
|
+
}
|
|
1335
|
+
async setAnimation(animationName) {
|
|
1336
|
+
const newAnim = this._animations.get(animationName);
|
|
1337
|
+
if (!newAnim) {
|
|
1338
|
+
throw new Error(`[TiledSprite] Animation "${animationName}" not found for sprite "${this.id}".`);
|
|
1339
|
+
}
|
|
1340
|
+
const switchingToSameAnimation = this._currentAnimation.name === animationName;
|
|
1341
|
+
const oldAnim = this._currentAnimation;
|
|
1342
|
+
if (!switchingToSameAnimation || !this._isVisibleState) {
|
|
1343
|
+
oldAnim?.deactivate();
|
|
1344
|
+
}
|
|
1345
|
+
this._currentAnimation = newAnim;
|
|
1346
|
+
if (this._isVisibleState) {
|
|
1347
|
+
const finalMatrix = this._calculateAnimationWorldMatrix(newAnim.state);
|
|
1348
|
+
newAnim.activate(finalMatrix);
|
|
1349
|
+
} else {
|
|
1350
|
+
newAnim.deactivate();
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
update(deltaTime) {
|
|
1354
|
+
if (this.visible) {
|
|
1355
|
+
this.currentAnimation.updateTime(deltaTime);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
destroy() {
|
|
1359
|
+
this._animations.forEach((anim) => {
|
|
1360
|
+
anim.deactivate();
|
|
1361
|
+
anim.releaseInstanceSlot();
|
|
1362
|
+
});
|
|
1363
|
+
this._animations.clear();
|
|
1364
|
+
this._isVisibleState = false;
|
|
1365
|
+
}
|
|
1366
|
+
getCurrentAnimationName() {
|
|
1367
|
+
return this._currentAnimation.name;
|
|
1368
|
+
}
|
|
1369
|
+
getWorldTransform() {
|
|
1370
|
+
return this._calculateAnimationWorldMatrix(this._currentAnimation.state);
|
|
1371
|
+
}
|
|
1372
|
+
getWorldPlaneSize() {
|
|
1373
|
+
const animState = this._currentAnimation.state;
|
|
1374
|
+
const worldHeight = this._transformObject.scale.y;
|
|
1375
|
+
const frameActualWidthPx = animState.sheetTilesetWidth / animState.sheetNumFrames;
|
|
1376
|
+
const frameAspectRatio = frameActualWidthPx / animState.sheetTilesetHeight;
|
|
1377
|
+
const worldWidth = worldHeight * frameAspectRatio;
|
|
1378
|
+
return new THREE.Vector2(worldWidth, worldHeight);
|
|
1379
|
+
}
|
|
1380
|
+
get visible() {
|
|
1381
|
+
return this._isVisibleState;
|
|
1382
|
+
}
|
|
1383
|
+
set visible(value) {
|
|
1384
|
+
if (this._isVisibleState === value) {
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
this._isVisibleState = value;
|
|
1388
|
+
if (value) {
|
|
1389
|
+
const finalMatrix = this._calculateAnimationWorldMatrix(this._currentAnimation.state);
|
|
1390
|
+
this._currentAnimation.activate(finalMatrix);
|
|
1391
|
+
} else {
|
|
1392
|
+
this._currentAnimation.deactivate();
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
get definition() {
|
|
1396
|
+
return this.originalDefinition;
|
|
1397
|
+
}
|
|
1398
|
+
get currentTransform() {
|
|
1399
|
+
return {
|
|
1400
|
+
position: this._transformObject.position.clone(),
|
|
1401
|
+
quaternion: this._transformObject.quaternion.clone(),
|
|
1402
|
+
scale: this._transformObject.scale.clone()
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
class SpriteAnimator {
|
|
1408
|
+
scene;
|
|
1409
|
+
instances = new Map;
|
|
1410
|
+
_idCounter = 0;
|
|
1411
|
+
instanceManagers = new Map;
|
|
1412
|
+
constructor(scene) {
|
|
1413
|
+
this.scene = scene;
|
|
1414
|
+
}
|
|
1415
|
+
createSpriteAnimationMaterial(resource, frameAttribute, flipAttribute, materialFactory) {
|
|
1416
|
+
const texture = resource.texture;
|
|
1417
|
+
const sheetProps = resource.sheetProperties;
|
|
1418
|
+
const uvTileWidth = 1 / sheetProps.sheetNumFrames;
|
|
1419
|
+
const uvTileHeight = 1;
|
|
1420
|
+
const uvTileSize = new THREE.Vector2(uvTileWidth, uvTileHeight);
|
|
1421
|
+
const tileSizeUniform = uniform(uvTileSize);
|
|
1422
|
+
const epsilon = float(0.000001);
|
|
1423
|
+
const baseUV = uv();
|
|
1424
|
+
const oneFloat = float(1);
|
|
1425
|
+
const a_frameIndex = bufferAttribute(frameAttribute);
|
|
1426
|
+
const a_flip = bufferAttribute(flipAttribute);
|
|
1427
|
+
const calculatedTileCoordX = a_frameIndex.mul(tileSizeUniform.x);
|
|
1428
|
+
const calculatedTileCoord = vec2(calculatedTileCoordX, float(0));
|
|
1429
|
+
const flippedX = mix(baseUV.x, oneFloat.sub(baseUV.x), a_flip.x);
|
|
1430
|
+
const flippedY = mix(baseUV.y, oneFloat.sub(baseUV.y), a_flip.y);
|
|
1431
|
+
const finalLocalUV = vec2(flippedX, flippedY);
|
|
1432
|
+
const mapNode = tslTexture(texture);
|
|
1433
|
+
const finalUV = finalLocalUV.mul(tileSizeUniform).min(tileSizeUniform.sub(epsilon)).add(calculatedTileCoord);
|
|
1434
|
+
const sampledColor = mapNode.sample(finalUV);
|
|
1435
|
+
const material = materialFactory();
|
|
1436
|
+
material.colorNode = sampledColor;
|
|
1437
|
+
return material;
|
|
1438
|
+
}
|
|
1439
|
+
getOrCreateInstanceManager(resource, maxInstances, renderOrder, depthWrite, materialFactory) {
|
|
1440
|
+
const key = `${resource.sheetProperties.imagePath}_${maxInstances}_${renderOrder}_${depthWrite}`;
|
|
1441
|
+
let manager = this.instanceManagers.get(key);
|
|
1442
|
+
if (!manager) {
|
|
1443
|
+
const geometry = new THREE.PlaneGeometry(1, 1);
|
|
1444
|
+
const frameArray = new Float32Array(maxInstances);
|
|
1445
|
+
const frameAttribute = new THREE.InstancedBufferAttribute(frameArray, 1);
|
|
1446
|
+
frameAttribute.setUsage(THREE.DynamicDrawUsage);
|
|
1447
|
+
const flipArray = new Float32Array(maxInstances * 2);
|
|
1448
|
+
const flipAttribute = new THREE.InstancedBufferAttribute(flipArray, 2);
|
|
1449
|
+
flipAttribute.setUsage(THREE.DynamicDrawUsage);
|
|
1450
|
+
const material = this.createSpriteAnimationMaterial(resource, frameAttribute, flipAttribute, materialFactory);
|
|
1451
|
+
geometry.setAttribute("a_frameIndexInstanced", frameAttribute);
|
|
1452
|
+
geometry.setAttribute("a_flipInstanced", flipAttribute);
|
|
1453
|
+
for (let i = 0;i < maxInstances; i++) {
|
|
1454
|
+
flipAttribute.setXY(i, 0, 0);
|
|
1455
|
+
}
|
|
1456
|
+
flipAttribute.needsUpdate = true;
|
|
1457
|
+
const instanceManager = resource.createInstanceManager(geometry, material, {
|
|
1458
|
+
maxInstances,
|
|
1459
|
+
renderOrder,
|
|
1460
|
+
depthWrite,
|
|
1461
|
+
name: `SpriteAnimator_${key}`
|
|
1462
|
+
});
|
|
1463
|
+
const uvTileWidth = 1 / resource.sheetProperties.sheetNumFrames;
|
|
1464
|
+
const uvTileSize = new THREE.Vector2(uvTileWidth, 1);
|
|
1465
|
+
manager = {
|
|
1466
|
+
instanceManager,
|
|
1467
|
+
frameAttribute,
|
|
1468
|
+
flipAttribute,
|
|
1469
|
+
uvTileSize
|
|
1470
|
+
};
|
|
1471
|
+
this.instanceManagers.set(key, manager);
|
|
1472
|
+
}
|
|
1473
|
+
return manager;
|
|
1474
|
+
}
|
|
1475
|
+
async createSprite(userSpriteDefinition, materialFactory) {
|
|
1476
|
+
const id = userSpriteDefinition.id ?? `sprite_${this._idCounter++}`;
|
|
1477
|
+
const animationInstanceParams = [];
|
|
1478
|
+
const resolvedMaterialFactory = materialFactory ?? (() => new MeshBasicNodeMaterial({
|
|
1479
|
+
transparent: true,
|
|
1480
|
+
alphaTest: 0.1,
|
|
1481
|
+
depthWrite: true
|
|
1482
|
+
}));
|
|
1483
|
+
const resourceManagers = new Map;
|
|
1484
|
+
for (const animName in userSpriteDefinition.animations) {
|
|
1485
|
+
const animDef = userSpriteDefinition.animations[animName];
|
|
1486
|
+
const resource = animDef.resource;
|
|
1487
|
+
let managerInfo = resourceManagers.get(resource);
|
|
1488
|
+
if (!managerInfo) {
|
|
1489
|
+
const maxInstances = userSpriteDefinition.maxInstances ?? 1024;
|
|
1490
|
+
const renderOrder = userSpriteDefinition.renderOrder ?? 0;
|
|
1491
|
+
const depthWrite = userSpriteDefinition.depthWrite ?? true;
|
|
1492
|
+
managerInfo = this.getOrCreateInstanceManager(resource, maxInstances, renderOrder, depthWrite, resolvedMaterialFactory);
|
|
1493
|
+
resourceManagers.set(resource, managerInfo);
|
|
1494
|
+
}
|
|
1495
|
+
const instanceIndex = managerInfo.instanceManager.acquireInstanceSlot();
|
|
1496
|
+
const resolvedState = {
|
|
1497
|
+
imagePath: resource.sheetProperties.imagePath,
|
|
1498
|
+
sheetTilesetWidth: resource.sheetProperties.sheetTilesetWidth,
|
|
1499
|
+
sheetTilesetHeight: resource.sheetProperties.sheetTilesetHeight,
|
|
1500
|
+
sheetNumFrames: resource.sheetProperties.sheetNumFrames,
|
|
1501
|
+
animNumFrames: animDef.animNumFrames ?? resource.sheetProperties.sheetNumFrames,
|
|
1502
|
+
animFrameOffset: animDef.animFrameOffset ?? 0,
|
|
1503
|
+
frameDuration: animDef.frameDuration ?? DEFAULT_FRAME_DURATION,
|
|
1504
|
+
loop: animDef.loop ?? true,
|
|
1505
|
+
initialFrame: animDef.initialFrame ?? DEFAULT_INITIAL_FRAME,
|
|
1506
|
+
flipX: animDef.flipX ?? DEFAULT_FLIP_X,
|
|
1507
|
+
flipY: animDef.flipY ?? DEFAULT_FLIP_Y,
|
|
1508
|
+
texture: resource.texture
|
|
1509
|
+
};
|
|
1510
|
+
animationInstanceParams.push({
|
|
1511
|
+
name: animName,
|
|
1512
|
+
state: resolvedState,
|
|
1513
|
+
resource,
|
|
1514
|
+
index: instanceIndex,
|
|
1515
|
+
instanceManager: managerInfo.instanceManager,
|
|
1516
|
+
frameAttribute: managerInfo.frameAttribute,
|
|
1517
|
+
flipAttribute: managerInfo.flipAttribute
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
if (!userSpriteDefinition.initialAnimation || !userSpriteDefinition.animations[userSpriteDefinition.initialAnimation]) {
|
|
1521
|
+
let found = false;
|
|
1522
|
+
for (const p of animationInstanceParams)
|
|
1523
|
+
if (p.name === userSpriteDefinition.initialAnimation)
|
|
1524
|
+
found = true;
|
|
1525
|
+
if (!found) {
|
|
1526
|
+
for (const params of animationInstanceParams) {
|
|
1527
|
+
params.instanceManager.releaseInstanceSlot(params.index);
|
|
1528
|
+
}
|
|
1529
|
+
throw new Error(`[SpriteAnimator] initialAnimation "${userSpriteDefinition.initialAnimation}" not found or invalid for sprite "${id}".`);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
const tiledSprite = new TiledSprite(id, userSpriteDefinition, this, animationInstanceParams);
|
|
1533
|
+
this.instances.set(id, tiledSprite);
|
|
1534
|
+
return tiledSprite;
|
|
1535
|
+
}
|
|
1536
|
+
update(deltaTime) {
|
|
1537
|
+
for (const sprite of this.instances.values()) {
|
|
1538
|
+
sprite.update(deltaTime);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
removeSprite(id) {
|
|
1542
|
+
const sprite = this.instances.get(id);
|
|
1543
|
+
if (sprite) {
|
|
1544
|
+
sprite.destroy();
|
|
1545
|
+
this.instances.delete(id);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
removeAllSprites() {
|
|
1549
|
+
const ids = Array.from(this.instances.keys());
|
|
1550
|
+
for (const id of ids) {
|
|
1551
|
+
this.removeSprite(id);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
// src/3d/animation/ExplodingSpriteEffect.ts
|
|
1556
|
+
import * as THREE2 from "three";
|
|
1557
|
+
import {
|
|
1558
|
+
uniform as uniform2,
|
|
1559
|
+
attribute,
|
|
1560
|
+
texture as tslTexture2,
|
|
1561
|
+
uv as uv2,
|
|
1562
|
+
float as float2,
|
|
1563
|
+
vec2 as vec22,
|
|
1564
|
+
vec3,
|
|
1565
|
+
vec4,
|
|
1566
|
+
step,
|
|
1567
|
+
max,
|
|
1568
|
+
sin,
|
|
1569
|
+
cos,
|
|
1570
|
+
positionLocal,
|
|
1571
|
+
mat3
|
|
1572
|
+
} from "three/tsl";
|
|
1573
|
+
import { MeshBasicNodeMaterial as MeshBasicNodeMaterial2 } from "three/webgpu";
|
|
1574
|
+
var DEFAULT_EXPLOSION_PARAMETERS = {
|
|
1575
|
+
numRows: 5,
|
|
1576
|
+
numCols: 5,
|
|
1577
|
+
durationMs: 2000,
|
|
1578
|
+
strength: 5,
|
|
1579
|
+
strengthVariation: 0.5,
|
|
1580
|
+
gravity: 9.8,
|
|
1581
|
+
gravityScale: 0.15,
|
|
1582
|
+
fadeOut: true,
|
|
1583
|
+
angularVelocityMin: new THREE2.Vector3(-Math.PI, -Math.PI, -Math.PI),
|
|
1584
|
+
angularVelocityMax: new THREE2.Vector3(Math.PI, Math.PI, Math.PI),
|
|
1585
|
+
initialVelocityYBoost: 1,
|
|
1586
|
+
zVariationStrength: 0.3,
|
|
1587
|
+
materialFactory: () => new MeshBasicNodeMaterial2({
|
|
1588
|
+
transparent: true,
|
|
1589
|
+
alphaTest: 0.01,
|
|
1590
|
+
side: THREE2.DoubleSide,
|
|
1591
|
+
depthWrite: true
|
|
1592
|
+
})
|
|
1593
|
+
};
|
|
1594
|
+
|
|
1595
|
+
class ExplodingSpriteEffect {
|
|
1596
|
+
static baseMaterialCache = new Map;
|
|
1597
|
+
scene;
|
|
1598
|
+
resource;
|
|
1599
|
+
frameUvOffset;
|
|
1600
|
+
frameUvSize;
|
|
1601
|
+
spriteWorldTransform;
|
|
1602
|
+
params;
|
|
1603
|
+
instancedMesh;
|
|
1604
|
+
material;
|
|
1605
|
+
numParticles;
|
|
1606
|
+
uniformRefs;
|
|
1607
|
+
isActive = true;
|
|
1608
|
+
timeElapsedMs = 0;
|
|
1609
|
+
constructor(scene, resource, frameUvOffset, frameUvSize, spriteWorldTransform, userParams) {
|
|
1610
|
+
this.scene = scene;
|
|
1611
|
+
this.resource = resource;
|
|
1612
|
+
this.frameUvOffset = frameUvOffset;
|
|
1613
|
+
this.frameUvSize = frameUvSize;
|
|
1614
|
+
this.spriteWorldTransform = spriteWorldTransform;
|
|
1615
|
+
this.params = { ...DEFAULT_EXPLOSION_PARAMETERS, ...userParams };
|
|
1616
|
+
this.numParticles = this.params.numRows * this.params.numCols;
|
|
1617
|
+
const materialFactory = userParams?.materialFactory ?? DEFAULT_EXPLOSION_PARAMETERS.materialFactory;
|
|
1618
|
+
this._createGPUParticles(materialFactory);
|
|
1619
|
+
}
|
|
1620
|
+
_createGPUParticles(materialFactory) {
|
|
1621
|
+
if (this.numParticles === 0)
|
|
1622
|
+
return;
|
|
1623
|
+
const particleUnitWidth = 1 / this.params.numCols;
|
|
1624
|
+
const particleUnitHeight = 1 / this.params.numRows;
|
|
1625
|
+
const poolKey = `${this.params.numRows}x${this.params.numCols}`;
|
|
1626
|
+
this._createGPUMaterial(materialFactory);
|
|
1627
|
+
this.instancedMesh = this.resource.meshPool.acquireMesh(poolKey, {
|
|
1628
|
+
geometry: () => {
|
|
1629
|
+
const geometry = new THREE2.PlaneGeometry(particleUnitWidth, particleUnitHeight);
|
|
1630
|
+
geometry.setAttribute("a_particleData", new THREE2.InstancedBufferAttribute(new Float32Array(this.numParticles * 4), 4));
|
|
1631
|
+
geometry.setAttribute("a_velocity", new THREE2.InstancedBufferAttribute(new Float32Array(this.numParticles * 4), 4));
|
|
1632
|
+
geometry.setAttribute("a_angularVel", new THREE2.InstancedBufferAttribute(new Float32Array(this.numParticles * 4), 4));
|
|
1633
|
+
geometry.setAttribute("a_uvOffset", new THREE2.InstancedBufferAttribute(new Float32Array(this.numParticles * 4), 4));
|
|
1634
|
+
return geometry;
|
|
1635
|
+
},
|
|
1636
|
+
material: this.material,
|
|
1637
|
+
maxInstances: this.numParticles,
|
|
1638
|
+
name: `ExplodingSprite_${poolKey}`
|
|
1639
|
+
});
|
|
1640
|
+
const particleData = this.instancedMesh.geometry.getAttribute("a_particleData").array;
|
|
1641
|
+
const velocityData = this.instancedMesh.geometry.getAttribute("a_velocity").array;
|
|
1642
|
+
const angularVelData = this.instancedMesh.geometry.getAttribute("a_angularVel").array;
|
|
1643
|
+
const uvOffsetData = this.instancedMesh.geometry.getAttribute("a_uvOffset").array;
|
|
1644
|
+
const spriteWorldCenter = new THREE2.Vector3().setFromMatrixPosition(this.spriteWorldTransform);
|
|
1645
|
+
let particleIndex = 0;
|
|
1646
|
+
for (let r = 0;r < this.params.numRows; r++) {
|
|
1647
|
+
for (let c = 0;c < this.params.numCols; c++) {
|
|
1648
|
+
const localParticlePosX = (c + 0.5) * particleUnitWidth - 0.5;
|
|
1649
|
+
const localParticlePosY = (r + 0.5) * particleUnitHeight - 0.5;
|
|
1650
|
+
const initialLocalPosition = new THREE2.Vector3(localParticlePosX, localParticlePosY, 0);
|
|
1651
|
+
const worldPosition = initialLocalPosition.clone().applyMatrix4(this.spriteWorldTransform);
|
|
1652
|
+
let velocityDir = worldPosition.clone().sub(spriteWorldCenter);
|
|
1653
|
+
if (velocityDir.lengthSq() < 0.0001) {
|
|
1654
|
+
velocityDir.set(Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5);
|
|
1655
|
+
}
|
|
1656
|
+
velocityDir.normalize();
|
|
1657
|
+
const strengthVariationRange = this.params.strengthVariation;
|
|
1658
|
+
const minStrengthFactor = 1 - strengthVariationRange * 0.5;
|
|
1659
|
+
const maxStrengthFactor = 1 + strengthVariationRange * 0.5;
|
|
1660
|
+
const strengthFactor = minStrengthFactor + Math.random() * (maxStrengthFactor - minStrengthFactor);
|
|
1661
|
+
const strength = this.params.strength * strengthFactor * 0.1;
|
|
1662
|
+
const velocity = velocityDir.multiplyScalar(strength);
|
|
1663
|
+
if (Math.abs(this.spriteWorldTransform.elements[10]) < 0.1 && Math.abs(this.spriteWorldTransform.elements[11]) < 0.1) {
|
|
1664
|
+
velocity.z += (Math.random() - 0.5) * strength * this.params.zVariationStrength;
|
|
1665
|
+
}
|
|
1666
|
+
velocity.y += this.params.strength * this.params.initialVelocityYBoost * Math.random();
|
|
1667
|
+
const angularVelocity = new THREE2.Vector3(THREE2.MathUtils.randFloat(this.params.angularVelocityMin.x, this.params.angularVelocityMax.x), THREE2.MathUtils.randFloat(this.params.angularVelocityMin.y, this.params.angularVelocityMax.y), THREE2.MathUtils.randFloat(this.params.angularVelocityMin.z, this.params.angularVelocityMax.z));
|
|
1668
|
+
const lifeVariation = 0.8 + Math.random() * 0.4;
|
|
1669
|
+
const randomSeed = Math.random();
|
|
1670
|
+
const u0 = this.frameUvOffset.x + c / this.params.numCols * this.frameUvSize.x;
|
|
1671
|
+
const v0 = this.frameUvOffset.y + r / this.params.numRows * this.frameUvSize.y;
|
|
1672
|
+
const uSize = this.frameUvSize.x / this.params.numCols;
|
|
1673
|
+
const vSize = this.frameUvSize.y / this.params.numRows;
|
|
1674
|
+
const baseIndex = particleIndex * 4;
|
|
1675
|
+
particleData[baseIndex] = localParticlePosX;
|
|
1676
|
+
particleData[baseIndex + 1] = localParticlePosY;
|
|
1677
|
+
particleData[baseIndex + 2] = randomSeed;
|
|
1678
|
+
particleData[baseIndex + 3] = lifeVariation;
|
|
1679
|
+
velocityData[baseIndex] = velocity.x;
|
|
1680
|
+
velocityData[baseIndex + 1] = velocity.y;
|
|
1681
|
+
velocityData[baseIndex + 2] = velocity.z;
|
|
1682
|
+
velocityData[baseIndex + 3] = 0;
|
|
1683
|
+
angularVelData[baseIndex] = angularVelocity.x;
|
|
1684
|
+
angularVelData[baseIndex + 1] = angularVelocity.y;
|
|
1685
|
+
angularVelData[baseIndex + 2] = angularVelocity.z;
|
|
1686
|
+
angularVelData[baseIndex + 3] = 0;
|
|
1687
|
+
uvOffsetData[baseIndex] = u0;
|
|
1688
|
+
uvOffsetData[baseIndex + 1] = v0;
|
|
1689
|
+
uvOffsetData[baseIndex + 2] = uSize;
|
|
1690
|
+
uvOffsetData[baseIndex + 3] = vSize;
|
|
1691
|
+
particleIndex++;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
this.instancedMesh.onBeforeRender = () => {
|
|
1695
|
+
this.uniformRefs.time.value = this.timeElapsedMs / 1000;
|
|
1696
|
+
};
|
|
1697
|
+
this.timeElapsedMs = 0;
|
|
1698
|
+
this.instancedMesh.geometry.getAttribute("a_particleData").needsUpdate = true;
|
|
1699
|
+
this.instancedMesh.geometry.getAttribute("a_velocity").needsUpdate = true;
|
|
1700
|
+
this.instancedMesh.geometry.getAttribute("a_angularVel").needsUpdate = true;
|
|
1701
|
+
this.instancedMesh.geometry.getAttribute("a_uvOffset").needsUpdate = true;
|
|
1702
|
+
this.instancedMesh.frustumCulled = false;
|
|
1703
|
+
for (let i = 0;i < this.numParticles; i++) {
|
|
1704
|
+
this.instancedMesh.setMatrixAt(i, this.spriteWorldTransform);
|
|
1705
|
+
}
|
|
1706
|
+
this.instancedMesh.instanceMatrix.needsUpdate = true;
|
|
1707
|
+
this.scene.add(this.instancedMesh);
|
|
1708
|
+
}
|
|
1709
|
+
_createGPUMaterial(materialFactory) {
|
|
1710
|
+
const key = `${this.resource.texture.uuid}_${this.params.numRows}x${this.params.numCols}_${this.params.fadeOut ? 1 : 0}`;
|
|
1711
|
+
let template = ExplodingSpriteEffect.baseMaterialCache.get(key);
|
|
1712
|
+
if (!template) {
|
|
1713
|
+
template = ExplodingSpriteEffect._buildTemplateMaterial(this.resource.texture, this.params, materialFactory);
|
|
1714
|
+
ExplodingSpriteEffect.baseMaterialCache.set(key, template);
|
|
1715
|
+
}
|
|
1716
|
+
this.material = template;
|
|
1717
|
+
this.uniformRefs = template.userData.uniformRefs;
|
|
1718
|
+
}
|
|
1719
|
+
static _buildTemplateMaterial(texture, params, materialFactory) {
|
|
1720
|
+
const timeUniformNode = uniform2(0);
|
|
1721
|
+
timeUniformNode.name = "timeUniform";
|
|
1722
|
+
const durationUniformNode = uniform2(params.durationMs / 1000);
|
|
1723
|
+
durationUniformNode.name = "durationUniform";
|
|
1724
|
+
const gravityUniformNode = uniform2(params.gravity * params.gravityScale);
|
|
1725
|
+
gravityUniformNode.name = "gravityUniform";
|
|
1726
|
+
const a_particleData = attribute("a_particleData", "vec4");
|
|
1727
|
+
const a_velocity = attribute("a_velocity", "vec4");
|
|
1728
|
+
const a_angularVel = attribute("a_angularVel", "vec4");
|
|
1729
|
+
const a_uvOffset = attribute("a_uvOffset", "vec4");
|
|
1730
|
+
const localPos = vec22(a_particleData.x, a_particleData.y);
|
|
1731
|
+
const lifeVariation = a_particleData.w;
|
|
1732
|
+
const initialVelocity = vec3(a_velocity.x, a_velocity.y, a_velocity.z);
|
|
1733
|
+
const angularVelocity = vec3(a_angularVel.x, a_angularVel.y, a_angularVel.z);
|
|
1734
|
+
const uvOffset = vec22(a_uvOffset.x, a_uvOffset.y);
|
|
1735
|
+
const uvSize = vec22(a_uvOffset.z, a_uvOffset.w);
|
|
1736
|
+
const particleLifetime = durationUniformNode.mul(lifeVariation);
|
|
1737
|
+
const normalizedTime = timeUniformNode.div(particleLifetime);
|
|
1738
|
+
const isAlive = step(normalizedTime, float2(1));
|
|
1739
|
+
const deltaTime = timeUniformNode;
|
|
1740
|
+
const gravity = vec3(float2(0), gravityUniformNode.negate(), float2(0));
|
|
1741
|
+
const velocityContribution = initialVelocity.mul(deltaTime);
|
|
1742
|
+
const gravityContribution = gravity.mul(deltaTime).mul(deltaTime).mul(float2(0.5));
|
|
1743
|
+
const positionOffset = velocityContribution.add(gravityContribution);
|
|
1744
|
+
const rotationAmount = angularVelocity.mul(deltaTime);
|
|
1745
|
+
const cosX = cos(rotationAmount.x);
|
|
1746
|
+
const sinX = sin(rotationAmount.x);
|
|
1747
|
+
const cosY = cos(rotationAmount.y);
|
|
1748
|
+
const sinY = sin(rotationAmount.y);
|
|
1749
|
+
const cosZ = cos(rotationAmount.z);
|
|
1750
|
+
const sinZ = sin(rotationAmount.z);
|
|
1751
|
+
const rotationMatrix = mat3(cosY.mul(cosZ), cosY.mul(sinZ).negate(), sinY, sinX.mul(sinY).mul(cosZ).add(cosX.mul(sinZ)), sinX.mul(sinY).mul(sinZ).negate().add(cosX.mul(cosZ)), sinX.mul(cosY).negate(), cosX.mul(sinY).mul(cosZ).negate().add(sinX.mul(sinZ)), cosX.mul(sinY).mul(sinZ).add(sinX.mul(cosZ)), cosX.mul(cosY));
|
|
1752
|
+
const rotatedVertexPosition = rotationMatrix.mul(positionLocal);
|
|
1753
|
+
const finalOffset = vec3(localPos.x, localPos.y, float2(0)).add(positionOffset);
|
|
1754
|
+
let opacity = float2(1);
|
|
1755
|
+
if (params.fadeOut) {
|
|
1756
|
+
const fadeStart = float2(0.7);
|
|
1757
|
+
const fadeProgress = max(float2(0), normalizedTime.sub(fadeStart).div(float2(1).sub(fadeStart)));
|
|
1758
|
+
opacity = float2(1).sub(fadeProgress);
|
|
1759
|
+
}
|
|
1760
|
+
opacity = opacity.mul(isAlive);
|
|
1761
|
+
const baseUV = uv2();
|
|
1762
|
+
const finalUV = baseUV.mul(uvSize).add(uvOffset);
|
|
1763
|
+
const mapNode = tslTexture2(texture);
|
|
1764
|
+
const sampledColor = mapNode.sample(finalUV);
|
|
1765
|
+
const material = materialFactory();
|
|
1766
|
+
const finalColor = vec4(sampledColor.rgb, sampledColor.a.mul(opacity));
|
|
1767
|
+
material.colorNode = finalColor;
|
|
1768
|
+
material.positionNode = rotatedVertexPosition.add(finalOffset);
|
|
1769
|
+
material.userData.uniformRefs = {
|
|
1770
|
+
time: timeUniformNode,
|
|
1771
|
+
duration: durationUniformNode,
|
|
1772
|
+
gravity: gravityUniformNode
|
|
1773
|
+
};
|
|
1774
|
+
return material;
|
|
1775
|
+
}
|
|
1776
|
+
update(deltaTimeMs) {
|
|
1777
|
+
if (!this.isActive)
|
|
1778
|
+
return;
|
|
1779
|
+
this.timeElapsedMs += deltaTimeMs;
|
|
1780
|
+
if (this.timeElapsedMs >= this.params.durationMs) {
|
|
1781
|
+
this.dispose();
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
dispose() {
|
|
1785
|
+
if (!this.isActive)
|
|
1786
|
+
return;
|
|
1787
|
+
this.isActive = false;
|
|
1788
|
+
if (this.instancedMesh) {
|
|
1789
|
+
this.scene.remove(this.instancedMesh);
|
|
1790
|
+
const poolKey = `${this.params.numRows}x${this.params.numCols}`;
|
|
1791
|
+
this.resource.meshPool.releaseMesh(poolKey, this.instancedMesh);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
class ExplosionManager {
|
|
1797
|
+
scene;
|
|
1798
|
+
activeExplosions = [];
|
|
1799
|
+
constructor(scene) {
|
|
1800
|
+
this.scene = scene;
|
|
1801
|
+
}
|
|
1802
|
+
fillPool(resource, count, params = {}) {
|
|
1803
|
+
const effectParams = { ...DEFAULT_EXPLOSION_PARAMETERS, ...params };
|
|
1804
|
+
const poolKey = `${effectParams.numRows}x${effectParams.numCols}`;
|
|
1805
|
+
const particleUnitWidth = 1 / effectParams.numCols;
|
|
1806
|
+
const particleUnitHeight = 1 / effectParams.numRows;
|
|
1807
|
+
const numParticles = effectParams.numRows * effectParams.numCols;
|
|
1808
|
+
const materialFactory = params.materialFactory ?? DEFAULT_EXPLOSION_PARAMETERS.materialFactory;
|
|
1809
|
+
const material = ExplodingSpriteEffect._buildTemplateMaterial(resource.texture, effectParams, materialFactory);
|
|
1810
|
+
resource.meshPool.fill(poolKey, {
|
|
1811
|
+
geometry: () => {
|
|
1812
|
+
const geometry = new THREE2.PlaneGeometry(particleUnitWidth, particleUnitHeight);
|
|
1813
|
+
const particleData = new Float32Array(numParticles * 4);
|
|
1814
|
+
const velocityData = new Float32Array(numParticles * 4);
|
|
1815
|
+
const angularVelData = new Float32Array(numParticles * 4);
|
|
1816
|
+
const uvOffsetData = new Float32Array(numParticles * 4);
|
|
1817
|
+
const particleDataAttribute = new THREE2.InstancedBufferAttribute(particleData, 4);
|
|
1818
|
+
const velocityAttribute = new THREE2.InstancedBufferAttribute(velocityData, 4);
|
|
1819
|
+
const angularVelAttribute = new THREE2.InstancedBufferAttribute(angularVelData, 4);
|
|
1820
|
+
const uvOffsetAttribute = new THREE2.InstancedBufferAttribute(uvOffsetData, 4);
|
|
1821
|
+
geometry.setAttribute("a_particleData", particleDataAttribute);
|
|
1822
|
+
geometry.setAttribute("a_velocity", velocityAttribute);
|
|
1823
|
+
geometry.setAttribute("a_angularVel", angularVelAttribute);
|
|
1824
|
+
geometry.setAttribute("a_uvOffset", uvOffsetAttribute);
|
|
1825
|
+
particleDataAttribute.needsUpdate = true;
|
|
1826
|
+
velocityAttribute.needsUpdate = true;
|
|
1827
|
+
angularVelAttribute.needsUpdate = true;
|
|
1828
|
+
uvOffsetAttribute.needsUpdate = true;
|
|
1829
|
+
return geometry;
|
|
1830
|
+
},
|
|
1831
|
+
material,
|
|
1832
|
+
maxInstances: numParticles,
|
|
1833
|
+
name: `ExplodingSprite_${poolKey}`
|
|
1834
|
+
}, count);
|
|
1835
|
+
}
|
|
1836
|
+
_createEffectCreationData(sprite) {
|
|
1837
|
+
const animState = sprite.currentAnimation.state;
|
|
1838
|
+
const resource = sprite.currentAnimation.getResource();
|
|
1839
|
+
const currentAbsoluteFrame = animState.animFrameOffset + sprite.currentAnimation.currentLocalFrame;
|
|
1840
|
+
const frameUOffset = currentAbsoluteFrame * resource.uvTileSize.x;
|
|
1841
|
+
return {
|
|
1842
|
+
resource,
|
|
1843
|
+
frameUvOffset: new THREE2.Vector2(frameUOffset, 0),
|
|
1844
|
+
frameUvSize: resource.uvTileSize.clone(),
|
|
1845
|
+
spriteWorldTransform: sprite.getWorldTransform()
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
createExplosionForSprite(spriteToExplode, userParams) {
|
|
1849
|
+
const effectCreationData = this._createEffectCreationData(spriteToExplode);
|
|
1850
|
+
const definition = spriteToExplode.definition;
|
|
1851
|
+
const transform = spriteToExplode.currentTransform;
|
|
1852
|
+
let spriteRecreationData = {
|
|
1853
|
+
definition,
|
|
1854
|
+
currentTransform: transform
|
|
1855
|
+
};
|
|
1856
|
+
spriteToExplode.destroy();
|
|
1857
|
+
const effect = new ExplodingSpriteEffect(this.scene, effectCreationData.resource, effectCreationData.frameUvOffset, effectCreationData.frameUvSize, effectCreationData.spriteWorldTransform, userParams);
|
|
1858
|
+
this.activeExplosions.push(effect);
|
|
1859
|
+
const handle = {
|
|
1860
|
+
effect,
|
|
1861
|
+
recreationData: spriteRecreationData,
|
|
1862
|
+
hasBeenRestored: false,
|
|
1863
|
+
restoreSprite: async (spriteAnimator) => {
|
|
1864
|
+
if (handle.hasBeenRestored) {
|
|
1865
|
+
return null;
|
|
1866
|
+
}
|
|
1867
|
+
handle.effect.dispose();
|
|
1868
|
+
const newSprite = await spriteAnimator.createSprite(handle.recreationData.definition);
|
|
1869
|
+
const currentSpriteTransform = handle.recreationData.currentTransform;
|
|
1870
|
+
newSprite.setTransform(currentSpriteTransform.position, currentSpriteTransform.quaternion, currentSpriteTransform.scale);
|
|
1871
|
+
handle.hasBeenRestored = true;
|
|
1872
|
+
return newSprite;
|
|
1873
|
+
}
|
|
1874
|
+
};
|
|
1875
|
+
return handle;
|
|
1876
|
+
}
|
|
1877
|
+
update(deltaTimeMs) {
|
|
1878
|
+
for (let i = this.activeExplosions.length - 1;i >= 0; i--) {
|
|
1879
|
+
const explosion = this.activeExplosions[i];
|
|
1880
|
+
explosion.update(deltaTimeMs);
|
|
1881
|
+
if (!explosion.isActive) {
|
|
1882
|
+
this.activeExplosions.splice(i, 1);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
disposeAll() {
|
|
1887
|
+
this.activeExplosions.forEach((exp) => exp.dispose());
|
|
1888
|
+
this.activeExplosions = [];
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
// src/3d/animation/SpriteParticleGenerator.ts
|
|
1892
|
+
import * as THREE3 from "three";
|
|
1893
|
+
import {
|
|
1894
|
+
uniform as uniform3,
|
|
1895
|
+
texture as tslTexture3,
|
|
1896
|
+
uv as uv3,
|
|
1897
|
+
float as float3,
|
|
1898
|
+
vec2 as vec23,
|
|
1899
|
+
vec3 as vec32,
|
|
1900
|
+
vec4 as vec42,
|
|
1901
|
+
bufferAttribute as bufferAttribute2,
|
|
1902
|
+
step as step2,
|
|
1903
|
+
max as max2,
|
|
1904
|
+
sin as sin2,
|
|
1905
|
+
cos as cos2,
|
|
1906
|
+
positionLocal as positionLocal2,
|
|
1907
|
+
mat3 as mat32,
|
|
1908
|
+
mix as mix2,
|
|
1909
|
+
floor
|
|
1910
|
+
} from "three/tsl";
|
|
1911
|
+
import { MeshBasicNodeMaterial as MeshBasicNodeMaterial3 } from "three/webgpu";
|
|
1912
|
+
|
|
1913
|
+
class SpriteParticleGenerator {
|
|
1914
|
+
scene;
|
|
1915
|
+
baseConfig;
|
|
1916
|
+
autoSpawnConfig = null;
|
|
1917
|
+
_currentOriginIndex = 0;
|
|
1918
|
+
instanceManager = null;
|
|
1919
|
+
material = null;
|
|
1920
|
+
texture = null;
|
|
1921
|
+
particleDataAttribute = null;
|
|
1922
|
+
velocityAttribute = null;
|
|
1923
|
+
angularVelAttribute = null;
|
|
1924
|
+
scaleDataAttribute = null;
|
|
1925
|
+
timeUniform;
|
|
1926
|
+
gravityUniform;
|
|
1927
|
+
animationUniform;
|
|
1928
|
+
sheetNumFramesUniform;
|
|
1929
|
+
particleSlots = [];
|
|
1930
|
+
currentTime = 0;
|
|
1931
|
+
maxParticles;
|
|
1932
|
+
isInitialized = false;
|
|
1933
|
+
constructor(scene, initialBaseConfig) {
|
|
1934
|
+
this.scene = scene;
|
|
1935
|
+
this.baseConfig = { ...initialBaseConfig };
|
|
1936
|
+
this.maxParticles = this.baseConfig.maxParticles;
|
|
1937
|
+
if (!this.baseConfig.resource) {
|
|
1938
|
+
throw new Error("[SpriteParticleGenerator] resource is mandatory in initialBaseConfig.");
|
|
1939
|
+
}
|
|
1940
|
+
this.timeUniform = uniform3(0);
|
|
1941
|
+
this.gravityUniform = uniform3(this.baseConfig.gravity || new THREE3.Vector3(0, -9.8, 0));
|
|
1942
|
+
this.animationUniform = uniform3(new THREE3.Vector4);
|
|
1943
|
+
this.sheetNumFramesUniform = uniform3(1);
|
|
1944
|
+
}
|
|
1945
|
+
async _ensureInitialized() {
|
|
1946
|
+
if (this.isInitialized)
|
|
1947
|
+
return;
|
|
1948
|
+
await this._initializeGPUParticleSystem();
|
|
1949
|
+
this.isInitialized = true;
|
|
1950
|
+
}
|
|
1951
|
+
async _initializeGPUParticleSystem() {
|
|
1952
|
+
const resource = this.baseConfig.resource;
|
|
1953
|
+
this.texture = resource.texture;
|
|
1954
|
+
const frameDuration = (this.baseConfig.frameDuration ?? 100) / 1000;
|
|
1955
|
+
const animNumFrames = this.baseConfig.animNumFrames ?? resource.sheetProperties.sheetNumFrames;
|
|
1956
|
+
const loop = this.baseConfig.loop ?? true ? 1 : 0;
|
|
1957
|
+
const animFrameOffset = this.baseConfig.animFrameOffset ?? 0;
|
|
1958
|
+
this.animationUniform.value.set(frameDuration, animNumFrames, loop, animFrameOffset);
|
|
1959
|
+
this.sheetNumFramesUniform.value = resource.sheetProperties.sheetNumFrames;
|
|
1960
|
+
const particleData = new Float32Array(this.maxParticles * 4);
|
|
1961
|
+
const velocityData = new Float32Array(this.maxParticles * 4);
|
|
1962
|
+
const angularVelData = new Float32Array(this.maxParticles * 4);
|
|
1963
|
+
const scaleData = new Float32Array(this.maxParticles * 4);
|
|
1964
|
+
this.particleDataAttribute = new THREE3.InstancedBufferAttribute(particleData, 4);
|
|
1965
|
+
this.velocityAttribute = new THREE3.InstancedBufferAttribute(velocityData, 4);
|
|
1966
|
+
this.angularVelAttribute = new THREE3.InstancedBufferAttribute(angularVelData, 4);
|
|
1967
|
+
this.scaleDataAttribute = new THREE3.InstancedBufferAttribute(scaleData, 4);
|
|
1968
|
+
this.particleDataAttribute.setUsage(THREE3.DynamicDrawUsage);
|
|
1969
|
+
this.velocityAttribute.setUsage(THREE3.DynamicDrawUsage);
|
|
1970
|
+
this.angularVelAttribute.setUsage(THREE3.DynamicDrawUsage);
|
|
1971
|
+
this.scaleDataAttribute.setUsage(THREE3.DynamicDrawUsage);
|
|
1972
|
+
for (let i = 0;i < this.maxParticles; i++) {
|
|
1973
|
+
this.particleSlots.push({ isActive: false, spawnTime: 0, lifespan: 0 });
|
|
1974
|
+
particleData[i * 4 + 3] = -1;
|
|
1975
|
+
}
|
|
1976
|
+
const frameAspectRatio = this.texture.image.width / resource.sheetProperties.sheetNumFrames / this.texture.image.height;
|
|
1977
|
+
const scale = this.baseConfig.scale ?? 1;
|
|
1978
|
+
const geometry = new THREE3.PlaneGeometry(scale * frameAspectRatio, scale);
|
|
1979
|
+
geometry.setAttribute("a_particleData", this.particleDataAttribute);
|
|
1980
|
+
geometry.setAttribute("a_velocity", this.velocityAttribute);
|
|
1981
|
+
geometry.setAttribute("a_angularVel", this.angularVelAttribute);
|
|
1982
|
+
geometry.setAttribute("a_scaleData", this.scaleDataAttribute);
|
|
1983
|
+
const materialFactory = this.baseConfig.materialFactory ?? (() => new MeshBasicNodeMaterial3({
|
|
1984
|
+
transparent: true,
|
|
1985
|
+
alphaTest: 0.01,
|
|
1986
|
+
side: THREE3.DoubleSide,
|
|
1987
|
+
depthWrite: this.baseConfig.depthWrite ?? false
|
|
1988
|
+
}));
|
|
1989
|
+
const material = this._createGPUMaterial(materialFactory);
|
|
1990
|
+
this.instanceManager = resource.createInstanceManager(geometry, material, {
|
|
1991
|
+
maxInstances: this.maxParticles,
|
|
1992
|
+
renderOrder: this.baseConfig.renderOrder ?? 0,
|
|
1993
|
+
depthWrite: this.baseConfig.depthWrite ?? true,
|
|
1994
|
+
name: `SpriteParticleGenerator_${resource.sheetProperties.imagePath.replace(/[^a-zA-Z0-9_]/g, "_")}`,
|
|
1995
|
+
matrix: new THREE3.Matrix4
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
_createGPUMaterial(materialFactory) {
|
|
1999
|
+
const a_particleData = bufferAttribute2(this.particleDataAttribute);
|
|
2000
|
+
const a_velocity = bufferAttribute2(this.velocityAttribute);
|
|
2001
|
+
const a_angularVel = bufferAttribute2(this.angularVelAttribute);
|
|
2002
|
+
const a_scaleData = bufferAttribute2(this.scaleDataAttribute);
|
|
2003
|
+
const origin = vec32(a_particleData.x, a_particleData.y, a_particleData.z);
|
|
2004
|
+
const spawnTime = a_particleData.w;
|
|
2005
|
+
const initialVelocity = vec32(a_velocity.x, a_velocity.y, a_velocity.z);
|
|
2006
|
+
const gravityFactor = a_velocity.w;
|
|
2007
|
+
const angularVelocity = vec32(a_angularVel.x, a_angularVel.y, a_angularVel.z);
|
|
2008
|
+
const lifespan = a_angularVel.w;
|
|
2009
|
+
const initialScale = a_scaleData.x;
|
|
2010
|
+
const scaleMin = a_scaleData.y;
|
|
2011
|
+
const scaleMax = a_scaleData.z;
|
|
2012
|
+
const randomSeed = a_scaleData.w;
|
|
2013
|
+
const age = this.timeUniform.sub(spawnTime);
|
|
2014
|
+
const normalizedAge = age.div(lifespan);
|
|
2015
|
+
const isAlive = step2(float3(0), spawnTime).mul(step2(normalizedAge, float3(1)));
|
|
2016
|
+
const gravity = this.gravityUniform.mul(gravityFactor);
|
|
2017
|
+
const velocityContribution = initialVelocity.mul(age);
|
|
2018
|
+
const gravityContribution = gravity.mul(age).mul(age).mul(float3(0.5));
|
|
2019
|
+
const currentPosition = origin.add(velocityContribution).add(gravityContribution);
|
|
2020
|
+
const rotationAmount = angularVelocity.mul(age);
|
|
2021
|
+
const cosX = cos2(rotationAmount.x);
|
|
2022
|
+
const sinX = sin2(rotationAmount.x);
|
|
2023
|
+
const cosY = cos2(rotationAmount.y);
|
|
2024
|
+
const sinY = sin2(rotationAmount.y);
|
|
2025
|
+
const cosZ = cos2(rotationAmount.z);
|
|
2026
|
+
const sinZ = sin2(rotationAmount.z);
|
|
2027
|
+
const rotationMatrix = mat32(cosY.mul(cosZ), cosY.mul(sinZ).negate(), sinY, sinX.mul(sinY).mul(cosZ).add(cosX.mul(sinZ)), sinX.mul(sinY).mul(sinZ).negate().add(cosX.mul(cosZ)), sinX.mul(cosY).negate(), cosX.mul(sinY).mul(cosZ).negate().add(sinX.mul(sinZ)), cosX.mul(sinY).mul(sinZ).add(sinX.mul(cosZ)), cosX.mul(cosY));
|
|
2028
|
+
const rotatedVertexPosition = rotationMatrix.mul(positionLocal2);
|
|
2029
|
+
let currentScale = initialScale;
|
|
2030
|
+
if (this.baseConfig.scaleOverLifeMinMax) {
|
|
2031
|
+
const scaleMultiplier = mix2(scaleMin, scaleMax, normalizedAge);
|
|
2032
|
+
currentScale = initialScale.mul(scaleMultiplier);
|
|
2033
|
+
}
|
|
2034
|
+
const scaledPosition = rotatedVertexPosition.mul(currentScale);
|
|
2035
|
+
const finalPosition = scaledPosition.add(currentPosition);
|
|
2036
|
+
let opacity = float3(1);
|
|
2037
|
+
if (this.baseConfig.fadeOut) {
|
|
2038
|
+
const fadeStart = float3(0.7);
|
|
2039
|
+
const fadeProgress = max2(float3(0), normalizedAge.sub(fadeStart).div(float3(1).sub(fadeStart)));
|
|
2040
|
+
opacity = float3(1).sub(fadeProgress);
|
|
2041
|
+
}
|
|
2042
|
+
opacity = opacity.mul(isAlive);
|
|
2043
|
+
const frameDuration = this.animationUniform.x;
|
|
2044
|
+
const animNumFrames = this.animationUniform.y;
|
|
2045
|
+
const loopFlag = this.animationUniform.z;
|
|
2046
|
+
const animFrameOffset = this.animationUniform.w;
|
|
2047
|
+
const frameFloat = age.div(frameDuration);
|
|
2048
|
+
const rawFrameIndex = floor(frameFloat);
|
|
2049
|
+
const maxFrame = animNumFrames.sub(float3(1));
|
|
2050
|
+
const clampedFrame = max2(float3(0), rawFrameIndex).min(maxFrame);
|
|
2051
|
+
const loopedFrame = rawFrameIndex.mod(animNumFrames);
|
|
2052
|
+
const finalLocalFrame = mix2(clampedFrame, loopedFrame, loopFlag);
|
|
2053
|
+
const frameIndex = animFrameOffset.add(finalLocalFrame);
|
|
2054
|
+
const uvTileWidth = float3(1).div(this.sheetNumFramesUniform);
|
|
2055
|
+
const uvOffset = vec23(frameIndex.mul(uvTileWidth), float3(0));
|
|
2056
|
+
const uvSize = vec23(uvTileWidth, float3(1));
|
|
2057
|
+
const baseUV = uv3();
|
|
2058
|
+
const finalUV = baseUV.mul(uvSize).add(uvOffset);
|
|
2059
|
+
const mapNode = tslTexture3(this.texture);
|
|
2060
|
+
const sampledColor = mapNode.sample(finalUV);
|
|
2061
|
+
this.material = materialFactory();
|
|
2062
|
+
const finalColor = vec42(sampledColor.rgb, sampledColor.a.mul(opacity));
|
|
2063
|
+
this.material.colorNode = finalColor;
|
|
2064
|
+
this.material.positionNode = finalPosition;
|
|
2065
|
+
return this.material;
|
|
2066
|
+
}
|
|
2067
|
+
_resolveCurrentOrigin(originsArray) {
|
|
2068
|
+
const currentOrigin = originsArray[this._currentOriginIndex];
|
|
2069
|
+
this._currentOriginIndex = (this._currentOriginIndex + 1) % originsArray.length;
|
|
2070
|
+
return currentOrigin;
|
|
2071
|
+
}
|
|
2072
|
+
getActiveParticleCount() {
|
|
2073
|
+
return this.particleSlots.filter((slot) => slot.isActive).length;
|
|
2074
|
+
}
|
|
2075
|
+
_resolveSpawnRadius(spawnRadius) {
|
|
2076
|
+
return typeof spawnRadius === "number" ? new THREE3.Vector3(spawnRadius, spawnRadius, spawnRadius) : spawnRadius;
|
|
2077
|
+
}
|
|
2078
|
+
_spawnParticle(effectiveParams, spawnRadiusVec) {
|
|
2079
|
+
if (!this.instanceManager?.hasFreeIndices)
|
|
2080
|
+
return;
|
|
2081
|
+
const index = this.instanceManager.acquireInstanceSlot();
|
|
2082
|
+
const particleOrigin = this._resolveCurrentOrigin(effectiveParams.origins);
|
|
2083
|
+
const spawnOffset = new THREE3.Vector3((Math.random() - 0.5) * 2 * spawnRadiusVec.x, (Math.random() - 0.5) * 2 * spawnRadiusVec.y, (Math.random() - 0.5) * 2 * spawnRadiusVec.z);
|
|
2084
|
+
const initialPosition = new THREE3.Vector3().copy(particleOrigin).add(spawnOffset);
|
|
2085
|
+
const velocity = new THREE3.Vector3(THREE3.MathUtils.randFloat(effectiveParams.initialVelocityMin.x, effectiveParams.initialVelocityMax.x), THREE3.MathUtils.randFloat(effectiveParams.initialVelocityMin.y, effectiveParams.initialVelocityMax.y), THREE3.MathUtils.randFloat(effectiveParams.initialVelocityMin.z, effectiveParams.initialVelocityMax.z));
|
|
2086
|
+
const angularVelocity = new THREE3.Vector3(THREE3.MathUtils.randFloat(effectiveParams.angularVelocityMin.x, effectiveParams.angularVelocityMax.x), THREE3.MathUtils.randFloat(effectiveParams.angularVelocityMin.y, effectiveParams.angularVelocityMax.y), THREE3.MathUtils.randFloat(effectiveParams.angularVelocityMin.z, effectiveParams.angularVelocityMax.z));
|
|
2087
|
+
const lifespan = THREE3.MathUtils.randFloat(effectiveParams.lifetimeMsMin, effectiveParams.lifetimeMsMax) / 1000;
|
|
2088
|
+
let gravityFactor = 1;
|
|
2089
|
+
if (effectiveParams.randomGravityFactorMinMax) {
|
|
2090
|
+
gravityFactor = THREE3.MathUtils.randFloat(effectiveParams.randomGravityFactorMinMax.x, effectiveParams.randomGravityFactorMinMax.y);
|
|
2091
|
+
}
|
|
2092
|
+
const initialScale = effectiveParams.scale ?? 1;
|
|
2093
|
+
let scaleMin = initialScale;
|
|
2094
|
+
let scaleMax = initialScale;
|
|
2095
|
+
if (effectiveParams.scaleOverLifeMinMax) {
|
|
2096
|
+
scaleMin = initialScale * effectiveParams.scaleOverLifeMinMax.x;
|
|
2097
|
+
scaleMax = initialScale * effectiveParams.scaleOverLifeMinMax.y;
|
|
2098
|
+
}
|
|
2099
|
+
this.particleDataAttribute.setXYZW(index, initialPosition.x, initialPosition.y, initialPosition.z, this.currentTime);
|
|
2100
|
+
this.velocityAttribute.setXYZW(index, velocity.x, velocity.y, velocity.z, gravityFactor);
|
|
2101
|
+
this.angularVelAttribute.setXYZW(index, angularVelocity.x, angularVelocity.y, angularVelocity.z, lifespan);
|
|
2102
|
+
this.scaleDataAttribute.setXYZW(index, initialScale, scaleMin, scaleMax, Math.random());
|
|
2103
|
+
this.particleSlots[index] = {
|
|
2104
|
+
isActive: true,
|
|
2105
|
+
spawnTime: this.currentTime,
|
|
2106
|
+
lifespan
|
|
2107
|
+
};
|
|
2108
|
+
this.particleDataAttribute.needsUpdate = true;
|
|
2109
|
+
this.velocityAttribute.needsUpdate = true;
|
|
2110
|
+
this.angularVelAttribute.needsUpdate = true;
|
|
2111
|
+
this.scaleDataAttribute.needsUpdate = true;
|
|
2112
|
+
}
|
|
2113
|
+
async spawnParticles(count, overrides = {}) {
|
|
2114
|
+
await this._ensureInitialized();
|
|
2115
|
+
if (count <= 0)
|
|
2116
|
+
return;
|
|
2117
|
+
const finalParams = {
|
|
2118
|
+
...this.baseConfig,
|
|
2119
|
+
...overrides
|
|
2120
|
+
};
|
|
2121
|
+
const spawnRadiusVec = this._resolveSpawnRadius(finalParams.spawnRadius);
|
|
2122
|
+
for (let i = 0;i < count; i++) {
|
|
2123
|
+
this._spawnParticle(finalParams, spawnRadiusVec);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
setAutoSpawn(ratePerSecond, autoSpawnParamOverrides = {}) {
|
|
2127
|
+
if (ratePerSecond <= 0) {
|
|
2128
|
+
this.stopAutoSpawn();
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
const originalOverridesToStore = Object.keys(autoSpawnParamOverrides).length > 0 ? { ...autoSpawnParamOverrides } : undefined;
|
|
2132
|
+
this.autoSpawnConfig = {
|
|
2133
|
+
resolvedParams: { ...this.baseConfig, ...autoSpawnParamOverrides },
|
|
2134
|
+
originalOverrides: originalOverridesToStore,
|
|
2135
|
+
ratePerSecond,
|
|
2136
|
+
accumulator: 0
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
hasAutoSpawn() {
|
|
2140
|
+
return this.autoSpawnConfig !== null;
|
|
2141
|
+
}
|
|
2142
|
+
stopAutoSpawn() {
|
|
2143
|
+
this.autoSpawnConfig = null;
|
|
2144
|
+
}
|
|
2145
|
+
async update(deltaTimeMs) {
|
|
2146
|
+
await this._ensureInitialized();
|
|
2147
|
+
this.currentTime += deltaTimeMs / 1000;
|
|
2148
|
+
this.timeUniform.value = this.currentTime;
|
|
2149
|
+
if (this.autoSpawnConfig) {
|
|
2150
|
+
this.autoSpawnConfig.accumulator += deltaTimeMs;
|
|
2151
|
+
const particlesToSpawnThisFrame = Math.floor(this.autoSpawnConfig.accumulator * (this.autoSpawnConfig.ratePerSecond / 1000));
|
|
2152
|
+
if (particlesToSpawnThisFrame > 0) {
|
|
2153
|
+
const spawnRadiusVec = this._resolveSpawnRadius(this.autoSpawnConfig.resolvedParams.spawnRadius);
|
|
2154
|
+
for (let i = 0;i < particlesToSpawnThisFrame; i++) {
|
|
2155
|
+
this._spawnParticle(this.autoSpawnConfig.resolvedParams, spawnRadiusVec);
|
|
2156
|
+
}
|
|
2157
|
+
this.autoSpawnConfig.accumulator -= particlesToSpawnThisFrame * 1000 / this.autoSpawnConfig.ratePerSecond;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
for (let i = 0;i < this.particleSlots.length; i++) {
|
|
2161
|
+
const slot = this.particleSlots[i];
|
|
2162
|
+
if (slot.isActive && this.currentTime - slot.spawnTime >= slot.lifespan) {
|
|
2163
|
+
slot.isActive = false;
|
|
2164
|
+
this.particleDataAttribute.setW(i, -1);
|
|
2165
|
+
this.instanceManager.releaseInstanceSlot(i);
|
|
2166
|
+
this.particleDataAttribute.needsUpdate = true;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
dispose() {
|
|
2171
|
+
if (this.instanceManager) {
|
|
2172
|
+
this.instanceManager.dispose();
|
|
2173
|
+
this.material?.dispose();
|
|
2174
|
+
}
|
|
2175
|
+
this.stopAutoSpawn();
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
// src/3d/animation/PhysicsExplodingSpriteEffect.ts
|
|
2179
|
+
import * as THREE4 from "three";
|
|
2180
|
+
import { texture as tslTexture4, uv as uv4, vec2 as vec24, attribute as attribute2 } from "three/tsl";
|
|
2181
|
+
import { MeshBasicNodeMaterial as MeshBasicNodeMaterial4 } from "three/webgpu";
|
|
2182
|
+
var DEFAULT_PHYSICS_EXPLOSION_PARAMETERS = {
|
|
2183
|
+
numRows: 5,
|
|
2184
|
+
numCols: 5,
|
|
2185
|
+
durationMs: 3000,
|
|
2186
|
+
explosionForce: 25,
|
|
2187
|
+
forceVariation: 0.4,
|
|
2188
|
+
torqueStrength: 15,
|
|
2189
|
+
gravityScale: 1,
|
|
2190
|
+
fadeOut: true,
|
|
2191
|
+
linearDamping: 0.8,
|
|
2192
|
+
angularDamping: 0.5,
|
|
2193
|
+
restitution: 0.3,
|
|
2194
|
+
friction: 0.7,
|
|
2195
|
+
density: 1,
|
|
2196
|
+
materialFactory: () => new MeshBasicNodeMaterial4({
|
|
2197
|
+
transparent: true,
|
|
2198
|
+
alphaTest: 0.01,
|
|
2199
|
+
depthWrite: false
|
|
2200
|
+
})
|
|
2201
|
+
};
|
|
2202
|
+
|
|
2203
|
+
class PhysicsExplodingSpriteEffect {
|
|
2204
|
+
static materialCache = new Map;
|
|
2205
|
+
scene;
|
|
2206
|
+
physicsWorld;
|
|
2207
|
+
resource;
|
|
2208
|
+
frameUvOffset;
|
|
2209
|
+
frameUvSize;
|
|
2210
|
+
spriteWorldTransform;
|
|
2211
|
+
params;
|
|
2212
|
+
particles = [];
|
|
2213
|
+
numParticles;
|
|
2214
|
+
instancedMesh;
|
|
2215
|
+
material;
|
|
2216
|
+
uvOffsetAttribute;
|
|
2217
|
+
isActive = true;
|
|
2218
|
+
timeElapsedMs = 0;
|
|
2219
|
+
particleIdCounter = 0;
|
|
2220
|
+
constructor(scene, physicsWorld, resource, frameUvOffset, frameUvSize, spriteWorldTransform, userParams) {
|
|
2221
|
+
this.scene = scene;
|
|
2222
|
+
this.physicsWorld = physicsWorld;
|
|
2223
|
+
this.resource = resource;
|
|
2224
|
+
this.frameUvOffset = frameUvOffset;
|
|
2225
|
+
this.frameUvSize = frameUvSize;
|
|
2226
|
+
this.spriteWorldTransform = spriteWorldTransform;
|
|
2227
|
+
this.params = { ...DEFAULT_PHYSICS_EXPLOSION_PARAMETERS, ...userParams };
|
|
2228
|
+
this.numParticles = this.params.numRows * this.params.numCols;
|
|
2229
|
+
const materialFactory = userParams?.materialFactory ?? DEFAULT_PHYSICS_EXPLOSION_PARAMETERS.materialFactory;
|
|
2230
|
+
this._createPhysicsParticles(materialFactory);
|
|
2231
|
+
}
|
|
2232
|
+
_createPhysicsParticles(materialFactory) {
|
|
2233
|
+
if (this.numParticles === 0)
|
|
2234
|
+
return;
|
|
2235
|
+
const particleUnitWidth = 1 / this.params.numCols;
|
|
2236
|
+
const particleUnitHeight = 1 / this.params.numRows;
|
|
2237
|
+
const spriteWorldCenter = new THREE4.Vector3().setFromMatrixPosition(this.spriteWorldTransform);
|
|
2238
|
+
const spriteScale = new THREE4.Vector3().setFromMatrixScale(this.spriteWorldTransform);
|
|
2239
|
+
const avgScale = (spriteScale.x + spriteScale.y) * 0.5;
|
|
2240
|
+
const uvOffsetData = new Float32Array(this.numParticles * 4);
|
|
2241
|
+
let particleIndex = 0;
|
|
2242
|
+
for (let r = 0;r < this.params.numRows; r++) {
|
|
2243
|
+
for (let c = 0;c < this.params.numCols; c++) {
|
|
2244
|
+
const localParticlePosX = (c + 0.5) * particleUnitWidth - 0.5;
|
|
2245
|
+
const localParticlePosY = (r + 0.5) * particleUnitHeight - 0.5;
|
|
2246
|
+
const initialLocalPosition = new THREE4.Vector3(localParticlePosX, localParticlePosY, 0);
|
|
2247
|
+
const worldPosition = initialLocalPosition.clone().applyMatrix4(this.spriteWorldTransform);
|
|
2248
|
+
const rigidBodyDesc = {
|
|
2249
|
+
translation: { x: worldPosition.x, y: worldPosition.y },
|
|
2250
|
+
linearDamping: this.params.linearDamping,
|
|
2251
|
+
angularDamping: this.params.angularDamping
|
|
2252
|
+
};
|
|
2253
|
+
const rigidBody = this.physicsWorld.createRigidBody(rigidBodyDesc);
|
|
2254
|
+
const particlePhysicsWidth = particleUnitWidth * avgScale * 0.8;
|
|
2255
|
+
const particlePhysicsHeight = particleUnitHeight * avgScale * 0.8;
|
|
2256
|
+
const colliderDesc = {
|
|
2257
|
+
width: particlePhysicsWidth,
|
|
2258
|
+
height: particlePhysicsHeight,
|
|
2259
|
+
restitution: this.params.restitution,
|
|
2260
|
+
friction: this.params.friction,
|
|
2261
|
+
density: this.params.density
|
|
2262
|
+
};
|
|
2263
|
+
this.physicsWorld.createCollider(colliderDesc, rigidBody);
|
|
2264
|
+
let explosionDir = worldPosition.clone().sub(spriteWorldCenter);
|
|
2265
|
+
if (explosionDir.lengthSq() < 0.0001) {
|
|
2266
|
+
explosionDir.set(Math.random() - 0.5, Math.random() - 0.5, 0);
|
|
2267
|
+
}
|
|
2268
|
+
explosionDir.normalize();
|
|
2269
|
+
const forceVariationRange = this.params.forceVariation;
|
|
2270
|
+
const minForceFactor = 1 - forceVariationRange * 0.5;
|
|
2271
|
+
const maxForceFactor = 1 + forceVariationRange * 0.5;
|
|
2272
|
+
const forceFactor = minForceFactor + Math.random() * (maxForceFactor - minForceFactor);
|
|
2273
|
+
const explosionForce = this.params.explosionForce * forceFactor;
|
|
2274
|
+
const forceVector = {
|
|
2275
|
+
x: explosionDir.x * explosionForce,
|
|
2276
|
+
y: explosionDir.y * explosionForce + explosionForce * 0.3
|
|
2277
|
+
};
|
|
2278
|
+
rigidBody.applyImpulse(forceVector);
|
|
2279
|
+
const torque = (Math.random() - 0.5) * this.params.torqueStrength;
|
|
2280
|
+
rigidBody.applyTorqueImpulse(torque);
|
|
2281
|
+
const u0 = this.frameUvOffset.x + c / this.params.numCols * this.frameUvSize.x;
|
|
2282
|
+
const v0 = this.frameUvOffset.y + r / this.params.numRows * this.frameUvSize.y;
|
|
2283
|
+
const uSize = this.frameUvSize.x / this.params.numCols;
|
|
2284
|
+
const vSize = this.frameUvSize.y / this.params.numRows;
|
|
2285
|
+
const baseIndex = particleIndex * 4;
|
|
2286
|
+
uvOffsetData[baseIndex] = u0;
|
|
2287
|
+
uvOffsetData[baseIndex + 1] = v0;
|
|
2288
|
+
uvOffsetData[baseIndex + 2] = uSize;
|
|
2289
|
+
uvOffsetData[baseIndex + 3] = vSize;
|
|
2290
|
+
const particleId = `explosion_particle_${this.particleIdCounter++}`;
|
|
2291
|
+
const lifeVariation = 0.8 + Math.random() * 0.4;
|
|
2292
|
+
const particle = {
|
|
2293
|
+
rigidBody,
|
|
2294
|
+
instanceIndex: particleIndex,
|
|
2295
|
+
uvOffset: new THREE4.Vector2(u0, v0),
|
|
2296
|
+
uvSize: new THREE4.Vector2(uSize, vSize),
|
|
2297
|
+
initialOpacity: 1,
|
|
2298
|
+
lifeVariation,
|
|
2299
|
+
id: particleId
|
|
2300
|
+
};
|
|
2301
|
+
this.particles.push(particle);
|
|
2302
|
+
particleIndex++;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
this.uvOffsetAttribute = new THREE4.InstancedBufferAttribute(uvOffsetData, 4);
|
|
2306
|
+
this.material = PhysicsExplodingSpriteEffect.getSharedMaterial(this.resource.texture, materialFactory);
|
|
2307
|
+
const poolKey = `${this.params.numRows}x${this.params.numCols}`;
|
|
2308
|
+
this.instancedMesh = this.resource.meshPool.acquireMesh(poolKey, {
|
|
2309
|
+
geometry: () => new THREE4.PlaneGeometry(particleUnitWidth, particleUnitHeight),
|
|
2310
|
+
material: this.material,
|
|
2311
|
+
maxInstances: this.numParticles,
|
|
2312
|
+
name: `PhysicsExplodingSprite_${poolKey}`
|
|
2313
|
+
});
|
|
2314
|
+
this.instancedMesh.geometry.setAttribute("a_uvOffset", this.uvOffsetAttribute);
|
|
2315
|
+
this.instancedMesh.frustumCulled = false;
|
|
2316
|
+
for (let i = 0;i < this.numParticles; i++) {
|
|
2317
|
+
this.instancedMesh.setMatrixAt(i, this.spriteWorldTransform);
|
|
2318
|
+
}
|
|
2319
|
+
this.instancedMesh.instanceMatrix.needsUpdate = true;
|
|
2320
|
+
this.scene.add(this.instancedMesh);
|
|
2321
|
+
}
|
|
2322
|
+
static getSharedMaterial(texture, materialFactory) {
|
|
2323
|
+
const key = texture.uuid;
|
|
2324
|
+
const cached = PhysicsExplodingSpriteEffect.materialCache.get(key);
|
|
2325
|
+
if (cached)
|
|
2326
|
+
return cached;
|
|
2327
|
+
const a_uvOffset = attribute2("a_uvOffset", "vec4");
|
|
2328
|
+
const uvOffset = vec24(a_uvOffset.x, a_uvOffset.y);
|
|
2329
|
+
const uvSize = vec24(a_uvOffset.z, a_uvOffset.w);
|
|
2330
|
+
const baseUV = uv4();
|
|
2331
|
+
const finalUV = baseUV.mul(uvSize).add(uvOffset);
|
|
2332
|
+
const mapNode = tslTexture4(texture);
|
|
2333
|
+
const sampledColor = mapNode.sample(finalUV);
|
|
2334
|
+
const material = materialFactory();
|
|
2335
|
+
material.colorNode = sampledColor;
|
|
2336
|
+
PhysicsExplodingSpriteEffect.materialCache.set(key, material);
|
|
2337
|
+
return material;
|
|
2338
|
+
}
|
|
2339
|
+
update(deltaTimeMs) {
|
|
2340
|
+
if (!this.isActive)
|
|
2341
|
+
return;
|
|
2342
|
+
this.timeElapsedMs += deltaTimeMs;
|
|
2343
|
+
const tempMatrix = new THREE4.Matrix4;
|
|
2344
|
+
const tempScale = new THREE4.Vector3;
|
|
2345
|
+
this.spriteWorldTransform.decompose(new THREE4.Vector3, new THREE4.Quaternion, tempScale);
|
|
2346
|
+
const axis = new THREE4.Vector3(0, 0, 1);
|
|
2347
|
+
for (const particle of this.particles) {
|
|
2348
|
+
const position = particle.rigidBody.getTranslation();
|
|
2349
|
+
const rotation = particle.rigidBody.getRotation();
|
|
2350
|
+
const quaternion = new THREE4.Quaternion().setFromAxisAngle(axis, rotation);
|
|
2351
|
+
tempMatrix.compose(new THREE4.Vector3(position.x, position.y, 0), quaternion, tempScale);
|
|
2352
|
+
this.instancedMesh.setMatrixAt(particle.instanceIndex, tempMatrix);
|
|
2353
|
+
}
|
|
2354
|
+
this.instancedMesh.instanceMatrix.needsUpdate = true;
|
|
2355
|
+
if (this.timeElapsedMs >= this.params.durationMs) {
|
|
2356
|
+
this.dispose();
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
dispose() {
|
|
2360
|
+
if (!this.isActive)
|
|
2361
|
+
return;
|
|
2362
|
+
this.isActive = false;
|
|
2363
|
+
if (this.instancedMesh) {
|
|
2364
|
+
this.scene.remove(this.instancedMesh);
|
|
2365
|
+
const poolKey = `${this.params.numRows}x${this.params.numCols}`;
|
|
2366
|
+
this.resource.meshPool.releaseMesh(poolKey, this.instancedMesh);
|
|
2367
|
+
}
|
|
2368
|
+
for (const particle of this.particles) {
|
|
2369
|
+
this.physicsWorld.removeRigidBody(particle.rigidBody);
|
|
2370
|
+
}
|
|
2371
|
+
this.particles = [];
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
class PhysicsExplosionManager {
|
|
2376
|
+
scene;
|
|
2377
|
+
physicsWorld;
|
|
2378
|
+
activeExplosions = [];
|
|
2379
|
+
constructor(scene, physicsWorld) {
|
|
2380
|
+
this.scene = scene;
|
|
2381
|
+
this.physicsWorld = physicsWorld;
|
|
2382
|
+
}
|
|
2383
|
+
fillPool(resource, count, params = {}) {
|
|
2384
|
+
const effectParams = { ...DEFAULT_PHYSICS_EXPLOSION_PARAMETERS, ...params };
|
|
2385
|
+
const poolKey = `${effectParams.numRows}x${effectParams.numCols}`;
|
|
2386
|
+
const particleUnitWidth = 1 / effectParams.numCols;
|
|
2387
|
+
const particleUnitHeight = 1 / effectParams.numRows;
|
|
2388
|
+
const numParticles = effectParams.numRows * effectParams.numCols;
|
|
2389
|
+
const materialFactory = params.materialFactory ?? DEFAULT_PHYSICS_EXPLOSION_PARAMETERS.materialFactory;
|
|
2390
|
+
const material = PhysicsExplodingSpriteEffect.getSharedMaterial(resource.texture, materialFactory);
|
|
2391
|
+
const geometry = new THREE4.PlaneGeometry(particleUnitWidth, particleUnitHeight);
|
|
2392
|
+
resource.meshPool.fill(poolKey, {
|
|
2393
|
+
geometry: () => geometry,
|
|
2394
|
+
material,
|
|
2395
|
+
maxInstances: numParticles,
|
|
2396
|
+
name: `PhysicsExplodingSprite_${poolKey}`
|
|
2397
|
+
}, count);
|
|
2398
|
+
}
|
|
2399
|
+
_createEffectCreationData(sprite) {
|
|
2400
|
+
const animState = sprite.currentAnimation.state;
|
|
2401
|
+
const resource = sprite.currentAnimation.getResource();
|
|
2402
|
+
const currentAbsoluteFrame = animState.animFrameOffset + sprite.currentAnimation.currentLocalFrame;
|
|
2403
|
+
const frameUOffset = currentAbsoluteFrame * resource.uvTileSize.x;
|
|
2404
|
+
return {
|
|
2405
|
+
resource,
|
|
2406
|
+
frameUvOffset: new THREE4.Vector2(frameUOffset, 0),
|
|
2407
|
+
frameUvSize: resource.uvTileSize.clone(),
|
|
2408
|
+
spriteWorldTransform: sprite.getWorldTransform()
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
async createExplosionForSprite(spriteToExplode, userParams) {
|
|
2412
|
+
const effectCreationData = this._createEffectCreationData(spriteToExplode);
|
|
2413
|
+
const definition = spriteToExplode.definition;
|
|
2414
|
+
const transform = spriteToExplode.currentTransform;
|
|
2415
|
+
const spriteRecreationData = {
|
|
2416
|
+
definition,
|
|
2417
|
+
currentTransform: transform
|
|
2418
|
+
};
|
|
2419
|
+
spriteToExplode.destroy();
|
|
2420
|
+
const effect = new PhysicsExplodingSpriteEffect(this.scene, this.physicsWorld, effectCreationData.resource, effectCreationData.frameUvOffset, effectCreationData.frameUvSize, effectCreationData.spriteWorldTransform, userParams);
|
|
2421
|
+
this.activeExplosions.push(effect);
|
|
2422
|
+
const handle = {
|
|
2423
|
+
effect,
|
|
2424
|
+
recreationData: spriteRecreationData,
|
|
2425
|
+
hasBeenRestored: false,
|
|
2426
|
+
restoreSprite: async (spriteAnimator) => {
|
|
2427
|
+
if (handle.hasBeenRestored) {
|
|
2428
|
+
return null;
|
|
2429
|
+
}
|
|
2430
|
+
handle.effect.dispose();
|
|
2431
|
+
const newSprite = await spriteAnimator.createSprite(handle.recreationData.definition);
|
|
2432
|
+
const currentSpriteTransform = handle.recreationData.currentTransform;
|
|
2433
|
+
newSprite.setTransform(currentSpriteTransform.position, currentSpriteTransform.quaternion, currentSpriteTransform.scale);
|
|
2434
|
+
handle.hasBeenRestored = true;
|
|
2435
|
+
return newSprite;
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2438
|
+
return handle;
|
|
2439
|
+
}
|
|
2440
|
+
update(deltaTimeMs) {
|
|
2441
|
+
for (let i = this.activeExplosions.length - 1;i >= 0; i--) {
|
|
2442
|
+
const explosion = this.activeExplosions[i];
|
|
2443
|
+
explosion.update(deltaTimeMs);
|
|
2444
|
+
if (!explosion.isActive) {
|
|
2445
|
+
this.activeExplosions.splice(i, 1);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
disposeAll() {
|
|
2450
|
+
this.activeExplosions.forEach((exp) => exp.dispose());
|
|
2451
|
+
this.activeExplosions = [];
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
// src/3d/physics/RapierPhysicsAdapter.ts
|
|
2455
|
+
import RAPIER from "@dimforge/rapier2d-simd-compat";
|
|
2456
|
+
|
|
2457
|
+
class RapierRigidBody {
|
|
2458
|
+
rapierBody;
|
|
2459
|
+
constructor(rapierBody) {
|
|
2460
|
+
this.rapierBody = rapierBody;
|
|
2461
|
+
}
|
|
2462
|
+
applyImpulse(force) {
|
|
2463
|
+
this.rapierBody.applyImpulse(force, true);
|
|
2464
|
+
}
|
|
2465
|
+
applyTorqueImpulse(torque) {
|
|
2466
|
+
this.rapierBody.applyTorqueImpulse(torque, true);
|
|
2467
|
+
}
|
|
2468
|
+
getTranslation() {
|
|
2469
|
+
const pos = this.rapierBody.translation();
|
|
2470
|
+
return { x: pos.x, y: pos.y };
|
|
2471
|
+
}
|
|
2472
|
+
getRotation() {
|
|
2473
|
+
return this.rapierBody.rotation();
|
|
2474
|
+
}
|
|
2475
|
+
get nativeBody() {
|
|
2476
|
+
return this.rapierBody;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
class RapierPhysicsWorld {
|
|
2481
|
+
rapierWorld;
|
|
2482
|
+
constructor(rapierWorld) {
|
|
2483
|
+
this.rapierWorld = rapierWorld;
|
|
2484
|
+
}
|
|
2485
|
+
createRigidBody(desc) {
|
|
2486
|
+
const rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(desc.translation.x, desc.translation.y).setLinearDamping(desc.linearDamping).setAngularDamping(desc.angularDamping);
|
|
2487
|
+
const rapierBody = this.rapierWorld.createRigidBody(rigidBodyDesc);
|
|
2488
|
+
return new RapierRigidBody(rapierBody);
|
|
2489
|
+
}
|
|
2490
|
+
createCollider(colliderDesc, rigidBody) {
|
|
2491
|
+
const rapierColliderDesc = RAPIER.ColliderDesc.cuboid(colliderDesc.width * 0.5, colliderDesc.height * 0.5).setRestitution(colliderDesc.restitution).setFriction(colliderDesc.friction).setDensity(colliderDesc.density);
|
|
2492
|
+
const rapierRigidBody = rigidBody.nativeBody;
|
|
2493
|
+
this.rapierWorld.createCollider(rapierColliderDesc, rapierRigidBody);
|
|
2494
|
+
}
|
|
2495
|
+
removeRigidBody(rigidBody) {
|
|
2496
|
+
const rapierRigidBody = rigidBody.nativeBody;
|
|
2497
|
+
this.rapierWorld.removeRigidBody(rapierRigidBody);
|
|
2498
|
+
}
|
|
2499
|
+
static createFromRapierWorld(rapierWorld) {
|
|
2500
|
+
return new RapierPhysicsWorld(rapierWorld);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
// src/3d/physics/PlanckPhysicsAdapter.ts
|
|
2504
|
+
import * as planck from "planck";
|
|
2505
|
+
|
|
2506
|
+
class PlanckRigidBody {
|
|
2507
|
+
planckBody;
|
|
2508
|
+
constructor(planckBody) {
|
|
2509
|
+
this.planckBody = planckBody;
|
|
2510
|
+
}
|
|
2511
|
+
applyImpulse(force) {
|
|
2512
|
+
this.planckBody.applyLinearImpulse(planck.Vec2(force.x, force.y), this.planckBody.getWorldCenter());
|
|
2513
|
+
}
|
|
2514
|
+
applyTorqueImpulse(torque) {
|
|
2515
|
+
this.planckBody.applyAngularImpulse(torque);
|
|
2516
|
+
}
|
|
2517
|
+
getTranslation() {
|
|
2518
|
+
const pos = this.planckBody.getPosition();
|
|
2519
|
+
return { x: pos.x, y: pos.y };
|
|
2520
|
+
}
|
|
2521
|
+
getRotation() {
|
|
2522
|
+
return this.planckBody.getAngle();
|
|
2523
|
+
}
|
|
2524
|
+
get nativeBody() {
|
|
2525
|
+
return this.planckBody;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
class PlanckPhysicsWorld {
|
|
2530
|
+
planckWorld;
|
|
2531
|
+
constructor(planckWorld) {
|
|
2532
|
+
this.planckWorld = planckWorld;
|
|
2533
|
+
}
|
|
2534
|
+
createRigidBody(desc) {
|
|
2535
|
+
const bodyDef = {
|
|
2536
|
+
type: "dynamic",
|
|
2537
|
+
position: planck.Vec2(desc.translation.x, desc.translation.y),
|
|
2538
|
+
linearDamping: desc.linearDamping,
|
|
2539
|
+
angularDamping: desc.angularDamping
|
|
2540
|
+
};
|
|
2541
|
+
const planckBody = this.planckWorld.createBody(bodyDef);
|
|
2542
|
+
return new PlanckRigidBody(planckBody);
|
|
2543
|
+
}
|
|
2544
|
+
createCollider(colliderDesc, rigidBody) {
|
|
2545
|
+
const shape = planck.Box(colliderDesc.width * 0.5, colliderDesc.height * 0.5);
|
|
2546
|
+
const fixtureDef = {
|
|
2547
|
+
shape,
|
|
2548
|
+
density: colliderDesc.density,
|
|
2549
|
+
friction: colliderDesc.friction,
|
|
2550
|
+
restitution: colliderDesc.restitution
|
|
2551
|
+
};
|
|
2552
|
+
const planckRigidBody = rigidBody.nativeBody;
|
|
2553
|
+
planckRigidBody.createFixture(fixtureDef);
|
|
2554
|
+
}
|
|
2555
|
+
removeRigidBody(rigidBody) {
|
|
2556
|
+
const planckRigidBody = rigidBody.nativeBody;
|
|
2557
|
+
this.planckWorld.destroyBody(planckRigidBody);
|
|
2558
|
+
}
|
|
2559
|
+
static createFromPlanckWorld(planckWorld) {
|
|
2560
|
+
return new PlanckPhysicsWorld(planckWorld);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
// src/3d/SpriteResourceManager.ts
|
|
2564
|
+
import * as THREE5 from "three";
|
|
2565
|
+
var HIDDEN_MATRIX2 = new THREE5.Matrix4().scale(new THREE5.Vector3(0, 0, 0));
|
|
2566
|
+
|
|
2567
|
+
class MeshPool {
|
|
2568
|
+
pools = new Map;
|
|
2569
|
+
acquireMesh(poolId, options) {
|
|
2570
|
+
const poolArray = this.pools.get(poolId) ?? [];
|
|
2571
|
+
this.pools.set(poolId, poolArray);
|
|
2572
|
+
if (poolArray.length > 0) {
|
|
2573
|
+
const mesh2 = poolArray.pop();
|
|
2574
|
+
mesh2.material = options.material;
|
|
2575
|
+
mesh2.count = options.maxInstances;
|
|
2576
|
+
return mesh2;
|
|
2577
|
+
}
|
|
2578
|
+
const mesh = new THREE5.InstancedMesh(options.geometry(), options.material, options.maxInstances);
|
|
2579
|
+
if (options.name) {
|
|
2580
|
+
mesh.name = options.name;
|
|
2581
|
+
}
|
|
2582
|
+
return mesh;
|
|
2583
|
+
}
|
|
2584
|
+
releaseMesh(poolId, mesh) {
|
|
2585
|
+
const poolArray = this.pools.get(poolId) ?? [];
|
|
2586
|
+
poolArray.push(mesh);
|
|
2587
|
+
this.pools.set(poolId, poolArray);
|
|
2588
|
+
}
|
|
2589
|
+
fill(poolId, options, count) {
|
|
2590
|
+
const poolArray = this.pools.get(poolId) ?? [];
|
|
2591
|
+
this.pools.set(poolId, poolArray);
|
|
2592
|
+
for (let i = 0;i < count; i++) {
|
|
2593
|
+
const mesh = new THREE5.InstancedMesh(options.geometry(), options.material, options.maxInstances);
|
|
2594
|
+
if (options.name) {
|
|
2595
|
+
mesh.name = `${options.name}_${i}`;
|
|
2596
|
+
}
|
|
2597
|
+
poolArray.push(mesh);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
clearPool(poolId) {
|
|
2601
|
+
const poolArray = this.pools.get(poolId);
|
|
2602
|
+
if (poolArray) {
|
|
2603
|
+
poolArray.forEach((mesh) => {
|
|
2604
|
+
mesh.geometry.dispose();
|
|
2605
|
+
if (Array.isArray(mesh.material)) {
|
|
2606
|
+
mesh.material.forEach((mat) => mat.dispose());
|
|
2607
|
+
} else {
|
|
2608
|
+
mesh.material.dispose();
|
|
2609
|
+
}
|
|
2610
|
+
});
|
|
2611
|
+
poolArray.length = 0;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
clearAllPools() {
|
|
2615
|
+
for (const poolId of this.pools.keys()) {
|
|
2616
|
+
this.clearPool(poolId);
|
|
2617
|
+
}
|
|
2618
|
+
this.pools.clear();
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
class InstanceManager {
|
|
2623
|
+
scene;
|
|
2624
|
+
instancedMesh;
|
|
2625
|
+
material;
|
|
2626
|
+
maxInstances;
|
|
2627
|
+
_freeIndices = [];
|
|
2628
|
+
instanceCount = 0;
|
|
2629
|
+
_matrix;
|
|
2630
|
+
constructor(scene, geometry, material, options) {
|
|
2631
|
+
this.scene = scene;
|
|
2632
|
+
this.material = material;
|
|
2633
|
+
this.maxInstances = options.maxInstances;
|
|
2634
|
+
this._matrix = options.matrix ?? HIDDEN_MATRIX2;
|
|
2635
|
+
this.instancedMesh = new THREE5.InstancedMesh(geometry, material, this.maxInstances);
|
|
2636
|
+
this.instancedMesh.renderOrder = options.renderOrder ?? 0;
|
|
2637
|
+
this.instancedMesh.frustumCulled = options.frustumCulled ?? false;
|
|
2638
|
+
this.instancedMesh.instanceMatrix.setUsage(THREE5.DynamicDrawUsage);
|
|
2639
|
+
if (options.name) {
|
|
2640
|
+
this.instancedMesh.name = options.name;
|
|
2641
|
+
}
|
|
2642
|
+
for (let i = 0;i < this.maxInstances; i++) {
|
|
2643
|
+
this._freeIndices.push(i);
|
|
2644
|
+
this.instancedMesh.setMatrixAt(i, this._matrix);
|
|
2645
|
+
}
|
|
2646
|
+
this.instancedMesh.instanceMatrix.needsUpdate = true;
|
|
2647
|
+
this.scene.add(this.instancedMesh);
|
|
2648
|
+
}
|
|
2649
|
+
acquireInstanceSlot() {
|
|
2650
|
+
if (this._freeIndices.length === 0) {
|
|
2651
|
+
throw new Error(`[InstanceManager] Max instances (${this.maxInstances}) reached. Cannot acquire slot.`);
|
|
2652
|
+
}
|
|
2653
|
+
const instanceIndex = this._freeIndices.pop();
|
|
2654
|
+
this.instanceCount++;
|
|
2655
|
+
return instanceIndex;
|
|
2656
|
+
}
|
|
2657
|
+
releaseInstanceSlot(instanceIndex) {
|
|
2658
|
+
if (instanceIndex >= 0 && instanceIndex < this.maxInstances) {
|
|
2659
|
+
this.instancedMesh.setMatrixAt(instanceIndex, this._matrix);
|
|
2660
|
+
this.instancedMesh.instanceMatrix.needsUpdate = true;
|
|
2661
|
+
if (!this._freeIndices.includes(instanceIndex)) {
|
|
2662
|
+
this._freeIndices.push(instanceIndex);
|
|
2663
|
+
this._freeIndices.sort((a, b) => a - b);
|
|
2664
|
+
this.instanceCount--;
|
|
2665
|
+
}
|
|
2666
|
+
} else {
|
|
2667
|
+
console.warn(`[InstanceManager] Attempted to release invalid instanceIndex ${instanceIndex}`);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
getInstanceCount() {
|
|
2671
|
+
return this.instanceCount;
|
|
2672
|
+
}
|
|
2673
|
+
getMaxInstances() {
|
|
2674
|
+
return this.maxInstances;
|
|
2675
|
+
}
|
|
2676
|
+
get hasFreeIndices() {
|
|
2677
|
+
return this._freeIndices.length > 0;
|
|
2678
|
+
}
|
|
2679
|
+
get mesh() {
|
|
2680
|
+
return this.instancedMesh;
|
|
2681
|
+
}
|
|
2682
|
+
dispose() {
|
|
2683
|
+
this.scene.remove(this.instancedMesh);
|
|
2684
|
+
this.instancedMesh.geometry.dispose();
|
|
2685
|
+
if (Array.isArray(this.material)) {
|
|
2686
|
+
this.material.forEach((mat) => mat.dispose());
|
|
2687
|
+
} else {
|
|
2688
|
+
this.material.dispose();
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
class SpriteResource {
|
|
2694
|
+
_texture;
|
|
2695
|
+
_sheetProperties;
|
|
2696
|
+
scene;
|
|
2697
|
+
_meshPool;
|
|
2698
|
+
constructor(texture, sheetProperties, scene) {
|
|
2699
|
+
this._texture = texture;
|
|
2700
|
+
this._sheetProperties = sheetProperties;
|
|
2701
|
+
this.scene = scene;
|
|
2702
|
+
this._meshPool = new MeshPool;
|
|
2703
|
+
}
|
|
2704
|
+
get texture() {
|
|
2705
|
+
return this._texture;
|
|
2706
|
+
}
|
|
2707
|
+
get sheetProperties() {
|
|
2708
|
+
return this._sheetProperties;
|
|
2709
|
+
}
|
|
2710
|
+
get meshPool() {
|
|
2711
|
+
return this._meshPool;
|
|
2712
|
+
}
|
|
2713
|
+
createInstanceManager(geometry, material, options) {
|
|
2714
|
+
const managerOptions = {
|
|
2715
|
+
...options,
|
|
2716
|
+
name: options.name ?? `InstancedSprites_${this._sheetProperties.imagePath.replace(/[^a-zA-Z0-9_]/g, "_")}`
|
|
2717
|
+
};
|
|
2718
|
+
return new InstanceManager(this.scene, geometry, material, managerOptions);
|
|
2719
|
+
}
|
|
2720
|
+
get uvTileSize() {
|
|
2721
|
+
const uvTileWidth = 1 / this._sheetProperties.sheetNumFrames;
|
|
2722
|
+
const uvTileHeight = 1;
|
|
2723
|
+
return new THREE5.Vector2(uvTileWidth, uvTileHeight);
|
|
2724
|
+
}
|
|
2725
|
+
dispose() {
|
|
2726
|
+
this._meshPool.clearAllPools();
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
class SpriteResourceManager {
|
|
2731
|
+
resources = new Map;
|
|
2732
|
+
textureCache = new Map;
|
|
2733
|
+
scene;
|
|
2734
|
+
constructor(scene) {
|
|
2735
|
+
this.scene = scene;
|
|
2736
|
+
}
|
|
2737
|
+
getResourceKey(sheetProps) {
|
|
2738
|
+
return sheetProps.imagePath;
|
|
2739
|
+
}
|
|
2740
|
+
async getOrCreateResource(texture, sheetProps) {
|
|
2741
|
+
const resourceKey = this.getResourceKey(sheetProps);
|
|
2742
|
+
let resource = this.resources.get(resourceKey);
|
|
2743
|
+
if (!resource) {
|
|
2744
|
+
resource = new SpriteResource(texture, sheetProps, this.scene);
|
|
2745
|
+
this.resources.set(resourceKey, resource);
|
|
2746
|
+
}
|
|
2747
|
+
return resource;
|
|
2748
|
+
}
|
|
2749
|
+
async createResource(config) {
|
|
2750
|
+
let texture = this.textureCache.get(config.imagePath);
|
|
2751
|
+
if (!texture) {
|
|
2752
|
+
const loadedTexture = await TextureUtils.fromFile(config.imagePath);
|
|
2753
|
+
if (!loadedTexture) {
|
|
2754
|
+
throw new Error(`[SpriteResourceManager] Failed to load texture for ${config.imagePath}`);
|
|
2755
|
+
}
|
|
2756
|
+
loadedTexture.needsUpdate = true;
|
|
2757
|
+
texture = loadedTexture;
|
|
2758
|
+
this.textureCache.set(config.imagePath, texture);
|
|
2759
|
+
}
|
|
2760
|
+
const sheetProps = {
|
|
2761
|
+
imagePath: config.imagePath,
|
|
2762
|
+
sheetTilesetWidth: texture.image.width,
|
|
2763
|
+
sheetTilesetHeight: texture.image.height,
|
|
2764
|
+
sheetNumFrames: config.sheetNumFrames
|
|
2765
|
+
};
|
|
2766
|
+
return await this.getOrCreateResource(texture, sheetProps);
|
|
2767
|
+
}
|
|
2768
|
+
clearCache() {
|
|
2769
|
+
this.resources.clear();
|
|
2770
|
+
this.textureCache.clear();
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
// src/3d.ts
|
|
2774
|
+
import * as THREE6 from "three";
|
|
2775
|
+
export {
|
|
2776
|
+
TiledSprite,
|
|
2777
|
+
ThreeRenderable,
|
|
2778
|
+
ThreeCliRenderer,
|
|
2779
|
+
TextureUtils,
|
|
2780
|
+
THREE6 as THREE,
|
|
2781
|
+
SuperSampleType,
|
|
2782
|
+
SuperSampleAlgorithm,
|
|
2783
|
+
SpriteUtils,
|
|
2784
|
+
SpriteResourceManager,
|
|
2785
|
+
SpriteResource,
|
|
2786
|
+
SpriteParticleGenerator,
|
|
2787
|
+
SpriteAnimator,
|
|
2788
|
+
SheetSprite,
|
|
2789
|
+
RapierRigidBody,
|
|
2790
|
+
RapierPhysicsWorld,
|
|
2791
|
+
PlanckRigidBody,
|
|
2792
|
+
PlanckPhysicsWorld,
|
|
2793
|
+
PhysicsExplosionManager,
|
|
2794
|
+
PhysicsExplodingSpriteEffect,
|
|
2795
|
+
MeshPool,
|
|
2796
|
+
InstanceManager,
|
|
2797
|
+
ExplosionManager,
|
|
2798
|
+
ExplodingSpriteEffect,
|
|
2799
|
+
DEFAULT_PHYSICS_EXPLOSION_PARAMETERS,
|
|
2800
|
+
DEFAULT_EXPLOSION_PARAMETERS,
|
|
2801
|
+
CLICanvas
|
|
2802
|
+
};
|
|
2803
|
+
|
|
2804
|
+
//# debugId=23E2BC997AA0860D64756E2164756E21
|
|
2805
|
+
//# sourceMappingURL=3d.js.map
|