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 CHANGED
@@ -1,44 +1,117 @@
1
1
  # Reze Engine
2
2
 
3
- A lightweight engine built with WebGPU and TypeScript for real-time 3D anime character MMD model rendering.
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 lighting, alpha blending, rim lighting, outlines, MSAA 4x
8
- - Post alpha eye rendering (see-through eyes)
9
- - Bone and morph API, VMD animation (multiple named, non-interruptible), IK solver, Ammo/Bullet physics
10
- - Multi-model (per-model materials, IK, physics)
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
- ## Usage
20
+ ## Quick Start
13
21
 
14
22
  ```javascript
15
- import { Engine, Model } from "reze-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
- const model = await engine.loadModel("/models/reze/reze.pmx")
20
- await model.loadAnimation("default", "/animations/dance.vmd")
21
- model.show("default")
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
- engine.runRenderLoop(() => {})
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 (summary)
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
- - **Multi-model:** `engine.loadModel(path)` / `engine.loadModel(name, path)`, `engine.addModel(model, pmxPath, name?)`, `getModel(name)`, `getModelNames()`, `removeModel(name)`, `setMaterialVisible(modelName, materialName, visible)`, `setIKEnabled(enabled)`, `setPhysicsEnabled(enabled)`, `getIKEnabled()`, `getPhysicsEnabled()`, `resetPhysics()`, `markVertexBufferDirty(modelName?)`
29
- - **Animation:** `model.loadAnimation(name, vmdUrl)`; `model.show(name)`; `model.play()` / `model.play(name)`; `model.pause()`; `model.stop()`; `model.seek(t)`; `model.getAnimationProgress()`. Animations are non-interruptible (next is queued).
30
- - **Bones / morphs:** `model.rotateBones()`, `model.moveBones()`, `model.setMorphWeight()`, `model.resetAllBones()`, `model.resetAllMorphs()`
31
- - **Engine:** `runRenderLoop()`, `addGround({ mode: "reflection" | "shadow", ... })`, `onRaycast: (modelName, material, screenX, screenY) => {}`
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)** - Online real-time motion capture for MMD using webcam and MediaPipe
36
- - **[Popo](https://popo.love)** - Fine-tuned LLM that generates MMD poses from natural language descriptions
37
- - **[MPL](https://mmd-mpl.vercel.app)** - Semantic motion programming language for scripting MMD animations with intuitive syntax
38
- - **[Mixamo-MMD](https://mixamo-mmd.vercel.app)** - Retarget Mixamo FBX animation to VMD in one click
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;
@@ -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;AAInC,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,CAAM;IAGjB,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,CAAM;IAClB,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;IAkBhG,WAAW,IAAI,IAAI;IAQnB,aAAa,IAAI,IAAI;IAQrB,OAAO,CAAC,gBAAgB;IA0CxB,OAAO,CAAC,SAAS;IAiBjB,mBAAmB,IAAI,IAAI;IAI3B,aAAa,CAAC,MAAM,EAAE,iBAAiB;IAiBvC,aAAa;IAkBb,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,WAAW;IAqBnB,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,OAAO;IAYf,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,YAAY;IA6BpB,OAAO,CAAC,WAAW;IAiFnB,OAAO,CAAC,UAAU;CA+BnB"}
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
- const FAR = 2000;
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 = 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 = FAR;
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
- // Expand far plane to keep scene visible when zooming out
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
- // Expand far plane for pinch zoom as well
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;
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
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 scaleFactor = 0.01;
575
- let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
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
- depthWriteEnabled: true,
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
@@ -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
@@ -2,3 +2,4 @@ export { Engine } from "./engine";
2
2
  export { Model } from "./model";
3
3
  export { Vec3, Quat, Mat4 } from "./math";
4
4
  export { AnimationState, } from "./animation";
5
+ export { Physics } from "./physics";
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;
@@ -1 +1 @@
1
- {"version":3,"file":"physics.d.ts","sourceRoot":"","sources":["../src/physics.ts"],"names":[],"mappings":"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"}
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
- // Disable offset for constraint frame for MMD compatibility (Bullet 2.75 behavior)
250
- if (this.forceDisableOffsetForConstraintFrame) {
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) {
@@ -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() * 2; // double the size for better visibility
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.0",
4
- "description": "A WebGPU game engine for MMD/PMX models",
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
- const FAR = 2000
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 = FAR
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 = FAR
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
- // Expand far plane to keep scene visible when zooming out
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
- // Expand far plane for pinch zoom as well
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 type RequiredEngineOptions = Required<Omit<EngineOptions, "onRaycast">> & Pick<EngineOptions, "onRaycast">
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 scaleFactor = 0.01;
701
- let expandedPos = worldPos + worldNormal * material.edgeSize * scaleFactor;
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
- depthWriteEnabled: true,
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
@@ -7,3 +7,4 @@ export {
7
7
  type BoneKeyframe,
8
8
  type MorphKeyframe,
9
9
  } from "./animation"
10
+ export { Physics, type PhysicsOptions } from "./physics"
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
- // Disable offset for constraint frame for MMD compatibility (Bullet 2.75 behavior)
365
- if (this.forceDisableOffsetForConstraintFrame) {
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()*2 // double the size for better visibility
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)