reze-engine 0.16.2 → 0.16.3
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.map +1 -1
- package/dist/engine.js +5 -5
- package/dist/model.d.ts +4 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +14 -1
- package/dist/physics/body.d.ts +1 -0
- package/dist/physics/body.d.ts.map +1 -1
- package/dist/physics/body.js +2 -0
- package/dist/physics/constraint.d.ts +1 -1
- package/dist/physics/constraint.d.ts.map +1 -1
- package/dist/physics/constraint.js +6 -1
- package/dist/physics/physics.d.ts +3 -0
- package/dist/physics/physics.d.ts.map +1 -1
- package/dist/physics/physics.js +69 -1
- package/dist/physics/solver.js +16 -8
- package/dist/physics/types.d.ts +2 -1
- package/dist/physics/types.d.ts.map +1 -1
- package/dist/physics/types.js +5 -0
- package/dist/physics/world.d.ts +3 -0
- package/dist/physics/world.d.ts.map +1 -1
- package/dist/physics/world.js +19 -3
- package/dist/pmx-loader.d.ts +2 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +37 -20
- package/dist/shaders/materials/body.d.ts +1 -1
- package/dist/shaders/materials/body.d.ts.map +1 -1
- package/dist/shaders/materials/body.js +90 -85
- package/dist/shaders/materials/cloth_rough.d.ts +1 -1
- package/dist/shaders/materials/cloth_rough.d.ts.map +1 -1
- package/dist/shaders/materials/cloth_rough.js +1 -1
- package/dist/shaders/materials/cloth_smooth.d.ts +1 -1
- package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -1
- package/dist/shaders/materials/cloth_smooth.js +2 -2
- package/dist/shaders/materials/common.d.ts +1 -1
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +10 -0
- package/dist/shaders/materials/default.d.ts +1 -1
- package/dist/shaders/materials/default.d.ts.map +1 -1
- package/dist/shaders/materials/default.js +37 -32
- package/dist/shaders/materials/eye.d.ts +1 -1
- package/dist/shaders/materials/eye.d.ts.map +1 -1
- package/dist/shaders/materials/eye.js +40 -35
- package/dist/shaders/materials/face.d.ts +1 -1
- package/dist/shaders/materials/face.d.ts.map +1 -1
- package/dist/shaders/materials/face.js +90 -85
- package/dist/shaders/materials/hair.d.ts +1 -1
- package/dist/shaders/materials/hair.d.ts.map +1 -1
- package/dist/shaders/materials/hair.js +85 -75
- package/dist/shaders/materials/metal.d.ts +1 -1
- package/dist/shaders/materials/metal.d.ts.map +1 -1
- package/dist/shaders/materials/metal.js +1 -1
- package/dist/shaders/materials/stockings.d.ts +1 -1
- package/dist/shaders/materials/stockings.d.ts.map +1 -1
- package/dist/shaders/materials/stockings.js +1 -1
- package/package.json +1 -1
- package/src/engine.ts +36 -21
- package/src/model.ts +18 -1
- package/src/physics/body.ts +5 -0
- package/src/physics/constraint.ts +6 -1
- package/src/physics/physics.ts +68 -1
- package/src/physics/solver.ts +16 -8
- package/src/physics/types.ts +10 -2
- package/src/physics/world.ts +20 -3
- package/src/pmx-loader.ts +38 -20
- package/src/shaders/materials/body.ts +97 -92
- package/src/shaders/materials/cloth_rough.ts +1 -1
- package/src/shaders/materials/cloth_smooth.ts +2 -2
- package/src/shaders/materials/common.ts +10 -0
- package/src/shaders/materials/default.ts +45 -40
- package/src/shaders/materials/eye.ts +48 -43
- package/src/shaders/materials/face.ts +97 -92
- package/src/shaders/materials/hair.ts +92 -82
- package/src/shaders/materials/metal.ts +1 -1
- package/src/shaders/materials/stockings.ts +1 -1
package/src/physics/physics.ts
CHANGED
|
@@ -50,6 +50,15 @@ export class RezePhysics {
|
|
|
50
50
|
private kinRoot: Int32Array
|
|
51
51
|
// Kinematic bodies whose target jumped discontinuously this frame.
|
|
52
52
|
private teleportFlags: Uint8Array
|
|
53
|
+
// PMX mode-2 bodies jointed *directly* to a bone-follow body (breasts,
|
|
54
|
+
// chain roots). Only these get MMD's bone-position alignment: their bone's
|
|
55
|
+
// animated matrix is trustworthy, whereas deeper chain bones' animated
|
|
56
|
+
// matrices don't include the physics deflection of their parents, so
|
|
57
|
+
// pinning those would freeze the chain.
|
|
58
|
+
private alignPinned: Uint8Array
|
|
59
|
+
// Debug counter: frames on which a teleport (scrub/jump discontinuity)
|
|
60
|
+
// was detected and settled.
|
|
61
|
+
teleportCount = 0
|
|
53
62
|
|
|
54
63
|
constructor(rigidbodies: Rigidbody[], joints: Joint[] = []) {
|
|
55
64
|
this.rigidbodies = rigidbodies
|
|
@@ -66,6 +75,17 @@ export class RezePhysics {
|
|
|
66
75
|
this.kinTargetAngVel = new Float32Array(this.store.count * 3)
|
|
67
76
|
this.kinRoot = this.buildKinematicRoots()
|
|
68
77
|
this.teleportFlags = new Uint8Array(this.store.count)
|
|
78
|
+
|
|
79
|
+
this.alignPinned = new Uint8Array(this.store.count)
|
|
80
|
+
const types = this.store.type
|
|
81
|
+
for (const c of this.constraints) {
|
|
82
|
+
const tA = types[c.bodyA]
|
|
83
|
+
const tB = types[c.bodyB]
|
|
84
|
+
const aFollows = tA === RigidbodyType.Static || tA === RigidbodyType.Kinematic
|
|
85
|
+
const bFollows = tB === RigidbodyType.Static || tB === RigidbodyType.Kinematic
|
|
86
|
+
if (aFollows && this.store.aligned[c.bodyB]) this.alignPinned[c.bodyB] = 1
|
|
87
|
+
if (bFollows && this.store.aligned[c.bodyA]) this.alignPinned[c.bodyA] = 1
|
|
88
|
+
}
|
|
69
89
|
}
|
|
70
90
|
|
|
71
91
|
// BFS over the joint graph from every kinematic body, assigning each
|
|
@@ -140,6 +160,13 @@ export class RezePhysics {
|
|
|
140
160
|
if (this.firstFrame) return
|
|
141
161
|
this.snapBodiesToBones(boneWorldMatrices)
|
|
142
162
|
this.savePrevState() // prev == curr after a snap, so no interpolation jump
|
|
163
|
+
// Reseed kinematic targets from the snapped pose — otherwise the next
|
|
164
|
+
// step derives the target-trajectory velocity against the pre-reset
|
|
165
|
+
// targets and feeds one frame of enormous anchor velocity to the solver.
|
|
166
|
+
this.kinTargetPos.set(this.store.positions)
|
|
167
|
+
this.kinTargetOri.set(this.store.orientations)
|
|
168
|
+
this.kinTargetVel.fill(0)
|
|
169
|
+
this.kinTargetAngVel.fill(0)
|
|
143
170
|
this.timeAccum = 0
|
|
144
171
|
}
|
|
145
172
|
|
|
@@ -165,6 +192,7 @@ export class RezePhysics {
|
|
|
165
192
|
// raw derived velocity is what used to explode the solver. Chains under
|
|
166
193
|
// unaffected roots keep their momentum untouched.
|
|
167
194
|
if (this.computeKinematicTargets(boneWorldMatrices, dt)) {
|
|
195
|
+
this.teleportCount++
|
|
168
196
|
this.carryDynamicThroughTeleport()
|
|
169
197
|
this.snapKinematicToTargets(true)
|
|
170
198
|
this.savePrevState() // prev == curr so interpolation doesn't streak
|
|
@@ -197,6 +225,10 @@ export class RezePhysics {
|
|
|
197
225
|
this.snapKinematicToTargets(false)
|
|
198
226
|
}
|
|
199
227
|
|
|
228
|
+
// MMD mode-2 bone alignment for pinned (depth-1) bodies: position
|
|
229
|
+
// re-pins to the animated bone, rotation stays simulated.
|
|
230
|
+
this.alignPinnedBodiesToBones(boneWorldMatrices)
|
|
231
|
+
|
|
200
232
|
// Fraction into the next (not-yet-taken) step; always in [0, 1).
|
|
201
233
|
const alpha = this.fixedTimeStep > 0 ? this.timeAccum / this.fixedTimeStep : 0
|
|
202
234
|
this.applyDynamicsToBones(boneWorldMatrices, alpha)
|
|
@@ -500,6 +532,32 @@ export class RezePhysics {
|
|
|
500
532
|
}
|
|
501
533
|
}
|
|
502
534
|
|
|
535
|
+
// Overwrite pinned mode-2 bodies' positions with boneWorld × bodyOffset
|
|
536
|
+
// from the animated bone pose, keeping simulated orientation and
|
|
537
|
+
// velocities. prevPositions follows so interpolation doesn't streak.
|
|
538
|
+
private alignPinnedBodiesToBones(boneWorldMatrices: Mat4[]): void {
|
|
539
|
+
const N = this.store.count
|
|
540
|
+
const pinned = this.alignPinned
|
|
541
|
+
const boneIdx = this.store.boneIndex
|
|
542
|
+
const offsets = this.store.bodyOffsetMatrix
|
|
543
|
+
const positions = this.store.positions
|
|
544
|
+
const prevPos = this.prevPositions
|
|
545
|
+
|
|
546
|
+
for (let i = 0; i < N; i++) {
|
|
547
|
+
if (!pinned[i]) continue
|
|
548
|
+
const b = boneIdx[i]
|
|
549
|
+
if (b < 0 || b >= boneWorldMatrices.length) continue
|
|
550
|
+
Mat4.multiplyArrays(boneWorldMatrices[b].values, 0, offsets, i * 16, _bodyMat, 0)
|
|
551
|
+
const i3 = i * 3
|
|
552
|
+
positions[i3 + 0] = _bodyMat[12]
|
|
553
|
+
positions[i3 + 1] = _bodyMat[13]
|
|
554
|
+
positions[i3 + 2] = _bodyMat[14]
|
|
555
|
+
prevPos[i3 + 0] = _bodyMat[12]
|
|
556
|
+
prevPos[i3 + 1] = _bodyMat[13]
|
|
557
|
+
prevPos[i3 + 2] = _bodyMat[14]
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
503
561
|
// Backstop: if a dynamic body's state went non-finite despite the velocity
|
|
504
562
|
// caps, restore its previous-substep pose with zero velocity instead of
|
|
505
563
|
// letting NaNs spread through constraints and contacts. Runs after every
|
|
@@ -589,7 +647,16 @@ export class RezePhysics {
|
|
|
589
647
|
|
|
590
648
|
// Sanity gate against NaN / extreme values — silently drop the update.
|
|
591
649
|
if (Number.isFinite(_boneMat[0]) && Math.abs(_boneMat[0]) < 1e6) {
|
|
592
|
-
boneWorldMatrices[b].values
|
|
650
|
+
const v = boneWorldMatrices[b].values
|
|
651
|
+
if (this.alignPinned[i]) {
|
|
652
|
+
// Pinned mode-2: the bone keeps its animated position, physics
|
|
653
|
+
// drives rotation only.
|
|
654
|
+
v[0] = _boneMat[0]; v[1] = _boneMat[1]; v[2] = _boneMat[2]
|
|
655
|
+
v[4] = _boneMat[4]; v[5] = _boneMat[5]; v[6] = _boneMat[6]
|
|
656
|
+
v[8] = _boneMat[8]; v[9] = _boneMat[9]; v[10] = _boneMat[10]
|
|
657
|
+
} else {
|
|
658
|
+
v.set(_boneMat)
|
|
659
|
+
}
|
|
593
660
|
}
|
|
594
661
|
}
|
|
595
662
|
}
|
package/src/physics/solver.ts
CHANGED
|
@@ -182,12 +182,15 @@ function setupConstraint(
|
|
|
182
182
|
tgt[i] = target
|
|
183
183
|
act[i] = denom > 0 ? active : 0
|
|
184
184
|
con.cacheLinLimitImp[i] = 0
|
|
185
|
-
|
|
185
|
+
// A spring on a locked axis is redundant — the bilateral limit row
|
|
186
|
+
// already welds the DOF, and driving it twice overshoots every
|
|
187
|
+
// iteration (PMX rigs routinely put k=100000 springs on locked axes,
|
|
188
|
+
// which turned welded weight-bodies into energy pumps).
|
|
189
|
+
if (con.springEnabled[i] && denom > 0 && lo !== hi) {
|
|
186
190
|
// Clamp k to the deadbeat limit: an explicit spring with k·dt² > 1
|
|
187
191
|
// overshoots equilibrium every step and pumps energy — the classic
|
|
188
|
-
// pre-Spring2 Bullet 6dof instability this port inherited.
|
|
189
|
-
|
|
190
|
-
const k = Math.min(con.springStiffness[i], 0.25 * invDt * invDt)
|
|
192
|
+
// pre-Spring2 Bullet 6dof instability this port inherited.
|
|
193
|
+
const k = Math.min(con.springStiffness[i], invDt * invDt)
|
|
191
194
|
const serr = curr - con.equilibriumPoint[i]
|
|
192
195
|
con.cacheLinSpringTarget[i] = -k * serr * dt
|
|
193
196
|
con.cacheLinSpringActive[i] = 1
|
|
@@ -238,9 +241,11 @@ function setupConstraint(
|
|
|
238
241
|
const angAct = con.cacheAngActive
|
|
239
242
|
for (let i = 0; i < 3; i++) {
|
|
240
243
|
const idx = i + 3
|
|
241
|
-
|
|
244
|
+
// Springs on locked axes are skipped — the limit row welds those, and
|
|
245
|
+
// double-driving a DOF overshoots every iteration (see the linear loop).
|
|
246
|
+
if (con.springEnabled[idx] && angDenom > 0 && con.angularMin[i] !== con.angularMax[i]) {
|
|
242
247
|
// Same deadbeat clamp as the linear springs.
|
|
243
|
-
const k = Math.min(con.springStiffness[idx],
|
|
248
|
+
const k = Math.min(con.springStiffness[idx], invDt * invDt)
|
|
244
249
|
const serr = _angDiffScratch[i] - con.equilibriumPoint[idx]
|
|
245
250
|
angTgt[i] = k * serr * dt
|
|
246
251
|
angAct[i] = 1
|
|
@@ -388,9 +393,12 @@ function iterateConstraint(
|
|
|
388
393
|
}
|
|
389
394
|
|
|
390
395
|
// Spring row: velocity-target drive; the deadbeat k-clamp at setup
|
|
391
|
-
// bounds its aggression.
|
|
396
|
+
// bounds its aggression. relVel is refreshed with the limit impulse
|
|
397
|
+
// applied just above (j·denom = j / jac) — driving the spring off the
|
|
398
|
+
// stale value double-corrects the DOF and overshoots every iteration.
|
|
392
399
|
if (sprAct[i]) {
|
|
393
|
-
j
|
|
400
|
+
const relVelNow = j !== 0 ? relVel + j / jac[i] : relVel
|
|
401
|
+
j += (sprTgt[i] - relVelNow) * jac[i]
|
|
394
402
|
}
|
|
395
403
|
|
|
396
404
|
if (j === 0) continue
|
package/src/physics/types.ts
CHANGED
|
@@ -6,10 +6,15 @@ export enum RigidbodyShape {
|
|
|
6
6
|
Capsule = 2,
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
// PMX physics mode: 0 = follow bone, 1 = physics, 2 = physics + bone-position
|
|
10
|
+
// alignment. Mode 2 is DYNAMIC — the loader maps it to Dynamic with
|
|
11
|
+
// `aligned: true` (bone gets rotation only, body position re-pins to the
|
|
12
|
+
// animated bone each frame). Mapping the raw byte 1:1 onto this enum froze
|
|
13
|
+
// mode-2 bodies (most modern cloth rigs, and every 胸_回転 breast body).
|
|
9
14
|
export enum RigidbodyType {
|
|
10
|
-
Static = 0,
|
|
15
|
+
Static = 0, // follows bone (anchor)
|
|
11
16
|
Dynamic = 1,
|
|
12
|
-
Kinematic = 2,
|
|
17
|
+
Kinematic = 2, // follows bone (legacy alias; loader no longer emits it)
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export interface Rigidbody {
|
|
@@ -28,6 +33,9 @@ export interface Rigidbody {
|
|
|
28
33
|
restitution: number
|
|
29
34
|
friction: number
|
|
30
35
|
type: RigidbodyType
|
|
36
|
+
// PMX mode 2: dynamic body whose bone takes rotation only; the body's
|
|
37
|
+
// position re-pins to the animated bone each frame.
|
|
38
|
+
aligned?: boolean
|
|
31
39
|
bodyOffsetMatrixInverse: Mat4 // Inverse of body offset matrix, used to sync rigidbody to bone
|
|
32
40
|
bodyOffsetMatrix?: Mat4 // Cached non-inverse for performance (computed once during initialization)
|
|
33
41
|
}
|
package/src/physics/world.ts
CHANGED
|
@@ -14,6 +14,12 @@ export class World {
|
|
|
14
14
|
readonly gravity: Vec3
|
|
15
15
|
solverIterations = 10
|
|
16
16
|
|
|
17
|
+
// Per-body damping factors pow(1−damping, dt), cached because damping and
|
|
18
|
+
// the fixed dt never change — two Math.pow per body per substep otherwise.
|
|
19
|
+
private dampCacheDt = -1
|
|
20
|
+
private linDampFactor: Float32Array | null = null
|
|
21
|
+
private angDampFactor: Float32Array | null = null
|
|
22
|
+
|
|
17
23
|
constructor(gravity: Vec3) {
|
|
18
24
|
this.gravity = new Vec3(gravity.x, gravity.y, gravity.z)
|
|
19
25
|
}
|
|
@@ -43,15 +49,26 @@ export class World {
|
|
|
43
49
|
|
|
44
50
|
// 1. Predict — gravity + damping. The pow form (vs the linear
|
|
45
51
|
// 1−damping·dt approximation) stays stable at high PMX damping
|
|
46
|
-
// values like 0.99.
|
|
52
|
+
// values like 0.99. Factors are cached (damping and dt are constant).
|
|
53
|
+
if (this.dampCacheDt !== dt || !this.linDampFactor || this.linDampFactor.length !== N) {
|
|
54
|
+
this.dampCacheDt = dt
|
|
55
|
+
this.linDampFactor = new Float32Array(N)
|
|
56
|
+
this.angDampFactor = new Float32Array(N)
|
|
57
|
+
for (let i = 0; i < N; i++) {
|
|
58
|
+
this.linDampFactor[i] = Math.pow(Math.max(0, 1 - ldamp[i]), dt)
|
|
59
|
+
this.angDampFactor[i] = Math.pow(Math.max(0, 1 - adamp[i]), dt)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const linDamp = this.linDampFactor
|
|
63
|
+
const angDamp = this.angDampFactor!
|
|
47
64
|
for (let i = 0; i < N; i++) {
|
|
48
65
|
if (types[i] !== RigidbodyType.Dynamic || invMass[i] <= 0) continue
|
|
49
66
|
const i3 = i * 3
|
|
50
67
|
lv[i3 + 0] += gx * dt
|
|
51
68
|
lv[i3 + 1] += gy * dt
|
|
52
69
|
lv[i3 + 2] += gz * dt
|
|
53
|
-
const ld =
|
|
54
|
-
const ad =
|
|
70
|
+
const ld = linDamp[i]
|
|
71
|
+
const ad = angDamp[i]
|
|
55
72
|
lv[i3 + 0] *= ld; lv[i3 + 1] *= ld; lv[i3 + 2] *= ld
|
|
56
73
|
av[i3 + 0] *= ad; av[i3 + 1] *= ad; av[i3 + 2] *= ad
|
|
57
74
|
}
|
package/src/pmx-loader.ts
CHANGED
|
@@ -37,11 +37,22 @@ export class PmxLoader {
|
|
|
37
37
|
private vertexCount: number = 0
|
|
38
38
|
private rigidbodies: Rigidbody[] = []
|
|
39
39
|
private joints: Joint[] = []
|
|
40
|
+
private warnings: string[] = []
|
|
40
41
|
|
|
41
42
|
private constructor(buffer: ArrayBuffer) {
|
|
42
43
|
this.view = new DataView(buffer)
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
// Parse problems are non-fatal (the mesh may still render), but they must
|
|
47
|
+
// be queryable afterwards — a rig whose rigidbody section failed to parse
|
|
48
|
+
// silently loses physics, and console.warn alone drowns in browser log
|
|
49
|
+
// limits. Collected warnings ship on the Model (getLoadWarnings()).
|
|
50
|
+
private warn(message: string, error?: unknown): void {
|
|
51
|
+
const full = error instanceof Error ? `${message}: ${error.message}` : message
|
|
52
|
+
this.warnings.push(full)
|
|
53
|
+
console.warn(message, error ?? "")
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
static async load(url: string): Promise<Model> {
|
|
46
57
|
return PmxLoader.loadFromReader(createFetchAssetReader(), url)
|
|
47
58
|
}
|
|
@@ -82,7 +93,7 @@ export class PmxLoader {
|
|
|
82
93
|
const version = this.getFloat32()
|
|
83
94
|
if (version < 2.0 || version > 2.2) {
|
|
84
95
|
// Continue, but warn for unexpected version
|
|
85
|
-
|
|
96
|
+
this.warn(`PMX version ${version} may not be fully supported`)
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
// PMX: globals count (uint8) followed by that many bytes describing encoding and index sizes
|
|
@@ -472,7 +483,7 @@ export class PmxLoader {
|
|
|
472
483
|
}
|
|
473
484
|
this.bones = bones
|
|
474
485
|
} catch (e) {
|
|
475
|
-
|
|
486
|
+
this.warn("Error parsing bones:", e)
|
|
476
487
|
this.bones = []
|
|
477
488
|
}
|
|
478
489
|
}
|
|
@@ -487,7 +498,7 @@ export class PmxLoader {
|
|
|
487
498
|
const count = this.getInt32()
|
|
488
499
|
if (count < 0 || count > 100000) {
|
|
489
500
|
// Suspicious count, likely corrupted
|
|
490
|
-
|
|
501
|
+
this.warn(`Suspicious morph count: ${count}`)
|
|
491
502
|
this.morphs = []
|
|
492
503
|
return
|
|
493
504
|
}
|
|
@@ -623,11 +634,11 @@ export class PmxLoader {
|
|
|
623
634
|
this.morphs.push(morph)
|
|
624
635
|
} catch (e) {
|
|
625
636
|
// If we fail to read a morph, skip it
|
|
626
|
-
|
|
637
|
+
this.warn(`Error reading morph ${i}:`, e)
|
|
627
638
|
}
|
|
628
639
|
}
|
|
629
640
|
} catch (e) {
|
|
630
|
-
|
|
641
|
+
this.warn("Error parsing morphs:", e)
|
|
631
642
|
this.morphs = []
|
|
632
643
|
}
|
|
633
644
|
}
|
|
@@ -669,13 +680,13 @@ export class PmxLoader {
|
|
|
669
680
|
}
|
|
670
681
|
} catch (e) {
|
|
671
682
|
// If we fail to read a frame, stop skipping
|
|
672
|
-
|
|
683
|
+
this.warn(`Error reading display frame ${i}:`, e)
|
|
673
684
|
return false
|
|
674
685
|
}
|
|
675
686
|
}
|
|
676
687
|
return true
|
|
677
688
|
} catch (e) {
|
|
678
|
-
|
|
689
|
+
this.warn("Error skipping display frames:", e)
|
|
679
690
|
return false
|
|
680
691
|
}
|
|
681
692
|
}
|
|
@@ -685,7 +696,7 @@ export class PmxLoader {
|
|
|
685
696
|
// Note: morphs and display frames are already skipped in parse() before calling this
|
|
686
697
|
// Check bounds before reading rigidbody count
|
|
687
698
|
if (this.offset + 4 > this.view.buffer.byteLength) {
|
|
688
|
-
|
|
699
|
+
this.warn("Not enough bytes for rigidbody count")
|
|
689
700
|
this.rigidbodies = []
|
|
690
701
|
return
|
|
691
702
|
}
|
|
@@ -693,7 +704,7 @@ export class PmxLoader {
|
|
|
693
704
|
const count = this.getInt32()
|
|
694
705
|
if (count < 0 || count > 10000) {
|
|
695
706
|
// Suspicious count
|
|
696
|
-
|
|
707
|
+
this.warn(`Suspicious rigidbody count: ${count}`)
|
|
697
708
|
this.rigidbodies = []
|
|
698
709
|
return
|
|
699
710
|
}
|
|
@@ -704,7 +715,7 @@ export class PmxLoader {
|
|
|
704
715
|
try {
|
|
705
716
|
// Check bounds before reading each rigidbody
|
|
706
717
|
if (this.offset >= this.view.buffer.byteLength) {
|
|
707
|
-
|
|
718
|
+
this.warn(`Reached end of buffer while reading rigidbody ${i} of ${count}`)
|
|
708
719
|
break
|
|
709
720
|
}
|
|
710
721
|
|
|
@@ -737,7 +748,12 @@ export class PmxLoader {
|
|
|
737
748
|
const angularDamping = this.getFloat32()
|
|
738
749
|
const restitution = this.getFloat32()
|
|
739
750
|
const friction = this.getFloat32()
|
|
740
|
-
|
|
751
|
+
// PMX physics mode: 0 = follow bone, 1 = physics,
|
|
752
|
+
// 2 = physics + bone-position alignment. Mode 2 is dynamic — it
|
|
753
|
+
// must NOT be cast onto RigidbodyType (2 = Kinematic there, which
|
|
754
|
+
// freezes the body to its bone and kills physics for most modern
|
|
755
|
+
// cloth rigs and every 胸_回転 breast body).
|
|
756
|
+
const mode = this.getUint8()
|
|
741
757
|
|
|
742
758
|
this.rigidbodies.push({
|
|
743
759
|
name,
|
|
@@ -754,17 +770,18 @@ export class PmxLoader {
|
|
|
754
770
|
angularDamping,
|
|
755
771
|
restitution,
|
|
756
772
|
friction,
|
|
757
|
-
type:
|
|
773
|
+
type: mode === 0 ? RigidbodyType.Static : RigidbodyType.Dynamic,
|
|
774
|
+
aligned: mode === 2,
|
|
758
775
|
bodyOffsetMatrixInverse: Mat4.identity(),
|
|
759
776
|
})
|
|
760
777
|
} catch (e) {
|
|
761
|
-
|
|
778
|
+
this.warn(`Error reading rigidbody ${i} of ${count}:`, e)
|
|
762
779
|
// Stop parsing if we encounter an error
|
|
763
780
|
break
|
|
764
781
|
}
|
|
765
782
|
}
|
|
766
783
|
} catch (e) {
|
|
767
|
-
|
|
784
|
+
this.warn("Error parsing rigidbodies:", e)
|
|
768
785
|
this.rigidbodies = []
|
|
769
786
|
}
|
|
770
787
|
}
|
|
@@ -773,14 +790,14 @@ export class PmxLoader {
|
|
|
773
790
|
try {
|
|
774
791
|
// Check bounds before reading joint count
|
|
775
792
|
if (this.offset + 4 > this.view.buffer.byteLength) {
|
|
776
|
-
|
|
793
|
+
this.warn("Not enough bytes for joint count")
|
|
777
794
|
this.joints = []
|
|
778
795
|
return
|
|
779
796
|
}
|
|
780
797
|
|
|
781
798
|
const count = this.getInt32()
|
|
782
799
|
if (count < 0 || count > 10000) {
|
|
783
|
-
|
|
800
|
+
this.warn(`Suspicious joint count: ${count}`)
|
|
784
801
|
this.joints = []
|
|
785
802
|
return
|
|
786
803
|
}
|
|
@@ -791,7 +808,7 @@ export class PmxLoader {
|
|
|
791
808
|
try {
|
|
792
809
|
// Check bounds before reading each joint
|
|
793
810
|
if (this.offset >= this.view.buffer.byteLength) {
|
|
794
|
-
|
|
811
|
+
this.warn(`Reached end of buffer while reading joint ${i} of ${count}`)
|
|
795
812
|
break
|
|
796
813
|
}
|
|
797
814
|
|
|
@@ -857,13 +874,13 @@ export class PmxLoader {
|
|
|
857
874
|
springRotation: new Vec3(springRotX, springRotY, springRotZ), // Store as Vec3 (stiffness values)
|
|
858
875
|
})
|
|
859
876
|
} catch (e) {
|
|
860
|
-
|
|
877
|
+
this.warn(`Error reading joint ${i} of ${count}:`, e)
|
|
861
878
|
// Stop parsing if we encounter an error
|
|
862
879
|
break
|
|
863
880
|
}
|
|
864
881
|
}
|
|
865
882
|
} catch (e) {
|
|
866
|
-
|
|
883
|
+
this.warn("Error parsing joints:", e)
|
|
867
884
|
this.joints = []
|
|
868
885
|
}
|
|
869
886
|
}
|
|
@@ -1044,7 +1061,8 @@ export class PmxLoader {
|
|
|
1044
1061
|
skinning,
|
|
1045
1062
|
morphing,
|
|
1046
1063
|
this.rigidbodies,
|
|
1047
|
-
this.joints
|
|
1064
|
+
this.joints,
|
|
1065
|
+
this.warnings
|
|
1048
1066
|
)
|
|
1049
1067
|
}
|
|
1050
1068
|
|
|
@@ -1,92 +1,97 @@
|
|
|
1
|
-
// M_Body — 仿深空之眼渲染预设v1.0_by_小绿毛猫 "M_Body". Toon + warm rim + rim1/rim2
|
|
2
|
-
// stack mixed 50/50 against a Principled BSDF with noise-bumped normal.
|
|
3
|
-
|
|
4
|
-
import { NODES_WGSL } from "./nodes"
|
|
5
|
-
import { COMMON_MATERIAL_PRELUDE_WGSL } from "./common"
|
|
6
|
-
|
|
7
|
-
export const BODY_SHADER_WGSL = /* wgsl */ `
|
|
8
|
-
|
|
9
|
-
${NODES_WGSL}
|
|
10
|
-
${COMMON_MATERIAL_PRELUDE_WGSL}
|
|
11
|
-
|
|
12
|
-
const BODY_ROUGHNESS: f32 = 0.3;
|
|
13
|
-
const BODY_SPECULAR: f32 = 0.5;
|
|
14
|
-
const BODY_MIX_NPR: f32 = 0.5;
|
|
15
|
-
const BODY_SPEC_CLAMP: f32 = 10.0;
|
|
16
|
-
const BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;
|
|
17
|
-
const BODY_RIM2_POW: f32 = 1.4300000667572021;
|
|
18
|
-
const BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);
|
|
19
|
-
const BODY_WARM_STR: f32 = 0.30000001192092896;
|
|
20
|
-
|
|
21
|
-
// smoothstep-based ramp: t*t*(3-2*t) between two color stops
|
|
22
|
-
fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
23
|
-
let t = saturate((f - p0) / max(p1 - p0, 1e-6));
|
|
24
|
-
let ss = t * t * (3.0 - 2.0 * t);
|
|
25
|
-
return mix(c0, c1, ss);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
29
|
-
let
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
let
|
|
38
|
-
|
|
39
|
-
let
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
let
|
|
48
|
-
let
|
|
49
|
-
|
|
50
|
-
let
|
|
51
|
-
|
|
52
|
-
let
|
|
53
|
-
let
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
let
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
let
|
|
62
|
-
|
|
63
|
-
let
|
|
64
|
-
let
|
|
65
|
-
|
|
66
|
-
let
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
let
|
|
78
|
-
|
|
79
|
-
let
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
let
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
1
|
+
// M_Body — 仿深空之眼渲染预设v1.0_by_小绿毛猫 "M_Body". Toon + warm rim + rim1/rim2
|
|
2
|
+
// stack mixed 50/50 against a Principled BSDF with noise-bumped normal.
|
|
3
|
+
|
|
4
|
+
import { NODES_WGSL } from "./nodes"
|
|
5
|
+
import { COMMON_MATERIAL_PRELUDE_WGSL } from "./common"
|
|
6
|
+
|
|
7
|
+
export const BODY_SHADER_WGSL = /* wgsl */ `
|
|
8
|
+
|
|
9
|
+
${NODES_WGSL}
|
|
10
|
+
${COMMON_MATERIAL_PRELUDE_WGSL}
|
|
11
|
+
|
|
12
|
+
const BODY_ROUGHNESS: f32 = 0.3;
|
|
13
|
+
const BODY_SPECULAR: f32 = 0.5;
|
|
14
|
+
const BODY_MIX_NPR: f32 = 0.5;
|
|
15
|
+
const BODY_SPEC_CLAMP: f32 = 10.0;
|
|
16
|
+
const BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;
|
|
17
|
+
const BODY_RIM2_POW: f32 = 1.4300000667572021;
|
|
18
|
+
const BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);
|
|
19
|
+
const BODY_WARM_STR: f32 = 0.30000001192092896;
|
|
20
|
+
|
|
21
|
+
// smoothstep-based ramp: t*t*(3-2*t) between two color stops
|
|
22
|
+
fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
23
|
+
let t = saturate((f - p0) / max(p1 - p0, 1e-6));
|
|
24
|
+
let ss = t * t * (3.0 - 2.0 * t);
|
|
25
|
+
return mix(c0, c1, ss);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
29
|
+
let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
|
|
30
|
+
// MMD alpha semantics: material alpha × texture alpha. Hair/lace/accessory
|
|
31
|
+
// textures cut their shapes in the alpha channel — ignoring it renders each
|
|
32
|
+
// card's full quad with the texture's padding color (white shimmer on dark
|
|
33
|
+
// hair for models whose textures pad with white).
|
|
34
|
+
let alpha = material.alpha * tex_s.a;
|
|
35
|
+
if (alpha < 0.001) { discard; }
|
|
36
|
+
|
|
37
|
+
let n = safe_normal(input.normal);
|
|
38
|
+
let v = normalize(camera.viewPos - input.worldPos);
|
|
39
|
+
let l = -light.lights[0].direction.xyz;
|
|
40
|
+
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
41
|
+
let amb = light.ambientColor.xyz;
|
|
42
|
+
let shadow = sampleShadow(input.worldPos, n);
|
|
43
|
+
|
|
44
|
+
let tex_color = tex_s.rgb;
|
|
45
|
+
|
|
46
|
+
// ═══ NPR STACK ═══
|
|
47
|
+
let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
|
|
48
|
+
let toon = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;
|
|
49
|
+
|
|
50
|
+
let shadow_tint = hue_sat_id(2.0, 0.3499999940395355, 1.0, tex_color);
|
|
51
|
+
let lit_tint = hue_sat_id(1.5, 1.0, 1.0, tex_color);
|
|
52
|
+
let toon_color = mix_blend(toon, shadow_tint, lit_tint);
|
|
53
|
+
let bc = bright_contrast(toon_color, 0.1, 0.2);
|
|
54
|
+
|
|
55
|
+
let emission3 = bc * 4.0;
|
|
56
|
+
|
|
57
|
+
let warm_input = clamp(toon + 0.5, 0.0, 1.0);
|
|
58
|
+
let warm_color = ramp_cardinal(warm_input, 0.2409,
|
|
59
|
+
vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,
|
|
60
|
+
vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;
|
|
61
|
+
let warm_emission = warm_color * BODY_WARM_STR;
|
|
62
|
+
|
|
63
|
+
let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24000005424022675, n, v);
|
|
64
|
+
let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;
|
|
65
|
+
|
|
66
|
+
let facing_raw = layer_weight_facing(BODY_RIM2_LAYER_BLEND, n, v);
|
|
67
|
+
let facing_pow = math_power(facing_raw, BODY_RIM2_POW);
|
|
68
|
+
let rim2_fac = ramp_ease(facing_pow, 0.0, vec4f(0,0,0,1), 0.5052, vec4f(1,1,1,1)).r;
|
|
69
|
+
let rim2_mixed = mix(emission3, BODY_RIM2_BG, rim2_fac);
|
|
70
|
+
|
|
71
|
+
let npr_stack = rim1 + rim2_mixed + warm_emission;
|
|
72
|
+
|
|
73
|
+
// ═══ PRINCIPLED BSDF with noise bump ═══
|
|
74
|
+
// Mapping loc=rot=0 in the Blender graph folds to a plain scale multiply.
|
|
75
|
+
// Height in rest space (restPos) so the bump rides with the skin; geometric basis stays
|
|
76
|
+
// worldPos so the perturbed normal matches the world-space n (see face.ts note).
|
|
77
|
+
let noise_val = tex_noise_d2(input.restPos * vec3f(1.0, 1.0, 1.5), 1.0);
|
|
78
|
+
let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
|
|
79
|
+
let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);
|
|
80
|
+
|
|
81
|
+
let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6831911206245422, 0.19474034011363983, 0.13732507824897766));
|
|
82
|
+
let p_emission = bc * 0.2;
|
|
83
|
+
|
|
84
|
+
let principled = eval_principled(
|
|
85
|
+
PrincipledIn(principled_base, 0.0, BODY_SPECULAR, BODY_ROUGHNESS, BODY_SPEC_CLAMP, 0.0, 0.0),
|
|
86
|
+
bumped_n, l, v, sun, amb, shadow
|
|
87
|
+
) + p_emission;
|
|
88
|
+
|
|
89
|
+
let final_color = mix(npr_stack, principled, BODY_MIX_NPR);
|
|
90
|
+
|
|
91
|
+
var out: FSOut;
|
|
92
|
+
out.color = vec4f(final_color, alpha);
|
|
93
|
+
out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
`
|
|
@@ -20,7 +20,7 @@ const CLOTH_R_BUMP_STR: f32 = 1.0;
|
|
|
20
20
|
const CLOTH_R_SPEC_CLAMP: f32 = 10.0;
|
|
21
21
|
|
|
22
22
|
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
23
|
-
let n =
|
|
23
|
+
let n = safe_normal(input.normal);
|
|
24
24
|
let v = normalize(camera.viewPos - input.worldPos);
|
|
25
25
|
let l = -light.lights[0].direction.xyz;
|
|
26
26
|
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
@@ -17,7 +17,7 @@ const NPR_EMIT_STR: f32 = 18.200000762939453;
|
|
|
17
17
|
const NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;
|
|
18
18
|
|
|
19
19
|
@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
20
|
-
let n =
|
|
20
|
+
let n = safe_normal(input.normal);
|
|
21
21
|
let v = normalize(camera.viewPos - input.worldPos);
|
|
22
22
|
let l = -light.lights[0].direction.xyz;
|
|
23
23
|
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
@@ -47,7 +47,7 @@ const NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;
|
|
|
47
47
|
// ═══ PRINCIPLED BSDF ═══
|
|
48
48
|
let principled_base = hue_sat_id(1.0, 0.800000011920929, 1.0, tex_rgb);
|
|
49
49
|
let principled = eval_principled(
|
|
50
|
-
PrincipledIn(principled_base, 0.0, CLOTH_SPECULAR, CLOTH_ROUGHNESS,
|
|
50
|
+
PrincipledIn(principled_base, 0.0, CLOTH_SPECULAR, CLOTH_ROUGHNESS, 10.0, 0.0, 0.0),
|
|
51
51
|
n, l, v, sun, amb, shadow
|
|
52
52
|
);
|
|
53
53
|
|