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 +13 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +227 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +1 -5
- package/package.json +2 -3
- package/src/engine.ts +294 -2
- package/src/model.ts +1 -4
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
|
|
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;
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
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);
|
package/dist/model.d.ts.map
CHANGED
|
@@ -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();
|
|
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
|
+
"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
|
|
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()
|
|
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
|
|