reze-engine 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/engine.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Camera } from "./camera";
2
- import { Vec3 } from "./math";
2
+ import { Mat4, Vec3 } from "./math";
3
3
  import { PmxLoader } from "./pmx-loader";
4
4
  export const DEFAULT_ENGINE_OPTIONS = {
5
5
  ambientColor: new Vec3(0.85, 0.85, 0.85),
@@ -21,8 +21,12 @@ export class Engine {
21
21
  // Constants
22
22
  this.STENCIL_EYE_VALUE = 1;
23
23
  this.BLOOM_DOWNSCALE_FACTOR = 2;
24
+ this.groundHasReflections = false;
24
25
  this.cachedSkinMatricesVersion = -1;
25
26
  this.skinMatricesVersion = 0;
27
+ // Double-tap detection
28
+ this.lastTouchTime = 0;
29
+ this.DOUBLE_TAP_DELAY = 300; // ms
26
30
  this.currentModel = null;
27
31
  this.modelDir = "";
28
32
  this.textureCache = new Map();
@@ -42,7 +46,7 @@ export class Engine {
42
46
  };
43
47
  this.animationFrameId = null;
44
48
  this.renderLoopCallback = null;
45
- this.handleCanvasClick = (event) => {
49
+ this.handleCanvasDoubleClick = (event) => {
46
50
  if (!this.onRaycast || !this.currentModel)
47
51
  return;
48
52
  const rect = this.canvas.getBoundingClientRect();
@@ -50,6 +54,31 @@ export class Engine {
50
54
  const y = event.clientY - rect.top;
51
55
  this.performRaycast(x, y);
52
56
  };
57
+ this.handleCanvasTouch = (event) => {
58
+ if (!this.onRaycast || !this.currentModel)
59
+ return;
60
+ // Prevent default to avoid triggering mouse events
61
+ event.preventDefault();
62
+ // Get the first touch
63
+ const touch = event.changedTouches[0];
64
+ if (!touch)
65
+ return;
66
+ const currentTime = Date.now();
67
+ const timeDiff = currentTime - this.lastTouchTime;
68
+ // Check for double-tap (within delay threshold)
69
+ if (timeDiff < this.DOUBLE_TAP_DELAY) {
70
+ const rect = this.canvas.getBoundingClientRect();
71
+ const x = touch.clientX - rect.left;
72
+ const y = touch.clientY - rect.top;
73
+ this.performRaycast(x, y);
74
+ // Reset last touch time to prevent triple-tap triggering double-tap
75
+ this.lastTouchTime = 0;
76
+ }
77
+ else {
78
+ // Single tap - update last touch time for potential double-tap
79
+ this.lastTouchTime = currentTime;
80
+ }
81
+ };
53
82
  this.canvas = canvas;
54
83
  if (options) {
55
84
  this.ambientColor = options.ambientColor ?? DEFAULT_ENGINE_OPTIONS.ambientColor;
@@ -324,6 +353,154 @@ export class Engine {
324
353
  depthCompare: "less-equal",
325
354
  },
326
355
  });
356
+ // Create ground/reflection pipeline with reflection texture support
357
+ this.groundBindGroupLayout = this.device.createBindGroupLayout({
358
+ label: "ground bind group layout",
359
+ entries: [
360
+ { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // camera
361
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // light
362
+ { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // reflectionTexture
363
+ { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // reflectionSampler
364
+ { binding: 4, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // groundMaterial
365
+ ],
366
+ });
367
+ const groundPipelineLayout = this.device.createPipelineLayout({
368
+ label: "ground pipeline layout",
369
+ bindGroupLayouts: [this.groundBindGroupLayout],
370
+ });
371
+ const groundShaderModule = this.device.createShaderModule({
372
+ label: "ground shaders",
373
+ code: /* wgsl */ `
374
+ struct CameraUniforms {
375
+ view: mat4x4f,
376
+ projection: mat4x4f,
377
+ viewPos: vec3f,
378
+ _padding: f32,
379
+ };
380
+
381
+ struct LightUniforms {
382
+ ambientColor: vec4f,
383
+ lights: array<Light, 4>,
384
+ };
385
+
386
+ struct Light {
387
+ direction: vec4f,
388
+ color: vec4f,
389
+ };
390
+
391
+ struct GroundMaterialUniforms {
392
+ diffuseColor: vec3f,
393
+ reflectionLevel: f32,
394
+ fadeStart: f32,
395
+ fadeEnd: f32,
396
+ _padding1: f32,
397
+ _padding2: f32,
398
+ };
399
+
400
+ @group(0) @binding(0) var<uniform> camera: CameraUniforms;
401
+ @group(0) @binding(1) var<uniform> light: LightUniforms;
402
+ @group(0) @binding(2) var reflectionTexture: texture_2d<f32>;
403
+ @group(0) @binding(3) var reflectionSampler: sampler;
404
+ @group(0) @binding(4) var<uniform> material: GroundMaterialUniforms;
405
+
406
+ struct VertexOutput {
407
+ @builtin(position) position: vec4f,
408
+ @location(0) normal: vec3f,
409
+ @location(1) uv: vec2f,
410
+ @location(2) worldPos: vec3f,
411
+ };
412
+
413
+ @vertex fn vs(
414
+ @location(0) position: vec3f,
415
+ @location(1) normal: vec3f,
416
+ @location(2) uv: vec2f,
417
+ ) -> VertexOutput {
418
+ var output: VertexOutput;
419
+ let worldPos = position;
420
+ output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
421
+ output.normal = normal;
422
+ output.uv = uv;
423
+ output.worldPos = worldPos;
424
+ return output;
425
+ }
426
+
427
+ @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
428
+ let n = normalize(input.normal);
429
+
430
+ let clipPos = camera.projection * camera.view * vec4f(input.worldPos, 1.0);
431
+ let ndcPos = clipPos.xyz / clipPos.w;
432
+ var reflectionUV = vec2f(ndcPos.x * 0.5 + 0.5, 0.5 - ndcPos.y * 0.5);
433
+
434
+ let sampledReflectionColor = textureSample(reflectionTexture, reflectionSampler, reflectionUV).rgb;
435
+ let isValidReflection = clipPos.w > 0.0 &&
436
+ all(reflectionUV >= vec2f(0.0)) && all(reflectionUV <= vec2f(1.0));
437
+ var reflectionColor = select(vec3f(1.0, 1.0, 1.0), sampledReflectionColor, isValidReflection);
438
+
439
+ let distanceFromCamera = length(input.worldPos - camera.viewPos);
440
+ let fadeFactor = clamp((distanceFromCamera - 15.0) / 20.0, 0.0, 1.0);
441
+ reflectionColor *= (1.0 - fadeFactor * 0.3);
442
+
443
+ let diffuseColor = material.diffuseColor;
444
+ var finalColor = mix(diffuseColor, reflectionColor, material.reflectionLevel);
445
+
446
+ // Ground edge fade effect - smooth fade out at edges based on distance from center
447
+ let centerDist = length(input.worldPos.xz); // Distance from ground center in XZ plane
448
+
449
+ // Smoothstep for much smoother gradient transition
450
+ let t = clamp((centerDist - material.fadeStart) / (material.fadeEnd - material.fadeStart), 0.0, 1.0);
451
+ let edgeFade = 1.0 - smoothstep(0.0, 1.0, t);
452
+ finalColor *= edgeFade;
453
+
454
+ // Accumulate light contribution
455
+ var lightAccum = light.ambientColor.xyz;
456
+ for (var i = 0u; i < 4u; i++) {
457
+ let intensity = light.lights[i].color.w;
458
+ if (intensity > 0.0) {
459
+ let l = -light.lights[i].direction.xyz;
460
+ let nDotL = max(dot(n, l), 0.0);
461
+ let radiance = light.lights[i].color.xyz * intensity;
462
+ lightAccum += radiance * nDotL;
463
+ }
464
+ }
465
+
466
+ // Apply lighting to the blended color
467
+ let litColor = finalColor * lightAccum;
468
+
469
+ return vec4f(litColor, edgeFade);
470
+ }
471
+ `,
472
+ });
473
+ this.groundPipeline = this.createRenderPipeline({
474
+ label: "ground pipeline",
475
+ layout: groundPipelineLayout,
476
+ shaderModule: groundShaderModule,
477
+ vertexBuffers: fullVertexBuffers,
478
+ fragmentTarget: standardBlend,
479
+ cullMode: "back",
480
+ depthStencil: {
481
+ format: "depth24plus-stencil8",
482
+ depthWriteEnabled: true,
483
+ depthCompare: "less-equal",
484
+ },
485
+ });
486
+ // Create reflection pipeline (multisampled version for higher quality)
487
+ this.reflectionPipeline = this.createRenderPipeline({
488
+ label: "reflection pipeline",
489
+ layout: mainPipelineLayout,
490
+ shaderModule,
491
+ vertexBuffers: fullVertexBuffers,
492
+ fragmentTarget: {
493
+ format: this.presentationFormat,
494
+ blend: standardBlend.blend,
495
+ },
496
+ multisample: { count: this.sampleCount }, // Use same multisampling as main render
497
+ cullMode: "none",
498
+ depthStencil: {
499
+ format: "depth24plus-stencil8",
500
+ depthWriteEnabled: true,
501
+ depthCompare: "less-equal",
502
+ },
503
+ });
327
504
  // Create bind group layout for outline pipelines
328
505
  this.outlineBindGroupLayout = this.device.createBindGroupLayout({
329
506
  label: "outline bind group layout",
@@ -849,9 +1026,10 @@ export class Engine {
849
1026
  this.resizeObserver = new ResizeObserver(() => this.handleResize());
850
1027
  this.resizeObserver.observe(this.canvas);
851
1028
  this.handleResize();
852
- // Setup raycasting click handler
1029
+ // Setup raycasting double-click handler for desktop
853
1030
  if (this.onRaycast) {
854
- this.canvas.addEventListener("click", this.handleCanvasClick);
1031
+ this.canvas.addEventListener("dblclick", this.handleCanvasDoubleClick);
1032
+ this.canvas.addEventListener("touchend", this.handleCanvasTouch);
855
1033
  }
856
1034
  }
857
1035
  handleResize() {
@@ -980,6 +1158,30 @@ export class Engine {
980
1158
  this.updateLightBuffer();
981
1159
  return true;
982
1160
  }
1161
+ addGround(options) {
1162
+ const opts = {
1163
+ width: 100,
1164
+ height: 100,
1165
+ diffuseColor: new Vec3(1, 1, 1),
1166
+ reflectionLevel: 0.5,
1167
+ reflectionTextureSize: 1024,
1168
+ fadeStart: 5.0,
1169
+ fadeEnd: 60.0,
1170
+ ...options,
1171
+ };
1172
+ // Create ground geometry
1173
+ this.createGroundGeometry(opts.width, opts.height);
1174
+ this.createGroundMaterialBuffer(opts.diffuseColor, opts.reflectionLevel, opts.fadeStart, opts.fadeEnd);
1175
+ this.createReflectionTexture(opts.reflectionTextureSize);
1176
+ this.groundHasReflections = true;
1177
+ this.drawCalls.push({
1178
+ type: "ground",
1179
+ count: 6, // 2 triangles, 3 indices each
1180
+ firstIndex: 0,
1181
+ bindGroup: this.groundReflectionBindGroup,
1182
+ materialName: "Ground",
1183
+ });
1184
+ }
983
1185
  updateLightBuffer() {
984
1186
  this.device.queue.writeBuffer(this.lightUniformBuffer, 0, this.lightData);
985
1187
  }
@@ -1029,6 +1231,11 @@ export class Engine {
1029
1231
  this.stopAnimation();
1030
1232
  if (this.camera)
1031
1233
  this.camera.detachControl();
1234
+ // Remove raycasting event listeners
1235
+ if (this.onRaycast) {
1236
+ this.canvas.removeEventListener("dblclick", this.handleCanvasDoubleClick);
1237
+ this.canvas.removeEventListener("touchend", this.handleCanvasTouch);
1238
+ }
1032
1239
  if (this.resizeObserver) {
1033
1240
  this.resizeObserver.disconnect();
1034
1241
  this.resizeObserver = null;
@@ -1142,6 +1349,124 @@ export class Engine {
1142
1349
  }
1143
1350
  await this.setupMaterials(model);
1144
1351
  }
1352
+ createGroundGeometry(width = 100, height = 100) {
1353
+ const halfWidth = width / 2;
1354
+ const halfHeight = height / 2;
1355
+ const vertices = new Float32Array([
1356
+ // Bottom-left
1357
+ -halfWidth,
1358
+ 0,
1359
+ -halfHeight, // position
1360
+ 0,
1361
+ 1,
1362
+ 0, // normal (up)
1363
+ 0,
1364
+ 0, // uv
1365
+ // Bottom-right
1366
+ halfWidth,
1367
+ 0,
1368
+ -halfHeight, // position
1369
+ 0,
1370
+ 1,
1371
+ 0, // normal (up)
1372
+ 1,
1373
+ 0, // uv
1374
+ // Top-right
1375
+ halfWidth,
1376
+ 0,
1377
+ halfHeight, // position
1378
+ 0,
1379
+ 1,
1380
+ 0, // normal (up)
1381
+ 1,
1382
+ 1, // uv
1383
+ // Top-left
1384
+ -halfWidth,
1385
+ 0,
1386
+ halfHeight, // position
1387
+ 0,
1388
+ 1,
1389
+ 0, // normal (up)
1390
+ 0,
1391
+ 1, // uv
1392
+ ]);
1393
+ // Create indices for two triangles
1394
+ const indices = new Uint16Array([
1395
+ 0,
1396
+ 1,
1397
+ 2, // First triangle
1398
+ 0,
1399
+ 2,
1400
+ 3, // Second triangle
1401
+ ]);
1402
+ // Create vertex buffer
1403
+ this.groundVertexBuffer = this.device.createBuffer({
1404
+ label: "ground vertex buffer",
1405
+ size: vertices.byteLength,
1406
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
1407
+ });
1408
+ this.device.queue.writeBuffer(this.groundVertexBuffer, 0, vertices);
1409
+ this.groundIndexBuffer = this.device.createBuffer({
1410
+ label: "ground index buffer",
1411
+ size: indices.byteLength,
1412
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
1413
+ });
1414
+ this.device.queue.writeBuffer(this.groundIndexBuffer, 0, indices);
1415
+ }
1416
+ createGroundMaterialBuffer(diffuseColor = new Vec3(1, 1, 1), reflectionLevel = 0.5, fadeStart = 5.0, fadeEnd = 60.0) {
1417
+ const materialData = new Float32Array([
1418
+ diffuseColor.x,
1419
+ diffuseColor.y,
1420
+ diffuseColor.z, // diffuseColor (12 bytes)
1421
+ reflectionLevel, // reflectionLevel (4 bytes)
1422
+ fadeStart, // fadeStart (4 bytes)
1423
+ fadeEnd, // fadeEnd (4 bytes)
1424
+ 0, // padding (4 bytes)
1425
+ 0, // padding (4 bytes)
1426
+ 0, // padding (4 bytes)
1427
+ 0, // padding (4 bytes)
1428
+ ]);
1429
+ this.groundMaterialUniformBuffer = this.device.createBuffer({
1430
+ label: "ground material uniform buffer",
1431
+ size: materialData.byteLength,
1432
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1433
+ });
1434
+ this.device.queue.writeBuffer(this.groundMaterialUniformBuffer, 0, materialData);
1435
+ }
1436
+ createReflectionTexture(size = 1024) {
1437
+ this.groundReflectionTexture = this.device.createTexture({
1438
+ label: "ground reflection texture",
1439
+ size: [size, size],
1440
+ sampleCount: this.sampleCount,
1441
+ format: this.presentationFormat,
1442
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
1443
+ });
1444
+ this.groundReflectionResolveTexture = this.device.createTexture({
1445
+ label: "ground reflection resolve texture",
1446
+ size: [size, size],
1447
+ format: this.presentationFormat,
1448
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
1449
+ });
1450
+ this.groundReflectionDepthTexture = this.device.createTexture({
1451
+ label: "ground reflection depth texture",
1452
+ size: [size, size],
1453
+ sampleCount: this.sampleCount,
1454
+ format: "depth24plus-stencil8",
1455
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
1456
+ });
1457
+ // Create a bind group for the reflection texture that can be used in the ground material
1458
+ this.groundReflectionBindGroup = this.device.createBindGroup({
1459
+ label: "ground reflection bind group",
1460
+ layout: this.groundBindGroupLayout,
1461
+ entries: [
1462
+ { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1463
+ { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1464
+ { binding: 2, resource: this.groundReflectionResolveTexture.createView() }, // Use resolve texture for sampling
1465
+ { binding: 3, resource: this.materialSampler },
1466
+ { binding: 4, resource: { buffer: this.groundMaterialUniformBuffer } },
1467
+ ],
1468
+ });
1469
+ }
1145
1470
  async setupMaterials(model) {
1146
1471
  const materials = model.getMaterials();
1147
1472
  if (materials.length === 0) {
@@ -1335,18 +1660,119 @@ export class Engine {
1335
1660
  }
1336
1661
  }
1337
1662
  // Helper: Render eyes with stencil writing (for post-alpha-eye effect)
1338
- renderEyes(pass) {
1339
- pass.setPipeline(this.eyePipeline);
1340
- pass.setStencilReference(this.STENCIL_EYE_VALUE);
1663
+ renderEyes(pass, useReflectionPipeline = false) {
1664
+ if (useReflectionPipeline) {
1665
+ // For reflections, use the basic reflection pipeline instead of specialized eye pipeline
1666
+ pass.setPipeline(this.reflectionPipeline);
1667
+ for (const draw of this.drawCalls) {
1668
+ if (draw.type === "eye") {
1669
+ pass.setBindGroup(0, draw.bindGroup);
1670
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1671
+ }
1672
+ }
1673
+ }
1674
+ else {
1675
+ pass.setPipeline(this.eyePipeline);
1676
+ pass.setStencilReference(this.STENCIL_EYE_VALUE);
1677
+ for (const draw of this.drawCalls) {
1678
+ if (draw.type === "eye" && this.shouldRenderDrawCall(draw)) {
1679
+ pass.setBindGroup(0, draw.bindGroup);
1680
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1681
+ }
1682
+ }
1683
+ }
1684
+ }
1685
+ renderGround(pass) {
1686
+ if (!this.groundHasReflections || !this.groundVertexBuffer || !this.groundIndexBuffer) {
1687
+ return;
1688
+ }
1689
+ if (this.groundReflectionTexture) {
1690
+ this.renderReflectionTexture();
1691
+ }
1692
+ pass.setPipeline(this.groundPipeline);
1693
+ pass.setVertexBuffer(0, this.groundVertexBuffer);
1694
+ pass.setIndexBuffer(this.groundIndexBuffer, "uint16");
1341
1695
  for (const draw of this.drawCalls) {
1342
- if (draw.type === "eye" && this.shouldRenderDrawCall(draw)) {
1696
+ if (draw.type === "ground" && this.shouldRenderDrawCall(draw)) {
1343
1697
  pass.setBindGroup(0, draw.bindGroup);
1344
1698
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1345
1699
  }
1346
1700
  }
1347
1701
  }
1702
+ renderReflectionTexture() {
1703
+ if (!this.groundReflectionTexture)
1704
+ return;
1705
+ const mirrorMatrix = this.createMirrorMatrix(new Vec3(0, 1, 0), 0);
1706
+ this.updateCameraUniforms();
1707
+ const reflectionEncoder = this.device.createCommandEncoder();
1708
+ const reflectionPassDescriptor = {
1709
+ label: "reflection render pass",
1710
+ colorAttachments: [
1711
+ {
1712
+ view: this.groundReflectionTexture.createView(),
1713
+ resolveTarget: this.groundReflectionResolveTexture.createView(),
1714
+ clearValue: { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, // White
1715
+ loadOp: "clear",
1716
+ storeOp: "store",
1717
+ },
1718
+ ],
1719
+ depthStencilAttachment: {
1720
+ view: this.groundReflectionDepthTexture.createView(),
1721
+ depthClearValue: 1.0,
1722
+ depthLoadOp: "clear",
1723
+ depthStoreOp: "store",
1724
+ stencilClearValue: 0,
1725
+ stencilLoadOp: "clear",
1726
+ stencilStoreOp: "discard",
1727
+ },
1728
+ };
1729
+ const reflectionPass = reflectionEncoder.beginRenderPass(reflectionPassDescriptor);
1730
+ if (this.currentModel) {
1731
+ reflectionPass.setVertexBuffer(0, this.vertexBuffer);
1732
+ reflectionPass.setVertexBuffer(1, this.jointsBuffer);
1733
+ reflectionPass.setVertexBuffer(2, this.weightsBuffer);
1734
+ reflectionPass.setIndexBuffer(this.indexBuffer, "uint32");
1735
+ this.writeMirrorTransformedSkinMatrices(mirrorMatrix);
1736
+ reflectionPass.setPipeline(this.reflectionPipeline);
1737
+ for (const draw of this.drawCalls) {
1738
+ if (draw.type === "opaque" && this.shouldRenderDrawCall(draw)) {
1739
+ reflectionPass.setBindGroup(0, draw.bindGroup);
1740
+ reflectionPass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1741
+ }
1742
+ }
1743
+ // Render eyes (using reflection pipeline)
1744
+ this.renderEyes(reflectionPass, true);
1745
+ // Render hair (using reflection pipeline)
1746
+ this.renderHair(reflectionPass, true);
1747
+ // Render transparent objects
1748
+ for (const draw of this.drawCalls) {
1749
+ if (draw.type === "transparent" && this.shouldRenderDrawCall(draw)) {
1750
+ reflectionPass.setBindGroup(0, draw.bindGroup);
1751
+ reflectionPass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1752
+ }
1753
+ }
1754
+ this.drawOutlines(reflectionPass, true, true);
1755
+ }
1756
+ reflectionPass.end();
1757
+ // Submit reflection rendering commands
1758
+ const reflectionCommandBuffer = reflectionEncoder.finish();
1759
+ this.device.queue.submit([reflectionCommandBuffer]);
1760
+ // Restore original skin matrices
1761
+ this.updateSkinMatrices();
1762
+ }
1348
1763
  // Helper: Render hair with post-alpha-eye effect (depth pre-pass + stencil-based shading + outlines)
1349
- renderHair(pass) {
1764
+ renderHair(pass, useReflectionPipeline = false) {
1765
+ if (useReflectionPipeline) {
1766
+ // For reflections, use the basic reflection pipeline for all hair
1767
+ pass.setPipeline(this.reflectionPipeline);
1768
+ for (const draw of this.drawCalls) {
1769
+ if (draw.type === "hair-over-eyes" || draw.type === "hair-over-non-eyes") {
1770
+ pass.setBindGroup(0, draw.bindGroup);
1771
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1772
+ }
1773
+ }
1774
+ return;
1775
+ }
1350
1776
  // Hair depth pre-pass (reduces overdraw via early depth rejection)
1351
1777
  const hasHair = this.drawCalls.some((d) => (d.type === "hair-over-eyes" || d.type === "hair-over-non-eyes") && this.shouldRenderDrawCall(d));
1352
1778
  if (hasHair) {
@@ -1396,7 +1822,7 @@ export class Engine {
1396
1822
  // Get camera matrices
1397
1823
  const viewMatrix = this.camera.getViewMatrix();
1398
1824
  const projectionMatrix = this.camera.getProjectionMatrix();
1399
- // Convert screen coordinates to world space ray (like Babylon.js)
1825
+ // Convert screen coordinates to world space ray
1400
1826
  const canvas = this.canvas;
1401
1827
  const rect = canvas.getBoundingClientRect();
1402
1828
  // Convert to clip space (-1 to 1)
@@ -1430,7 +1856,7 @@ export class Engine {
1430
1856
  const skinning = this.currentModel.getSkinning();
1431
1857
  if (!baseVertices || !indices || !skinning) {
1432
1858
  if (this.onRaycast) {
1433
- this.onRaycast(null);
1859
+ this.onRaycast(null, screenX, screenY);
1434
1860
  }
1435
1861
  return;
1436
1862
  }
@@ -1556,7 +1982,7 @@ export class Engine {
1556
1982
  }
1557
1983
  // Call the callback with the result
1558
1984
  if (this.onRaycast) {
1559
- this.onRaycast(closestHit?.materialName || null);
1985
+ this.onRaycast(closestHit?.materialName || null, screenX, screenY);
1560
1986
  }
1561
1987
  }
1562
1988
  // Render strategy: 1) Opaque non-eye/hair 2) Eyes (stencil=1) 3) Hair (depth pre-pass + split by stencil) 4) Transparent 5) Bloom
@@ -1602,7 +2028,11 @@ export class Engine {
1602
2028
  this.drawOutlines(pass, false);
1603
2029
  // Pass 3: Hair rendering (depth pre-pass + shading + outlines)
1604
2030
  this.renderHair(pass);
1605
- // Pass 4: Transparent
2031
+ // Pass 4: Ground (with reflections)
2032
+ if (this.groundHasReflections) {
2033
+ this.renderGround(pass);
2034
+ }
2035
+ // Pass 5: Transparent
1606
2036
  pass.setPipeline(this.modelPipeline);
1607
2037
  for (const draw of this.drawCalls) {
1608
2038
  if (draw.type === "transparent" && this.shouldRenderDrawCall(draw)) {
@@ -1733,7 +2163,11 @@ export class Engine {
1733
2163
  // Increment version to invalidate cached skinned vertices
1734
2164
  this.skinMatricesVersion++;
1735
2165
  }
1736
- drawOutlines(pass, transparent) {
2166
+ drawOutlines(pass, transparent, useReflectionPipeline = false) {
2167
+ if (useReflectionPipeline) {
2168
+ // Skip outlines for reflections - not critical for the effect
2169
+ return;
2170
+ }
1737
2171
  pass.setPipeline(this.outlinePipeline);
1738
2172
  const outlineType = transparent ? "transparent-outline" : "opaque-outline";
1739
2173
  for (const draw of this.drawCalls) {
@@ -1765,4 +2199,44 @@ export class Engine {
1765
2199
  this.lastFpsUpdate = now;
1766
2200
  }
1767
2201
  }
2202
+ createMirrorMatrix(planeNormal, planeDistance) {
2203
+ // Create reflection matrix across a plane
2204
+ const n = planeNormal.normalize();
2205
+ return new Mat4(new Float32Array([
2206
+ 1 - 2 * n.x * n.x,
2207
+ -2 * n.x * n.y,
2208
+ -2 * n.x * n.z,
2209
+ 0,
2210
+ -2 * n.y * n.x,
2211
+ 1 - 2 * n.y * n.y,
2212
+ -2 * n.y * n.z,
2213
+ 0,
2214
+ -2 * n.z * n.x,
2215
+ -2 * n.z * n.y,
2216
+ 1 - 2 * n.z * n.z,
2217
+ 0,
2218
+ -2 * planeDistance * n.x,
2219
+ -2 * planeDistance * n.y,
2220
+ -2 * planeDistance * n.z,
2221
+ 1,
2222
+ ]));
2223
+ }
2224
+ writeMirrorTransformedSkinMatrices(mirrorMatrix) {
2225
+ if (!this.currentModel || !this.skinMatrixBuffer)
2226
+ return;
2227
+ const originalMatrices = this.currentModel.getSkinMatrices();
2228
+ const transformedMatrices = new Float32Array(originalMatrices.length);
2229
+ for (let i = 0; i < originalMatrices.length; i += 16) {
2230
+ const boneMatrixValues = new Float32Array(16);
2231
+ for (let j = 0; j < 16; j++) {
2232
+ boneMatrixValues[j] = originalMatrices[i + j];
2233
+ }
2234
+ const boneMatrix = new Mat4(boneMatrixValues);
2235
+ const transformed = mirrorMatrix.multiply(boneMatrix);
2236
+ for (let j = 0; j < 16; j++) {
2237
+ transformedMatrices[i + j] = transformed.values[j];
2238
+ }
2239
+ }
2240
+ this.device.queue.writeBuffer(this.skinMatrixBuffer, 0, transformedMatrices);
2241
+ }
1768
2242
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "A WebGPU-based MMD model renderer",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -41,6 +41,5 @@
41
41
  "devDependencies": {
42
42
  "@types/node": "^20",
43
43
  "typescript": "^5"
44
- },
45
- "peerDependencies": {}
44
+ }
46
45
  }