reze-engine 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,526 @@
1
+ // Debug overlay drawing every rigidbody as a wireframe + semitransparent solid
2
+ // primitive (sphere / box / capsule), color-coded by type:
3
+ // yellow = static (FollowBone)
4
+ // cyan = kinematic
5
+ // red = dynamic
6
+ //
7
+ // Rendered in its own swapchain pass AFTER composite (see Engine.renderPhysicsDebug),
8
+ // so it sits cleanly on top of the final tonemapped image. No depth, no MSAA,
9
+ // no MRT, no interaction with the HDR alpha gate — bodies are always fully
10
+ // visible regardless of camera angle, model occlusion, or stacking. Reads body
11
+ // transforms straight from RezePhysics' SoA store — zero per-frame allocations.
12
+ import { Mat4 } from "./math";
13
+ import { RigidbodyShape, RigidbodyType } from "./physics";
14
+ import { PHYSICS_DEBUG_SHADER_WGSL } from "./shaders/passes/physics-debug";
15
+ const STRIDE_BYTES = 96; // 4 mat4 cols + size+pad + color = 6 vec4
16
+ const STRIDE_FLOATS = STRIDE_BYTES / 4;
17
+ // Per-instance color.alpha is the WIREFRAME alpha (1.0 for crisp edges). Solid
18
+ // pipelines override the SOLID_ALPHA shader constant to ~0.20 so the fill stays
19
+ // gentle where many bodies overlap.
20
+ // Hues chosen so wire+solid both read against the typical pink/grey reze studio
21
+ // scene background. Red [1, 0.3, 0.3] sat too close to the pink bg's hue —
22
+ // solid fill (α≈0.2) blended into the bg and the wire didn't separate from it.
23
+ // Orange-red shifts ~30° on the hue wheel, giving real chroma against pink.
24
+ const COLOR_STATIC = [1.0, 0.85, 0.15, 1.0]; // yellow
25
+ const COLOR_KINEMATIC = [0.25, 0.7, 1.0, 1.0]; // cyan-blue
26
+ const COLOR_DYNAMIC = [1.0, 0.35, 0.0, 1.0]; // orange-red
27
+ const SOLID_ALPHA = 0.2;
28
+ export class PhysicsDebugRenderer {
29
+ constructor(device, cameraUniformBuffer, presentationFormat) {
30
+ this.device = device;
31
+ const wireSphere = buildSphereWireGeometry();
32
+ const wireBox = buildBoxWireGeometry();
33
+ const wireCapsule = buildCapsuleWireGeometry();
34
+ this.wireSphereCount = wireSphere.length / 4;
35
+ this.wireBoxCount = wireBox.length / 4;
36
+ this.wireCapsuleCount = wireCapsule.length / 4;
37
+ const solidSphere = buildSphereSolidGeometry();
38
+ const solidBox = buildBoxSolidGeometry();
39
+ const solidCapsule = buildCapsuleSolidGeometry();
40
+ this.solidSphereCount = solidSphere.length / 4;
41
+ this.solidBoxCount = solidBox.length / 4;
42
+ this.solidCapsuleCount = solidCapsule.length / 4;
43
+ const upload = (label, data) => {
44
+ const buf = device.createBuffer({
45
+ label,
46
+ size: data.byteLength,
47
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
48
+ });
49
+ device.queue.writeBuffer(buf, 0, data.buffer, data.byteOffset, data.byteLength);
50
+ return buf;
51
+ };
52
+ this.wireSphereBuffer = upload("physics-debug wire sphere", wireSphere);
53
+ this.wireBoxBuffer = upload("physics-debug wire box", wireBox);
54
+ this.wireCapsuleBuffer = upload("physics-debug wire capsule", wireCapsule);
55
+ this.solidSphereBuffer = upload("physics-debug solid sphere", solidSphere);
56
+ this.solidBoxBuffer = upload("physics-debug solid box", solidBox);
57
+ this.solidCapsuleBuffer = upload("physics-debug solid capsule", solidCapsule);
58
+ this.instanceCapacity = 512;
59
+ this.instanceData = new Float32Array(this.instanceCapacity * STRIDE_FLOATS);
60
+ this.instanceBuffer = device.createBuffer({
61
+ label: "physics-debug instances",
62
+ size: this.instanceCapacity * STRIDE_BYTES,
63
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
64
+ });
65
+ const bgl = device.createBindGroupLayout({
66
+ label: "physics-debug bgl",
67
+ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } }],
68
+ });
69
+ this.bindGroup = device.createBindGroup({
70
+ label: "physics-debug bg",
71
+ layout: bgl,
72
+ entries: [{ binding: 0, resource: { buffer: cameraUniformBuffer } }],
73
+ });
74
+ const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [bgl] });
75
+ const shader = device.createShaderModule({
76
+ label: "physics-debug shader",
77
+ code: PHYSICS_DEBUG_SHADER_WGSL,
78
+ });
79
+ const vertexBuffers = [
80
+ {
81
+ arrayStride: 16,
82
+ attributes: [{ shaderLocation: 0, offset: 0, format: "float32x4" }],
83
+ },
84
+ {
85
+ arrayStride: STRIDE_BYTES,
86
+ stepMode: "instance",
87
+ attributes: [
88
+ { shaderLocation: 1, offset: 0, format: "float32x4" },
89
+ { shaderLocation: 2, offset: 16, format: "float32x4" },
90
+ { shaderLocation: 3, offset: 32, format: "float32x4" },
91
+ { shaderLocation: 4, offset: 48, format: "float32x4" },
92
+ { shaderLocation: 5, offset: 64, format: "float32x4" },
93
+ { shaderLocation: 6, offset: 80, format: "float32x4" },
94
+ ],
95
+ },
96
+ ];
97
+ const targets = [
98
+ {
99
+ format: presentationFormat,
100
+ blend: {
101
+ color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha", operation: "add" },
102
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
103
+ },
104
+ },
105
+ ];
106
+ const buildPipeline = (label, kind, topology, solidAlpha) => device.createRenderPipeline({
107
+ label,
108
+ layout: pipelineLayout,
109
+ vertex: {
110
+ module: shader,
111
+ entryPoint: "vsMain",
112
+ buffers: vertexBuffers,
113
+ constants: { SHAPE_KIND: kind },
114
+ },
115
+ fragment: {
116
+ module: shader,
117
+ entryPoint: "fsMain",
118
+ targets,
119
+ constants: { SOLID_ALPHA: solidAlpha },
120
+ },
121
+ primitive: { topology, cullMode: "none" },
122
+ // No depth attachment, single multisample — pass renders straight to
123
+ // the swapchain after composite, so the overlay sits on top of the
124
+ // tonemapped scene without interacting with depth/stencil/MSAA.
125
+ multisample: { count: 1 },
126
+ });
127
+ this.wirePipelineSphere = buildPipeline("physics-debug wire sphere", 0, "line-list", 1.0);
128
+ this.wirePipelineBox = buildPipeline("physics-debug wire box", 1, "line-list", 1.0);
129
+ this.wirePipelineCapsule = buildPipeline("physics-debug wire capsule", 2, "line-list", 1.0);
130
+ this.solidPipelineSphere = buildPipeline("physics-debug solid sphere", 0, "triangle-list", SOLID_ALPHA);
131
+ this.solidPipelineBox = buildPipeline("physics-debug solid box", 1, "triangle-list", SOLID_ALPHA);
132
+ this.solidPipelineCapsule = buildPipeline("physics-debug solid capsule", 2, "triangle-list", SOLID_ALPHA);
133
+ }
134
+ render(pass, physics) {
135
+ const rigidbodies = physics.getRigidbodies();
136
+ const N = rigidbodies.length;
137
+ if (N === 0)
138
+ return;
139
+ const store = physics.getStore();
140
+ const positions = store.positions;
141
+ const orientations = store.orientations;
142
+ if (N > this.instanceCapacity) {
143
+ this.instanceCapacity = Math.max(N, this.instanceCapacity * 2);
144
+ this.instanceData = new Float32Array(this.instanceCapacity * STRIDE_FLOATS);
145
+ this.instanceBuffer.destroy();
146
+ this.instanceBuffer = this.device.createBuffer({
147
+ label: "physics-debug instances",
148
+ size: this.instanceCapacity * STRIDE_BYTES,
149
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
150
+ });
151
+ }
152
+ // Two passes: count per-shape, then fill packed by shape so we can issue
153
+ // 3 instanced draws with vertex-buffer offsets.
154
+ let sphereCount = 0;
155
+ let boxCount = 0;
156
+ let capsuleCount = 0;
157
+ for (let i = 0; i < N; i++) {
158
+ const s = rigidbodies[i].shape;
159
+ if (s === RigidbodyShape.Sphere)
160
+ sphereCount++;
161
+ else if (s === RigidbodyShape.Box)
162
+ boxCount++;
163
+ else
164
+ capsuleCount++;
165
+ }
166
+ const sphereOffset = 0;
167
+ const boxOffset = sphereCount;
168
+ const capsuleOffset = sphereCount + boxCount;
169
+ const data = this.instanceData;
170
+ let sIdx = sphereOffset;
171
+ let bIdx = boxOffset;
172
+ let cIdx = capsuleOffset;
173
+ for (let i = 0; i < N; i++) {
174
+ const rb = rigidbodies[i];
175
+ const i3 = i * 3;
176
+ const i4 = i * 4;
177
+ let slot;
178
+ if (rb.shape === RigidbodyShape.Sphere)
179
+ slot = sIdx++;
180
+ else if (rb.shape === RigidbodyShape.Box)
181
+ slot = bIdx++;
182
+ else
183
+ slot = cIdx++;
184
+ const dst = slot * STRIDE_FLOATS;
185
+ // Model matrix: rotation from quat into [dst..dst+15], then translation.
186
+ Mat4.fromQuatInto(orientations[i4 + 0], orientations[i4 + 1], orientations[i4 + 2], orientations[i4 + 3], data, dst);
187
+ data[dst + 12] = positions[i3 + 0];
188
+ data[dst + 13] = positions[i3 + 1];
189
+ data[dst + 14] = positions[i3 + 2];
190
+ // size + pad
191
+ data[dst + 16] = rb.size.x;
192
+ data[dst + 17] = rb.size.y;
193
+ data[dst + 18] = rb.size.z;
194
+ data[dst + 19] = 0;
195
+ const c = rb.type === RigidbodyType.Static
196
+ ? COLOR_STATIC
197
+ : rb.type === RigidbodyType.Kinematic
198
+ ? COLOR_KINEMATIC
199
+ : COLOR_DYNAMIC;
200
+ data[dst + 20] = c[0];
201
+ data[dst + 21] = c[1];
202
+ data[dst + 22] = c[2];
203
+ data[dst + 23] = c[3];
204
+ }
205
+ this.device.queue.writeBuffer(this.instanceBuffer, 0, data.buffer, data.byteOffset, N * STRIDE_BYTES);
206
+ pass.setBindGroup(0, this.bindGroup);
207
+ // Solid fills first (gentle ~20% alpha for shape ID), wireframe edges on top
208
+ // (95% alpha for crisp silhouette).
209
+ if (sphereCount > 0) {
210
+ const off = sphereOffset * STRIDE_BYTES;
211
+ const size = sphereCount * STRIDE_BYTES;
212
+ pass.setPipeline(this.solidPipelineSphere);
213
+ pass.setVertexBuffer(0, this.solidSphereBuffer);
214
+ pass.setVertexBuffer(1, this.instanceBuffer, off, size);
215
+ pass.draw(this.solidSphereCount, sphereCount);
216
+ pass.setPipeline(this.wirePipelineSphere);
217
+ pass.setVertexBuffer(0, this.wireSphereBuffer);
218
+ pass.setVertexBuffer(1, this.instanceBuffer, off, size);
219
+ pass.draw(this.wireSphereCount, sphereCount);
220
+ }
221
+ if (boxCount > 0) {
222
+ const off = boxOffset * STRIDE_BYTES;
223
+ const size = boxCount * STRIDE_BYTES;
224
+ pass.setPipeline(this.solidPipelineBox);
225
+ pass.setVertexBuffer(0, this.solidBoxBuffer);
226
+ pass.setVertexBuffer(1, this.instanceBuffer, off, size);
227
+ pass.draw(this.solidBoxCount, boxCount);
228
+ pass.setPipeline(this.wirePipelineBox);
229
+ pass.setVertexBuffer(0, this.wireBoxBuffer);
230
+ pass.setVertexBuffer(1, this.instanceBuffer, off, size);
231
+ pass.draw(this.wireBoxCount, boxCount);
232
+ }
233
+ if (capsuleCount > 0) {
234
+ const off = capsuleOffset * STRIDE_BYTES;
235
+ const size = capsuleCount * STRIDE_BYTES;
236
+ pass.setPipeline(this.solidPipelineCapsule);
237
+ pass.setVertexBuffer(0, this.solidCapsuleBuffer);
238
+ pass.setVertexBuffer(1, this.instanceBuffer, off, size);
239
+ pass.draw(this.solidCapsuleCount, capsuleCount);
240
+ pass.setPipeline(this.wirePipelineCapsule);
241
+ pass.setVertexBuffer(0, this.wireCapsuleBuffer);
242
+ pass.setVertexBuffer(1, this.instanceBuffer, off, size);
243
+ pass.draw(this.wireCapsuleCount, capsuleCount);
244
+ }
245
+ }
246
+ destroy() {
247
+ this.wireSphereBuffer.destroy();
248
+ this.wireBoxBuffer.destroy();
249
+ this.wireCapsuleBuffer.destroy();
250
+ this.solidSphereBuffer.destroy();
251
+ this.solidBoxBuffer.destroy();
252
+ this.solidCapsuleBuffer.destroy();
253
+ this.instanceBuffer.destroy();
254
+ }
255
+ }
256
+ // ── Geometry builders ─────────────────────────────────────────────────────────
257
+ // Each vertex is vec4(unitPos.xyz, axialAnchor) packed as 4 floats.
258
+ function buildSphereWireGeometry() {
259
+ const segs = 32;
260
+ const out = new Float32Array(3 * segs * 2 * 4); // 3 great circles
261
+ let p = 0;
262
+ for (let plane = 0; plane < 3; plane++) {
263
+ for (let i = 0; i < segs; i++) {
264
+ const a0 = (i / segs) * Math.PI * 2;
265
+ const a1 = ((i + 1) / segs) * Math.PI * 2;
266
+ const c0 = Math.cos(a0), s0 = Math.sin(a0);
267
+ const c1 = Math.cos(a1), s1 = Math.sin(a1);
268
+ let p0x = 0, p0y = 0, p0z = 0;
269
+ let p1x = 0, p1y = 0, p1z = 0;
270
+ if (plane === 0) {
271
+ p0x = c0;
272
+ p0y = s0;
273
+ p1x = c1;
274
+ p1y = s1;
275
+ }
276
+ else if (plane === 1) {
277
+ p0x = c0;
278
+ p0z = s0;
279
+ p1x = c1;
280
+ p1z = s1;
281
+ }
282
+ else {
283
+ p0y = c0;
284
+ p0z = s0;
285
+ p1y = c1;
286
+ p1z = s1;
287
+ }
288
+ out[p++] = p0x;
289
+ out[p++] = p0y;
290
+ out[p++] = p0z;
291
+ out[p++] = 0;
292
+ out[p++] = p1x;
293
+ out[p++] = p1y;
294
+ out[p++] = p1z;
295
+ out[p++] = 0;
296
+ }
297
+ }
298
+ return out;
299
+ }
300
+ function buildBoxWireGeometry() {
301
+ const edges = [
302
+ // 4 along X
303
+ [-1, -1, -1, +1, -1, -1], [-1, -1, +1, +1, -1, +1],
304
+ [-1, +1, -1, +1, +1, -1], [-1, +1, +1, +1, +1, +1],
305
+ // 4 along Y
306
+ [-1, -1, -1, -1, +1, -1], [+1, -1, -1, +1, +1, -1],
307
+ [-1, -1, +1, -1, +1, +1], [+1, -1, +1, +1, +1, +1],
308
+ // 4 along Z
309
+ [-1, -1, -1, -1, -1, +1], [+1, -1, -1, +1, -1, +1],
310
+ [-1, +1, -1, -1, +1, +1], [+1, +1, -1, +1, +1, +1],
311
+ ];
312
+ const out = new Float32Array(edges.length * 2 * 4);
313
+ let p = 0;
314
+ for (const e of edges) {
315
+ out[p++] = e[0];
316
+ out[p++] = e[1];
317
+ out[p++] = e[2];
318
+ out[p++] = 0;
319
+ out[p++] = e[3];
320
+ out[p++] = e[4];
321
+ out[p++] = e[5];
322
+ out[p++] = 0;
323
+ }
324
+ return out;
325
+ }
326
+ function buildCapsuleWireGeometry() {
327
+ const ringSegs = 32;
328
+ const arcSegs = 16;
329
+ // 2 cap rings + 4 hemisphere arcs + 4 cylinder verticals
330
+ const lineCount = ringSegs * 2 + arcSegs * 4 + 4;
331
+ const out = new Float32Array(lineCount * 2 * 4);
332
+ let p = 0;
333
+ const push = (x, y, z, axial) => {
334
+ out[p++] = x;
335
+ out[p++] = y;
336
+ out[p++] = z;
337
+ out[p++] = axial;
338
+ };
339
+ // Top ring (axial=+1) in XZ plane
340
+ for (let i = 0; i < ringSegs; i++) {
341
+ const a0 = (i / ringSegs) * Math.PI * 2;
342
+ const a1 = ((i + 1) / ringSegs) * Math.PI * 2;
343
+ push(Math.cos(a0), 0, Math.sin(a0), +1);
344
+ push(Math.cos(a1), 0, Math.sin(a1), +1);
345
+ }
346
+ // Bottom ring (axial=-1)
347
+ for (let i = 0; i < ringSegs; i++) {
348
+ const a0 = (i / ringSegs) * Math.PI * 2;
349
+ const a1 = ((i + 1) / ringSegs) * Math.PI * 2;
350
+ push(Math.cos(a0), 0, Math.sin(a0), -1);
351
+ push(Math.cos(a1), 0, Math.sin(a1), -1);
352
+ }
353
+ // Top cap arcs in XY and YZ planes (θ ∈ [0, π], pos = (cosθ, sinθ, 0) etc.)
354
+ for (let i = 0; i < arcSegs; i++) {
355
+ const t0 = (i / arcSegs) * Math.PI;
356
+ const t1 = ((i + 1) / arcSegs) * Math.PI;
357
+ push(Math.cos(t0), Math.sin(t0), 0, +1);
358
+ push(Math.cos(t1), Math.sin(t1), 0, +1);
359
+ }
360
+ for (let i = 0; i < arcSegs; i++) {
361
+ const t0 = (i / arcSegs) * Math.PI;
362
+ const t1 = ((i + 1) / arcSegs) * Math.PI;
363
+ push(0, Math.sin(t0), Math.cos(t0), +1);
364
+ push(0, Math.sin(t1), Math.cos(t1), +1);
365
+ }
366
+ // Bottom cap arcs
367
+ for (let i = 0; i < arcSegs; i++) {
368
+ const t0 = (i / arcSegs) * Math.PI;
369
+ const t1 = ((i + 1) / arcSegs) * Math.PI;
370
+ push(Math.cos(t0), -Math.sin(t0), 0, -1);
371
+ push(Math.cos(t1), -Math.sin(t1), 0, -1);
372
+ }
373
+ for (let i = 0; i < arcSegs; i++) {
374
+ const t0 = (i / arcSegs) * Math.PI;
375
+ const t1 = ((i + 1) / arcSegs) * Math.PI;
376
+ push(0, -Math.sin(t0), Math.cos(t0), -1);
377
+ push(0, -Math.sin(t1), Math.cos(t1), -1);
378
+ }
379
+ // 4 cylinder verticals: bottom rim (axial=-1) → top rim (+1) at fixed θ
380
+ push(+1, 0, 0, -1);
381
+ push(+1, 0, 0, +1);
382
+ push(-1, 0, 0, -1);
383
+ push(-1, 0, 0, +1);
384
+ push(0, 0, +1, -1);
385
+ push(0, 0, +1, +1);
386
+ push(0, 0, -1, -1);
387
+ push(0, 0, -1, +1);
388
+ return out;
389
+ }
390
+ // ── Solid (triangle-list) geometry builders ─────────────────────────────────
391
+ // Each vertex is vec4(unitPos.xyz, axialAnchor) — same layout as wireframe.
392
+ // UV sphere — stacks × slices quads, each split into 2 triangles. Polar quads
393
+ // degenerate to triangles, which the GPU silently drops.
394
+ function buildSphereSolidGeometry() {
395
+ const stacks = 12;
396
+ const slices = 18;
397
+ const out = new Float32Array(stacks * slices * 6 * 4);
398
+ let p = 0;
399
+ const vert = (phi, theta) => {
400
+ out[p++] = Math.cos(phi) * Math.cos(theta);
401
+ out[p++] = Math.sin(phi);
402
+ out[p++] = Math.cos(phi) * Math.sin(theta);
403
+ out[p++] = 0;
404
+ };
405
+ for (let s = 0; s < stacks; s++) {
406
+ const phi0 = -Math.PI / 2 + (s / stacks) * Math.PI;
407
+ const phi1 = -Math.PI / 2 + ((s + 1) / stacks) * Math.PI;
408
+ for (let l = 0; l < slices; l++) {
409
+ const th0 = (l / slices) * Math.PI * 2;
410
+ const th1 = ((l + 1) / slices) * Math.PI * 2;
411
+ vert(phi0, th0);
412
+ vert(phi1, th0);
413
+ vert(phi1, th1);
414
+ vert(phi0, th0);
415
+ vert(phi1, th1);
416
+ vert(phi0, th1);
417
+ }
418
+ }
419
+ return out;
420
+ }
421
+ // 6 cube faces × 2 triangles × 3 verts = 36 verts.
422
+ function buildBoxSolidGeometry() {
423
+ const faces = [
424
+ // +X face
425
+ [+1, -1, -1, +1, +1, -1, +1, +1, +1, +1, -1, +1],
426
+ // -X face
427
+ [-1, -1, -1, -1, -1, +1, -1, +1, +1, -1, +1, -1],
428
+ // +Y face
429
+ [-1, +1, -1, -1, +1, +1, +1, +1, +1, +1, +1, -1],
430
+ // -Y face
431
+ [-1, -1, -1, +1, -1, -1, +1, -1, +1, -1, -1, +1],
432
+ // +Z face
433
+ [-1, -1, +1, +1, -1, +1, +1, +1, +1, -1, +1, +1],
434
+ // -Z face
435
+ [-1, -1, -1, -1, +1, -1, +1, +1, -1, +1, -1, -1],
436
+ ];
437
+ const out = new Float32Array(faces.length * 6 * 4);
438
+ let p = 0;
439
+ const v = (x, y, z) => {
440
+ out[p++] = x;
441
+ out[p++] = y;
442
+ out[p++] = z;
443
+ out[p++] = 0;
444
+ };
445
+ for (const f of faces) {
446
+ // f = [a, b, c, d] as 4 corners; emit tris (a,b,c) (a,c,d)
447
+ v(f[0], f[1], f[2]);
448
+ v(f[3], f[4], f[5]);
449
+ v(f[6], f[7], f[8]);
450
+ v(f[0], f[1], f[2]);
451
+ v(f[6], f[7], f[8]);
452
+ v(f[9], f[10], f[11]);
453
+ }
454
+ return out;
455
+ }
456
+ // Capsule: two hemispheres at axial=±1 + cylinder side connecting their equators.
457
+ // Vertex shader does: world.y = unit.y * radius + halfHeight * axial.
458
+ function buildCapsuleSolidGeometry() {
459
+ const slices = 18;
460
+ const capStacks = 6;
461
+ const cylTris = slices * 2; // 1 stack of quads → 2 tris/slice
462
+ const capTris = capStacks * slices * 2;
463
+ const totalVerts = (cylTris + capTris * 2) * 3;
464
+ const out = new Float32Array(totalVerts * 4);
465
+ let p = 0;
466
+ const v = (x, y, z, axial) => {
467
+ out[p++] = x;
468
+ out[p++] = y;
469
+ out[p++] = z;
470
+ out[p++] = axial;
471
+ };
472
+ // Cylinder side: two rings at axial=±1, both at unit y=0.
473
+ for (let l = 0; l < slices; l++) {
474
+ const th0 = (l / slices) * Math.PI * 2;
475
+ const th1 = ((l + 1) / slices) * Math.PI * 2;
476
+ const c0 = Math.cos(th0), s0 = Math.sin(th0);
477
+ const c1 = Math.cos(th1), s1 = Math.sin(th1);
478
+ // bottom-left, top-left, top-right
479
+ v(c0, 0, s0, -1);
480
+ v(c0, 0, s0, +1);
481
+ v(c1, 0, s1, +1);
482
+ // bottom-left, top-right, bottom-right
483
+ v(c0, 0, s0, -1);
484
+ v(c1, 0, s1, +1);
485
+ v(c1, 0, s1, -1);
486
+ }
487
+ // Top hemisphere (axial=+1): φ ∈ [0, π/2], pos = (cosφ cosθ, sinφ, cosφ sinθ).
488
+ for (let s = 0; s < capStacks; s++) {
489
+ const ph0 = (s / capStacks) * (Math.PI / 2);
490
+ const ph1 = ((s + 1) / capStacks) * (Math.PI / 2);
491
+ for (let l = 0; l < slices; l++) {
492
+ const th0 = (l / slices) * Math.PI * 2;
493
+ const th1 = ((l + 1) / slices) * Math.PI * 2;
494
+ const a = (ph, th) => [
495
+ Math.cos(ph) * Math.cos(th), Math.sin(ph), Math.cos(ph) * Math.sin(th),
496
+ ];
497
+ const p00 = a(ph0, th0), p10 = a(ph1, th0), p11 = a(ph1, th1), p01 = a(ph0, th1);
498
+ v(p00[0], p00[1], p00[2], +1);
499
+ v(p10[0], p10[1], p10[2], +1);
500
+ v(p11[0], p11[1], p11[2], +1);
501
+ v(p00[0], p00[1], p00[2], +1);
502
+ v(p11[0], p11[1], p11[2], +1);
503
+ v(p01[0], p01[1], p01[2], +1);
504
+ }
505
+ }
506
+ // Bottom hemisphere (axial=-1): same but y = -sinφ.
507
+ for (let s = 0; s < capStacks; s++) {
508
+ const ph0 = (s / capStacks) * (Math.PI / 2);
509
+ const ph1 = ((s + 1) / capStacks) * (Math.PI / 2);
510
+ for (let l = 0; l < slices; l++) {
511
+ const th0 = (l / slices) * Math.PI * 2;
512
+ const th1 = ((l + 1) / slices) * Math.PI * 2;
513
+ const a = (ph, th) => [
514
+ Math.cos(ph) * Math.cos(th), -Math.sin(ph), Math.cos(ph) * Math.sin(th),
515
+ ];
516
+ const p00 = a(ph0, th0), p10 = a(ph1, th0), p11 = a(ph1, th1), p01 = a(ph0, th1);
517
+ v(p00[0], p00[1], p00[2], -1);
518
+ v(p11[0], p11[1], p11[2], -1);
519
+ v(p10[0], p10[1], p10[2], -1);
520
+ v(p00[0], p00[1], p00[2], -1);
521
+ v(p01[0], p01[1], p01[2], -1);
522
+ v(p11[0], p11[1], p11[2], -1);
523
+ }
524
+ }
525
+ return out;
526
+ }
@@ -0,0 +1,2 @@
1
+ export declare const PHYSICS_DEBUG_SHADER_WGSL = "\nstruct CameraUniforms {\n view: mat4x4f,\n projection: mat4x4f,\n viewPos: vec3f,\n _pad: f32,\n};\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n\noverride SHAPE_KIND: u32 = 0u;\noverride SOLID_ALPHA: f32 = 1.0;\n\nstruct VsIn {\n @location(0) unit: vec4f, // xyz = unit pos; w = capsule axial anchor\n @location(1) modelCol0: vec4f,\n @location(2) modelCol1: vec4f,\n @location(3) modelCol2: vec4f,\n @location(4) modelCol3: vec4f,\n @location(5) size: vec4f, // xyz = size; w unused\n @location(6) color: vec4f,\n};\n\nstruct VsOut {\n @builtin(position) pos: vec4f,\n @location(0) color: vec4f,\n};\n\n@vertex\nfn vsMain(in: VsIn) -> VsOut {\n let model = mat4x4f(in.modelCol0, in.modelCol1, in.modelCol2, in.modelCol3);\n var local: vec3f;\n if (SHAPE_KIND == 1u) {\n local = in.unit.xyz * in.size.xyz;\n } else if (SHAPE_KIND == 2u) {\n // PMX capsule: size.x = radius, size.y = full cylinder height. Bullet 2.x's\n // btCapsuleShape(radius, height) stores height/2 internally, so the actual\n // collision cylinder spans \u00B1size.y/2 \u2014 halve here to match.\n let r = in.size.x;\n let halfH = in.size.y * 0.5;\n local = vec3f(in.unit.x * r, in.unit.y * r + halfH * in.unit.w, in.unit.z * r);\n } else {\n local = in.unit.xyz * in.size.x;\n }\n let world = model * vec4f(local, 1.0);\n var out: VsOut;\n out.pos = camera.projection * camera.view * world;\n out.color = in.color;\n return out;\n}\n\n@fragment\nfn fsMain(in: VsOut) -> @location(0) vec4f {\n return vec4f(in.color.rgb, in.color.a * SOLID_ALPHA);\n}\n";
2
+ //# sourceMappingURL=physics-debug.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"physics-debug.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/physics-debug.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,yBAAyB,2kDAuDrC,CAAA"}
@@ -0,0 +1,69 @@
1
+ // Wireframe + solid overlay for physics rigid bodies (sphere / box / capsule).
2
+ // Rendered in its own pass after composite, straight to the swapchain — no
3
+ // MSAA, no depth, no MRT — so it sits cleanly on top of the tonemapped scene
4
+ // regardless of camera angle, model occlusion, or the composite alpha gate.
5
+ //
6
+ // SHAPE_KIND 0 = sphere → unit-sphere line/tri primitive scaled by size.x (radius)
7
+ // 1 = box → unit cube ±1 scaled by size.xyz (half-extents)
8
+ // 2 = capsule → unit cap+ring with per-vertex `axial` (-1/0/+1)
9
+ // that lifts cap rings/arcs ±halfHeight along Y;
10
+ // XZ scales by size.x (radius), Y by size.y/2.
11
+ // SOLID_ALPHA fragment alpha multiplier. Wireframe pipelines leave this at
12
+ // 1.0 for crisp edges; solid pipelines set it to ~0.2 so the fill
13
+ // stays gentle under stacked bodies.
14
+ export const PHYSICS_DEBUG_SHADER_WGSL = /* wgsl */ `
15
+ struct CameraUniforms {
16
+ view: mat4x4f,
17
+ projection: mat4x4f,
18
+ viewPos: vec3f,
19
+ _pad: f32,
20
+ };
21
+
22
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
23
+
24
+ override SHAPE_KIND: u32 = 0u;
25
+ override SOLID_ALPHA: f32 = 1.0;
26
+
27
+ struct VsIn {
28
+ @location(0) unit: vec4f, // xyz = unit pos; w = capsule axial anchor
29
+ @location(1) modelCol0: vec4f,
30
+ @location(2) modelCol1: vec4f,
31
+ @location(3) modelCol2: vec4f,
32
+ @location(4) modelCol3: vec4f,
33
+ @location(5) size: vec4f, // xyz = size; w unused
34
+ @location(6) color: vec4f,
35
+ };
36
+
37
+ struct VsOut {
38
+ @builtin(position) pos: vec4f,
39
+ @location(0) color: vec4f,
40
+ };
41
+
42
+ @vertex
43
+ fn vsMain(in: VsIn) -> VsOut {
44
+ let model = mat4x4f(in.modelCol0, in.modelCol1, in.modelCol2, in.modelCol3);
45
+ var local: vec3f;
46
+ if (SHAPE_KIND == 1u) {
47
+ local = in.unit.xyz * in.size.xyz;
48
+ } else if (SHAPE_KIND == 2u) {
49
+ // PMX capsule: size.x = radius, size.y = full cylinder height. Bullet 2.x's
50
+ // btCapsuleShape(radius, height) stores height/2 internally, so the actual
51
+ // collision cylinder spans ±size.y/2 — halve here to match.
52
+ let r = in.size.x;
53
+ let halfH = in.size.y * 0.5;
54
+ local = vec3f(in.unit.x * r, in.unit.y * r + halfH * in.unit.w, in.unit.z * r);
55
+ } else {
56
+ local = in.unit.xyz * in.size.x;
57
+ }
58
+ let world = model * vec4f(local, 1.0);
59
+ var out: VsOut;
60
+ out.pos = camera.projection * camera.view * world;
61
+ out.color = in.color;
62
+ return out;
63
+ }
64
+
65
+ @fragment
66
+ fn fsMain(in: VsOut) -> @location(0) vec4f {
67
+ return vec4f(in.color.rgb, in.color.a * SOLID_ALPHA);
68
+ }
69
+ `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -23,7 +23,7 @@
23
23
  "build": "tsc",
24
24
  "dev": "tsc --watch",
25
25
  "test": "node --import ./tests/register.mjs --test tests/*.test.mjs",
26
- "prepublishOnly": "cp ../README.md ./README.md && npm run build"
26
+ "prepublishOnly": "node -e \"require('fs').copyFileSync('../README.md','README.md')\" && npm run build"
27
27
  },
28
28
  "keywords": [
29
29
  "webgpu",
@@ -41,4 +41,4 @@
41
41
  "@types/node": "^20",
42
42
  "typescript": "^5"
43
43
  }
44
- }
44
+ }
package/src/camera.ts CHANGED
@@ -101,10 +101,11 @@ export class Camera {
101
101
  // MMD camera: look at `target` from `distance` back, oriented by the euler rotation.
102
102
  // forward = q·(0,0,1) (LH), up = q·(0,1,0) — read as columns 2 and 1 of the rot matrix.
103
103
  // eye = target + forward·distance (distance is negative in VMD, so eye sits behind).
104
- // NOTE: euler sign/order follows the engine's fromEuler; if a loaded shot looks
105
- // mirrored or rolled, this is the one line to flip (negate axes / reorder).
104
+ // Euler is NEGATED on all three axes: babylon-mmd builds the shot with
105
+ // RotationYawPitchRoll(-ry, -rx, -rz), and our fromEuler(rx,ry,rz) equals
106
+ // RotationYawPitchRoll(ry,rx,rz) — so the MMD-authentic rotation is fromEuler(-r).
106
107
  const r = this._vmdRotation
107
- const q = Quat.fromEuler(r.x, r.y, r.z)
108
+ const q = Quat.fromEuler(-r.x, -r.y, -r.z)
108
109
  Mat4.fromQuatInto(q.x, q.y, q.z, q.w, this._quatScratch, 0)
109
110
  const s = this._quatScratch
110
111
  const t = this._vmdTarget
@@ -244,7 +245,7 @@ export class Camera {
244
245
  }
245
246
 
246
247
  private onMouseMove(e: MouseEvent) {
247
- if (this.inputLocked) return
248
+ if (this.inputLocked || this.vmdDriven) return // VMD owns the camera; orbit/pan is inert
248
249
  if (!this.isDragging) return
249
250
 
250
251
  const deltaX = e.clientX - this.lastMousePos.x
@@ -272,6 +273,7 @@ export class Camera {
272
273
 
273
274
  private onWheel(e: WheelEvent) {
274
275
  e.preventDefault()
276
+ if (this.vmdDriven) return // VMD owns the camera; zoom is inert
275
277
 
276
278
  // Update camera radius (zoom)
277
279
  this.radius += e.deltaY * this.wheelPrecision
@@ -318,6 +320,7 @@ export class Camera {
318
320
  private onTouchMove(e: TouchEvent) {
319
321
  if (this.inputLocked) return
320
322
  e.preventDefault()
323
+ if (this.vmdDriven) return // VMD owns the camera; pinch/pan/rotate is inert
321
324
 
322
325
  if (this.isPinching && e.touches.length === 2) {
323
326
  // Two-finger gesture: can be pinch zoom or pan