reze-engine 0.14.0 → 0.15.0

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.
Files changed (93) hide show
  1. package/README.md +81 -108
  2. package/dist/engine.d.ts +1 -7
  3. package/dist/engine.d.ts.map +1 -1
  4. package/dist/engine.js +4 -7
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/physics/body.d.ts +30 -0
  9. package/dist/physics/body.d.ts.map +1 -0
  10. package/dist/physics/body.js +215 -0
  11. package/dist/physics/constraint.d.ts +17 -0
  12. package/dist/physics/constraint.d.ts.map +1 -0
  13. package/dist/physics/constraint.js +102 -0
  14. package/dist/physics/contact.d.ts +32 -0
  15. package/dist/physics/contact.d.ts.map +1 -0
  16. package/dist/physics/contact.js +728 -0
  17. package/dist/physics/index.d.ts +4 -0
  18. package/dist/physics/index.d.ts.map +1 -0
  19. package/dist/physics/index.js +3 -0
  20. package/dist/physics/physics.d.ts +31 -0
  21. package/dist/physics/physics.d.ts.map +1 -0
  22. package/dist/physics/physics.js +211 -0
  23. package/dist/physics/solver.d.ts +5 -0
  24. package/dist/physics/solver.d.ts.map +1 -0
  25. package/dist/physics/solver.js +416 -0
  26. package/dist/physics/types.d.ts +46 -0
  27. package/dist/physics/types.d.ts.map +1 -0
  28. package/dist/physics/types.js +12 -0
  29. package/dist/physics/world.d.ts +12 -0
  30. package/dist/physics/world.d.ts.map +1 -0
  31. package/dist/physics/world.js +146 -0
  32. package/dist/physics-debug.d.ts +30 -0
  33. package/dist/physics-debug.d.ts.map +1 -0
  34. package/dist/physics-debug.js +526 -0
  35. package/dist/shaders/materials/hair.d.ts +1 -1
  36. package/dist/shaders/materials/hair.d.ts.map +1 -1
  37. package/dist/shaders/materials/hair.js +2 -2
  38. package/dist/shaders/passes/physics-debug.d.ts +2 -0
  39. package/dist/shaders/passes/physics-debug.d.ts.map +1 -0
  40. package/dist/shaders/passes/physics-debug.js +69 -0
  41. package/package.json +3 -6
  42. package/src/engine.ts +5 -9
  43. package/src/index.ts +1 -1
  44. package/src/physics/body.ts +305 -0
  45. package/src/physics/constraint.ts +151 -0
  46. package/src/physics/contact.ts +983 -0
  47. package/src/physics/index.ts +8 -0
  48. package/src/physics/physics.ts +255 -0
  49. package/src/physics/solver.ts +430 -0
  50. package/src/physics/types.ts +50 -0
  51. package/src/physics/world.ts +152 -0
  52. package/src/shaders/materials/hair.ts +2 -2
  53. package/dist/ammo-loader.d.ts +0 -3
  54. package/dist/ammo-loader.d.ts.map +0 -1
  55. package/dist/ammo-loader.js +0 -26
  56. package/dist/physics.d.ts +0 -86
  57. package/dist/physics.d.ts.map +0 -1
  58. package/dist/physics.js +0 -527
  59. package/dist/shaders/body.d.ts +0 -2
  60. package/dist/shaders/body.d.ts.map +0 -1
  61. package/dist/shaders/body.js +0 -199
  62. package/dist/shaders/classify.d.ts +0 -4
  63. package/dist/shaders/classify.d.ts.map +0 -1
  64. package/dist/shaders/classify.js +0 -12
  65. package/dist/shaders/cloth_rough.d.ts +0 -2
  66. package/dist/shaders/cloth_rough.d.ts.map +0 -1
  67. package/dist/shaders/cloth_rough.js +0 -178
  68. package/dist/shaders/cloth_smooth.d.ts +0 -2
  69. package/dist/shaders/cloth_smooth.d.ts.map +0 -1
  70. package/dist/shaders/cloth_smooth.js +0 -174
  71. package/dist/shaders/default.d.ts +0 -2
  72. package/dist/shaders/default.d.ts.map +0 -1
  73. package/dist/shaders/default.js +0 -171
  74. package/dist/shaders/eye.d.ts +0 -2
  75. package/dist/shaders/eye.d.ts.map +0 -1
  76. package/dist/shaders/eye.js +0 -146
  77. package/dist/shaders/face.d.ts +0 -2
  78. package/dist/shaders/face.d.ts.map +0 -1
  79. package/dist/shaders/face.js +0 -199
  80. package/dist/shaders/hair.d.ts +0 -2
  81. package/dist/shaders/hair.d.ts.map +0 -1
  82. package/dist/shaders/hair.js +0 -176
  83. package/dist/shaders/metal.d.ts +0 -2
  84. package/dist/shaders/metal.d.ts.map +0 -1
  85. package/dist/shaders/metal.js +0 -174
  86. package/dist/shaders/nodes.d.ts +0 -2
  87. package/dist/shaders/nodes.d.ts.map +0 -1
  88. package/dist/shaders/nodes.js +0 -456
  89. package/dist/shaders/stockings.d.ts +0 -2
  90. package/dist/shaders/stockings.d.ts.map +0 -1
  91. package/dist/shaders/stockings.js +0 -244
  92. package/src/ammo-loader.ts +0 -31
  93. package/src/physics.ts +0 -706
package/dist/physics.js DELETED
@@ -1,527 +0,0 @@
1
- import { Quat, Vec3, Mat4 } from "./math";
2
- // Physics-local scratch pool for per-frame sync (syncFromBones, applyAmmoRigidbodiesToBones).
3
- // Each method uses only these slots and completes synchronously before the next is called.
4
- const _physMat = [
5
- new Float32Array(16), new Float32Array(16), new Float32Array(16),
6
- ];
7
- const _physQuat = new Quat(0, 0, 0, 1);
8
- import { loadAmmo } from "./ammo-loader";
9
- export var RigidbodyShape;
10
- (function (RigidbodyShape) {
11
- RigidbodyShape[RigidbodyShape["Sphere"] = 0] = "Sphere";
12
- RigidbodyShape[RigidbodyShape["Box"] = 1] = "Box";
13
- RigidbodyShape[RigidbodyShape["Capsule"] = 2] = "Capsule";
14
- })(RigidbodyShape || (RigidbodyShape = {}));
15
- export var RigidbodyType;
16
- (function (RigidbodyType) {
17
- RigidbodyType[RigidbodyType["Static"] = 0] = "Static";
18
- RigidbodyType[RigidbodyType["Dynamic"] = 1] = "Dynamic";
19
- RigidbodyType[RigidbodyType["Kinematic"] = 2] = "Kinematic";
20
- })(RigidbodyType || (RigidbodyType = {}));
21
- export class Physics {
22
- constructor(rigidbodies, joints = [], options) {
23
- this.gravity = new Vec3(0, -98, 0); // Gravity acceleration (cm/s²), MMD-style default
24
- this.constraintSolverPattern = null;
25
- this.ammoInitialized = false;
26
- this.ammoPromise = null;
27
- this.ammo = null;
28
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
- this.dynamicsWorld = null; // btDiscreteDynamicsWorld
30
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
- this.ammoRigidbodies = []; // btRigidBody instances
32
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
- this.ammoConstraints = []; // btTypedConstraint instances
34
- this.rigidbodiesInitialized = false; // bodyOffsetMatrixInverse computed and bodies positioned
35
- this.jointsCreated = false; // Joints delayed until after rigidbodies are positioned
36
- this.firstFrame = true; // Needed to reposition bodies before creating joints
37
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
- this.zeroVector = null; // Cached zero vector for velocity clearing
39
- this.rigidbodies = rigidbodies;
40
- this.joints = joints;
41
- const keywords = options?.constraintSolverKeywords ?? [];
42
- if (keywords.length > 0) {
43
- this.constraintSolverPattern = new RegExp(keywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "i");
44
- }
45
- this.initAmmo();
46
- }
47
- async initAmmo() {
48
- if (this.ammoInitialized || this.ammoPromise)
49
- return;
50
- this.ammoPromise = loadAmmo();
51
- try {
52
- this.ammo = await this.ammoPromise;
53
- this.createAmmoWorld();
54
- this.ammoInitialized = true;
55
- }
56
- catch (error) {
57
- console.error("[Physics] Failed to initialize Ammo:", error);
58
- this.ammoPromise = null;
59
- }
60
- }
61
- setGravity(gravity) {
62
- this.gravity = gravity;
63
- if (this.dynamicsWorld && this.ammo) {
64
- const Ammo = this.ammo;
65
- const gravityVec = new Ammo.btVector3(gravity.x, gravity.y, gravity.z);
66
- this.dynamicsWorld.setGravity(gravityVec);
67
- Ammo.destroy(gravityVec);
68
- }
69
- }
70
- getGravity() {
71
- return this.gravity;
72
- }
73
- getRigidbodies() {
74
- return this.rigidbodies;
75
- }
76
- getJoints() {
77
- return this.joints;
78
- }
79
- getRigidbodyTransforms() {
80
- const transforms = [];
81
- if (!this.ammo || !this.ammoRigidbodies.length) {
82
- for (let i = 0; i < this.rigidbodies.length; i++) {
83
- transforms.push({
84
- position: new Vec3(this.rigidbodies[i].shapePosition.x, this.rigidbodies[i].shapePosition.y, this.rigidbodies[i].shapePosition.z),
85
- rotation: Quat.fromEuler(this.rigidbodies[i].shapeRotation.x, this.rigidbodies[i].shapeRotation.y, this.rigidbodies[i].shapeRotation.z),
86
- });
87
- }
88
- return transforms;
89
- }
90
- for (let i = 0; i < this.ammoRigidbodies.length; i++) {
91
- const ammoBody = this.ammoRigidbodies[i];
92
- if (!ammoBody) {
93
- const rb = this.rigidbodies[i];
94
- transforms.push({
95
- position: new Vec3(rb.shapePosition.x, rb.shapePosition.y, rb.shapePosition.z),
96
- rotation: Quat.fromEuler(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z),
97
- });
98
- continue;
99
- }
100
- const transform = ammoBody.getWorldTransform();
101
- const origin = transform.getOrigin();
102
- const rotQuat = transform.getRotation();
103
- transforms.push({
104
- position: new Vec3(origin.x(), origin.y(), origin.z()),
105
- rotation: new Quat(rotQuat.x(), rotQuat.y(), rotQuat.z(), rotQuat.w()),
106
- });
107
- }
108
- return transforms;
109
- }
110
- createAmmoWorld() {
111
- if (!this.ammo)
112
- return;
113
- const Ammo = this.ammo;
114
- const collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
115
- const dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
116
- const overlappingPairCache = new Ammo.btDbvtBroadphase();
117
- const solver = new Ammo.btSequentialImpulseConstraintSolver();
118
- this.dynamicsWorld = new Ammo.btDiscreteDynamicsWorld(dispatcher, overlappingPairCache, solver, collisionConfiguration);
119
- const gravityVec = new Ammo.btVector3(this.gravity.x, this.gravity.y, this.gravity.z);
120
- this.dynamicsWorld.setGravity(gravityVec);
121
- Ammo.destroy(gravityVec);
122
- this.createAmmoRigidbodies();
123
- }
124
- createAmmoRigidbodies() {
125
- if (!this.ammo || !this.dynamicsWorld)
126
- return;
127
- const Ammo = this.ammo;
128
- this.ammoRigidbodies = [];
129
- for (let i = 0; i < this.rigidbodies.length; i++) {
130
- const rb = this.rigidbodies[i];
131
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
132
- let shape = null;
133
- const size = rb.size;
134
- switch (rb.shape) {
135
- case RigidbodyShape.Sphere:
136
- const radius = size.x;
137
- shape = new Ammo.btSphereShape(radius);
138
- break;
139
- case RigidbodyShape.Box:
140
- const sizeVector = new Ammo.btVector3(size.x, size.y, size.z);
141
- shape = new Ammo.btBoxShape(sizeVector);
142
- Ammo.destroy(sizeVector);
143
- break;
144
- case RigidbodyShape.Capsule:
145
- const capsuleRadius = size.x;
146
- const capsuleHalfHeight = size.y;
147
- shape = new Ammo.btCapsuleShape(capsuleRadius, capsuleHalfHeight);
148
- break;
149
- default:
150
- const defaultHalfExtents = new Ammo.btVector3(size.x / 2, size.y / 2, size.z / 2);
151
- shape = new Ammo.btBoxShape(defaultHalfExtents);
152
- Ammo.destroy(defaultHalfExtents);
153
- break;
154
- }
155
- // Bodies must start at correct position to avoid explosions when joints are created
156
- const transform = new Ammo.btTransform();
157
- transform.setIdentity();
158
- const shapePos = new Ammo.btVector3(rb.shapePosition.x, rb.shapePosition.y, rb.shapePosition.z);
159
- transform.setOrigin(shapePos);
160
- Ammo.destroy(shapePos);
161
- const shapeRotQuat = Quat.fromEuler(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z);
162
- const quat = new Ammo.btQuaternion(shapeRotQuat.x, shapeRotQuat.y, shapeRotQuat.z, shapeRotQuat.w);
163
- transform.setRotation(quat);
164
- Ammo.destroy(quat);
165
- // All types use the same motionState constructor
166
- const motionState = new Ammo.btDefaultMotionState(transform);
167
- const mass = rb.type === RigidbodyType.Dynamic ? rb.mass : 0;
168
- const isDynamic = rb.type === RigidbodyType.Dynamic;
169
- const localInertia = new Ammo.btVector3(0, 0, 0);
170
- if (isDynamic && mass > 0) {
171
- shape.calculateLocalInertia(mass, localInertia);
172
- }
173
- const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, shape, localInertia);
174
- rbInfo.set_m_restitution(rb.restitution);
175
- rbInfo.set_m_friction(rb.friction);
176
- rbInfo.set_m_linearDamping(rb.linearDamping);
177
- rbInfo.set_m_angularDamping(rb.angularDamping);
178
- const body = new Ammo.btRigidBody(rbInfo);
179
- body.setSleepingThresholds(0.0, 0.0);
180
- // Static (FollowBone) should be kinematic, not static - must follow bones
181
- if (rb.type === RigidbodyType.Static || rb.type === RigidbodyType.Kinematic) {
182
- body.setCollisionFlags(body.getCollisionFlags() | 2); // CF_KINEMATIC_OBJECT
183
- body.setActivationState(4); // DISABLE_DEACTIVATION
184
- }
185
- const collisionGroup = 1 << rb.group;
186
- const collisionMask = rb.collisionMask;
187
- const isZeroVolume = (rb.shape === RigidbodyShape.Sphere && rb.size.x === 0) ||
188
- (rb.shape === RigidbodyShape.Box && (rb.size.x === 0 || rb.size.y === 0 || rb.size.z === 0)) ||
189
- (rb.shape === RigidbodyShape.Capsule && (rb.size.x === 0 || rb.size.y === 0));
190
- if (collisionMask === 0 || isZeroVolume) {
191
- body.setCollisionFlags(body.getCollisionFlags() | 4); // CF_NO_CONTACT_RESPONSE
192
- }
193
- this.dynamicsWorld.addRigidBody(body, collisionGroup, collisionMask);
194
- this.ammoRigidbodies.push(body);
195
- Ammo.destroy(rbInfo);
196
- Ammo.destroy(localInertia);
197
- }
198
- }
199
- createAmmoJoints() {
200
- if (!this.ammo || !this.dynamicsWorld || this.ammoRigidbodies.length === 0)
201
- return;
202
- const Ammo = this.ammo;
203
- this.ammoConstraints = [];
204
- for (const joint of this.joints) {
205
- const rbIndexA = joint.rigidbodyIndexA;
206
- const rbIndexB = joint.rigidbodyIndexB;
207
- if (rbIndexA < 0 ||
208
- rbIndexA >= this.ammoRigidbodies.length ||
209
- rbIndexB < 0 ||
210
- rbIndexB >= this.ammoRigidbodies.length) {
211
- console.warn(`[Physics] Invalid joint indices: ${rbIndexA}, ${rbIndexB}`);
212
- continue;
213
- }
214
- const bodyA = this.ammoRigidbodies[rbIndexA];
215
- const bodyB = this.ammoRigidbodies[rbIndexB];
216
- if (!bodyA || !bodyB) {
217
- console.warn(`[Physics] Body not found for joint ${joint.name}: bodyA=${!!bodyA}, bodyB=${!!bodyB}`);
218
- continue;
219
- }
220
- // Compute joint frames using actual current body positions (after repositioning)
221
- const bodyATransform = bodyA.getWorldTransform();
222
- const bodyBTransform = bodyB.getWorldTransform();
223
- const bodyAOrigin = bodyATransform.getOrigin();
224
- const bodyARotQuat = bodyATransform.getRotation();
225
- const bodyAPos = new Vec3(bodyAOrigin.x(), bodyAOrigin.y(), bodyAOrigin.z());
226
- const bodyARot = new Quat(bodyARotQuat.x(), bodyARotQuat.y(), bodyARotQuat.z(), bodyARotQuat.w());
227
- const bodyAMat = Mat4.fromPositionRotation(bodyAPos, bodyARot);
228
- const bodyBOrigin = bodyBTransform.getOrigin();
229
- const bodyBRotQuat = bodyBTransform.getRotation();
230
- const bodyBPos = new Vec3(bodyBOrigin.x(), bodyBOrigin.y(), bodyBOrigin.z());
231
- const bodyBRot = new Quat(bodyBRotQuat.x(), bodyBRotQuat.y(), bodyBRotQuat.z(), bodyBRotQuat.w());
232
- const bodyBMat = Mat4.fromPositionRotation(bodyBPos, bodyBRot);
233
- const scalingFactor = 1.0;
234
- const jointRotQuat = Quat.fromEuler(joint.rotation.x, joint.rotation.y, joint.rotation.z);
235
- const jointPos = new Vec3(joint.position.x * scalingFactor, joint.position.y * scalingFactor, joint.position.z * scalingFactor);
236
- const jointTransform = Mat4.fromPositionRotation(jointPos, jointRotQuat);
237
- // Transform joint world position to body A's local space
238
- const frameInAMat = bodyAMat.inverse().multiply(jointTransform);
239
- const framePosA = frameInAMat.getPosition();
240
- const frameRotA = frameInAMat.toQuat();
241
- // Transform joint world position to body B's local space
242
- const frameInBMat = bodyBMat.inverse().multiply(jointTransform);
243
- const framePosB = frameInBMat.getPosition();
244
- const frameRotB = frameInBMat.toQuat();
245
- const frameInA = new Ammo.btTransform();
246
- frameInA.setIdentity();
247
- const pivotInA = new Ammo.btVector3(framePosA.x, framePosA.y, framePosA.z);
248
- frameInA.setOrigin(pivotInA);
249
- const quatA = new Ammo.btQuaternion(frameRotA.x, frameRotA.y, frameRotA.z, frameRotA.w);
250
- frameInA.setRotation(quatA);
251
- const frameInB = new Ammo.btTransform();
252
- frameInB.setIdentity();
253
- const pivotInB = new Ammo.btVector3(framePosB.x, framePosB.y, framePosB.z);
254
- frameInB.setOrigin(pivotInB);
255
- const quatB = new Ammo.btQuaternion(frameRotB.x, frameRotB.y, frameRotB.z, frameRotB.w);
256
- frameInB.setRotation(quatB);
257
- const useLinearReferenceFrameA = true;
258
- const constraint = new Ammo.btGeneric6DofSpringConstraint(bodyA, bodyB, frameInA, frameInB, useLinearReferenceFrameA);
259
- // Per-joint Bullet 2.75 constraint solver: disable m_useOffsetForConstraintFrame for
260
- // joints whose name matches constraintSolverKeywords.
261
- if (this.constraintSolverPattern && this.constraintSolverPattern.test(joint.name)) {
262
- let jointPtr;
263
- if (typeof Ammo.getPointer === "function") {
264
- jointPtr = Ammo.getPointer(constraint);
265
- }
266
- else {
267
- const constraintWithPtr = constraint;
268
- jointPtr = constraintWithPtr.ptr;
269
- }
270
- if (jointPtr !== undefined && Ammo.HEAP8) {
271
- const heap8 = Ammo.HEAP8;
272
- if (heap8[jointPtr + 1300] === (useLinearReferenceFrameA ? 1 : 0) && heap8[jointPtr + 1301] === 1) {
273
- heap8[jointPtr + 1301] = 0;
274
- }
275
- }
276
- }
277
- for (let i = 0; i < 6; ++i) {
278
- constraint.setParam(2, 0.475, i); // BT_CONSTRAINT_STOP_ERP
279
- }
280
- const lowerLinear = new Ammo.btVector3(joint.positionMin.x, joint.positionMin.y, joint.positionMin.z);
281
- const upperLinear = new Ammo.btVector3(joint.positionMax.x, joint.positionMax.y, joint.positionMax.z);
282
- constraint.setLinearLowerLimit(lowerLinear);
283
- constraint.setLinearUpperLimit(upperLinear);
284
- const lowerAngular = new Ammo.btVector3(this.normalizeAngle(joint.rotationMin.x), this.normalizeAngle(joint.rotationMin.y), this.normalizeAngle(joint.rotationMin.z));
285
- const upperAngular = new Ammo.btVector3(this.normalizeAngle(joint.rotationMax.x), this.normalizeAngle(joint.rotationMax.y), this.normalizeAngle(joint.rotationMax.z));
286
- constraint.setAngularLowerLimit(lowerAngular);
287
- constraint.setAngularUpperLimit(upperAngular);
288
- // Linear springs: only enable if stiffness is non-zero
289
- if (joint.springPosition.x !== 0) {
290
- constraint.setStiffness(0, joint.springPosition.x);
291
- constraint.enableSpring(0, true);
292
- }
293
- else {
294
- constraint.enableSpring(0, false);
295
- }
296
- if (joint.springPosition.y !== 0) {
297
- constraint.setStiffness(1, joint.springPosition.y);
298
- constraint.enableSpring(1, true);
299
- }
300
- else {
301
- constraint.enableSpring(1, false);
302
- }
303
- if (joint.springPosition.z !== 0) {
304
- constraint.setStiffness(2, joint.springPosition.z);
305
- constraint.enableSpring(2, true);
306
- }
307
- else {
308
- constraint.enableSpring(2, false);
309
- }
310
- // Angular springs: always enable
311
- constraint.setStiffness(3, joint.springRotation.x);
312
- constraint.enableSpring(3, true);
313
- constraint.setStiffness(4, joint.springRotation.y);
314
- constraint.enableSpring(4, true);
315
- constraint.setStiffness(5, joint.springRotation.z);
316
- constraint.enableSpring(5, true);
317
- this.dynamicsWorld.addConstraint(constraint, false);
318
- this.ammoConstraints.push(constraint);
319
- Ammo.destroy(pivotInA);
320
- Ammo.destroy(pivotInB);
321
- Ammo.destroy(quatA);
322
- Ammo.destroy(quatB);
323
- Ammo.destroy(lowerLinear);
324
- Ammo.destroy(upperLinear);
325
- Ammo.destroy(lowerAngular);
326
- Ammo.destroy(upperAngular);
327
- }
328
- }
329
- // Normalize angle to [-π, π] range
330
- normalizeAngle(angle) {
331
- const pi = Math.PI;
332
- const twoPi = 2 * pi;
333
- angle = angle % twoPi;
334
- if (angle < -pi) {
335
- angle += twoPi;
336
- }
337
- else if (angle > pi) {
338
- angle -= twoPi;
339
- }
340
- return angle;
341
- }
342
- // Re-snap all rigidbodies to current bone poses and zero velocities / forces.
343
- // Use when simulation has diverged (explosion, NaN, extreme external teleport).
344
- reset(boneWorldMatrices) {
345
- if (!this.ammoInitialized || !this.ammo || !this.dynamicsWorld)
346
- return;
347
- if (!this.rigidbodiesInitialized)
348
- return;
349
- this.positionBodiesFromBones(boneWorldMatrices, boneWorldMatrices.length);
350
- if (this.dynamicsWorld.clearForces) {
351
- this.dynamicsWorld.clearForces();
352
- }
353
- if (this.dynamicsWorld.stepSimulation) {
354
- this.dynamicsWorld.stepSimulation(0, 0, 0);
355
- }
356
- }
357
- // Syncs bones to rigidbodies, simulates dynamics, solves constraints
358
- // Modifies boneWorldMatrices in-place for dynamic rigidbodies that drive bones
359
- step(dt, boneWorldMatrices, boneInverseBindMatrices) {
360
- // Wait for Ammo to initialize
361
- if (!this.ammoInitialized || !this.ammo || !this.dynamicsWorld) {
362
- return;
363
- }
364
- const boneCount = boneWorldMatrices.length;
365
- if (this.firstFrame) {
366
- if (!this.rigidbodiesInitialized) {
367
- this.computeBodyOffsets(boneInverseBindMatrices, boneCount);
368
- this.rigidbodiesInitialized = true;
369
- }
370
- // Position bodies based on current bone poses (not bind pose) before creating joints
371
- this.positionBodiesFromBones(boneWorldMatrices, boneCount);
372
- if (!this.jointsCreated) {
373
- this.createAmmoJoints();
374
- this.jointsCreated = true;
375
- }
376
- if (this.dynamicsWorld.stepSimulation) {
377
- this.dynamicsWorld.stepSimulation(0, 0, 0);
378
- }
379
- this.firstFrame = false;
380
- }
381
- // Step order: 1) Sync Static/Kinematic from bones, 2) Step physics, 3) Apply dynamic to bones
382
- this.syncFromBones(boneWorldMatrices, boneCount);
383
- this.stepAmmoPhysics(dt);
384
- this.applyAmmoRigidbodiesToBones(boneWorldMatrices, boneCount);
385
- }
386
- // Compute bodyOffsetMatrixInverse for all rigidbodies (called once during initialization)
387
- computeBodyOffsets(boneInverseBindMatrices, boneCount) {
388
- if (!this.ammo || !this.dynamicsWorld)
389
- return;
390
- for (let i = 0; i < this.rigidbodies.length; i++) {
391
- const rb = this.rigidbodies[i];
392
- if (rb.boneIndex >= 0 && rb.boneIndex < boneCount) {
393
- const boneIdx = rb.boneIndex;
394
- const invBindIdx = boneIdx * 16;
395
- const invBindMat = new Mat4(boneInverseBindMatrices.subarray(invBindIdx, invBindIdx + 16));
396
- // Compute shape transform in bone-local space: shapeLocal = boneInverseBind × shapeWorldBind
397
- const shapeRotQuat = Quat.fromEuler(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z);
398
- const shapeWorldBind = Mat4.fromPositionRotation(rb.shapePosition, shapeRotQuat);
399
- // shapeLocal = boneInverseBind × shapeWorldBind (not shapeWorldBind × boneInverseBind)
400
- const bodyOffsetMatrix = invBindMat.multiply(shapeWorldBind);
401
- rb.bodyOffsetMatrixInverse = bodyOffsetMatrix.inverse();
402
- rb.bodyOffsetMatrix = bodyOffsetMatrix; // Cache non-inverse to avoid expensive inverse() calls
403
- }
404
- else {
405
- rb.bodyOffsetMatrixInverse = Mat4.identity();
406
- rb.bodyOffsetMatrix = Mat4.identity(); // Cache non-inverse
407
- }
408
- }
409
- }
410
- // Position bodies based on current bone transforms (called on first frame only)
411
- positionBodiesFromBones(boneWorldMatrices, boneCount) {
412
- if (!this.ammo || !this.dynamicsWorld)
413
- return;
414
- const Ammo = this.ammo;
415
- for (let i = 0; i < this.rigidbodies.length; i++) {
416
- const rb = this.rigidbodies[i];
417
- const ammoBody = this.ammoRigidbodies[i];
418
- if (!ammoBody || rb.boneIndex < 0 || rb.boneIndex >= boneCount)
419
- continue;
420
- const boneIdx = rb.boneIndex;
421
- const boneWorldMat = boneWorldMatrices[boneIdx];
422
- // nodeWorld = boneWorld × shapeLocal (not shapeLocal × boneWorld)
423
- const bodyOffsetMatrix = rb.bodyOffsetMatrix || rb.bodyOffsetMatrixInverse.inverse();
424
- const nodeWorldMatrix = boneWorldMat.multiply(bodyOffsetMatrix);
425
- const worldPos = nodeWorldMatrix.getPosition();
426
- const worldRot = nodeWorldMatrix.toQuat();
427
- const transform = new Ammo.btTransform();
428
- const pos = new Ammo.btVector3(worldPos.x, worldPos.y, worldPos.z);
429
- const quat = new Ammo.btQuaternion(worldRot.x, worldRot.y, worldRot.z, worldRot.w);
430
- transform.setOrigin(pos);
431
- transform.setRotation(quat);
432
- if (rb.type === RigidbodyType.Static || rb.type === RigidbodyType.Kinematic) {
433
- ammoBody.setCollisionFlags(ammoBody.getCollisionFlags() | 2); // CF_KINEMATIC_OBJECT
434
- ammoBody.setActivationState(4); // DISABLE_DEACTIVATION
435
- }
436
- ammoBody.setWorldTransform(transform);
437
- ammoBody.getMotionState().setWorldTransform(transform);
438
- if (!this.zeroVector) {
439
- this.zeroVector = new Ammo.btVector3(0, 0, 0);
440
- }
441
- ammoBody.setLinearVelocity(this.zeroVector);
442
- ammoBody.setAngularVelocity(this.zeroVector);
443
- Ammo.destroy(pos);
444
- Ammo.destroy(quat);
445
- Ammo.destroy(transform);
446
- }
447
- }
448
- // Sync Static (FollowBone) and Kinematic rigidbodies to follow bone transforms
449
- syncFromBones(boneWorldMatrices, boneCount) {
450
- if (!this.ammo || !this.dynamicsWorld)
451
- return;
452
- const Ammo = this.ammo;
453
- for (let i = 0; i < this.rigidbodies.length; i++) {
454
- const rb = this.rigidbodies[i];
455
- const ammoBody = this.ammoRigidbodies[i];
456
- if (!ammoBody)
457
- continue;
458
- // Sync both Static (FollowBone) and Kinematic bodies - they both follow bones
459
- if ((rb.type === RigidbodyType.Static || rb.type === RigidbodyType.Kinematic) &&
460
- rb.boneIndex >= 0 &&
461
- rb.boneIndex < boneCount) {
462
- const boneIdx = rb.boneIndex;
463
- const boneWorldMat = boneWorldMatrices[boneIdx];
464
- // Lazy-cache bodyOffsetMatrix on first hit (cold path).
465
- if (!rb.bodyOffsetMatrix)
466
- rb.bodyOffsetMatrix = rb.bodyOffsetMatrixInverse.inverse();
467
- // nodeWorld = boneWorld × bodyOffsetMatrix → _physMat[0]
468
- Mat4.multiplyArrays(boneWorldMat.values, 0, rb.bodyOffsetMatrix.values, 0, _physMat[0], 0);
469
- const nodeVals = _physMat[0];
470
- const wx = nodeVals[12], wy = nodeVals[13], wz = nodeVals[14];
471
- Mat4.toQuatFromArrayInto(nodeVals, 0, _physQuat);
472
- const transform = new Ammo.btTransform();
473
- const pos = new Ammo.btVector3(wx, wy, wz);
474
- const quat = new Ammo.btQuaternion(_physQuat.x, _physQuat.y, _physQuat.z, _physQuat.w);
475
- transform.setOrigin(pos);
476
- transform.setRotation(quat);
477
- ammoBody.setWorldTransform(transform);
478
- ammoBody.getMotionState().setWorldTransform(transform);
479
- if (!this.zeroVector) {
480
- this.zeroVector = new Ammo.btVector3(0, 0, 0);
481
- }
482
- ammoBody.setLinearVelocity(this.zeroVector);
483
- ammoBody.setAngularVelocity(this.zeroVector);
484
- Ammo.destroy(pos);
485
- Ammo.destroy(quat);
486
- Ammo.destroy(transform);
487
- }
488
- }
489
- }
490
- // Step Ammo physics simulation
491
- stepAmmoPhysics(dt) {
492
- if (!this.ammo || !this.dynamicsWorld)
493
- return;
494
- const fixedTimeStep = 1 / 75;
495
- const maxSubSteps = 10;
496
- this.dynamicsWorld.stepSimulation(dt, maxSubSteps, fixedTimeStep);
497
- }
498
- // Apply dynamic rigidbody world transforms to bone world matrices in-place
499
- applyAmmoRigidbodiesToBones(boneWorldMatrices, boneCount) {
500
- if (!this.ammo || !this.dynamicsWorld)
501
- return;
502
- for (let i = 0; i < this.rigidbodies.length; i++) {
503
- const rb = this.rigidbodies[i];
504
- const ammoBody = this.ammoRigidbodies[i];
505
- if (!ammoBody)
506
- continue;
507
- // Only dynamic rigidbodies drive bones (Static/Kinematic follow bones)
508
- if (rb.type === RigidbodyType.Dynamic && rb.boneIndex >= 0 && rb.boneIndex < boneCount) {
509
- const boneIdx = rb.boneIndex;
510
- const transform = ammoBody.getWorldTransform();
511
- const origin = transform.getOrigin();
512
- const rotation = transform.getRotation();
513
- // nodeWorldMatrix → _physMat[0] (from ammo position/rotation directly)
514
- Mat4.fromPositionRotationInto(origin.x(), origin.y(), origin.z(), rotation.x(), rotation.y(), rotation.z(), rotation.w(), _physMat[0]);
515
- // boneWorld = nodeWorld × bodyOffsetMatrixInverse → _physMat[1]
516
- const boneVals = _physMat[1];
517
- Mat4.multiplyArrays(_physMat[0], 0, rb.bodyOffsetMatrixInverse.values, 0, boneVals, 0);
518
- if (!isNaN(boneVals[0]) && !isNaN(boneVals[15]) && Math.abs(boneVals[0]) < 1e6 && Math.abs(boneVals[15]) < 1e6) {
519
- boneWorldMatrices[boneIdx].values.set(boneVals);
520
- }
521
- else {
522
- console.warn(`[Physics] Invalid bone world matrix for rigidbody ${i} (${rb.name}), skipping update`);
523
- }
524
- }
525
- }
526
- }
527
- }
@@ -1,2 +0,0 @@
1
- export declare const BODY_SHADER_WGSL = "\n\n\n\n// Baked 64\u00D764 rgba8unorm combined BRDF LUT \u2014 created once at engine init by dfg_lut.ts.\n// .rg = split-sum DFG (Karis: tint = f0\u00B7x + f90\u00B7y) \u2192 F_brdf_*_scatter\n// .ba = Heitz 2016 LTC magnitude (ltc_mag_ggx) \u2192 ltc_brdf_scale_from_lut\n// Paired with group(0) binding(2) diffuseSampler (linear filter). Sample once per\n// fragment via brdf_lut_sample() \u2014 callers feed .rg and the whole vec4 into the\n// helpers below, halving LUT taps on the default Principled path.\n@group(0) @binding(9) var brdfLut: texture_2d<f32>;\n\n// \u2500\u2500\u2500 RGB \u2194 HSV \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn rgb_to_hsv(rgb: vec3f) -> vec3f {\n let c_max = max(rgb.r, max(rgb.g, rgb.b));\n let c_min = min(rgb.r, min(rgb.g, rgb.b));\n let delta = c_max - c_min;\n\n var h = 0.0;\n if (delta > 1e-6) {\n if (c_max == rgb.r) {\n h = (rgb.g - rgb.b) / delta;\n if (h < 0.0) { h += 6.0; }\n } else if (c_max == rgb.g) {\n h = 2.0 + (rgb.b - rgb.r) / delta;\n } else {\n h = 4.0 + (rgb.r - rgb.g) / delta;\n }\n h /= 6.0;\n }\n let s = select(0.0, delta / c_max, c_max > 1e-6);\n return vec3f(h, s, c_max);\n}\n\nfn hsv_to_rgb(hsv: vec3f) -> vec3f {\n let h = hsv.x;\n let s = hsv.y;\n let v = hsv.z;\n if (s < 1e-6) { return vec3f(v); }\n\n let hh = fract(h) * 6.0;\n let sector = u32(hh);\n let f = hh - f32(sector);\n let p = v * (1.0 - s);\n let q = v * (1.0 - s * f);\n let t = v * (1.0 - s * (1.0 - f));\n\n switch (sector) {\n case 0u: { return vec3f(v, t, p); }\n case 1u: { return vec3f(q, v, p); }\n case 2u: { return vec3f(p, v, t); }\n case 3u: { return vec3f(p, q, v); }\n case 4u: { return vec3f(t, p, v); }\n default: { return vec3f(v, p, q); }\n }\n}\n\n// \u2500\u2500\u2500 HUE_SAT node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn hue_sat(hue: f32, saturation: f32, value: f32, fac: f32, color: vec3f) -> vec3f {\n var hsv = rgb_to_hsv(color);\n hsv.x = fract(hsv.x + hue - 0.5);\n hsv.y = clamp(hsv.y * saturation, 0.0, 1.0);\n hsv.z *= value;\n return mix(color, hsv_to_rgb(hsv), fac);\n}\n\n// hue_sat specialization for hue=0.5 (identity hue shift \u2014 fract(h + 0.5 - 0.5) = h).\n// Branchless equivalent that skips the rgb_to_hsv \u2192 hsv_to_rgb roundtrip: WebKit's\n// Metal backend serializes the 3-way if chain in rgb_to_hsv and the 6-way switch in\n// hsv_to_rgb, where this form compiles to linear SIMD ops + a single select.\nfn hue_sat_id(saturation: f32, value: f32, fac: f32, color: vec3f) -> vec3f {\n let m = max(max(color.r, color.g), color.b);\n let n = min(min(color.r, color.g), color.b);\n // Unclamped (sat*old_s \u2264 1): reproj = mix(vec3f(m), color, saturation).\n // Clamped (saturated to 1): reproj = (color - n) * m / (m - n).\n let range = max(m - n, 1e-6);\n let unclamped = mix(vec3f(m), color, saturation);\n let clamped = (color - vec3f(n)) * m / range;\n let needs_clamp = (m - n) * saturation >= m;\n let reproj = select(unclamped, clamped, needs_clamp);\n return mix(color, reproj * value, fac);\n}\n\n// \u2500\u2500\u2500 BRIGHTCONTRAST node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn bright_contrast(color: vec3f, bright: f32, contrast: f32) -> vec3f {\n let a = 1.0 + contrast;\n let b = bright - contrast * 0.5;\n return max(vec3f(0.0), color * a + vec3f(b));\n}\n\n// \u2500\u2500\u2500 INVERT node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn invert(fac: f32, color: vec3f) -> vec3f {\n return mix(color, vec3f(1.0) - color, fac);\n}\n\nfn invert_f(fac: f32, val: f32) -> f32 {\n return mix(val, 1.0 - val, fac);\n}\n\n// \u2500\u2500\u2500 Color ramp (VALTORGB) \u2014 2-stop variants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// All 7 presets use exclusively 2-stop ramps.\n\nfn ramp_constant(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {\n return select(c0, c1, f >= p1);\n}\n\n// CONSTANT ramp with screen-space edge AA \u2014 kills sparkle where fwidth(f) straddles a hard step (NPR terminator)\nfn ramp_constant_edge_aa(f: f32, edge: f32, c0: vec4f, c1: vec4f) -> vec4f {\n let w = max(fwidth(f) * 1.75, 6e-6);\n let t = smoothstep(edge - w, edge + w, f);\n return mix(c0, c1, t);\n}\n\nfn ramp_linear(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {\n let t = saturate((f - p0) / max(p1 - p0, 1e-6));\n return mix(c0, c1, t);\n}\n\nfn ramp_cardinal(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {\n // cardinal spline with 2 stops degrades to smoothstep\n let t = saturate((f - p0) / max(p1 - p0, 1e-6));\n let ss = t * t * (3.0 - 2.0 * t);\n return mix(c0, c1, ss);\n}\n\n// \u2500\u2500\u2500 MATH node operations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn math_add(a: f32, b: f32) -> f32 { return a + b; }\nfn math_multiply(a: f32, b: f32) -> f32 { return a * b; }\nfn math_power(a: f32, b: f32) -> f32 { return pow(max(a, 0.0), b); }\nfn math_greater_than(a: f32, b: f32) -> f32 { return select(0.0, 1.0, a > b); }\n\n// Blender's implicit Color \u2192 Float socket conversion uses BT.601 grayscale\n// (rgb_to_grayscale in blenkernel/intern/node.cc). When a material graph plugs a\n// Color output into a Math node's Value input, this is the scalar it actually sees.\nfn color_to_value(c: vec3f) -> f32 {\n return 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;\n}\n\n// \u2500\u2500\u2500 MIX node (blend_type variants) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn mix_blend(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, b, fac);\n}\n\nfn mix_overlay(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n let lo = 2.0 * a * b;\n let hi = vec3f(1.0) - 2.0 * (vec3f(1.0) - a) * (vec3f(1.0) - b);\n let overlay = select(hi, lo, a < vec3f(0.5));\n return mix(a, overlay, fac);\n}\n\nfn mix_multiply(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, a * b, fac);\n}\n\nfn mix_lighten(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, max(a, b), fac);\n}\n\n// Blender Mix (Color) blend LINEAR_LIGHT: result = mix(A, A + 2*B - 1, Fac)\nfn mix_linear_light(fac: f32, a: vec3f, b: vec3f) -> vec3f {\n return mix(a, a + 2.0 * b - vec3f(1.0), fac);\n}\n\n// Luminance for Shader\u2192RGB scalar gates (linear RGB, Rec.709 weights)\nfn luminance_rec709_linear(c: vec3f) -> f32 {\n return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));\n}\n\n// \u2500\u2500\u2500 FRESNEL node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Schlick approximation matching Blender's Fresnel node\n\nfn fresnel(ior: f32, n: vec3f, v: vec3f) -> f32 {\n let r = (ior - 1.0) / (ior + 1.0);\n let f0 = r * r;\n let cos_theta = clamp(dot(n, v), 0.0, 1.0);\n let m = 1.0 - cos_theta;\n let m2 = m * m;\n let m5 = m2 * m2 * m;\n return f0 + (1.0 - f0) * m5;\n}\n\n// \u2500\u2500\u2500 LAYER_WEIGHT node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn layer_weight_fresnel(blend: f32, n: vec3f, v: vec3f) -> f32 {\n let eta = max(1.0 - blend, 1e-4);\n let r = (1.0 - eta) / (1.0 + eta);\n let f0 = r * r;\n let cos_theta = clamp(abs(dot(n, v)), 0.0, 1.0);\n let m = 1.0 - cos_theta;\n let m2 = m * m;\n let m5 = m2 * m2 * m;\n return f0 + (1.0 - f0) * m5;\n}\n\nfn layer_weight_facing(blend: f32, n: vec3f, v: vec3f) -> f32 {\n var facing = abs(dot(n, v));\n let b = clamp(blend, 0.0, 0.99999);\n if (b != 0.5) {\n let exponent = select(2.0 * b, 0.5 / (1.0 - b), b >= 0.5);\n facing = pow(facing, exponent);\n }\n return 1.0 - facing;\n}\n\n// \u2500\u2500\u2500 SHADER_TO_RGB (white DiffuseBSDF) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Eevee captures lit diffuse: (albedo/\u03C0)*sun*N\u00B7L*shadow + ambient (linear). Albedo=1.\n// Matches default.ts direct term scale so VALTORGB thresholds from Blender JSON stay valid.\n\nfn shader_to_rgb_diffuse(n: vec3f, l: vec3f, sun_rgb: vec3f, ambient_rgb: vec3f, shadow: f32) -> f32 {\n const PI_S: f32 = 3.141592653589793;\n let ndotl = max(dot(n, l), 0.0);\n let rgb = sun_rgb * (ndotl * shadow / PI_S) + ambient_rgb;\n return luminance_rec709_linear(rgb);\n}\n\n// \u2500\u2500\u2500 BUMP node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Screen-space bump from a scalar height field. Needs dFdx/dFdy which\n// WGSL provides as dpdx/dpdy.\n\nfn bump(strength: f32, height: f32, normal: vec3f, world_pos: vec3f) -> vec3f {\n let dhdx = dpdx(height);\n let dhdy = dpdy(height);\n let dpdx_pos = dpdx(world_pos);\n let dpdy_pos = dpdy(world_pos);\n let perturbed = normalize(normal) - strength * (dhdx * normalize(cross(dpdy_pos, normal)) + dhdy * normalize(cross(normal, dpdx_pos)));\n return normalize(perturbed);\n}\n\n// LH engine + WebGPU fragment Y: flip dhdy contribution so height peaks read as outward bumps vs Blender reference\nfn bump_lh(strength: f32, height: f32, normal: vec3f, world_pos: vec3f) -> vec3f {\n let dhdx = dpdx(height);\n let dhdy = dpdy(height);\n let dpdx_pos = dpdx(world_pos);\n let dpdy_pos = dpdy(world_pos);\n let perturbed = normalize(normal) - strength * (dhdx * normalize(cross(dpdy_pos, normal)) - dhdy * normalize(cross(normal, dpdx_pos)));\n return normalize(perturbed);\n}\n\n// \u2500\u2500\u2500 NOISE texture (Perlin-style) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Simplified gradient noise matching Blender's default noise output.\n\n// PCG-style integer hash. Replaces the classic 'fract(sin(q) * LARGE)' trick because\n// WebKit's Metal backend compiles 'sin' to a full transcendental op (slow), while\n// Safari's Apple-GPU scalar ALU handles int muls/xors near free. Inputs arrive as\n// integer-valued floats (floor(p) + unit offsets) from _noise3, so vec3i cast is exact.\nfn _hash33(p: vec3f) -> vec3f {\n var h = vec3u(vec3i(p) + vec3i(32768));\n h = h * vec3u(1664525u, 1013904223u, 2654435761u);\n h = (h.yzx ^ h) * vec3u(2246822519u, 3266489917u, 668265263u);\n h = h ^ (h >> vec3u(16u));\n // Mask to 24 bits \u2014 above that f32 loses precision on the u32\u2192f32 convert.\n let hm = h & vec3u(16777215u);\n return vec3f(hm) * (2.0 / 16777216.0) - 1.0;\n}\n\nfn _noise3(p: vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n\n return mix(\n mix(\n mix(dot(_hash33(i + vec3f(0,0,0)), f - vec3f(0,0,0)),\n dot(_hash33(i + vec3f(1,0,0)), f - vec3f(1,0,0)), u.x),\n mix(dot(_hash33(i + vec3f(0,1,0)), f - vec3f(0,1,0)),\n dot(_hash33(i + vec3f(1,1,0)), f - vec3f(1,1,0)), u.x), u.y),\n mix(\n mix(dot(_hash33(i + vec3f(0,0,1)), f - vec3f(0,0,1)),\n dot(_hash33(i + vec3f(1,0,1)), f - vec3f(1,0,1)), u.x),\n mix(dot(_hash33(i + vec3f(0,1,1)), f - vec3f(0,1,1)),\n dot(_hash33(i + vec3f(1,1,1)), f - vec3f(1,1,1)), u.x), u.y),\n u.z);\n}\n\nfn tex_noise(p: vec3f, scale: f32, detail: f32, roughness: f32, distortion: f32) -> f32 {\n var q = p;\n if (abs(distortion) > 1e-6) {\n let w = _noise3(p * scale * 1.37 + vec3f(2.31, 5.17, 8.09));\n q = p + (w * 2.0 - 1.0) * distortion;\n }\n let coords = q * scale;\n var value = 0.0;\n var amplitude = 1.0;\n var frequency = 1.0;\n var total_amp = 0.0;\n let octaves = i32(clamp(detail, 0.0, 15.0)) + 1;\n for (var i = 0; i < octaves; i++) {\n value += amplitude * _noise3(coords * frequency);\n total_amp += amplitude;\n amplitude *= roughness;\n frequency *= 2.0;\n }\n return value / max(total_amp, 1e-6) * 0.5 + 0.5;\n}\n\n// tex_noise specialization: detail=2.0 (3 octaves), roughness=0.5, distortion=0.\n// WebKit can't unroll tex_noise's for-loop because 'octaves' is a runtime value;\n// this variant is fully unrolled with constants folded (total_amp = 1.75).\nfn tex_noise_d2(p: vec3f, scale: f32) -> f32 {\n let c = p * scale;\n let v = _noise3(c) + 0.5 * _noise3(c * 2.0) + 0.25 * _noise3(c * 4.0);\n return v * (1.0 / 1.75) * 0.5 + 0.5;\n}\n\n// \u2500\u2500\u2500 TEX_GRADIENT (linear) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Used by Stockings preset. Maps the input vector's X to a 0\u20131 gradient.\n\nfn tex_gradient_linear(uv: vec3f) -> f32 {\n return clamp(uv.x, 0.0, 1.0);\n}\n\n// \u2500\u2500\u2500 TEX_VORONOI (distance only) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Used by Metal preset. Simplified F1 cell noise.\n\nfn tex_voronoi_f1(p: vec3f, scale: f32) -> f32 {\n let coords = p * scale;\n let i = floor(coords);\n let f = fract(coords);\n var min_dist = 1e10;\n for (var z = -1; z <= 1; z++) {\n for (var y = -1; y <= 1; y++) {\n for (var x = -1; x <= 1; x++) {\n let neighbor = vec3f(f32(x), f32(y), f32(z));\n let point = _hash33(i + neighbor) * 0.5 + 0.5;\n let diff = neighbor + point - f;\n min_dist = min(min_dist, dot(diff, diff));\n }\n }\n }\n return sqrt(min_dist);\n}\n\n// \u2500\u2500\u2500 SEPXYZ node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn separate_xyz(v: vec3f) -> vec3f { return v; }\n\n// \u2500\u2500\u2500 VECT_MATH (cross product) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfn vect_math_cross(a: vec3f, b: vec3f) -> vec3f { return cross(a, b); }\n\n// \u2500\u2500\u2500 MAPPING node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Point-type mapping: scale, rotate (euler XYZ), translate.\n\nfn mapping_point(v: vec3f, loc: vec3f, rot: vec3f, scl: vec3f) -> vec3f {\n var p = v * scl;\n // simplified: skip rotation when all angles are zero (common case)\n if (abs(rot.x) + abs(rot.y) + abs(rot.z) > 1e-6) {\n let cx = cos(rot.x); let sx = sin(rot.x);\n let cy = cos(rot.y); let sy = sin(rot.y);\n let cz = cos(rot.z); let sz = sin(rot.z);\n let rx = vec3f(p.x, cx*p.y - sx*p.z, sx*p.y + cx*p.z);\n let ry = vec3f(cy*rx.x + sy*rx.z, rx.y, -sy*rx.x + cy*rx.z);\n p = vec3f(cz*ry.x - sz*ry.y, sz*ry.x + cz*ry.y, ry.z);\n }\n return p + loc;\n}\n\n// \u2500\u2500\u2500 NORMAL_MAP node (tangent-space) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Applies a tangent-space normal map. Requires TBN from vertex stage.\n\nfn normal_map(strength: f32, map_color: vec3f, normal: vec3f, tangent: vec3f, bitangent: vec3f) -> vec3f {\n let ts = map_color * 2.0 - 1.0;\n let perturbed = normalize(tangent * ts.x + bitangent * ts.y + normal * ts.z);\n return normalize(mix(normal, perturbed, strength));\n}\n\n// \u2500\u2500\u2500 EEVEE Principled BSDF primitives \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Ports from Blender 3.6 source/blender/draw/engines/eevee/shaders/\n// bsdf_common_lib.glsl + gpu_shader_material_principled.glsl.\n// Usage pattern (see material shaders): direct spec = bsdf_ggx \u00D7 sun \u00D7 shadow\n// (NL baked in, no F yet); ambient spec = probe_radiance; tint both with\n// reflection_color = F_brdf_multi_scatter(f0, f90, split_sum) AFTER summing.\n\nconst EEVEE_PI: f32 = 3.141592653589793;\n\n// Fused analytic GGX specular (direct lights). Returns BRDF \u00D7 NL.\n// 4\u00B7NL\u00B7NV is cancelled via G1_Smith reciprocal form \u2014 see bsdf_common_lib.glsl:115.\n// Caller passes NL, NV (already computed for diffuse + brdf_lut_sample) so WebKit\n// can reuse them instead of recomputing dot products across the function boundary.\nfn bsdf_ggx(N: vec3f, L: vec3f, V: vec3f, NL_in: f32, NV_in: f32, roughness: f32) -> f32 {\n let a = max(roughness, 1e-4);\n let a2 = a * a;\n let H = normalize(L + V);\n let NH = max(dot(N, H), 1e-8);\n let NL = max(NL_in, 1e-8);\n let NV = max(NV_in, 1e-8);\n // G1_Smith_GGX_opti reciprocal form \u2014 denominator piece only.\n let G1L = NL + sqrt(NL * (NL - NL * a2) + a2);\n let G1V = NV + sqrt(NV * (NV - NV * a2) + a2);\n let G = G1L * G1V;\n // D_ggx_opti = pi * denom\u00B2 \u2014 reciprocal D \u00D7 a\u00B2.\n let tmp = (NH * a2 - NH) * NH + 1.0;\n let D_opti = EEVEE_PI * tmp * tmp;\n return NL * a2 / (D_opti * G);\n}\n\n// Split-sum DFG LUT \u2014 Karis 2013 curve fit stand-in for the 64\u00D764 baked LUT.\n// Returns (lut.x, lut.y) in Blender convention: tint = f0\u00B7lut.x + f90\u00B7lut.y.\nfn brdf_lut_approx(NV: f32, roughness: f32) -> vec2f {\n let c0 = vec4f(-1.0, -0.0275, -0.572, 0.022);\n let c1 = vec4f(1.0, 0.0425, 1.04, -0.04);\n let r = roughness * c0 + c1;\n let a004 = min(r.x * r.x, exp2(-9.28 * NV)) * r.x + r.y;\n return vec2f(-1.04, 1.04) * a004 + r.zw;\n}\n\n// Baked combined BRDF LUT \u2014 exact port of Blender bsdf_lut_frag.glsl packed with\n// ltc_mag_ggx from eevee_lut.c. Single sample returns DFG (.rg) and LTC mag (.ba).\n// Addressed as Blender's common_utiltex_lib.glsl:lut_coords:\n// coords = (roughness, sqrt(1 - NV)), then half-texel bias for filtering.\n// Requires group(0) binding(9) brdfLut + binding(2) diffuseSampler in the host shader.\nfn brdf_lut_sample(NV: f32, roughness: f32) -> vec4f {\n let LUT_SIZE: f32 = 64.0;\n var uv = vec2f(saturate(roughness), sqrt(saturate(1.0 - NV)));\n uv = uv * ((LUT_SIZE - 1.0) / LUT_SIZE) + 0.5 / LUT_SIZE;\n return textureSampleLevel(brdfLut, diffuseSampler, uv, 0.0);\n}\n\nfn F_brdf_single_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {\n return lut.y * f90 + lut.x * f0;\n}\n\n// Fdez-Ag\u00FCera 2019 multi-scatter compensation (EEVEE do_multiscatter=1).\nfn F_brdf_multi_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {\n let FssEss = lut.y * f90 + lut.x * f0;\n let Ess = lut.x + lut.y;\n let Ems = 1.0 - Ess;\n let Favg = f0 + (1.0 - f0) / 21.0;\n let Fms = FssEss * Favg / (1.0 - (1.0 - Ess) * Favg);\n return FssEss + Fms * Ems;\n}\n\n// EEVEE direct-specular energy compensation factor \u2014 closure_eval_glossy_lib.glsl:79-81:\n// ltc_brdf_scale = (ltc.x + ltc.y) / (split_sum.x + split_sum.y)\n// Blender evaluates direct lights via LTC (Heitz 2016) but indirect via split-sum;\n// direct radiance is rescaled so total-energy matches the split-sum LUT.\n// Takes a pre-sampled vec4f from brdf_lut_sample() to share the fetch with\n// F_brdf_multi_scatter on the same fragment.\nfn ltc_brdf_scale_from_lut(lut: vec4f) -> f32 {\n return (lut.z + lut.w) / max(lut.x + lut.y, 1e-6);\n}\n\n// Luminance-normalized hue extraction \u2014 Blender tint_from_color (isolates hue+sat).\nfn tint_from_color(color: vec3f) -> vec3f {\n let lum = dot(color, vec3f(0.3, 0.6, 0.1));\n return select(vec3f(1.0), color / lum, lum > 0.0);\n}\n\n\n\nstruct CameraUniforms {\n view: mat4x4f,\n projection: mat4x4f,\n viewPos: vec3f,\n _padding: f32,\n};\n\nstruct Light {\n direction: vec4f,\n color: vec4f,\n};\n\nstruct LightUniforms {\n ambientColor: vec4f,\n lights: array<Light, 4>,\n};\n\nstruct MaterialUniforms {\n diffuseColor: vec3f,\n alpha: f32,\n};\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) normal: vec3f,\n @location(1) uv: vec2f,\n @location(2) worldPos: vec3f,\n};\n\nstruct LightVP { viewProj: mat4x4f, };\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var<uniform> light: LightUniforms;\n@group(0) @binding(2) var diffuseSampler: sampler;\n@group(0) @binding(3) var shadowMap: texture_depth_2d;\n@group(0) @binding(4) var shadowSampler: sampler_comparison;\n@group(0) @binding(5) var<uniform> lightVP: LightVP;\n@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;\n@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;\n@group(2) @binding(1) var<uniform> material: MaterialUniforms;\n\nfn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {\n // Back-facing to key light: direct contribution is zero anyway, skip 9 texture samples.\n if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }\n let biasedPos = worldPos + n * 0.08;\n let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);\n let ndc = lclip.xyz / max(lclip.w, 1e-6);\n let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);\n let cmpZ = ndc.z - 0.001;\n let ts = 1.0 / 2048.0;\n // 3x3 PCF unrolled \u2014 Safari's Metal backend doesn't unroll nested shadow loops reliably.\n let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);\n let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);\n let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);\n let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);\n let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);\n let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);\n let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);\n let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);\n let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);\n return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);\n}\n\nconst PI_B: f32 = 3.141592653589793;\nconst BODY_ROUGHNESS: f32 = 0.3;\nconst BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;\nconst BODY_RIM2_POW: f32 = 1.4300000667572021;\nconst BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);\nconst BODY_WARM_STR: f32 = 0.30000001192092896;\nconst BODY_SPECULAR: f32 = 0.5;\nconst BODY_MIX_NPR: f32 = 0.5;\n// EEVEE Light Clamp equivalent \u2014 caps firefly specular from noise-bumped NDF aliasing.\nconst BODY_SPEC_CLAMP: f32 = 10.0;\n\n// smoothstep-based ramp: t*t*(3-2*t) between two color stops\nfn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {\n let t = saturate((f - p0) / max(p1 - p0, 1e-6));\n let ss = t * t * (3.0 - 2.0 * t);\n return mix(c0, c1, ss);\n}\n\n@vertex fn vs(\n @location(0) position: vec3f,\n @location(1) normal: vec3f,\n @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>,\n @location(4) weights0: vec4<f32>\n) -> VertexOutput {\n var output: VertexOutput;\n let pos4 = vec4f(position, 1.0);\n let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;\n let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);\n let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);\n var skinnedPos = vec4f(0.0);\n var skinnedNrm = vec3f(0.0);\n for (var i = 0u; i < 4u; i++) {\n let m = skinMats[joints0[i]];\n let w = nw[i];\n skinnedPos += (m * pos4) * w;\n skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;\n }\n output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);\n // Skip VS normalize \u2014 interpolation denormalizes anyway, and FS always does normalize(input.normal).\n output.normal = skinnedNrm;\n output.uv = uv;\n output.worldPos = skinnedPos.xyz;\n return output;\n}\n\nstruct FSOut {\n @location(0) color: vec4f,\n @location(1) mask: f32,\n};\n\n@fragment fn fs(input: VertexOutput) -> FSOut {\n let alpha = material.alpha;\n if (alpha < 0.001) { discard; }\n\n let n = normalize(input.normal);\n let v = normalize(camera.viewPos - input.worldPos);\n let l = -light.lights[0].direction.xyz;\n let sun = light.lights[0].color.xyz * light.lights[0].color.w;\n\n let tex_color = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;\n let shadow = sampleShadow(input.worldPos, n);\n\n let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, light.ambientColor.xyz, shadow);\n let toon = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;\n\n let shadow_tint = hue_sat_id(2.0, 0.3499999940395355, 1.0, tex_color);\n let lit_tint = hue_sat_id(1.5, 1.0, 1.0, tex_color);\n let toon_color = mix_blend(toon, shadow_tint, lit_tint);\n let bc = bright_contrast(toon_color, 0.1, 0.2);\n\n let emission3 = bc * 4.0;\n\n let warm_input = clamp(toon + 0.5, 0.0, 1.0);\n let warm_color = ramp_cardinal(warm_input, 0.2409,\n vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,\n vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;\n let warm_emission = warm_color * BODY_WARM_STR;\n\n let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24000005424022675, n, v);\n let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;\n\n let facing_raw = layer_weight_facing(BODY_RIM2_LAYER_BLEND, n, v);\n let facing_pow = math_power(facing_raw, BODY_RIM2_POW);\n let rim2_fac = ramp_ease(facing_pow, 0.0, vec4f(0,0,0,1), 0.5052, vec4f(1,1,1,1)).r;\n let rim2_mixed = mix(emission3, BODY_RIM2_BG, rim2_fac);\n\n let npr_stack = rim1 + rim2_mixed + warm_emission;\n\n // Noise bump \u2014 Mapping loc=rot=0 folds to a plain scale multiply.\n let noise_val = tex_noise_d2(input.worldPos * vec3f(1.0, 1.0, 1.5), 1.0);\n let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;\n let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);\n\n let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6831911206245422, 0.19474034011363983, 0.13732507824897766));\n let p_emission = bc * 0.2;\n\n // Principled BSDF (EEVEE port): metallic=0, specular=0.5, roughness=0.3, specular_tint=0.\n let NL = max(dot(bumped_n, l), 0.0);\n let NV = max(dot(bumped_n, v), 1e-4);\n\n // f0/f90 per gpu_shader_material_principled.glsl \u2014 specular_tint=0 \u2192 dielectric_f0_color=white.\n let f0 = vec3f(0.08 * BODY_SPECULAR);\n let f90 = mix(f0, vec3f(1.0), sqrt(BODY_SPECULAR));\n let brdf_lut = brdf_lut_sample(NV, BODY_ROUGHNESS);\n let reflection_color = F_brdf_multi_scatter(f0, f90, brdf_lut.xy);\n\n // Direct glossy \u2014 bsdf_ggx already includes NL; no F applied here (tinted after accum).\n // ltc_brdf_scale: EEVEE direct path uses LTC; split-sum LUT path is rescaled to match.\n let spec_direct_raw = bsdf_ggx(bumped_n, l, v, NL, NV, BODY_ROUGHNESS) * sun * shadow * ltc_brdf_scale_from_lut(brdf_lut);\n let spec_direct = min(spec_direct_raw, vec3f(BODY_SPEC_CLAMP));\n // Indirect glossy \u2014 flat world probe (solid color). Phase 2 adds cubemap.\n let spec_indirect = light.ambientColor.xyz;\n let spec_radiance = (spec_direct + spec_indirect) * reflection_color;\n\n // Indirect diffuse = base_color \u00D7 L_w per Blender closure_eval_surface_lib.glsl line 302;\n // probe_evaluate_world_diff returns radiance (SH-projected, not cosine-convolved).\n // No (1-F) factor per EEVEE \u2014 it doesn't energy-conserve spec<->diffuse.\n let diffuse_radiance = principled_base * (sun * NL * shadow / PI_B + light.ambientColor.xyz);\n let principled = diffuse_radiance + spec_radiance + p_emission;\n\n let final_color = mix(npr_stack, principled, BODY_MIX_NPR);\n\n var out: FSOut;\n out.color = vec4f(final_color, alpha);\n out.mask = 1.0;\n return out;\n}\n\n";
2
- //# sourceMappingURL=body.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../../src/shaders/body.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,gBAAgB,gu7BAoM5B,CAAA"}