reze-engine 0.3.17 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/engine.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Quat, Vec3 } from "./math";
2
+ export type RaycastCallback = (material: string | null, screenX: number, screenY: number) => void;
2
3
  export type EngineOptions = {
3
4
  ambientColor?: Vec3;
4
5
  bloomIntensity?: number;
@@ -7,8 +8,10 @@ export type EngineOptions = {
7
8
  cameraDistance?: number;
8
9
  cameraTarget?: Vec3;
9
10
  cameraFov?: number;
11
+ onRaycast?: RaycastCallback;
10
12
  };
11
- export declare const DEFAULT_ENGINE_OPTIONS: Required<EngineOptions>;
13
+ export type RequiredEngineOptions = Required<Omit<EngineOptions, "onRaycast">> & Pick<EngineOptions, "onRaycast">;
14
+ export declare const DEFAULT_ENGINE_OPTIONS: RequiredEngineOptions;
12
15
  export interface EngineStats {
13
16
  fps: number;
14
17
  frameTime: number;
@@ -69,6 +72,12 @@ export declare class Engine {
69
72
  private bloomThreshold;
70
73
  private bloomIntensity;
71
74
  private rimLightIntensity;
75
+ private onRaycast?;
76
+ private cachedSkinnedVertices?;
77
+ private cachedSkinMatricesVersion;
78
+ private skinMatricesVersion;
79
+ private lastTouchTime;
80
+ private readonly DOUBLE_TAP_DELAY;
72
81
  private currentModel;
73
82
  private modelDir;
74
83
  private materialSampler;
@@ -130,6 +139,9 @@ export declare class Engine {
130
139
  private createTextureFromPath;
131
140
  private renderEyes;
132
141
  private renderHair;
142
+ private handleCanvasDoubleClick;
143
+ private handleCanvasTouch;
144
+ private performRaycast;
133
145
  render(): void;
134
146
  private applyBloom;
135
147
  private updateCameraUniforms;
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAInC,MAAM,MAAM,aAAa,GAAG;IAC1B,YAAY,CAAC,EAAE,IAAI,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,YAAY,CAAC,EAAE,IAAI,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAAC,aAAa,CAQ1D,CAAA;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;CAClB;AAqBD,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,kBAAkB,CAAmB;IAC7C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,mBAAmB,CAAY;IACvC,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,kBAAkB,CAAY;IACtC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,UAAU,CAAI;IACtB,OAAO,CAAC,YAAY,CAAY;IAChC,OAAO,CAAC,WAAW,CAAC,CAAW;IAC/B,OAAO,CAAC,cAAc,CAA8B;IACpD,OAAO,CAAC,YAAY,CAAa;IAEjC,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,oBAAoB,CAAoB;IAChD,OAAO,CAAC,uBAAuB,CAAoB;IACnD,OAAO,CAAC,iBAAiB,CAAoB;IAE7C,OAAO,CAAC,eAAe,CAAoB;IAC3C,OAAO,CAAC,mBAAmB,CAAoB;IAC/C,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,sBAAsB,CAAqB;IACnD,OAAO,CAAC,YAAY,CAAY;IAChC,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,gBAAgB,CAAC,CAAW;IACpC,OAAO,CAAC,uBAAuB,CAAC,CAAW;IAC3C,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAI;IAChC,OAAO,CAAC,oBAAoB,CAA0B;IAEtD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAI;IACtC,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAI;IAG3C,OAAO,CAAC,YAAY,CAAO;IAE3B,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,sBAAsB,CAAiB;IAC/C,OAAO,CAAC,mBAAmB,CAAa;IACxC,OAAO,CAAC,iBAAiB,CAAa;IACtC,OAAO,CAAC,iBAAiB,CAAa;IAEtC,OAAO,CAAC,oBAAoB,CAAoB;IAChD,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,oBAAoB,CAAoB;IAChD,OAAO,CAAC,mBAAmB,CAAY;IACvC,OAAO,CAAC,oBAAoB,CAAY;IACxC,OAAO,CAAC,oBAAoB,CAAY;IACxC,OAAO,CAAC,aAAa,CAAa;IAElC,OAAO,CAAC,qBAAqB,CAAC,CAAc;IAC5C,OAAO,CAAC,mBAAmB,CAAC,CAAc;IAC1C,OAAO,CAAC,mBAAmB,CAAC,CAAc;IAC1C,OAAO,CAAC,qBAAqB,CAAC,CAAc;IAE5C,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,iBAAiB,CAAS;IAElC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,eAAe,CAAa;IACpC,OAAO,CAAC,YAAY,CAAgC;IACpD,OAAO,CAAC,uBAAuB,CAAQ;IAEvC,OAAO,CAAC,SAAS,CAAiB;IAElC,OAAO,CAAC,eAAe,CAAoB;IAE3C,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,qBAAqB,CAAI;IACjC,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,YAAY,CAAI;IACxB,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,KAAK,CAGZ;IACD,OAAO,CAAC,gBAAgB,CAAsB;IAC9C,OAAO,CAAC,kBAAkB,CAA4B;gBAE1C,MAAM,EAAE,iBAAiB,EAAE,OAAO,CAAC,EAAE,aAAa;IAcjD,IAAI;IA6BjB,OAAO,CAAC,oBAAoB;IA+B5B,OAAO,CAAC,eAAe;IA4dvB,OAAO,CAAC,oBAAoB;IA4O5B,OAAO,CAAC,UAAU;IA+DlB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,YAAY;IA+EpB,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,aAAa;IAiBrB,OAAO,CAAC,eAAe;IAShB,WAAW;IAUX,QAAQ,CAAC,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,GAAE,MAAY,GAAG,OAAO;IAmB/E,OAAO,CAAC,iBAAiB;IAIZ,aAAa,CAAC,GAAG,EAAE,MAAM;IAK/B,aAAa;IAIb,aAAa;IAIb,cAAc;IAId,aAAa,CAAC,IAAI,EAAE,MAAM;IAI1B,oBAAoB;;;;;IAIpB,QAAQ,IAAI,WAAW;IAIvB,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI;IAgBnC,cAAc;IAQd,OAAO;IAWD,SAAS,CAAC,IAAI,EAAE,MAAM;IAW5B,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM;IAKnE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,oBAAoB,EAAE,IAAI,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM;IAI5E,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI;IAQvE,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAQxD,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIxC,QAAQ,IAAI,MAAM,EAAE;IAIpB,SAAS,IAAI,MAAM,EAAE;IAIrB,YAAY,IAAI,MAAM,EAAE;IAI/B,OAAO,CAAC,kBAAkB;YAQZ,iBAAiB;YA8EjB,cAAc;IAoK5B,OAAO,CAAC,2BAA2B;IAMnC,OAAO,CAAC,mBAAmB;IAU3B,OAAO,CAAC,oBAAoB;YAId,qBAAqB;IAmCnC,OAAO,CAAC,UAAU;IAYlB,OAAO,CAAC,UAAU;IAkDX,MAAM;IA2Eb,OAAO,CAAC,UAAU;IAmGlB,OAAO,CAAC,oBAAoB;IAY5B,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,kBAAkB;IAa1B,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,WAAW;CAwBpB"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
package/dist/engine.js CHANGED
@@ -9,6 +9,7 @@ export const DEFAULT_ENGINE_OPTIONS = {
9
9
  cameraDistance: 26.6,
10
10
  cameraTarget: new Vec3(0, 12.5, 0),
11
11
  cameraFov: Math.PI / 4,
12
+ onRaycast: undefined,
12
13
  };
13
14
  export class Engine {
14
15
  constructor(canvas, options) {
@@ -20,6 +21,11 @@ export class Engine {
20
21
  // Constants
21
22
  this.STENCIL_EYE_VALUE = 1;
22
23
  this.BLOOM_DOWNSCALE_FACTOR = 2;
24
+ this.cachedSkinMatricesVersion = -1;
25
+ this.skinMatricesVersion = 0;
26
+ // Double-tap detection
27
+ this.lastTouchTime = 0;
28
+ this.DOUBLE_TAP_DELAY = 300; // ms
23
29
  this.currentModel = null;
24
30
  this.modelDir = "";
25
31
  this.textureCache = new Map();
@@ -39,6 +45,39 @@ export class Engine {
39
45
  };
40
46
  this.animationFrameId = null;
41
47
  this.renderLoopCallback = null;
48
+ this.handleCanvasDoubleClick = (event) => {
49
+ if (!this.onRaycast || !this.currentModel)
50
+ return;
51
+ const rect = this.canvas.getBoundingClientRect();
52
+ const x = event.clientX - rect.left;
53
+ const y = event.clientY - rect.top;
54
+ this.performRaycast(x, y);
55
+ };
56
+ this.handleCanvasTouch = (event) => {
57
+ if (!this.onRaycast || !this.currentModel)
58
+ return;
59
+ // Prevent default to avoid triggering mouse events
60
+ event.preventDefault();
61
+ // Get the first touch
62
+ const touch = event.changedTouches[0];
63
+ if (!touch)
64
+ return;
65
+ const currentTime = Date.now();
66
+ const timeDiff = currentTime - this.lastTouchTime;
67
+ // Check for double-tap (within delay threshold)
68
+ if (timeDiff < this.DOUBLE_TAP_DELAY) {
69
+ const rect = this.canvas.getBoundingClientRect();
70
+ const x = touch.clientX - rect.left;
71
+ const y = touch.clientY - rect.top;
72
+ this.performRaycast(x, y);
73
+ // Reset last touch time to prevent triple-tap triggering double-tap
74
+ this.lastTouchTime = 0;
75
+ }
76
+ else {
77
+ // Single tap - update last touch time for potential double-tap
78
+ this.lastTouchTime = currentTime;
79
+ }
80
+ };
42
81
  this.canvas = canvas;
43
82
  if (options) {
44
83
  this.ambientColor = options.ambientColor ?? DEFAULT_ENGINE_OPTIONS.ambientColor;
@@ -48,6 +87,7 @@ export class Engine {
48
87
  this.cameraDistance = options.cameraDistance ?? DEFAULT_ENGINE_OPTIONS.cameraDistance;
49
88
  this.cameraTarget = options.cameraTarget ?? DEFAULT_ENGINE_OPTIONS.cameraTarget;
50
89
  this.cameraFov = options.cameraFov ?? DEFAULT_ENGINE_OPTIONS.cameraFov;
90
+ this.onRaycast = options.onRaycast;
51
91
  }
52
92
  }
53
93
  // Step 1: Get WebGPU device and context
@@ -837,6 +877,11 @@ export class Engine {
837
877
  this.resizeObserver = new ResizeObserver(() => this.handleResize());
838
878
  this.resizeObserver.observe(this.canvas);
839
879
  this.handleResize();
880
+ // Setup raycasting double-click handler for desktop
881
+ if (this.onRaycast) {
882
+ this.canvas.addEventListener("dblclick", this.handleCanvasDoubleClick);
883
+ this.canvas.addEventListener("touchend", this.handleCanvasTouch);
884
+ }
840
885
  }
841
886
  handleResize() {
842
887
  const displayWidth = this.canvas.clientWidth;
@@ -1013,6 +1058,11 @@ export class Engine {
1013
1058
  this.stopAnimation();
1014
1059
  if (this.camera)
1015
1060
  this.camera.detachControl();
1061
+ // Remove raycasting event listeners
1062
+ if (this.onRaycast) {
1063
+ this.canvas.removeEventListener("dblclick", this.handleCanvasDoubleClick);
1064
+ this.canvas.removeEventListener("touchend", this.handleCanvasTouch);
1065
+ }
1016
1066
  if (this.resizeObserver) {
1017
1067
  this.resizeObserver.disconnect();
1018
1068
  this.resizeObserver = null;
@@ -1026,6 +1076,9 @@ export class Engine {
1026
1076
  this.modelDir = dir;
1027
1077
  const model = await PmxLoader.load(path);
1028
1078
  console.log(model);
1079
+ // Clear cached skinned vertices when loading a new model
1080
+ this.cachedSkinnedVertices = undefined;
1081
+ this.cachedSkinMatricesVersion = -1;
1029
1082
  await this.setupModelBuffers(model);
1030
1083
  }
1031
1084
  rotateBones(bones, rotations, durationMs) {
@@ -1368,6 +1421,178 @@ export class Engine {
1368
1421
  }
1369
1422
  }
1370
1423
  }
1424
+ performRaycast(screenX, screenY) {
1425
+ if (!this.currentModel || !this.onRaycast)
1426
+ return;
1427
+ const materials = this.currentModel.getMaterials();
1428
+ if (materials.length === 0)
1429
+ return;
1430
+ // Get camera matrices
1431
+ const viewMatrix = this.camera.getViewMatrix();
1432
+ const projectionMatrix = this.camera.getProjectionMatrix();
1433
+ // Convert screen coordinates to world space ray (like Babylon.js)
1434
+ const canvas = this.canvas;
1435
+ const rect = canvas.getBoundingClientRect();
1436
+ // Convert to clip space (-1 to 1)
1437
+ const clipX = (screenX / rect.width) * 2 - 1;
1438
+ const clipY = 1 - (screenY / rect.height) * 2; // Flip Y
1439
+ // Create ray in clip space at near and far planes
1440
+ const clipNear = new Vec3(clipX, clipY, -1); // Near plane
1441
+ const clipFar = new Vec3(clipX, clipY, 1); // Far plane
1442
+ // Transform to world space using inverse view-projection matrix
1443
+ const viewProjMatrix = projectionMatrix.multiply(viewMatrix);
1444
+ const inverseViewProj = viewProjMatrix.inverse();
1445
+ // Transform point through 4x4 matrix with perspective division
1446
+ const transformPoint = (matrix, point) => {
1447
+ const m = matrix.values;
1448
+ const x = point.x, y = point.y, z = point.z;
1449
+ // Compute transformed point (matrix * vec4(point, 1.0))
1450
+ const result = new Vec3(m[0] * x + m[4] * y + m[8] * z + m[12], m[1] * x + m[5] * y + m[9] * z + m[13], m[2] * x + m[6] * y + m[10] * z + m[14]);
1451
+ // Perspective division
1452
+ const w = m[3] * x + m[7] * y + m[11] * z + m[15];
1453
+ const invW = w !== 0 ? 1 / w : 1;
1454
+ return result.scale(invW);
1455
+ };
1456
+ const worldNear = transformPoint(inverseViewProj, clipNear);
1457
+ const worldFar = transformPoint(inverseViewProj, clipFar);
1458
+ // Create ray from camera position through the clicked point
1459
+ const rayOrigin = this.camera.getPosition();
1460
+ const rayDirection = worldFar.subtract(worldNear).normalize();
1461
+ // Get model geometry for ray-triangle intersection
1462
+ const baseVertices = this.currentModel.getVertices();
1463
+ const indices = this.currentModel.getIndices();
1464
+ const skinning = this.currentModel.getSkinning();
1465
+ if (!baseVertices || !indices || !skinning) {
1466
+ if (this.onRaycast) {
1467
+ this.onRaycast(null, screenX, screenY);
1468
+ }
1469
+ return;
1470
+ }
1471
+ // Use cached skinned vertices if available and up-to-date
1472
+ let vertices;
1473
+ if (this.cachedSkinnedVertices && this.cachedSkinMatricesVersion === this.skinMatricesVersion) {
1474
+ vertices = this.cachedSkinnedVertices;
1475
+ }
1476
+ else {
1477
+ // Apply current skinning transformations to get animated vertex positions
1478
+ vertices = new Float32Array(baseVertices.length);
1479
+ const skinMatrices = this.currentModel.getSkinMatrices();
1480
+ // Helper function to transform point by 4x4 matrix
1481
+ const transformByMatrix = (matrix, offset, point) => {
1482
+ const m = matrix;
1483
+ const x = point.x, y = point.y, z = point.z;
1484
+ return new Vec3(m[offset + 0] * x + m[offset + 4] * y + m[offset + 8] * z + m[offset + 12], m[offset + 1] * x + m[offset + 5] * y + m[offset + 9] * z + m[offset + 13], m[offset + 2] * x + m[offset + 6] * y + m[offset + 10] * z + m[offset + 14]);
1485
+ };
1486
+ for (let i = 0; i < baseVertices.length; i += 8) {
1487
+ const vertexIndex = Math.floor(i / 8);
1488
+ const position = new Vec3(baseVertices[i], baseVertices[i + 1], baseVertices[i + 2]);
1489
+ // Get bone influences for this vertex
1490
+ const jointIndices = [
1491
+ skinning.joints[vertexIndex * 4],
1492
+ skinning.joints[vertexIndex * 4 + 1],
1493
+ skinning.joints[vertexIndex * 4 + 2],
1494
+ skinning.joints[vertexIndex * 4 + 3],
1495
+ ];
1496
+ const weights = [
1497
+ skinning.weights[vertexIndex * 4],
1498
+ skinning.weights[vertexIndex * 4 + 1],
1499
+ skinning.weights[vertexIndex * 4 + 2],
1500
+ skinning.weights[vertexIndex * 4 + 3],
1501
+ ];
1502
+ // Normalize weights (same as shader)
1503
+ const weightSum = weights[0] + weights[1] + weights[2] + weights[3];
1504
+ const invWeightSum = weightSum > 0.0001 ? 1.0 / weightSum : 1.0;
1505
+ const normalizedWeights = weightSum > 0.0001 ? weights.map((w) => w * invWeightSum) : [1.0, 0.0, 0.0, 0.0];
1506
+ // Apply skinning transformation (same as shader)
1507
+ let skinnedPosition = new Vec3(0, 0, 0);
1508
+ for (let j = 0; j < 4; j++) {
1509
+ const weight = normalizedWeights[j];
1510
+ if (weight > 0) {
1511
+ const matrixOffset = jointIndices[j] * 16;
1512
+ const transformed = transformByMatrix(skinMatrices, matrixOffset, position);
1513
+ skinnedPosition = skinnedPosition.add(transformed.scale(weight));
1514
+ }
1515
+ }
1516
+ // Store transformed position, copy other attributes unchanged
1517
+ vertices[i] = skinnedPosition.x;
1518
+ vertices[i + 1] = skinnedPosition.y;
1519
+ vertices[i + 2] = skinnedPosition.z;
1520
+ vertices[i + 3] = baseVertices[i + 3]; // normal X
1521
+ vertices[i + 4] = baseVertices[i + 4]; // normal Y
1522
+ vertices[i + 5] = baseVertices[i + 5]; // normal Z
1523
+ vertices[i + 6] = baseVertices[i + 6]; // UV X
1524
+ vertices[i + 7] = baseVertices[i + 7]; // UV Y
1525
+ }
1526
+ // Cache the result
1527
+ this.cachedSkinnedVertices = vertices;
1528
+ this.cachedSkinMatricesVersion = this.skinMatricesVersion;
1529
+ }
1530
+ let closestHit = null;
1531
+ const maxDistance = 1000; // Reasonable max distance
1532
+ // Test ray against all triangles (Möller-Trumbore algorithm)
1533
+ for (let i = 0; i < indices.length; i += 3) {
1534
+ const idx0 = indices[i] * 8; // Each vertex has 8 floats (pos + normal + uv)
1535
+ const idx1 = indices[i + 1] * 8;
1536
+ const idx2 = indices[i + 2] * 8;
1537
+ // Get triangle vertices in world space (first 3 floats are position)
1538
+ const v0 = new Vec3(vertices[idx0], vertices[idx0 + 1], vertices[idx0 + 2]);
1539
+ const v1 = new Vec3(vertices[idx1], vertices[idx1 + 1], vertices[idx1 + 2]);
1540
+ const v2 = new Vec3(vertices[idx2], vertices[idx2 + 1], vertices[idx2 + 2]);
1541
+ // Find which material this triangle belongs to
1542
+ // Each material has mat.vertexCount indices (3 per triangle)
1543
+ let triangleMaterialIndex = -1;
1544
+ let indexOffset = 0;
1545
+ for (let matIdx = 0; matIdx < materials.length; matIdx++) {
1546
+ const mat = materials[matIdx];
1547
+ if (i >= indexOffset && i < indexOffset + mat.vertexCount) {
1548
+ triangleMaterialIndex = matIdx;
1549
+ break;
1550
+ }
1551
+ indexOffset += mat.vertexCount;
1552
+ }
1553
+ if (triangleMaterialIndex === -1)
1554
+ continue;
1555
+ // Skip invisible materials
1556
+ const materialName = materials[triangleMaterialIndex].name;
1557
+ if (this.hiddenMaterials.has(materialName))
1558
+ continue;
1559
+ // Ray-triangle intersection test (Möller-Trumbore algorithm)
1560
+ const edge1 = v1.subtract(v0);
1561
+ const edge2 = v2.subtract(v0);
1562
+ const h = rayDirection.cross(edge2);
1563
+ const a = edge1.dot(h);
1564
+ if (Math.abs(a) < 0.0001)
1565
+ continue; // Ray is parallel to triangle
1566
+ const f = 1.0 / a;
1567
+ const s = rayOrigin.subtract(v0);
1568
+ const u = f * s.dot(h);
1569
+ if (u < 0.0 || u > 1.0)
1570
+ continue;
1571
+ const q = s.cross(edge1);
1572
+ const v = f * rayDirection.dot(q);
1573
+ if (v < 0.0 || u + v > 1.0)
1574
+ continue;
1575
+ // At this point we have a hit
1576
+ const t = f * edge2.dot(q);
1577
+ if (t > 0.0001 && t < maxDistance) {
1578
+ // Backface culling: only consider front-facing triangles
1579
+ const triangleNormal = edge1.cross(edge2).normalize();
1580
+ const isFrontFace = triangleNormal.dot(rayDirection) < 0;
1581
+ if (isFrontFace) {
1582
+ if (!closestHit || t < closestHit.distance) {
1583
+ closestHit = {
1584
+ materialName: materials[triangleMaterialIndex].name,
1585
+ distance: t,
1586
+ };
1587
+ }
1588
+ }
1589
+ }
1590
+ }
1591
+ // Call the callback with the result
1592
+ if (this.onRaycast) {
1593
+ this.onRaycast(closestHit?.materialName || null, screenX, screenY);
1594
+ }
1595
+ }
1371
1596
  // Render strategy: 1) Opaque non-eye/hair 2) Eyes (stencil=1) 3) Hair (depth pre-pass + split by stencil) 4) Transparent 5) Bloom
1372
1597
  render() {
1373
1598
  if (this.multisampleTexture && this.camera && this.device) {
@@ -1539,6 +1764,8 @@ export class Engine {
1539
1764
  return;
1540
1765
  const skinMatrices = this.currentModel.getSkinMatrices();
1541
1766
  this.device.queue.writeBuffer(this.skinMatrixBuffer, 0, skinMatrices.buffer, skinMatrices.byteOffset, skinMatrices.byteLength);
1767
+ // Increment version to invalidate cached skinned vertices
1768
+ this.skinMatricesVersion++;
1542
1769
  }
1543
1770
  drawOutlines(pass, transparent) {
1544
1771
  pass.setPipeline(this.outlinePipeline);
@@ -1 +1 @@
1
- {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
package/dist/model.js CHANGED
@@ -46,14 +46,10 @@ export class Model {
46
46
  this.initializeRuntimeSkeleton();
47
47
  this.initializeRuntimeMorph();
48
48
  this.initializeTweenBuffers();
49
- this.applyMorphs(); // Apply initial morphs (all weights are 0, so no change)
49
+ this.applyMorphs();
50
50
  // Initialize physics if rigidbodies exist
51
51
  if (rigidbodies.length > 0) {
52
52
  this.physics = new Physics(rigidbodies, joints);
53
- console.log(`[Model] Physics initialized with ${rigidbodies.length} rigidbodies`);
54
- }
55
- else {
56
- console.log("[Model] No rigidbodies found, physics disabled");
57
53
  }
58
54
  }
59
55
  initializeRuntimeSkeleton() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.3.17",
3
+ "version": "0.4.1",
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
  }
package/src/engine.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { Camera } from "./camera"
2
- import { Quat, Vec3 } from "./math"
2
+ import { Mat4, Quat, Vec3 } from "./math"
3
3
  import { Model } from "./model"
4
4
  import { PmxLoader } from "./pmx-loader"
5
5
 
6
+ export type RaycastCallback = (material: string | null, screenX: number, screenY: number) => void
7
+
6
8
  export type EngineOptions = {
7
9
  ambientColor?: Vec3
8
10
  bloomIntensity?: number
@@ -11,9 +13,12 @@ export type EngineOptions = {
11
13
  cameraDistance?: number
12
14
  cameraTarget?: Vec3
13
15
  cameraFov?: number
16
+ onRaycast?: RaycastCallback
14
17
  }
15
18
 
16
- export const DEFAULT_ENGINE_OPTIONS: Required<EngineOptions> = {
19
+ export type RequiredEngineOptions = Required<Omit<EngineOptions, "onRaycast">> & Pick<EngineOptions, "onRaycast">
20
+
21
+ export const DEFAULT_ENGINE_OPTIONS: RequiredEngineOptions = {
17
22
  ambientColor: new Vec3(0.85, 0.85, 0.85),
18
23
  bloomIntensity: 0.12,
19
24
  bloomThreshold: 0.5,
@@ -21,6 +26,7 @@ export const DEFAULT_ENGINE_OPTIONS: Required<EngineOptions> = {
21
26
  cameraDistance: 26.6,
22
27
  cameraTarget: new Vec3(0, 12.5, 0),
23
28
  cameraFov: Math.PI / 4,
29
+ onRaycast: undefined,
24
30
  }
25
31
 
26
32
  export interface EngineStats {
@@ -114,6 +120,15 @@ export class Engine {
114
120
  // Rim light settings
115
121
  private rimLightIntensity!: number
116
122
 
123
+ // Raycasting
124
+ private onRaycast?: RaycastCallback
125
+ private cachedSkinnedVertices?: Float32Array
126
+ private cachedSkinMatricesVersion = -1
127
+ private skinMatricesVersion = 0
128
+ // Double-tap detection
129
+ private lastTouchTime = 0
130
+ private readonly DOUBLE_TAP_DELAY = 300 // ms
131
+
117
132
  private currentModel: Model | null = null
118
133
  private modelDir: string = ""
119
134
  private materialSampler!: GPUSampler
@@ -146,6 +161,7 @@ export class Engine {
146
161
  this.cameraDistance = options.cameraDistance ?? DEFAULT_ENGINE_OPTIONS.cameraDistance
147
162
  this.cameraTarget = options.cameraTarget ?? DEFAULT_ENGINE_OPTIONS.cameraTarget
148
163
  this.cameraFov = options.cameraFov ?? DEFAULT_ENGINE_OPTIONS.cameraFov
164
+ this.onRaycast = options.onRaycast
149
165
  }
150
166
  }
151
167
 
@@ -989,6 +1005,12 @@ export class Engine {
989
1005
  this.resizeObserver = new ResizeObserver(() => this.handleResize())
990
1006
  this.resizeObserver.observe(this.canvas)
991
1007
  this.handleResize()
1008
+
1009
+ // Setup raycasting double-click handler for desktop
1010
+ if (this.onRaycast) {
1011
+ this.canvas.addEventListener("dblclick", this.handleCanvasDoubleClick)
1012
+ this.canvas.addEventListener("touchend", this.handleCanvasTouch)
1013
+ }
992
1014
  }
993
1015
 
994
1016
  private handleResize() {
@@ -1200,6 +1222,13 @@ export class Engine {
1200
1222
  this.stopRenderLoop()
1201
1223
  this.stopAnimation()
1202
1224
  if (this.camera) this.camera.detachControl()
1225
+
1226
+ // Remove raycasting event listeners
1227
+ if (this.onRaycast) {
1228
+ this.canvas.removeEventListener("dblclick", this.handleCanvasDoubleClick)
1229
+ this.canvas.removeEventListener("touchend", this.handleCanvasTouch)
1230
+ }
1231
+
1203
1232
  if (this.resizeObserver) {
1204
1233
  this.resizeObserver.disconnect()
1205
1234
  this.resizeObserver = null
@@ -1215,6 +1244,11 @@ export class Engine {
1215
1244
 
1216
1245
  const model = await PmxLoader.load(path)
1217
1246
  console.log(model)
1247
+
1248
+ // Clear cached skinned vertices when loading a new model
1249
+ this.cachedSkinnedVertices = undefined
1250
+ this.cachedSkinMatricesVersion = -1
1251
+
1218
1252
  await this.setupModelBuffers(model)
1219
1253
  }
1220
1254
 
@@ -1625,6 +1659,261 @@ export class Engine {
1625
1659
  }
1626
1660
  }
1627
1661
 
1662
+ private handleCanvasDoubleClick = (event: MouseEvent) => {
1663
+ if (!this.onRaycast || !this.currentModel) return
1664
+
1665
+ const rect = this.canvas.getBoundingClientRect()
1666
+ const x = event.clientX - rect.left
1667
+ const y = event.clientY - rect.top
1668
+
1669
+ this.performRaycast(x, y)
1670
+ }
1671
+
1672
+ private handleCanvasTouch = (event: TouchEvent) => {
1673
+ if (!this.onRaycast || !this.currentModel) return
1674
+
1675
+ // Prevent default to avoid triggering mouse events
1676
+ event.preventDefault()
1677
+
1678
+ // Get the first touch
1679
+ const touch = event.changedTouches[0]
1680
+ if (!touch) return
1681
+
1682
+ const currentTime = Date.now()
1683
+ const timeDiff = currentTime - this.lastTouchTime
1684
+
1685
+ // Check for double-tap (within delay threshold)
1686
+ if (timeDiff < this.DOUBLE_TAP_DELAY) {
1687
+ const rect = this.canvas.getBoundingClientRect()
1688
+ const x = touch.clientX - rect.left
1689
+ const y = touch.clientY - rect.top
1690
+
1691
+ this.performRaycast(x, y)
1692
+ // Reset last touch time to prevent triple-tap triggering double-tap
1693
+ this.lastTouchTime = 0
1694
+ } else {
1695
+ // Single tap - update last touch time for potential double-tap
1696
+ this.lastTouchTime = currentTime
1697
+ }
1698
+ }
1699
+
1700
+ private performRaycast(screenX: number, screenY: number) {
1701
+ if (!this.currentModel || !this.onRaycast) return
1702
+
1703
+ const materials = this.currentModel.getMaterials()
1704
+ if (materials.length === 0) return
1705
+
1706
+ // Get camera matrices
1707
+ const viewMatrix = this.camera.getViewMatrix()
1708
+ const projectionMatrix = this.camera.getProjectionMatrix()
1709
+
1710
+ // Convert screen coordinates to world space ray (like Babylon.js)
1711
+ const canvas = this.canvas
1712
+ const rect = canvas.getBoundingClientRect()
1713
+
1714
+ // Convert to clip space (-1 to 1)
1715
+ const clipX = (screenX / rect.width) * 2 - 1
1716
+ const clipY = 1 - (screenY / rect.height) * 2 // Flip Y
1717
+
1718
+ // Create ray in clip space at near and far planes
1719
+ const clipNear = new Vec3(clipX, clipY, -1) // Near plane
1720
+ const clipFar = new Vec3(clipX, clipY, 1) // Far plane
1721
+
1722
+ // Transform to world space using inverse view-projection matrix
1723
+ const viewProjMatrix = projectionMatrix.multiply(viewMatrix)
1724
+ const inverseViewProj = viewProjMatrix.inverse()
1725
+
1726
+ // Transform point through 4x4 matrix with perspective division
1727
+ const transformPoint = (matrix: Mat4, point: Vec3): Vec3 => {
1728
+ const m = matrix.values
1729
+ const x = point.x,
1730
+ y = point.y,
1731
+ z = point.z
1732
+
1733
+ // Compute transformed point (matrix * vec4(point, 1.0))
1734
+ const result = new Vec3(
1735
+ m[0] * x + m[4] * y + m[8] * z + m[12],
1736
+ m[1] * x + m[5] * y + m[9] * z + m[13],
1737
+ m[2] * x + m[6] * y + m[10] * z + m[14]
1738
+ )
1739
+
1740
+ // Perspective division
1741
+ const w = m[3] * x + m[7] * y + m[11] * z + m[15]
1742
+ const invW = w !== 0 ? 1 / w : 1
1743
+
1744
+ return result.scale(invW)
1745
+ }
1746
+
1747
+ const worldNear = transformPoint(inverseViewProj, clipNear)
1748
+ const worldFar = transformPoint(inverseViewProj, clipFar)
1749
+
1750
+ // Create ray from camera position through the clicked point
1751
+ const rayOrigin = this.camera.getPosition()
1752
+ const rayDirection = worldFar.subtract(worldNear).normalize()
1753
+
1754
+ // Get model geometry for ray-triangle intersection
1755
+ const baseVertices = this.currentModel.getVertices()
1756
+ const indices = this.currentModel.getIndices()
1757
+ const skinning = this.currentModel.getSkinning()
1758
+
1759
+ if (!baseVertices || !indices || !skinning) {
1760
+ if (this.onRaycast) {
1761
+ this.onRaycast(null, screenX, screenY)
1762
+ }
1763
+ return
1764
+ }
1765
+
1766
+ // Use cached skinned vertices if available and up-to-date
1767
+ let vertices: Float32Array
1768
+ if (this.cachedSkinnedVertices && this.cachedSkinMatricesVersion === this.skinMatricesVersion) {
1769
+ vertices = this.cachedSkinnedVertices
1770
+ } else {
1771
+ // Apply current skinning transformations to get animated vertex positions
1772
+ vertices = new Float32Array(baseVertices.length)
1773
+ const skinMatrices = this.currentModel.getSkinMatrices()
1774
+
1775
+ // Helper function to transform point by 4x4 matrix
1776
+ const transformByMatrix = (matrix: Float32Array, offset: number, point: Vec3): Vec3 => {
1777
+ const m = matrix
1778
+ const x = point.x,
1779
+ y = point.y,
1780
+ z = point.z
1781
+ return new Vec3(
1782
+ m[offset + 0] * x + m[offset + 4] * y + m[offset + 8] * z + m[offset + 12],
1783
+ m[offset + 1] * x + m[offset + 5] * y + m[offset + 9] * z + m[offset + 13],
1784
+ m[offset + 2] * x + m[offset + 6] * y + m[offset + 10] * z + m[offset + 14]
1785
+ )
1786
+ }
1787
+
1788
+ for (let i = 0; i < baseVertices.length; i += 8) {
1789
+ const vertexIndex = Math.floor(i / 8)
1790
+ const position = new Vec3(baseVertices[i], baseVertices[i + 1], baseVertices[i + 2])
1791
+
1792
+ // Get bone influences for this vertex
1793
+ const jointIndices = [
1794
+ skinning.joints[vertexIndex * 4],
1795
+ skinning.joints[vertexIndex * 4 + 1],
1796
+ skinning.joints[vertexIndex * 4 + 2],
1797
+ skinning.joints[vertexIndex * 4 + 3],
1798
+ ]
1799
+
1800
+ const weights = [
1801
+ skinning.weights[vertexIndex * 4],
1802
+ skinning.weights[vertexIndex * 4 + 1],
1803
+ skinning.weights[vertexIndex * 4 + 2],
1804
+ skinning.weights[vertexIndex * 4 + 3],
1805
+ ]
1806
+
1807
+ // Normalize weights (same as shader)
1808
+ const weightSum = weights[0] + weights[1] + weights[2] + weights[3]
1809
+ const invWeightSum = weightSum > 0.0001 ? 1.0 / weightSum : 1.0
1810
+ const normalizedWeights = weightSum > 0.0001 ? weights.map((w) => w * invWeightSum) : [1.0, 0.0, 0.0, 0.0]
1811
+
1812
+ // Apply skinning transformation (same as shader)
1813
+ let skinnedPosition = new Vec3(0, 0, 0)
1814
+
1815
+ for (let j = 0; j < 4; j++) {
1816
+ const weight = normalizedWeights[j]
1817
+ if (weight > 0) {
1818
+ const matrixOffset = jointIndices[j] * 16
1819
+ const transformed = transformByMatrix(skinMatrices, matrixOffset, position)
1820
+ skinnedPosition = skinnedPosition.add(transformed.scale(weight))
1821
+ }
1822
+ }
1823
+
1824
+ // Store transformed position, copy other attributes unchanged
1825
+ vertices[i] = skinnedPosition.x
1826
+ vertices[i + 1] = skinnedPosition.y
1827
+ vertices[i + 2] = skinnedPosition.z
1828
+ vertices[i + 3] = baseVertices[i + 3] // normal X
1829
+ vertices[i + 4] = baseVertices[i + 4] // normal Y
1830
+ vertices[i + 5] = baseVertices[i + 5] // normal Z
1831
+ vertices[i + 6] = baseVertices[i + 6] // UV X
1832
+ vertices[i + 7] = baseVertices[i + 7] // UV Y
1833
+ }
1834
+
1835
+ // Cache the result
1836
+ this.cachedSkinnedVertices = vertices
1837
+ this.cachedSkinMatricesVersion = this.skinMatricesVersion
1838
+ }
1839
+
1840
+ let closestHit: { materialName: string; distance: number } | null = null
1841
+ const maxDistance = 1000 // Reasonable max distance
1842
+
1843
+ // Test ray against all triangles (Möller-Trumbore algorithm)
1844
+ for (let i = 0; i < indices.length; i += 3) {
1845
+ const idx0 = indices[i] * 8 // Each vertex has 8 floats (pos + normal + uv)
1846
+ const idx1 = indices[i + 1] * 8
1847
+ const idx2 = indices[i + 2] * 8
1848
+
1849
+ // Get triangle vertices in world space (first 3 floats are position)
1850
+ const v0 = new Vec3(vertices[idx0], vertices[idx0 + 1], vertices[idx0 + 2])
1851
+ const v1 = new Vec3(vertices[idx1], vertices[idx1 + 1], vertices[idx1 + 2])
1852
+ const v2 = new Vec3(vertices[idx2], vertices[idx2 + 1], vertices[idx2 + 2])
1853
+
1854
+ // Find which material this triangle belongs to
1855
+ // Each material has mat.vertexCount indices (3 per triangle)
1856
+ let triangleMaterialIndex = -1
1857
+ let indexOffset = 0
1858
+ for (let matIdx = 0; matIdx < materials.length; matIdx++) {
1859
+ const mat = materials[matIdx]
1860
+ if (i >= indexOffset && i < indexOffset + mat.vertexCount) {
1861
+ triangleMaterialIndex = matIdx
1862
+ break
1863
+ }
1864
+ indexOffset += mat.vertexCount
1865
+ }
1866
+
1867
+ if (triangleMaterialIndex === -1) continue
1868
+
1869
+ // Skip invisible materials
1870
+ const materialName = materials[triangleMaterialIndex].name
1871
+ if (this.hiddenMaterials.has(materialName)) continue
1872
+
1873
+ // Ray-triangle intersection test (Möller-Trumbore algorithm)
1874
+ const edge1 = v1.subtract(v0)
1875
+ const edge2 = v2.subtract(v0)
1876
+ const h = rayDirection.cross(edge2)
1877
+ const a = edge1.dot(h)
1878
+
1879
+ if (Math.abs(a) < 0.0001) continue // Ray is parallel to triangle
1880
+
1881
+ const f = 1.0 / a
1882
+ const s = rayOrigin.subtract(v0)
1883
+ const u = f * s.dot(h)
1884
+
1885
+ if (u < 0.0 || u > 1.0) continue
1886
+
1887
+ const q = s.cross(edge1)
1888
+ const v = f * rayDirection.dot(q)
1889
+
1890
+ if (v < 0.0 || u + v > 1.0) continue
1891
+
1892
+ // At this point we have a hit
1893
+ const t = f * edge2.dot(q)
1894
+
1895
+ if (t > 0.0001 && t < maxDistance) {
1896
+ // Backface culling: only consider front-facing triangles
1897
+ const triangleNormal = edge1.cross(edge2).normalize()
1898
+ const isFrontFace = triangleNormal.dot(rayDirection) < 0
1899
+
1900
+ if (isFrontFace) {
1901
+ if (!closestHit || t < closestHit.distance) {
1902
+ closestHit = {
1903
+ materialName: materials[triangleMaterialIndex].name,
1904
+ distance: t,
1905
+ }
1906
+ }
1907
+ }
1908
+ }
1909
+ }
1910
+
1911
+ // Call the callback with the result
1912
+ if (this.onRaycast) {
1913
+ this.onRaycast(closestHit?.materialName || null, screenX, screenY)
1914
+ }
1915
+ }
1916
+
1628
1917
  // Render strategy: 1) Opaque non-eye/hair 2) Eyes (stencil=1) 3) Hair (depth pre-pass + split by stencil) 4) Transparent 5) Bloom
1629
1918
  public render() {
1630
1919
  if (this.multisampleTexture && this.camera && this.device) {
@@ -1833,6 +2122,9 @@ export class Engine {
1833
2122
  skinMatrices.byteOffset,
1834
2123
  skinMatrices.byteLength
1835
2124
  )
2125
+
2126
+ // Increment version to invalidate cached skinned vertices
2127
+ this.skinMatricesVersion++
1836
2128
  }
1837
2129
 
1838
2130
  private drawOutlines(pass: GPURenderPassEncoder, transparent: boolean) {
package/src/model.ts CHANGED
@@ -227,14 +227,11 @@ export class Model {
227
227
  this.initializeRuntimeSkeleton()
228
228
  this.initializeRuntimeMorph()
229
229
  this.initializeTweenBuffers()
230
- this.applyMorphs() // Apply initial morphs (all weights are 0, so no change)
230
+ this.applyMorphs()
231
231
 
232
232
  // Initialize physics if rigidbodies exist
233
233
  if (rigidbodies.length > 0) {
234
234
  this.physics = new Physics(rigidbodies, joints)
235
- console.log(`[Model] Physics initialized with ${rigidbodies.length} rigidbodies`)
236
- } else {
237
- console.log("[Model] No rigidbodies found, physics disabled")
238
235
  }
239
236
  }
240
237