@aura3d/physics 1.4.0 → 1.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aura3d/physics",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -13,5 +13,37 @@
13
13
  "types": "./dist/index.d.ts",
14
14
  "import": "./dist/index.js"
15
15
  }
16
- }
16
+ },
17
+ "description": "Deterministic rigid-body simulation, collision shapes, contacts, constraints, raycasts, fixed stepping, scene/ECS bridges, and debug draw extraction.",
18
+ "keywords": [
19
+ "aura3d",
20
+ "3d",
21
+ "browser-3d",
22
+ "web-3d",
23
+ "typescript",
24
+ "physics",
25
+ "rigid-body",
26
+ "collision",
27
+ "raycast",
28
+ "constraints",
29
+ "fixed-step",
30
+ "game-physics"
31
+ ],
32
+ "homepage": "https://aura3d.auraone.ai",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/auraoneai/aura3d.git",
36
+ "directory": "packages/physics"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/auraoneai/aura3d/issues"
40
+ },
41
+ "license": "MIT",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "files": [
46
+ "README.md",
47
+ "dist"
48
+ ]
17
49
  }
@@ -1,304 +0,0 @@
1
- import type { Collider } from "./Collider.js";
2
- import { KinematicBody, type KinematicBodyDescriptor, type KinematicBodyEvent, type KinematicBodySnapshot, type KinematicStepOptions } from "./KinematicBody.js";
3
- import type { PhysicsWorld } from "./PhysicsWorld.js";
4
- import type { RigidBody } from "./RigidBody.js";
5
- import { Shape, normalizeVec3, type Vec3 } from "./Shape.js";
6
-
7
- export type CharacterControllerDescriptor = {
8
- readonly position?: Vec3;
9
- readonly radius?: number;
10
- readonly halfHeight?: number;
11
- readonly maxSpeed?: number;
12
- readonly acceleration?: number;
13
- readonly jumpSpeed?: number;
14
- readonly groundProbeDistance?: number;
15
- readonly maxSlopeAngleRadians?: number;
16
- readonly collisionMask?: number;
17
- };
18
-
19
- export type CharacterControllerMoveInput = {
20
- readonly x: number;
21
- readonly z?: number;
22
- };
23
-
24
- export type CharacterControllerState = {
25
- readonly grounded: boolean;
26
- readonly groundNormal: Vec3;
27
- readonly groundColliderId: number | null;
28
- readonly desiredVelocity: Vec3;
29
- readonly velocity: Vec3;
30
- readonly speed: number;
31
- readonly jumpedThisFrame: boolean;
32
- };
33
-
34
- export type FightingCharacterControllerState =
35
- | "idle"
36
- | "walk"
37
- | "dash"
38
- | "jump"
39
- | "fast-fall"
40
- | "crouch"
41
- | "landing";
42
-
43
- export type FightingCharacterControllerDescriptor = KinematicBodyDescriptor & {
44
- readonly walkSpeed?: number;
45
- readonly crouchSpeed?: number;
46
- readonly fastFallSpeed?: number;
47
- };
48
-
49
- export type FightingCharacterControllerSnapshot = KinematicBodySnapshot & {
50
- readonly state: FightingCharacterControllerState;
51
- readonly walkSpeed: number;
52
- readonly crouchSpeed: number;
53
- readonly fastFallSpeed: number;
54
- };
55
-
56
- export class FightingCharacterController {
57
- readonly body: KinematicBody;
58
- readonly walkSpeed: number;
59
- readonly crouchSpeed: number;
60
- readonly fastFallSpeed: number;
61
- private state: FightingCharacterControllerState = "idle";
62
-
63
- constructor(descriptor: FightingCharacterControllerDescriptor = {}) {
64
- this.walkSpeed = positiveFinite(descriptor.walkSpeed ?? descriptor.maxSpeed ?? 4.5, "fighting controller walkSpeed");
65
- this.crouchSpeed = positiveFinite(descriptor.crouchSpeed ?? this.walkSpeed * 0.45, "fighting controller crouchSpeed");
66
- this.fastFallSpeed = positiveFinite(descriptor.fastFallSpeed ?? descriptor.maxFallSpeed ?? 20, "fighting controller fastFallSpeed");
67
- this.body = new KinematicBody({
68
- ...descriptor,
69
- id: descriptor.id ?? "fighter",
70
- halfExtents: descriptor.halfExtents ?? [0.32, 0.9, 0.25],
71
- maxSpeed: descriptor.maxSpeed ?? this.walkSpeed,
72
- acceleration: descriptor.acceleration ?? 54,
73
- airAcceleration: descriptor.airAcceleration ?? 24,
74
- groundFriction: descriptor.groundFriction ?? 56,
75
- airFriction: descriptor.airFriction ?? 4,
76
- gravity: descriptor.gravity ?? 26,
77
- jumpSpeed: descriptor.jumpSpeed ?? 9.4,
78
- maxFallSpeed: descriptor.maxFallSpeed ?? Math.max(this.fastFallSpeed, 20),
79
- dashSpeed: descriptor.dashSpeed ?? 8.6,
80
- dashDuration: descriptor.dashDuration ?? 0.12,
81
- dashCooldown: descriptor.dashCooldown ?? 0.18,
82
- groundSnapDistance: descriptor.groundSnapDistance ?? 0.06,
83
- lockDepth: descriptor.lockDepth ?? true
84
- });
85
- }
86
-
87
- walk(direction: number, speed?: number): void {
88
- const magnitude = Math.min(1, Math.abs(finiteOrZero(direction)));
89
- const facing = direction < 0 ? -1 : 1;
90
- const targetSpeed = speed ?? (this.body.snapshot().crouching ? this.crouchSpeed : this.walkSpeed);
91
- const normalizedSpeed = this.body.maxSpeed > 0 ? Math.min(1, positiveFinite(targetSpeed, "fighting controller walk speed") / this.body.maxSpeed) : 0;
92
- this.body.move(facing * magnitude * normalizedSpeed);
93
- this.state = magnitude > 0 ? "walk" : this.deriveState([]);
94
- }
95
-
96
- stop(): void {
97
- this.body.move(0);
98
- this.state = this.deriveState([]);
99
- }
100
-
101
- jump(): void {
102
- this.body.jump();
103
- this.state = "jump";
104
- }
105
-
106
- dash(direction?: number): void {
107
- this.body.dash(direction);
108
- this.state = "dash";
109
- }
110
-
111
- fastFall(speed = this.fastFallSpeed): void {
112
- this.body.fastFall(speed);
113
- if (!this.body.grounded) {
114
- this.state = "fast-fall";
115
- }
116
- }
117
-
118
- crouch(active = true): void {
119
- this.body.crouch(active);
120
- this.state = active ? "crouch" : this.deriveState([]);
121
- }
122
-
123
- step(dt: number, options: KinematicStepOptions = {}): readonly KinematicBodyEvent[] {
124
- const events = this.body.step(dt, options);
125
- this.state = this.deriveState(events);
126
- return events;
127
- }
128
-
129
- snapshot(): FightingCharacterControllerSnapshot {
130
- return {
131
- ...this.body.snapshot(),
132
- state: this.state,
133
- walkSpeed: this.walkSpeed,
134
- crouchSpeed: this.crouchSpeed,
135
- fastFallSpeed: this.fastFallSpeed
136
- };
137
- }
138
-
139
- private deriveState(events: readonly KinematicBodyEvent[]): FightingCharacterControllerState {
140
- if (events.some((event) => event.type === "land")) return "landing";
141
- const snapshot = this.body.snapshot();
142
- if (snapshot.crouching) return "crouch";
143
- if (snapshot.dashFramesRemaining > 0) return "dash";
144
- if (!snapshot.grounded) return snapshot.velocity[1] < -this.fastFallSpeed * 0.5 ? "fast-fall" : "jump";
145
- return Math.abs(snapshot.velocity[0]) > 0.05 ? "walk" : "idle";
146
- }
147
- }
148
-
149
- export function createFightingCharacterController(descriptor: FightingCharacterControllerDescriptor = {}): FightingCharacterController {
150
- return new FightingCharacterController(descriptor);
151
- }
152
-
153
- export class CharacterController {
154
- readonly body: RigidBody;
155
- readonly collider: Collider;
156
- readonly radius: number;
157
- readonly halfHeight: number;
158
- readonly maxSpeed: number;
159
- readonly acceleration: number;
160
- readonly jumpSpeed: number;
161
- readonly groundProbeDistance: number;
162
- readonly maxSlopeAngleRadians: number;
163
- private readonly collisionMask: number | undefined;
164
- private moveInput: [number, number] = [0, 0];
165
- private jumpQueued = false;
166
- private state: CharacterControllerState = {
167
- grounded: false,
168
- groundNormal: [0, 1, 0],
169
- groundColliderId: null,
170
- desiredVelocity: [0, 0, 0],
171
- velocity: [0, 0, 0],
172
- speed: 0,
173
- jumpedThisFrame: false
174
- };
175
-
176
- constructor(private readonly world: PhysicsWorld, descriptor: CharacterControllerDescriptor = {}) {
177
- this.radius = positiveFinite(descriptor.radius ?? 0.24, "character radius");
178
- this.halfHeight = positiveFinite(descriptor.halfHeight ?? 0.38, "character halfHeight");
179
- this.maxSpeed = positiveFinite(descriptor.maxSpeed ?? 3.5, "character maxSpeed");
180
- this.acceleration = positiveFinite(descriptor.acceleration ?? 32, "character acceleration");
181
- this.jumpSpeed = positiveFinite(descriptor.jumpSpeed ?? 4.2, "character jumpSpeed");
182
- this.groundProbeDistance = positiveFinite(descriptor.groundProbeDistance ?? 0.12, "character groundProbeDistance");
183
- this.maxSlopeAngleRadians = positiveFinite(descriptor.maxSlopeAngleRadians ?? Math.PI / 3, "character maxSlopeAngleRadians");
184
- this.collisionMask = descriptor.collisionMask;
185
- this.body = world.createRigidBody({
186
- type: "dynamic",
187
- position: descriptor.position ?? [0, this.halfHeight + this.radius, 0],
188
- mass: 1,
189
- linearDamping: 0.02,
190
- angularDamping: 1
191
- });
192
- this.collider = world.createCollider(this.body, {
193
- shape: Shape.capsule(this.radius, this.halfHeight),
194
- material: { friction: 0.05, restitution: 0 }
195
- });
196
- }
197
-
198
- setMoveInput(input: CharacterControllerMoveInput): void {
199
- const x = finiteOrZero(input.x);
200
- const z = finiteOrZero(input.z ?? 0);
201
- const length = Math.hypot(x, z);
202
- this.moveInput = length > 1 ? [x / length, z / length] : [x, z];
203
- }
204
-
205
- jump(): void {
206
- this.jumpQueued = true;
207
- }
208
-
209
- teleport(position: Vec3): void {
210
- this.body.setPosition(position);
211
- this.body.setVelocity([0, 0, 0]);
212
- this.state = {
213
- ...this.state,
214
- grounded: false,
215
- groundColliderId: null,
216
- velocity: [0, 0, 0],
217
- speed: 0,
218
- jumpedThisFrame: false
219
- };
220
- }
221
-
222
- step(dt: number): CharacterControllerState {
223
- if (!Number.isFinite(dt) || dt <= 0) {
224
- throw new Error("CharacterController.step dt must be finite and positive.");
225
- }
226
- const ground = this.probeGround();
227
- const desiredVelocity: Vec3 = [
228
- this.moveInput[0] * this.maxSpeed,
229
- this.body.velocity[1],
230
- this.moveInput[1] * this.maxSpeed
231
- ];
232
- const nextVelocity: [number, number, number] = [
233
- moveToward(this.body.velocity[0], desiredVelocity[0], this.acceleration * dt),
234
- this.body.velocity[1],
235
- moveToward(this.body.velocity[2], desiredVelocity[2], this.acceleration * dt)
236
- ];
237
- let jumpedThisFrame = false;
238
- if (ground.grounded && nextVelocity[1] < 0) {
239
- nextVelocity[1] = 0;
240
- }
241
- if (this.jumpQueued && ground.grounded) {
242
- nextVelocity[1] = this.jumpSpeed;
243
- jumpedThisFrame = true;
244
- }
245
- this.jumpQueued = false;
246
- this.body.setVelocity(nextVelocity);
247
- this.state = {
248
- grounded: ground.grounded && !jumpedThisFrame,
249
- groundNormal: ground.normal,
250
- groundColliderId: ground.colliderId,
251
- desiredVelocity,
252
- velocity: [...nextVelocity],
253
- speed: Math.hypot(nextVelocity[0], nextVelocity[2]),
254
- jumpedThisFrame
255
- };
256
- return this.state;
257
- }
258
-
259
- snapshot(): CharacterControllerState {
260
- return {
261
- ...this.state,
262
- groundNormal: [...this.state.groundNormal],
263
- desiredVelocity: [...this.state.desiredVelocity],
264
- velocity: [...this.state.velocity]
265
- };
266
- }
267
-
268
- private probeGround(): { readonly grounded: boolean; readonly normal: Vec3; readonly colliderId: number | null } {
269
- const origin: Vec3 = [
270
- this.body.position[0],
271
- this.body.position[1] - this.halfHeight + this.groundProbeDistance,
272
- this.body.position[2]
273
- ];
274
- const hit = this.world.sphereCast(origin, this.radius * 0.92, [0, -1, 0], {
275
- maxDistance: this.groundProbeDistance + this.radius * 0.35,
276
- includeSensors: false,
277
- ...(this.collisionMask === undefined ? {} : { mask: this.collisionMask })
278
- });
279
- const minGroundNormalY = Math.cos(this.maxSlopeAngleRadians);
280
- if (!hit || hit.normal[1] < minGroundNormalY) {
281
- return { grounded: false, normal: [0, 1, 0], colliderId: null };
282
- }
283
- return { grounded: true, normal: normalizeVec3(hit.normal), colliderId: hit.colliderId };
284
- }
285
- }
286
-
287
- function positiveFinite(value: number, name: string): number {
288
- if (!Number.isFinite(value) || value <= 0) {
289
- throw new Error(`${name} must be a finite positive number.`);
290
- }
291
- return value;
292
- }
293
-
294
- function finiteOrZero(value: number): number {
295
- return Number.isFinite(value) ? value : 0;
296
- }
297
-
298
- function moveToward(current: number, target: number, maxDelta: number): number {
299
- const delta = target - current;
300
- if (Math.abs(delta) <= maxDelta) {
301
- return target;
302
- }
303
- return current + Math.sign(delta) * maxDelta;
304
- }
@@ -1,359 +0,0 @@
1
- export interface ClothSimulationFixtureOptions {
2
- readonly seed?: number;
3
- readonly elapsedSeconds?: number;
4
- readonly segmentsX?: number;
5
- readonly segmentsY?: number;
6
- }
7
-
8
- export interface ClothSampleParticle {
9
- readonly index: number;
10
- readonly grid: readonly [number, number];
11
- readonly pinned: boolean;
12
- readonly position: readonly [number, number, number];
13
- readonly uv: readonly [number, number];
14
- }
15
-
16
- export interface ClothSimulationFixture {
17
- readonly id: "external-parity-old-branch-cloth-simulation-fixture";
18
- readonly source: "origin-master-cloth-pbd-material-adapted";
19
- readonly sourceFiles: readonly [
20
- "origin/master:src/simulation/cloth/ClothSimulation.ts",
21
- "origin/master:src/simulation/cloth/ClothCollisionSystem.ts",
22
- "origin/master:src/simulation/cloth/ClothTearingSystem.ts",
23
- "origin/master:src/materials/ClothMaterial.ts"
24
- ];
25
- readonly config: {
26
- readonly width: number;
27
- readonly height: number;
28
- readonly segmentsX: number;
29
- readonly segmentsY: number;
30
- readonly particleMass: number;
31
- readonly structuralStiffness: number;
32
- readonly shearStiffness: number;
33
- readonly bendingStiffness: number;
34
- readonly solverIterations: number;
35
- readonly damping: number;
36
- };
37
- readonly mesh: {
38
- readonly particleCount: number;
39
- readonly triangleCount: number;
40
- readonly normalCount: number;
41
- readonly uvCount: number;
42
- readonly indexCount: number;
43
- readonly pinnedCount: number;
44
- readonly pinnedPattern: "top-edge";
45
- readonly sampleParticles: readonly ClothSampleParticle[];
46
- };
47
- readonly constraints: {
48
- readonly structural: number;
49
- readonly shear: number;
50
- readonly bending: number;
51
- readonly total: number;
52
- readonly maxStrain: number;
53
- readonly tearThreshold: number;
54
- readonly tearCandidates: number;
55
- readonly cutPlaneConstraintCandidates: number;
56
- };
57
- readonly wind: {
58
- readonly direction: readonly [number, number, number];
59
- readonly turbulence: number;
60
- readonly frequency: number;
61
- readonly maxOffset: number;
62
- readonly affectedParticles: number;
63
- };
64
- readonly collision: {
65
- readonly shape: "sphere";
66
- readonly center: readonly [number, number, number];
67
- readonly radius: number;
68
- readonly penetrationCount: number;
69
- readonly resolvedParticles: number;
70
- readonly maxPenetration: number;
71
- readonly friction: number;
72
- readonly restitution: number;
73
- };
74
- readonly material: {
75
- readonly preset: "coarse-wool-flag";
76
- readonly sheenIntensity: number;
77
- readonly sheenRoughness: number;
78
- readonly subsurfaceIntensity: number;
79
- readonly anisotropyStrength: number;
80
- readonly fuzzIntensity: number;
81
- readonly roughness: number;
82
- readonly tiling: readonly [number, number];
83
- };
84
- readonly blockedClaims: readonly string[];
85
- readonly hash: string;
86
- readonly claimBoundary: string;
87
- }
88
-
89
- const sourceFiles = [
90
- "origin/master:src/simulation/cloth/ClothSimulation.ts",
91
- "origin/master:src/simulation/cloth/ClothCollisionSystem.ts",
92
- "origin/master:src/simulation/cloth/ClothTearingSystem.ts",
93
- "origin/master:src/materials/ClothMaterial.ts"
94
- ] as const;
95
-
96
- const blockedClaims = [
97
- "GPU cloth compute parity",
98
- "100k particle cloth performance",
99
- "production PBD solver parity",
100
- "cloth self-collision",
101
- "mesh-splitting topology tearing",
102
- "fabric shader parity",
103
- "Unity Cloth parity",
104
- "Unreal Chaos Cloth parity"
105
- ] as const;
106
-
107
- export function sampleClothSimulationFixture(options: ClothSimulationFixtureOptions = {}): ClothSimulationFixture {
108
- const seed = integer(options.seed ?? 0x3d2025, "seed");
109
- const elapsedSeconds = finiteNonNegative(options.elapsedSeconds ?? 0.4, "elapsedSeconds");
110
- const segmentsX = integerInRange(options.segmentsX ?? 12, "segmentsX", 2, 64);
111
- const segmentsY = integerInRange(options.segmentsY ?? 8, "segmentsY", 2, 64);
112
- const width = 2.4;
113
- const height = 1.5;
114
- const particleCount = (segmentsX + 1) * (segmentsY + 1);
115
- const triangleCount = segmentsX * segmentsY * 2;
116
- const structural = segmentsX * (segmentsY + 1) + (segmentsX + 1) * segmentsY;
117
- const shear = segmentsX * segmentsY * 2;
118
- const bending = Math.max(0, (segmentsX - 1) * (segmentsY + 1) + (segmentsY - 1) * (segmentsX + 1));
119
- const pinnedCount = segmentsX + 1;
120
- const windDirection = [3.2, 0.35, 1.15] as const;
121
- const turbulence = 0.35;
122
- const frequency = 1.8;
123
- const collisionCenter = [0.08, -0.18, 0.06] as const;
124
- const collisionRadius = 0.34;
125
- let maxOffset = 0;
126
- let affectedParticles = 0;
127
- let penetrationCount = 0;
128
- let resolvedParticles = 0;
129
- let maxPenetration = 0;
130
- let maxStrain = 1;
131
- let tearCandidates = 0;
132
- let cutPlaneConstraintCandidates = 0;
133
-
134
- const positions = new Map<number, readonly [number, number, number]>();
135
- const sampleParticles: ClothSampleParticle[] = [];
136
- const sampleKeys = new Set([
137
- particleIndex(0, 0, segmentsX),
138
- particleIndex(Math.floor(segmentsX / 2), 0, segmentsX),
139
- particleIndex(segmentsX, 0, segmentsX),
140
- particleIndex(0, Math.floor(segmentsY / 2), segmentsX),
141
- particleIndex(Math.floor(segmentsX / 2), Math.floor(segmentsY / 2), segmentsX),
142
- particleIndex(segmentsX, Math.floor(segmentsY / 2), segmentsX),
143
- particleIndex(0, segmentsY, segmentsX),
144
- particleIndex(Math.floor(segmentsX / 2), segmentsY, segmentsX),
145
- particleIndex(segmentsX, segmentsY, segmentsX)
146
- ]);
147
-
148
- for (let y = 0; y <= segmentsY; y += 1) {
149
- for (let x = 0; x <= segmentsX; x += 1) {
150
- const index = particleIndex(x, y, segmentsX);
151
- const vertical = y / segmentsY;
152
- const horizontal = x / segmentsX;
153
- const pinned = y === 0;
154
- const baseX = (horizontal - 0.5) * width;
155
- const sag = pinned ? 0 : Math.pow(vertical, 1.42) * (0.28 + hash01(seed, 7) * 0.08 + elapsedSeconds * 0.045);
156
- const gust = pinned ? 0 : Math.sin(elapsedSeconds * frequency + x * 0.73 + y * 0.41 + hash01(seed, x + y + 11) * 2) * turbulence * vertical;
157
- const xOffset = pinned ? 0 : windDirection[0] * 0.018 * vertical + gust * 0.06;
158
- const zOffset = pinned ? 0 : windDirection[2] * 0.035 * vertical + gust * 0.1;
159
- let position: readonly [number, number, number] = [
160
- round4(baseX + xOffset),
161
- round4(height * 0.5 - vertical * height - sag),
162
- round4(zOffset)
163
- ];
164
- const penetration = spherePenetration(position, collisionCenter, collisionRadius);
165
- if (penetration > 0) {
166
- penetrationCount += 1;
167
- resolvedParticles += 1;
168
- maxPenetration = Math.max(maxPenetration, penetration);
169
- position = resolveSphere(position, collisionCenter, collisionRadius);
170
- }
171
- const offset = Math.hypot(position[0] - baseX, position[2]);
172
- if (offset > 0.002 && !pinned) affectedParticles += 1;
173
- maxOffset = Math.max(maxOffset, offset);
174
- positions.set(index, position);
175
- if (sampleKeys.has(index)) {
176
- sampleParticles.push({
177
- index,
178
- grid: [x, y],
179
- pinned,
180
- position,
181
- uv: [round4(horizontal), round4(vertical)]
182
- });
183
- }
184
- }
185
- }
186
-
187
- for (let y = 0; y <= segmentsY; y += 1) {
188
- for (let x = 0; x <= segmentsX; x += 1) {
189
- const current = positions.get(particleIndex(x, y, segmentsX));
190
- if (!current) continue;
191
- if (x < segmentsX) {
192
- const strain = constraintStrain(current, positions.get(particleIndex(x + 1, y, segmentsX)), width / segmentsX);
193
- maxStrain = Math.max(maxStrain, strain);
194
- if (strain >= 1.035 && y >= segmentsY - 2) tearCandidates += 1;
195
- if (x < segmentsX / 2 && x + 1 >= segmentsX / 2 && y > 0) cutPlaneConstraintCandidates += 1;
196
- }
197
- if (y < segmentsY) {
198
- const strain = constraintStrain(current, positions.get(particleIndex(x, y + 1, segmentsX)), height / segmentsY);
199
- maxStrain = Math.max(maxStrain, strain);
200
- if (strain >= 1.05 && y >= segmentsY - 3) tearCandidates += 1;
201
- }
202
- }
203
- }
204
-
205
- const metricsForHash = [
206
- seed,
207
- elapsedSeconds,
208
- segmentsX,
209
- segmentsY,
210
- particleCount,
211
- triangleCount,
212
- structural,
213
- shear,
214
- bending,
215
- pinnedCount,
216
- maxOffset,
217
- affectedParticles,
218
- penetrationCount,
219
- maxPenetration,
220
- maxStrain,
221
- tearCandidates,
222
- cutPlaneConstraintCandidates
223
- ];
224
-
225
- return {
226
- id: "external-parity-old-branch-cloth-simulation-fixture",
227
- source: "origin-master-cloth-pbd-material-adapted",
228
- sourceFiles,
229
- config: {
230
- width,
231
- height,
232
- segmentsX,
233
- segmentsY,
234
- particleMass: 0.1,
235
- structuralStiffness: 0.9,
236
- shearStiffness: 0.8,
237
- bendingStiffness: 0.12,
238
- solverIterations: 6,
239
- damping: 0.01
240
- },
241
- mesh: {
242
- particleCount,
243
- triangleCount,
244
- normalCount: particleCount,
245
- uvCount: particleCount,
246
- indexCount: triangleCount * 3,
247
- pinnedCount,
248
- pinnedPattern: "top-edge",
249
- sampleParticles
250
- },
251
- constraints: {
252
- structural,
253
- shear,
254
- bending,
255
- total: structural + shear + bending,
256
- maxStrain: round4(maxStrain),
257
- tearThreshold: 1.035,
258
- tearCandidates,
259
- cutPlaneConstraintCandidates
260
- },
261
- wind: {
262
- direction: windDirection,
263
- turbulence,
264
- frequency,
265
- maxOffset: round4(maxOffset),
266
- affectedParticles
267
- },
268
- collision: {
269
- shape: "sphere",
270
- center: collisionCenter,
271
- radius: collisionRadius,
272
- penetrationCount,
273
- resolvedParticles,
274
- maxPenetration: round4(maxPenetration),
275
- friction: 0.42,
276
- restitution: 0.04
277
- },
278
- material: {
279
- preset: "coarse-wool-flag",
280
- sheenIntensity: 0.56,
281
- sheenRoughness: 0.34,
282
- subsurfaceIntensity: 0.24,
283
- anisotropyStrength: 0.42,
284
- fuzzIntensity: 0.22,
285
- roughness: 0.74,
286
- tiling: [3, 2]
287
- },
288
- blockedClaims,
289
- hash: hashNumbers(metricsForHash),
290
- claimBoundary: "Deterministic bounded cloth telemetry adapted from old PBD cloth, collision, tearing, and cloth-material concepts. It proves a pinned cloth grid, structural/shear/bending constraint counts, wind displacement, sphere-collision response, tear-candidate metrics, and fabric material parameters; it does not claim GPU cloth, self-collision, topology tearing, fabric shader parity, Unity Cloth parity, or Unreal Chaos Cloth parity."
291
- };
292
- }
293
-
294
- function particleIndex(x: number, y: number, segmentsX: number): number {
295
- return y * (segmentsX + 1) + x;
296
- }
297
-
298
- function constraintStrain(left: readonly [number, number, number], right: readonly [number, number, number] | undefined, restLength: number): number {
299
- if (!right) return 1;
300
- const length = Math.hypot(left[0] - right[0], left[1] - right[1], left[2] - right[2]);
301
- return restLength > 0 ? length / restLength : 1;
302
- }
303
-
304
- function spherePenetration(position: readonly [number, number, number], center: readonly [number, number, number], radius: number): number {
305
- const distance = Math.hypot(position[0] - center[0], position[1] - center[1], position[2] - center[2]);
306
- return Math.max(0, radius - distance);
307
- }
308
-
309
- function resolveSphere(position: readonly [number, number, number], center: readonly [number, number, number], radius: number): readonly [number, number, number] {
310
- const dx = position[0] - center[0];
311
- const dy = position[1] - center[1];
312
- const dz = position[2] - center[2];
313
- const length = Math.max(0.0001, Math.hypot(dx, dy, dz));
314
- return [
315
- round4(center[0] + dx / length * radius),
316
- round4(center[1] + dy / length * radius),
317
- round4(center[2] + dz / length * radius)
318
- ];
319
- }
320
-
321
- function hash01(seed: number, salt: number): number {
322
- let value = Math.imul(seed + salt * 1013904223, 1664525) + 1013904223;
323
- value = Math.imul(value ^ (value >>> 13), 1274126177);
324
- return ((value ^ (value >>> 16)) >>> 0) / 4294967296;
325
- }
326
-
327
- function hashNumbers(values: readonly number[]): string {
328
- let hash = 0x811c9dc5;
329
- for (const value of values) {
330
- const scaled = Math.round(value * 10_000);
331
- hash ^= scaled & 0xff;
332
- hash = Math.imul(hash, 0x01000193) >>> 0;
333
- hash ^= (scaled >>> 8) & 0xff;
334
- hash = Math.imul(hash, 0x01000193) >>> 0;
335
- hash ^= (scaled >>> 16) & 0xff;
336
- hash = Math.imul(hash, 0x01000193) >>> 0;
337
- }
338
- return hash.toString(16).padStart(8, "0");
339
- }
340
-
341
- function round4(value: number): number {
342
- return Number(value.toFixed(4));
343
- }
344
-
345
- function finiteNonNegative(value: number, label: string): number {
346
- if (!Number.isFinite(value) || value < 0) throw new RangeError(`Cloth fixture ${label} must be finite and non-negative.`);
347
- return value;
348
- }
349
-
350
- function integer(value: number, label: string): number {
351
- if (!Number.isInteger(value)) throw new RangeError(`Cloth fixture ${label} must be an integer.`);
352
- return value;
353
- }
354
-
355
- function integerInRange(value: number, label: string, min: number, max: number): number {
356
- const parsed = integer(value, label);
357
- if (parsed < min || parsed > max) throw new RangeError(`Cloth fixture ${label} must be in the [${min}, ${max}] range.`);
358
- return parsed;
359
- }