reze-engine 0.9.0 → 0.9.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/README.md +96 -23
- package/dist/camera.d.ts +2 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +14 -7
- package/dist/engine.d.ts +16 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +11 -13
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/physics.d.ts +5 -3
- package/dist/physics.d.ts.map +1 -1
- package/dist/physics.js +9 -62
- package/dist/pmx-loader.js +2 -2
- package/package.json +2 -2
- package/src/camera.ts +15 -7
- package/src/engine.ts +15 -18
- package/src/index.ts +1 -0
- package/src/physics.ts +16 -76
- package/src/pmx-loader.ts +2 -2
package/README.md
CHANGED
|
@@ -1,44 +1,117 @@
|
|
|
1
1
|
# Reze Engine
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A minimal-dependency WebGPU engine for real-time MMD/PMX rendering. Only external dependency is Ammo.js for physics.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install reze-engine
|
|
9
|
+
```
|
|
4
10
|
|
|
5
11
|
## Features
|
|
6
12
|
|
|
7
|
-
- Blinn-Phong
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
13
|
+
- Blinn-Phong shading, alpha blending, rim lighting, outlines, MSAA 4x
|
|
14
|
+
- VMD animation with IK solver and Bullet physics
|
|
15
|
+
- Orbit camera with bone-follow mode
|
|
16
|
+
- GPU picking (double-click/tap)
|
|
17
|
+
- Ground plane with PCF shadow mapping
|
|
18
|
+
- Multi-model support
|
|
11
19
|
|
|
12
|
-
##
|
|
20
|
+
## Quick Start
|
|
13
21
|
|
|
14
22
|
```javascript
|
|
15
|
-
import { Engine,
|
|
23
|
+
import { Engine, Vec3 } from "reze-engine"
|
|
16
24
|
|
|
17
|
-
const engine = new Engine(canvas, {
|
|
25
|
+
const engine = new Engine(canvas, {
|
|
26
|
+
ambientColor: new Vec3(0.88, 0.92, 0.99),
|
|
27
|
+
cameraDistance: 31.5, // MMD units (1 unit = 8 cm)
|
|
28
|
+
})
|
|
18
29
|
await engine.init()
|
|
19
|
-
|
|
20
|
-
await
|
|
21
|
-
model.
|
|
30
|
+
|
|
31
|
+
const model = await engine.loadModel("hero", "/models/hero/hero.pmx")
|
|
32
|
+
await model.loadAnimation("idle", "/animations/idle.vmd")
|
|
33
|
+
model.show("idle")
|
|
22
34
|
model.play()
|
|
23
|
-
|
|
35
|
+
|
|
36
|
+
engine.setCameraFollow(model, "センター", new Vec3(0, 3.5, 0))
|
|
37
|
+
engine.addGround({ width: 160, height: 160 })
|
|
38
|
+
engine.runRenderLoop()
|
|
24
39
|
```
|
|
25
40
|
|
|
26
|
-
## API
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
### Engine
|
|
44
|
+
|
|
45
|
+
| Method | Description |
|
|
46
|
+
|--------|-------------|
|
|
47
|
+
| `new Engine(canvas, options?)` | Create engine with optional config |
|
|
48
|
+
| `engine.init()` | Initialize WebGPU device and context |
|
|
49
|
+
| `engine.loadModel(path)` | Load PMX model (auto-named) |
|
|
50
|
+
| `engine.loadModel(name, path)` | Load PMX model with name |
|
|
51
|
+
| `engine.getModel(name)` | Get model by name |
|
|
52
|
+
| `engine.getModelNames()` | List all model names |
|
|
53
|
+
| `engine.removeModel(name)` | Remove model |
|
|
54
|
+
| `engine.setMaterialVisible(model, mat, visible)` | Show/hide material |
|
|
55
|
+
| `engine.toggleMaterialVisible(model, mat)` | Toggle material visibility |
|
|
56
|
+
| `engine.setIKEnabled(enabled)` | Enable/disable IK globally |
|
|
57
|
+
| `engine.setPhysicsEnabled(enabled)` | Enable/disable physics globally |
|
|
58
|
+
| `engine.setCameraFollow(model, bone?, offset?)` | Orbit center tracks a bone |
|
|
59
|
+
| `engine.setCameraTarget(vec3)` | Static camera target |
|
|
60
|
+
| `engine.setCameraDistance(d)` | Set orbit radius |
|
|
61
|
+
| `engine.setCameraAlpha(a)` | Set horizontal orbit angle |
|
|
62
|
+
| `engine.setCameraBeta(b)` | Set vertical orbit angle |
|
|
63
|
+
| `engine.addGround(options?)` | Add ground plane with shadows |
|
|
64
|
+
| `engine.runRenderLoop(callback?)` | Start render loop |
|
|
65
|
+
| `engine.stopRenderLoop()` | Stop render loop |
|
|
66
|
+
| `engine.getStats()` | Returns `{ fps, frameTime }` |
|
|
67
|
+
| `engine.dispose()` | Clean up all resources |
|
|
27
68
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
69
|
+
### Model
|
|
70
|
+
|
|
71
|
+
| Method | Description |
|
|
72
|
+
|--------|-------------|
|
|
73
|
+
| `model.loadAnimation(name, url)` | Load VMD animation |
|
|
74
|
+
| `model.show(name)` | Set pose at time 0 |
|
|
75
|
+
| `model.play(name?)` | Play animation (queued if busy) |
|
|
76
|
+
| `model.pause()` | Pause playback |
|
|
77
|
+
| `model.stop()` | Stop playback |
|
|
78
|
+
| `model.seek(time)` | Seek to time |
|
|
79
|
+
| `model.getAnimationProgress()` | `{ current, duration, percentage, animationName }` |
|
|
80
|
+
| `model.getAnimationState()` | Access animation controller |
|
|
81
|
+
| `model.rotateBones(rotations, ms?)` | Tween bone rotations |
|
|
82
|
+
| `model.moveBones(translations, ms?)` | Tween bone translations |
|
|
83
|
+
| `model.setMorphWeight(name, weight, ms?)` | Tween morph weight |
|
|
84
|
+
| `model.resetAllBones()` | Reset to bind pose |
|
|
85
|
+
| `model.resetAllMorphs()` | Reset all morph weights |
|
|
86
|
+
| `model.getBoneWorldPosition(name)` | World position of bone |
|
|
87
|
+
|
|
88
|
+
### Engine Options
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
{
|
|
92
|
+
ambientColor: Vec3,
|
|
93
|
+
directionalLightIntensity: number,
|
|
94
|
+
minSpecularIntensity: number,
|
|
95
|
+
rimLightIntensity: number,
|
|
96
|
+
cameraDistance: number,
|
|
97
|
+
cameraTarget: Vec3,
|
|
98
|
+
cameraFov: number,
|
|
99
|
+
onRaycast: (modelName, material, screenX, screenY) => void,
|
|
100
|
+
physicsOptions: {
|
|
101
|
+
constraintSolverKeywords: string[],
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`constraintSolverKeywords` — joints whose name contains any keyword use the Bullet 2.75 constraint solver; all others keep the stable Ammo 2.82+ default. See [babylon-mmd: Fix Constraint Behavior](https://noname0310.github.io/babylon-mmd/docs/reference/runtime/apply-physics-to-mmd-models/#fix-constraint-behavior) for details.
|
|
32
107
|
|
|
33
108
|
## Projects Using This Engine
|
|
34
109
|
|
|
35
|
-
- **[MiKaPo](https://mikapo.vercel.app)**
|
|
36
|
-
- **[Popo](https://popo.love)**
|
|
37
|
-
- **[MPL](https://mmd-mpl.vercel.app)**
|
|
38
|
-
- **[Mixamo-MMD](https://mixamo-mmd.vercel.app)**
|
|
110
|
+
- **[MiKaPo](https://mikapo.vercel.app)** — Real-time motion capture for MMD
|
|
111
|
+
- **[Popo](https://popo.love)** — LLM-generated MMD poses
|
|
112
|
+
- **[MPL](https://mmd-mpl.vercel.app)** — Motion programming language for MMD
|
|
113
|
+
- **[Mixamo-MMD](https://mixamo-mmd.vercel.app)** — Retarget Mixamo FBX to VMD
|
|
39
114
|
|
|
40
115
|
## Tutorial
|
|
41
116
|
|
|
42
|
-
Learn WebGPU from scratch by building an anime character renderer in incremental steps. The tutorial covers the complete rendering pipeline from a simple triangle to fully textured, skeletal-animated characters.
|
|
43
|
-
|
|
44
117
|
[How to Render an Anime Character with WebGPU](https://reze.one/tutorial)
|
package/dist/camera.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export declare class Camera {
|
|
|
31
31
|
getViewMatrix(): Mat4;
|
|
32
32
|
private getCameraVectors;
|
|
33
33
|
private panCamera;
|
|
34
|
+
/** Far plane grows with zoom-out so big floors / distant geometry stay visible */
|
|
35
|
+
private updateFarFromRadius;
|
|
34
36
|
getProjectionMatrix(): Mat4;
|
|
35
37
|
attachControl(canvas: HTMLCanvasElement): void;
|
|
36
38
|
detachControl(): void;
|
package/dist/camera.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"camera.d.ts","sourceRoot":"","sources":["../src/camera.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"camera.d.ts","sourceRoot":"","sources":["../src/camera.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAMnC,qBAAa,MAAM;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,IAAI,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAI;IAClB,IAAI,EAAE,MAAM,CAAO;IACnB,GAAG,EAAE,MAAM,CAAU;IAGrB,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,UAAU,CAAiB;IACnC,OAAO,CAAC,WAAW,CAAsB;IACzC,OAAO,CAAC,YAAY,CAAiB;IACrC,OAAO,CAAC,YAAY,CAAiB;IACrC,OAAO,CAAC,eAAe,CAAsB;IAC7C,OAAO,CAAC,UAAU,CAAiB;IACnC,OAAO,CAAC,iBAAiB,CAAY;IACrC,OAAO,CAAC,iBAAiB,CAAiB;IAC1C,OAAO,CAAC,oBAAoB,CAAY;IAGxC,kBAAkB,EAAE,MAAM,CAAQ;IAClC,cAAc,EAAE,MAAM,CAAS;IAC/B,cAAc,EAAE,MAAM,CAAO;IAC7B,cAAc,EAAE,MAAM,CAAO;IAC7B,IAAI,EAAE,MAAM,CAAO;IACnB,IAAI,EAAE,MAAM,CAAU;IACtB,cAAc,EAAE,MAAM,CAAQ;IAC9B,cAAc,EAAE,MAAM,CAAkB;gBAE5B,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAE,MAAoB;IAmBhG,WAAW,IAAI,IAAI;IAQnB,aAAa,IAAI,IAAI;IAQrB,OAAO,CAAC,gBAAgB;IA0CxB,OAAO,CAAC,SAAS;IAiBjB,kFAAkF;IAClF,OAAO,CAAC,mBAAmB;IAK3B,mBAAmB,IAAI,IAAI;IAK3B,aAAa,CAAC,MAAM,EAAE,iBAAiB;IAiBvC,aAAa;IAkBb,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,WAAW;IAqBnB,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,OAAO;IAWf,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,YAAY;IA6BpB,OAAO,CAAC,WAAW;IAgFnB,OAAO,CAAC,UAAU;CA+BnB"}
|
package/dist/camera.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Mat4, Vec3 } from "./math";
|
|
2
|
-
|
|
2
|
+
/** Far cap / zoom limit; large enough for wide shots without clipping distant ground */
|
|
3
|
+
const FAR_CAP = 8000;
|
|
4
|
+
const FAR_MIN = 200;
|
|
3
5
|
export class Camera {
|
|
4
6
|
constructor(alpha, beta, radius, target, fov = Math.PI / 4) {
|
|
5
7
|
this.aspect = 1;
|
|
6
8
|
this.near = 0.05;
|
|
7
|
-
this.far =
|
|
9
|
+
this.far = FAR_CAP;
|
|
8
10
|
// Input state
|
|
9
11
|
this.canvas = null;
|
|
10
12
|
this.isDragging = false;
|
|
@@ -22,7 +24,7 @@ export class Camera {
|
|
|
22
24
|
this.wheelPrecision = 0.01;
|
|
23
25
|
this.pinchPrecision = 0.05;
|
|
24
26
|
this.minZ = 0.05;
|
|
25
|
-
this.maxZ =
|
|
27
|
+
this.maxZ = FAR_CAP;
|
|
26
28
|
this.lowerBetaLimit = 0.001;
|
|
27
29
|
this.upperBetaLimit = Math.PI - 0.001;
|
|
28
30
|
this.alpha = alpha;
|
|
@@ -30,6 +32,7 @@ export class Camera {
|
|
|
30
32
|
this.radius = radius;
|
|
31
33
|
this.target = target;
|
|
32
34
|
this.fov = fov;
|
|
35
|
+
this.updateFarFromRadius();
|
|
33
36
|
// Bind event handlers
|
|
34
37
|
this.onMouseDown = this.onMouseDown.bind(this);
|
|
35
38
|
this.onMouseMove = this.onMouseMove.bind(this);
|
|
@@ -103,7 +106,13 @@ export class Camera {
|
|
|
103
106
|
// Update target position smoothly
|
|
104
107
|
this.target = this.target.add(panRight).add(panUp);
|
|
105
108
|
}
|
|
109
|
+
/** Far plane grows with zoom-out so big floors / distant geometry stay visible */
|
|
110
|
+
updateFarFromRadius() {
|
|
111
|
+
const margin = 600;
|
|
112
|
+
this.far = Math.min(FAR_CAP, Math.max(FAR_MIN, this.radius * 12 + margin));
|
|
113
|
+
}
|
|
106
114
|
getProjectionMatrix() {
|
|
115
|
+
this.updateFarFromRadius();
|
|
107
116
|
return Mat4.perspective(this.fov, this.aspect, this.near, this.far);
|
|
108
117
|
}
|
|
109
118
|
attachControl(canvas) {
|
|
@@ -168,8 +177,7 @@ export class Camera {
|
|
|
168
177
|
this.radius += e.deltaY * this.wheelPrecision;
|
|
169
178
|
// Clamp radius to reasonable bounds
|
|
170
179
|
this.radius = Math.max(this.minZ, Math.min(this.maxZ, this.radius));
|
|
171
|
-
|
|
172
|
-
this.far = Math.max(FAR, this.radius * 4);
|
|
180
|
+
this.updateFarFromRadius();
|
|
173
181
|
}
|
|
174
182
|
onContextMenu(e) {
|
|
175
183
|
e.preventDefault();
|
|
@@ -235,8 +243,7 @@ export class Camera {
|
|
|
235
243
|
this.radius += delta * this.pinchPrecision;
|
|
236
244
|
// Clamp radius to reasonable bounds
|
|
237
245
|
this.radius = Math.max(this.minZ, Math.min(this.maxZ, this.radius));
|
|
238
|
-
|
|
239
|
-
this.far = Math.max(FAR, this.radius * 4);
|
|
246
|
+
this.updateFarFromRadius();
|
|
240
247
|
}
|
|
241
248
|
if (isPanGesture) {
|
|
242
249
|
// Primary gesture is pan (two-finger drag)
|
package/dist/engine.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Vec3 } from "./math";
|
|
2
2
|
import { Model } from "./model";
|
|
3
|
+
import { type PhysicsOptions } from "./physics";
|
|
3
4
|
export type RaycastCallback = (modelName: string, material: string | null, screenX: number, screenY: number) => void;
|
|
4
5
|
export type EngineOptions = {
|
|
5
6
|
ambientColor?: Vec3;
|
|
@@ -10,9 +11,21 @@ export type EngineOptions = {
|
|
|
10
11
|
cameraTarget?: Vec3;
|
|
11
12
|
cameraFov?: number;
|
|
12
13
|
onRaycast?: RaycastCallback;
|
|
14
|
+
physicsOptions?: PhysicsOptions;
|
|
15
|
+
};
|
|
16
|
+
export declare const DEFAULT_ENGINE_OPTIONS: {
|
|
17
|
+
ambientColor: Vec3;
|
|
18
|
+
directionalLightIntensity: number;
|
|
19
|
+
minSpecularIntensity: number;
|
|
20
|
+
rimLightIntensity: number;
|
|
21
|
+
cameraDistance: number;
|
|
22
|
+
cameraTarget: Vec3;
|
|
23
|
+
cameraFov: number;
|
|
24
|
+
onRaycast: undefined;
|
|
25
|
+
physicsOptions: {
|
|
26
|
+
constraintSolverKeywords: string[];
|
|
27
|
+
};
|
|
13
28
|
};
|
|
14
|
-
export type RequiredEngineOptions = Required<Omit<EngineOptions, "onRaycast">> & Pick<EngineOptions, "onRaycast">;
|
|
15
|
-
export declare const DEFAULT_ENGINE_OPTIONS: RequiredEngineOptions;
|
|
16
29
|
export interface EngineStats {
|
|
17
30
|
fps: number;
|
|
18
31
|
frameTime: number;
|
|
@@ -69,6 +82,7 @@ export declare class Engine {
|
|
|
69
82
|
private shadowVPLightY;
|
|
70
83
|
private shadowVPLightZ;
|
|
71
84
|
private onRaycast?;
|
|
85
|
+
private physicsOptions;
|
|
72
86
|
private lastTouchTime;
|
|
73
87
|
private readonly DOUBLE_TAP_DELAY;
|
|
74
88
|
private pickPipeline;
|
|
@@ -148,7 +162,6 @@ export declare class Engine {
|
|
|
148
162
|
getIKEnabled(): boolean;
|
|
149
163
|
setPhysicsEnabled(enabled: boolean): void;
|
|
150
164
|
getPhysicsEnabled(): boolean;
|
|
151
|
-
resetPhysics(): void;
|
|
152
165
|
private forEachInstance;
|
|
153
166
|
private updateInstances;
|
|
154
167
|
private updateVertexBuffer;
|
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,EAAQ,IAAI,EAAE,MAAM,QAAQ,CAAA;AACnC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.js
CHANGED
|
@@ -11,6 +11,7 @@ export const DEFAULT_ENGINE_OPTIONS = {
|
|
|
11
11
|
cameraTarget: new Vec3(0, 12.5, 0),
|
|
12
12
|
cameraFov: Math.PI / 4,
|
|
13
13
|
onRaycast: undefined,
|
|
14
|
+
physicsOptions: { constraintSolverKeywords: ["胸"] },
|
|
14
15
|
};
|
|
15
16
|
export class Engine {
|
|
16
17
|
static getInstance() {
|
|
@@ -30,6 +31,7 @@ export class Engine {
|
|
|
30
31
|
this.shadowVPLightX = Number.NaN;
|
|
31
32
|
this.shadowVPLightY = Number.NaN;
|
|
32
33
|
this.shadowVPLightZ = Number.NaN;
|
|
34
|
+
this.physicsOptions = DEFAULT_ENGINE_OPTIONS.physicsOptions;
|
|
33
35
|
this.lastTouchTime = 0;
|
|
34
36
|
this.DOUBLE_TAP_DELAY = 300;
|
|
35
37
|
this.pendingPick = null;
|
|
@@ -96,6 +98,7 @@ export class Engine {
|
|
|
96
98
|
this.cameraTarget = options.cameraTarget ?? DEFAULT_ENGINE_OPTIONS.cameraTarget;
|
|
97
99
|
this.cameraFov = options.cameraFov ?? DEFAULT_ENGINE_OPTIONS.cameraFov;
|
|
98
100
|
this.onRaycast = options.onRaycast;
|
|
101
|
+
this.physicsOptions = options.physicsOptions ?? DEFAULT_ENGINE_OPTIONS.physicsOptions;
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
// Step 1: Get WebGPU device and context
|
|
@@ -570,9 +573,11 @@ export class Engine {
|
|
|
570
573
|
}
|
|
571
574
|
let worldPos = skinnedPos.xyz;
|
|
572
575
|
let worldNormal = normalize(skinnedNrm);
|
|
573
|
-
|
|
574
|
-
let
|
|
575
|
-
let
|
|
576
|
+
// Screen-stable edgeline: extrusion ∝ camera distance (same idea as MMD viewers / babylon-mmd-style scaling)
|
|
577
|
+
let camDist = max(length(camera.viewPos - worldPos), 0.25);
|
|
578
|
+
let refDist = 30.0;
|
|
579
|
+
let edgeScale = 0.03;
|
|
580
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * edgeScale * (camDist / refDist);
|
|
576
581
|
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
577
582
|
return output;
|
|
578
583
|
}
|
|
@@ -591,7 +596,8 @@ export class Engine {
|
|
|
591
596
|
cullMode: "back",
|
|
592
597
|
depthStencil: {
|
|
593
598
|
format: "depth24plus-stencil8",
|
|
594
|
-
|
|
599
|
+
// Don’t write outline into depth buffer — stops z-fighting / black cracks vs body (MMD-style; body depth stays authoritative)
|
|
600
|
+
depthWriteEnabled: false,
|
|
595
601
|
depthCompare: "less-equal",
|
|
596
602
|
},
|
|
597
603
|
});
|
|
@@ -996,14 +1002,6 @@ export class Engine {
|
|
|
996
1002
|
getPhysicsEnabled() {
|
|
997
1003
|
return this.physicsEnabled;
|
|
998
1004
|
}
|
|
999
|
-
resetPhysics() {
|
|
1000
|
-
this.forEachInstance((inst) => {
|
|
1001
|
-
if (!inst.physics)
|
|
1002
|
-
return;
|
|
1003
|
-
inst.model.computeWorldMatrices();
|
|
1004
|
-
inst.physics.reset(inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices());
|
|
1005
|
-
});
|
|
1006
|
-
}
|
|
1007
1005
|
forEachInstance(fn) {
|
|
1008
1006
|
for (const inst of this.modelInstances.values())
|
|
1009
1007
|
fn(inst);
|
|
@@ -1066,7 +1064,7 @@ export class Engine {
|
|
|
1066
1064
|
});
|
|
1067
1065
|
this.device.queue.writeBuffer(indexBuffer, 0, indices);
|
|
1068
1066
|
const rbs = model.getRigidbodies();
|
|
1069
|
-
const physics = rbs.length > 0 ? new Physics(rbs, model.getJoints()) : null;
|
|
1067
|
+
const physics = rbs.length > 0 ? new Physics(rbs, model.getJoints(), this.physicsOptions) : null;
|
|
1070
1068
|
const shadowBindGroup = this.device.createBindGroup({
|
|
1071
1069
|
label: `${name}: shadow bind`,
|
|
1072
1070
|
layout: this.shadowDepthPipeline.getBindGroupLayout(0),
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export { Engine, type EngineStats } from "./engine";
|
|
|
2
2
|
export { Model } from "./model";
|
|
3
3
|
export { Vec3, Quat, Mat4 } from "./math";
|
|
4
4
|
export { AnimationState, type AnimationClip, type BoneKeyframe, type MorphKeyframe, } from "./animation";
|
|
5
|
+
export { Physics, type PhysicsOptions } from "./physics";
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACzC,OAAO,EACL,cAAc,EACd,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,GACnB,MAAM,aAAa,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAA;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACzC,OAAO,EACL,cAAc,EACd,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,GACnB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,WAAW,CAAA"}
|
package/dist/index.js
CHANGED
package/dist/physics.d.ts
CHANGED
|
@@ -43,10 +43,14 @@ export interface Joint {
|
|
|
43
43
|
springPosition: Vec3;
|
|
44
44
|
springRotation: Vec3;
|
|
45
45
|
}
|
|
46
|
+
export interface PhysicsOptions {
|
|
47
|
+
constraintSolverKeywords?: string[];
|
|
48
|
+
}
|
|
46
49
|
export declare class Physics {
|
|
47
50
|
private rigidbodies;
|
|
48
51
|
private joints;
|
|
49
52
|
private gravity;
|
|
53
|
+
private constraintSolverPattern;
|
|
50
54
|
private ammoInitialized;
|
|
51
55
|
private ammoPromise;
|
|
52
56
|
private ammo;
|
|
@@ -56,9 +60,8 @@ export declare class Physics {
|
|
|
56
60
|
private rigidbodiesInitialized;
|
|
57
61
|
private jointsCreated;
|
|
58
62
|
private firstFrame;
|
|
59
|
-
private forceDisableOffsetForConstraintFrame;
|
|
60
63
|
private zeroVector;
|
|
61
|
-
constructor(rigidbodies: Rigidbody[], joints?: Joint[]);
|
|
64
|
+
constructor(rigidbodies: Rigidbody[], joints?: Joint[], options?: PhysicsOptions);
|
|
62
65
|
private initAmmo;
|
|
63
66
|
setGravity(gravity: Vec3): void;
|
|
64
67
|
getGravity(): Vec3;
|
|
@@ -72,7 +75,6 @@ export declare class Physics {
|
|
|
72
75
|
private createAmmoRigidbodies;
|
|
73
76
|
private createAmmoJoints;
|
|
74
77
|
private normalizeAngle;
|
|
75
|
-
reset(boneWorldMatrices: Mat4[], boneInverseBindMatrices: Float32Array): void;
|
|
76
78
|
step(dt: number, boneWorldMatrices: Mat4[], boneInverseBindMatrices: Float32Array): void;
|
|
77
79
|
private computeBodyOffsets;
|
|
78
80
|
private positionBodiesFromBones;
|
package/dist/physics.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"physics.d.ts","sourceRoot":"","sources":["../src/physics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAIzC,oBAAY,cAAc;IACxB,MAAM,IAAI;IACV,GAAG,IAAI;IACP,OAAO,IAAI;CACZ;AAED,oBAAY,aAAa;IACvB,MAAM,IAAI;IACV,OAAO,IAAI;IACX,SAAS,IAAI;CACd;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,MAAM,CAAA;IACrB,KAAK,EAAE,cAAc,CAAA;IACrB,IAAI,EAAE,IAAI,CAAA;IACV,aAAa,EAAE,IAAI,CAAA;IACnB,aAAa,EAAE,IAAI,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,aAAa,CAAA;IACnB,uBAAuB,EAAE,IAAI,CAAA;IAC7B,gBAAgB,CAAC,EAAE,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,CAAA;IACvB,QAAQ,EAAE,IAAI,CAAA;IACd,QAAQ,EAAE,IAAI,CAAA;IACd,WAAW,EAAE,IAAI,CAAA;IACjB,WAAW,EAAE,IAAI,CAAA;IACjB,WAAW,EAAE,IAAI,CAAA;IACjB,WAAW,EAAE,IAAI,CAAA;IACjB,cAAc,EAAE,IAAI,CAAA;IACpB,cAAc,EAAE,IAAI,CAAA;CACrB;AAED,qBAAa,OAAO;IAClB,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,WAAW,CAAqC;IACxD,OAAO,CAAC,IAAI,CAA4B;IAExC,OAAO,CAAC,aAAa,CAAY;IAEjC,OAAO,CAAC,eAAe,CAAY;IAEnC,OAAO,CAAC,eAAe,CAAY;IACnC,OAAO,CAAC,sBAAsB,CAAQ;IACtC,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,UAAU,CAAO;
|
|
1
|
+
{"version":3,"file":"physics.d.ts","sourceRoot":"","sources":["../src/physics.ts"],"names":[],"mappings":"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"}
|
package/dist/physics.js
CHANGED
|
@@ -13,8 +13,9 @@ export var RigidbodyType;
|
|
|
13
13
|
RigidbodyType[RigidbodyType["Kinematic"] = 2] = "Kinematic";
|
|
14
14
|
})(RigidbodyType || (RigidbodyType = {}));
|
|
15
15
|
export class Physics {
|
|
16
|
-
constructor(rigidbodies, joints = []) {
|
|
16
|
+
constructor(rigidbodies, joints = [], options) {
|
|
17
17
|
this.gravity = new Vec3(0, -98, 0); // Gravity acceleration (cm/s²), MMD-style default
|
|
18
|
+
this.constraintSolverPattern = null;
|
|
18
19
|
this.ammoInitialized = false;
|
|
19
20
|
this.ammoPromise = null;
|
|
20
21
|
this.ammo = null;
|
|
@@ -27,11 +28,14 @@ export class Physics {
|
|
|
27
28
|
this.rigidbodiesInitialized = false; // bodyOffsetMatrixInverse computed and bodies positioned
|
|
28
29
|
this.jointsCreated = false; // Joints delayed until after rigidbodies are positioned
|
|
29
30
|
this.firstFrame = true; // Needed to reposition bodies before creating joints
|
|
30
|
-
this.forceDisableOffsetForConstraintFrame = true; // MMD compatibility (Bullet 2.75 behavior)
|
|
31
31
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
32
|
this.zeroVector = null; // Cached zero vector for velocity clearing
|
|
33
33
|
this.rigidbodies = rigidbodies;
|
|
34
34
|
this.joints = joints;
|
|
35
|
+
const keywords = options?.constraintSolverKeywords ?? [];
|
|
36
|
+
if (keywords.length > 0) {
|
|
37
|
+
this.constraintSolverPattern = new RegExp(keywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "i");
|
|
38
|
+
}
|
|
35
39
|
this.initAmmo();
|
|
36
40
|
}
|
|
37
41
|
async initAmmo() {
|
|
@@ -246,8 +250,9 @@ export class Physics {
|
|
|
246
250
|
frameInB.setRotation(quatB);
|
|
247
251
|
const useLinearReferenceFrameA = true;
|
|
248
252
|
const constraint = new Ammo.btGeneric6DofSpringConstraint(bodyA, bodyB, frameInA, frameInB, useLinearReferenceFrameA);
|
|
249
|
-
//
|
|
250
|
-
|
|
253
|
+
// Per-joint Bullet 2.75 constraint solver: disable m_useOffsetForConstraintFrame for
|
|
254
|
+
// joints whose name matches constraintSolverKeywords.
|
|
255
|
+
if (this.constraintSolverPattern && this.constraintSolverPattern.test(joint.name)) {
|
|
251
256
|
let jointPtr;
|
|
252
257
|
if (typeof Ammo.getPointer === "function") {
|
|
253
258
|
jointPtr = Ammo.getPointer(constraint);
|
|
@@ -258,7 +263,6 @@ export class Physics {
|
|
|
258
263
|
}
|
|
259
264
|
if (jointPtr !== undefined && Ammo.HEAP8) {
|
|
260
265
|
const heap8 = Ammo.HEAP8;
|
|
261
|
-
// jointPtr + 1300 = m_useLinearReferenceFrameA, jointPtr + 1301 = m_useOffsetForConstraintFrame
|
|
262
266
|
if (heap8[jointPtr + 1300] === (useLinearReferenceFrameA ? 1 : 0) && heap8[jointPtr + 1301] === 1) {
|
|
263
267
|
heap8[jointPtr + 1301] = 0;
|
|
264
268
|
}
|
|
@@ -329,63 +333,6 @@ export class Physics {
|
|
|
329
333
|
}
|
|
330
334
|
return angle;
|
|
331
335
|
}
|
|
332
|
-
// Reset physics state (reposition bodies, clear velocities)
|
|
333
|
-
// Following babylon-mmd pattern: initialize all rigid body positions from current bone poses
|
|
334
|
-
// Call this when starting a new animation to prevent physics instability from sudden pose changes
|
|
335
|
-
reset(boneWorldMatrices, boneInverseBindMatrices) {
|
|
336
|
-
if (!this.ammoInitialized || !this.ammo || !this.dynamicsWorld) {
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
const boneCount = boneWorldMatrices.length;
|
|
340
|
-
const Ammo = this.ammo;
|
|
341
|
-
// Ensure body offsets are computed
|
|
342
|
-
if (!this.rigidbodiesInitialized) {
|
|
343
|
-
this.computeBodyOffsets(boneInverseBindMatrices, boneCount);
|
|
344
|
-
this.rigidbodiesInitialized = true;
|
|
345
|
-
}
|
|
346
|
-
// Reposition ALL rigid bodies from current bone poses (like babylon-mmd initialize)
|
|
347
|
-
// This ensures all bodies are correctly positioned before physics starts
|
|
348
|
-
for (let i = 0; i < this.rigidbodies.length; i++) {
|
|
349
|
-
const rb = this.rigidbodies[i];
|
|
350
|
-
const ammoBody = this.ammoRigidbodies[i];
|
|
351
|
-
if (!ammoBody || rb.boneIndex < 0 || rb.boneIndex >= boneCount)
|
|
352
|
-
continue;
|
|
353
|
-
const boneIdx = rb.boneIndex;
|
|
354
|
-
// Get bone world matrix
|
|
355
|
-
const boneWorldMat = boneWorldMatrices[boneIdx];
|
|
356
|
-
// Compute body world matrix: bodyWorld = boneWorld × bodyOffsetMatrix
|
|
357
|
-
// (like babylon-mmd: bodyWorldMatrix = bodyOffsetMatrix.multiplyToRef(bodyWorldMatrix))
|
|
358
|
-
const bodyOffsetMatrix = rb.bodyOffsetMatrix || rb.bodyOffsetMatrixInverse.inverse();
|
|
359
|
-
const bodyWorldMatrix = boneWorldMat.multiply(bodyOffsetMatrix);
|
|
360
|
-
const worldPos = bodyWorldMatrix.getPosition();
|
|
361
|
-
const worldRot = bodyWorldMatrix.toQuat();
|
|
362
|
-
// Set transform matrix
|
|
363
|
-
const transform = new Ammo.btTransform();
|
|
364
|
-
const pos = new Ammo.btVector3(worldPos.x, worldPos.y, worldPos.z);
|
|
365
|
-
const quat = new Ammo.btQuaternion(worldRot.x, worldRot.y, worldRot.z, worldRot.w);
|
|
366
|
-
transform.setOrigin(pos);
|
|
367
|
-
transform.setRotation(quat);
|
|
368
|
-
ammoBody.setWorldTransform(transform);
|
|
369
|
-
ammoBody.getMotionState().setWorldTransform(transform);
|
|
370
|
-
// Clear velocities for all rigidbodies
|
|
371
|
-
if (!this.zeroVector) {
|
|
372
|
-
this.zeroVector = new Ammo.btVector3(0, 0, 0);
|
|
373
|
-
}
|
|
374
|
-
ammoBody.setLinearVelocity(this.zeroVector);
|
|
375
|
-
ammoBody.setAngularVelocity(this.zeroVector);
|
|
376
|
-
// Explicitly activate dynamic rigidbodies after reset (wake them up)
|
|
377
|
-
// This is critical for dress pieces and other dynamic bodies to prevent teleporting
|
|
378
|
-
if (rb.type === RigidbodyType.Dynamic) {
|
|
379
|
-
ammoBody.activate(true); // Wake up the body
|
|
380
|
-
}
|
|
381
|
-
Ammo.destroy(pos);
|
|
382
|
-
Ammo.destroy(quat);
|
|
383
|
-
}
|
|
384
|
-
// Step simulation once to stabilize (like babylon-mmd)
|
|
385
|
-
if (this.dynamicsWorld.stepSimulation) {
|
|
386
|
-
this.dynamicsWorld.stepSimulation(0, 0, 0);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
336
|
// Syncs bones to rigidbodies, simulates dynamics, solves constraints
|
|
390
337
|
// Modifies boneWorldMatrices in-place for dynamic rigidbodies that drive bones
|
|
391
338
|
step(dt, boneWorldMatrices, boneInverseBindMatrices) {
|
package/dist/pmx-loader.js
CHANGED
|
@@ -217,8 +217,8 @@ export class PmxLoader {
|
|
|
217
217
|
this.getFloat32(),
|
|
218
218
|
this.getFloat32(),
|
|
219
219
|
];
|
|
220
|
-
// edgeSize float
|
|
221
|
-
const edgeSize = this.getFloat32()
|
|
220
|
+
// edgeSize float (outline width in PMX units; engine scales by camera distance for stable screen thickness)
|
|
221
|
+
const edgeSize = this.getFloat32();
|
|
222
222
|
const textureIndex = this.getNonVertexIndex(this.textureIndexSize);
|
|
223
223
|
const sphereTextureIndex = this.getNonVertexIndex(this.textureIndexSize);
|
|
224
224
|
const sphereTextureMode = this.getUint8();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reze-engine",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "A WebGPU
|
|
3
|
+
"version": "0.9.2",
|
|
4
|
+
"description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
package/src/camera.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Mat4, Vec3 } from "./math"
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** Far cap / zoom limit; large enough for wide shots without clipping distant ground */
|
|
4
|
+
const FAR_CAP = 8000
|
|
5
|
+
const FAR_MIN = 200
|
|
4
6
|
|
|
5
7
|
export class Camera {
|
|
6
8
|
alpha: number
|
|
@@ -10,7 +12,7 @@ export class Camera {
|
|
|
10
12
|
fov: number
|
|
11
13
|
aspect: number = 1
|
|
12
14
|
near: number = 0.05
|
|
13
|
-
far: number =
|
|
15
|
+
far: number = FAR_CAP
|
|
14
16
|
|
|
15
17
|
// Input state
|
|
16
18
|
private canvas: HTMLCanvasElement | null = null
|
|
@@ -30,7 +32,7 @@ export class Camera {
|
|
|
30
32
|
wheelPrecision: number = 0.01
|
|
31
33
|
pinchPrecision: number = 0.05
|
|
32
34
|
minZ: number = 0.05
|
|
33
|
-
maxZ: number =
|
|
35
|
+
maxZ: number = FAR_CAP
|
|
34
36
|
lowerBetaLimit: number = 0.001
|
|
35
37
|
upperBetaLimit: number = Math.PI - 0.001
|
|
36
38
|
|
|
@@ -40,6 +42,7 @@ export class Camera {
|
|
|
40
42
|
this.radius = radius
|
|
41
43
|
this.target = target
|
|
42
44
|
this.fov = fov
|
|
45
|
+
this.updateFarFromRadius()
|
|
43
46
|
|
|
44
47
|
// Bind event handlers
|
|
45
48
|
this.onMouseDown = this.onMouseDown.bind(this)
|
|
@@ -127,7 +130,14 @@ export class Camera {
|
|
|
127
130
|
this.target = this.target.add(panRight).add(panUp)
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
/** Far plane grows with zoom-out so big floors / distant geometry stay visible */
|
|
134
|
+
private updateFarFromRadius(): void {
|
|
135
|
+
const margin = 600
|
|
136
|
+
this.far = Math.min(FAR_CAP, Math.max(FAR_MIN, this.radius * 12 + margin))
|
|
137
|
+
}
|
|
138
|
+
|
|
130
139
|
getProjectionMatrix(): Mat4 {
|
|
140
|
+
this.updateFarFromRadius()
|
|
131
141
|
return Mat4.perspective(this.fov, this.aspect, this.near, this.far)
|
|
132
142
|
}
|
|
133
143
|
|
|
@@ -206,8 +216,7 @@ export class Camera {
|
|
|
206
216
|
|
|
207
217
|
// Clamp radius to reasonable bounds
|
|
208
218
|
this.radius = Math.max(this.minZ, Math.min(this.maxZ, this.radius))
|
|
209
|
-
|
|
210
|
-
this.far = Math.max(FAR, this.radius * 4)
|
|
219
|
+
this.updateFarFromRadius()
|
|
211
220
|
}
|
|
212
221
|
|
|
213
222
|
private onContextMenu(e: Event) {
|
|
@@ -285,8 +294,7 @@ export class Camera {
|
|
|
285
294
|
|
|
286
295
|
// Clamp radius to reasonable bounds
|
|
287
296
|
this.radius = Math.max(this.minZ, Math.min(this.maxZ, this.radius))
|
|
288
|
-
|
|
289
|
-
this.far = Math.max(FAR, this.radius * 4)
|
|
297
|
+
this.updateFarFromRadius()
|
|
290
298
|
}
|
|
291
299
|
|
|
292
300
|
if (isPanGesture) {
|
package/src/engine.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Camera } from "./camera"
|
|
|
2
2
|
import { Mat4, Vec3 } from "./math"
|
|
3
3
|
import { Model } from "./model"
|
|
4
4
|
import { PmxLoader } from "./pmx-loader"
|
|
5
|
-
import { Physics } from "./physics"
|
|
5
|
+
import { Physics, type PhysicsOptions } from "./physics"
|
|
6
6
|
|
|
7
7
|
export type RaycastCallback = (modelName: string, material: string | null, screenX: number, screenY: number) => void
|
|
8
8
|
|
|
@@ -15,11 +15,10 @@ export type EngineOptions = {
|
|
|
15
15
|
cameraTarget?: Vec3
|
|
16
16
|
cameraFov?: number
|
|
17
17
|
onRaycast?: RaycastCallback
|
|
18
|
+
physicsOptions?: PhysicsOptions
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
export const DEFAULT_ENGINE_OPTIONS: RequiredEngineOptions = {
|
|
21
|
+
export const DEFAULT_ENGINE_OPTIONS = {
|
|
23
22
|
ambientColor: new Vec3(0.88, 0.88, 0.88),
|
|
24
23
|
directionalLightIntensity: 0.24,
|
|
25
24
|
minSpecularIntensity: 0.3,
|
|
@@ -28,6 +27,7 @@ export const DEFAULT_ENGINE_OPTIONS: RequiredEngineOptions = {
|
|
|
28
27
|
cameraTarget: new Vec3(0, 12.5, 0),
|
|
29
28
|
cameraFov: Math.PI / 4,
|
|
30
29
|
onRaycast: undefined,
|
|
30
|
+
physicsOptions: { constraintSolverKeywords: ["胸"] },
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export interface EngineStats {
|
|
@@ -141,6 +141,7 @@ export class Engine {
|
|
|
141
141
|
private shadowVPLightZ = Number.NaN
|
|
142
142
|
|
|
143
143
|
private onRaycast?: RaycastCallback
|
|
144
|
+
private physicsOptions: PhysicsOptions = DEFAULT_ENGINE_OPTIONS.physicsOptions
|
|
144
145
|
private lastTouchTime = 0
|
|
145
146
|
private readonly DOUBLE_TAP_DELAY = 300
|
|
146
147
|
// GPU picking
|
|
@@ -183,7 +184,7 @@ export class Engine {
|
|
|
183
184
|
constructor(canvas: HTMLCanvasElement, options?: EngineOptions) {
|
|
184
185
|
this.canvas = canvas
|
|
185
186
|
if (options) {
|
|
186
|
-
this.ambientColor = options.ambientColor ?? DEFAULT_ENGINE_OPTIONS.ambientColor
|
|
187
|
+
this.ambientColor = options.ambientColor ?? DEFAULT_ENGINE_OPTIONS.ambientColor
|
|
187
188
|
this.directionalLightIntensity =
|
|
188
189
|
options.directionalLightIntensity ?? DEFAULT_ENGINE_OPTIONS.directionalLightIntensity
|
|
189
190
|
this.minSpecularIntensity = options.minSpecularIntensity ?? DEFAULT_ENGINE_OPTIONS.minSpecularIntensity
|
|
@@ -192,6 +193,7 @@ export class Engine {
|
|
|
192
193
|
this.cameraTarget = options.cameraTarget ?? DEFAULT_ENGINE_OPTIONS.cameraTarget
|
|
193
194
|
this.cameraFov = options.cameraFov ?? DEFAULT_ENGINE_OPTIONS.cameraFov
|
|
194
195
|
this.onRaycast = options.onRaycast
|
|
196
|
+
this.physicsOptions = options.physicsOptions ?? DEFAULT_ENGINE_OPTIONS.physicsOptions
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
199
|
|
|
@@ -696,9 +698,11 @@ export class Engine {
|
|
|
696
698
|
}
|
|
697
699
|
let worldPos = skinnedPos.xyz;
|
|
698
700
|
let worldNormal = normalize(skinnedNrm);
|
|
699
|
-
|
|
700
|
-
let
|
|
701
|
-
let
|
|
701
|
+
// Screen-stable edgeline: extrusion ∝ camera distance (same idea as MMD viewers / babylon-mmd-style scaling)
|
|
702
|
+
let camDist = max(length(camera.viewPos - worldPos), 0.25);
|
|
703
|
+
let refDist = 30.0;
|
|
704
|
+
let edgeScale = 0.03;
|
|
705
|
+
let expandedPos = worldPos + worldNormal * material.edgeSize * edgeScale * (camDist / refDist);
|
|
702
706
|
output.position = camera.projection * camera.view * vec4f(expandedPos, 1.0);
|
|
703
707
|
return output;
|
|
704
708
|
}
|
|
@@ -718,7 +722,8 @@ export class Engine {
|
|
|
718
722
|
cullMode: "back",
|
|
719
723
|
depthStencil: {
|
|
720
724
|
format: "depth24plus-stencil8",
|
|
721
|
-
|
|
725
|
+
// Don’t write outline into depth buffer — stops z-fighting / black cracks vs body (MMD-style; body depth stays authoritative)
|
|
726
|
+
depthWriteEnabled: false,
|
|
722
727
|
depthCompare: "less-equal",
|
|
723
728
|
},
|
|
724
729
|
})
|
|
@@ -1185,14 +1190,6 @@ export class Engine {
|
|
|
1185
1190
|
return this.physicsEnabled
|
|
1186
1191
|
}
|
|
1187
1192
|
|
|
1188
|
-
public resetPhysics(): void {
|
|
1189
|
-
this.forEachInstance((inst) => {
|
|
1190
|
-
if (!inst.physics) return
|
|
1191
|
-
inst.model.computeWorldMatrices()
|
|
1192
|
-
inst.physics.reset(inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices())
|
|
1193
|
-
})
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
1193
|
private forEachInstance(fn: (inst: ModelInstance) => void): void {
|
|
1197
1194
|
for (const inst of this.modelInstances.values()) fn(inst)
|
|
1198
1195
|
}
|
|
@@ -1275,7 +1272,7 @@ export class Engine {
|
|
|
1275
1272
|
this.device.queue.writeBuffer(indexBuffer, 0, indices)
|
|
1276
1273
|
|
|
1277
1274
|
const rbs = model.getRigidbodies()
|
|
1278
|
-
const physics = rbs.length > 0 ? new Physics(rbs, model.getJoints()) : null
|
|
1275
|
+
const physics = rbs.length > 0 ? new Physics(rbs, model.getJoints(), this.physicsOptions) : null
|
|
1279
1276
|
|
|
1280
1277
|
const shadowBindGroup = this.device.createBindGroup({
|
|
1281
1278
|
label: `${name}: shadow bind`,
|
package/src/index.ts
CHANGED
package/src/physics.ts
CHANGED
|
@@ -50,10 +50,18 @@ export interface Joint {
|
|
|
50
50
|
springRotation: Vec3 // Spring stiffness values
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export interface PhysicsOptions {
|
|
54
|
+
// Joint name keywords for per-joint Bullet 2.75 constraint solver behavior.
|
|
55
|
+
// Joints whose name contains any keyword get m_useOffsetForConstraintFrame
|
|
56
|
+
// disabled (matching Bullet 2.75). All others keep the stable Ammo 2.82+ default.
|
|
57
|
+
constraintSolverKeywords?: string[]
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
export class Physics {
|
|
54
61
|
private rigidbodies: Rigidbody[]
|
|
55
62
|
private joints: Joint[]
|
|
56
63
|
private gravity: Vec3 = new Vec3(0, -98, 0) // Gravity acceleration (cm/s²), MMD-style default
|
|
64
|
+
private constraintSolverPattern: RegExp | null = null
|
|
57
65
|
private ammoInitialized = false
|
|
58
66
|
private ammoPromise: Promise<AmmoInstance> | null = null
|
|
59
67
|
private ammo: AmmoInstance | null = null
|
|
@@ -66,13 +74,16 @@ export class Physics {
|
|
|
66
74
|
private rigidbodiesInitialized = false // bodyOffsetMatrixInverse computed and bodies positioned
|
|
67
75
|
private jointsCreated = false // Joints delayed until after rigidbodies are positioned
|
|
68
76
|
private firstFrame = true // Needed to reposition bodies before creating joints
|
|
69
|
-
private forceDisableOffsetForConstraintFrame = true // MMD compatibility (Bullet 2.75 behavior)
|
|
70
77
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
78
|
private zeroVector: any = null // Cached zero vector for velocity clearing
|
|
72
79
|
|
|
73
|
-
constructor(rigidbodies: Rigidbody[], joints: Joint[] = []) {
|
|
80
|
+
constructor(rigidbodies: Rigidbody[], joints: Joint[] = [], options?: PhysicsOptions) {
|
|
74
81
|
this.rigidbodies = rigidbodies
|
|
75
82
|
this.joints = joints
|
|
83
|
+
const keywords = options?.constraintSolverKeywords ?? []
|
|
84
|
+
if (keywords.length > 0) {
|
|
85
|
+
this.constraintSolverPattern = new RegExp(keywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "i")
|
|
86
|
+
}
|
|
76
87
|
this.initAmmo()
|
|
77
88
|
}
|
|
78
89
|
|
|
@@ -361,8 +372,9 @@ export class Physics {
|
|
|
361
372
|
useLinearReferenceFrameA
|
|
362
373
|
)
|
|
363
374
|
|
|
364
|
-
//
|
|
365
|
-
|
|
375
|
+
// Per-joint Bullet 2.75 constraint solver: disable m_useOffsetForConstraintFrame for
|
|
376
|
+
// joints whose name matches constraintSolverKeywords.
|
|
377
|
+
if (this.constraintSolverPattern && this.constraintSolverPattern.test(joint.name)) {
|
|
366
378
|
let jointPtr: number | undefined
|
|
367
379
|
if (typeof Ammo.getPointer === "function") {
|
|
368
380
|
jointPtr = Ammo.getPointer(constraint)
|
|
@@ -373,7 +385,6 @@ export class Physics {
|
|
|
373
385
|
|
|
374
386
|
if (jointPtr !== undefined && Ammo.HEAP8) {
|
|
375
387
|
const heap8 = Ammo.HEAP8 as Uint8Array
|
|
376
|
-
// jointPtr + 1300 = m_useLinearReferenceFrameA, jointPtr + 1301 = m_useOffsetForConstraintFrame
|
|
377
388
|
if (heap8[jointPtr + 1300] === (useLinearReferenceFrameA ? 1 : 0) && heap8[jointPtr + 1301] === 1) {
|
|
378
389
|
heap8[jointPtr + 1301] = 0
|
|
379
390
|
}
|
|
@@ -457,77 +468,6 @@ export class Physics {
|
|
|
457
468
|
return angle
|
|
458
469
|
}
|
|
459
470
|
|
|
460
|
-
// Reset physics state (reposition bodies, clear velocities)
|
|
461
|
-
// Following babylon-mmd pattern: initialize all rigid body positions from current bone poses
|
|
462
|
-
// Call this when starting a new animation to prevent physics instability from sudden pose changes
|
|
463
|
-
reset(boneWorldMatrices: Mat4[], boneInverseBindMatrices: Float32Array): void {
|
|
464
|
-
if (!this.ammoInitialized || !this.ammo || !this.dynamicsWorld) {
|
|
465
|
-
return
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const boneCount = boneWorldMatrices.length
|
|
469
|
-
const Ammo = this.ammo
|
|
470
|
-
|
|
471
|
-
// Ensure body offsets are computed
|
|
472
|
-
if (!this.rigidbodiesInitialized) {
|
|
473
|
-
this.computeBodyOffsets(boneInverseBindMatrices, boneCount)
|
|
474
|
-
this.rigidbodiesInitialized = true
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Reposition ALL rigid bodies from current bone poses (like babylon-mmd initialize)
|
|
478
|
-
// This ensures all bodies are correctly positioned before physics starts
|
|
479
|
-
for (let i = 0; i < this.rigidbodies.length; i++) {
|
|
480
|
-
const rb = this.rigidbodies[i]
|
|
481
|
-
const ammoBody = this.ammoRigidbodies[i]
|
|
482
|
-
if (!ammoBody || rb.boneIndex < 0 || rb.boneIndex >= boneCount) continue
|
|
483
|
-
|
|
484
|
-
const boneIdx = rb.boneIndex
|
|
485
|
-
|
|
486
|
-
// Get bone world matrix
|
|
487
|
-
const boneWorldMat = boneWorldMatrices[boneIdx]
|
|
488
|
-
|
|
489
|
-
// Compute body world matrix: bodyWorld = boneWorld × bodyOffsetMatrix
|
|
490
|
-
// (like babylon-mmd: bodyWorldMatrix = bodyOffsetMatrix.multiplyToRef(bodyWorldMatrix))
|
|
491
|
-
const bodyOffsetMatrix = rb.bodyOffsetMatrix || rb.bodyOffsetMatrixInverse.inverse()
|
|
492
|
-
const bodyWorldMatrix = boneWorldMat.multiply(bodyOffsetMatrix)
|
|
493
|
-
|
|
494
|
-
const worldPos = bodyWorldMatrix.getPosition()
|
|
495
|
-
const worldRot = bodyWorldMatrix.toQuat()
|
|
496
|
-
|
|
497
|
-
// Set transform matrix
|
|
498
|
-
const transform = new Ammo.btTransform()
|
|
499
|
-
const pos = new Ammo.btVector3(worldPos.x, worldPos.y, worldPos.z)
|
|
500
|
-
const quat = new Ammo.btQuaternion(worldRot.x, worldRot.y, worldRot.z, worldRot.w)
|
|
501
|
-
|
|
502
|
-
transform.setOrigin(pos)
|
|
503
|
-
transform.setRotation(quat)
|
|
504
|
-
|
|
505
|
-
ammoBody.setWorldTransform(transform)
|
|
506
|
-
ammoBody.getMotionState().setWorldTransform(transform)
|
|
507
|
-
|
|
508
|
-
// Clear velocities for all rigidbodies
|
|
509
|
-
if (!this.zeroVector) {
|
|
510
|
-
this.zeroVector = new Ammo.btVector3(0, 0, 0)
|
|
511
|
-
}
|
|
512
|
-
ammoBody.setLinearVelocity(this.zeroVector)
|
|
513
|
-
ammoBody.setAngularVelocity(this.zeroVector)
|
|
514
|
-
|
|
515
|
-
// Explicitly activate dynamic rigidbodies after reset (wake them up)
|
|
516
|
-
// This is critical for dress pieces and other dynamic bodies to prevent teleporting
|
|
517
|
-
if (rb.type === RigidbodyType.Dynamic) {
|
|
518
|
-
ammoBody.activate(true) // Wake up the body
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
Ammo.destroy(pos)
|
|
522
|
-
Ammo.destroy(quat)
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Step simulation once to stabilize (like babylon-mmd)
|
|
526
|
-
if (this.dynamicsWorld.stepSimulation) {
|
|
527
|
-
this.dynamicsWorld.stepSimulation(0, 0, 0)
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
471
|
// Syncs bones to rigidbodies, simulates dynamics, solves constraints
|
|
532
472
|
// Modifies boneWorldMatrices in-place for dynamic rigidbodies that drive bones
|
|
533
473
|
step(dt: number, boneWorldMatrices: Mat4[], boneInverseBindMatrices: Float32Array): void {
|
package/src/pmx-loader.ts
CHANGED
|
@@ -261,8 +261,8 @@ export class PmxLoader {
|
|
|
261
261
|
this.getFloat32(),
|
|
262
262
|
this.getFloat32(),
|
|
263
263
|
]
|
|
264
|
-
// edgeSize float
|
|
265
|
-
const edgeSize = this.getFloat32()
|
|
264
|
+
// edgeSize float (outline width in PMX units; engine scales by camera distance for stable screen thickness)
|
|
265
|
+
const edgeSize = this.getFloat32()
|
|
266
266
|
|
|
267
267
|
const textureIndex = this.getNonVertexIndex(this.textureIndexSize)
|
|
268
268
|
const sphereTextureIndex = this.getNonVertexIndex(this.textureIndexSize)
|