pelican-engine 0.3.21 → 0.3.23
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/AGENTS.md +10 -0
- package/distribution.json +12 -12
- package/docs/STATUS.md +3 -1
- package/docs/api/REFERENCE.md +116 -96
- package/docs/api/index.json +124 -97
- package/docs/npm/GAME-AUTHOR.md +10 -0
- package/engine.manifest.json +1 -1
- package/lib/sdk/index.d.ts +18 -1
- package/lib/sdk/index.js +1 -1
- package/lib/simulation/cloth-plugin.js +56 -8
- package/lib/simulation/cloth.js +82 -12
- package/package.json +1 -1
package/docs/npm/GAME-AUTHOR.md
CHANGED
|
@@ -233,6 +233,16 @@ manual cloth uses `ctx.cloth.step`. Use `setAnchor`, `releaseAnchor`, `applyForc
|
|
|
233
233
|
(next tick), `applyImpulse`, `setColliders`, `inspect`, and `destroy` to manage it.
|
|
234
234
|
The source checkout includes `examples/cloth-sheet`.
|
|
235
235
|
|
|
236
|
+
An automatic cloth may bind a live dynamic sphere with `bodyColliders: [{ id: "sphere-id", entity: "body-id" }]`. The plugin samples the real sphere after physics and uses it as a one-way moving obstacle. The body receives no cloth reaction impulse. Destroyed bodies are removed on the next automatic step. This is not complete cloth-rigid-body coupling.
|
|
237
|
+
|
|
238
|
+
New cloth also resolves detected sphere overlaps against triangle surface interiors
|
|
239
|
+
using closest-point barycentric projection. `inspect(id).surfaceContacts` reports
|
|
240
|
+
the triangle and barycentric position. `surfaceCollisions` defaults to true for new
|
|
241
|
+
cloth and can be disabled. This is sphere-only discrete surface contact; planes
|
|
242
|
+
remain particle-only, and there is no edge/face continuous collision or rigid-body
|
|
243
|
+
reaction. Surface contact state is included in snapshots and invalid state is
|
|
244
|
+
rejected atomically. It has not been runtime or stability validated.
|
|
245
|
+
|
|
236
246
|
New cloth also resolves discrete self-contact between non-adjacent particles by
|
|
237
247
|
projecting movable particles apart; shared triangle edges are excluded.
|
|
238
248
|
`selfCollisions` defaults to true for new cloth and is reported by
|
package/engine.manifest.json
CHANGED
package/lib/sdk/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Pelican Engine public API. No renderer or GPU implementation types escape this module. */
|
|
2
|
-
export declare const SDK_VERSION = "0.3.
|
|
2
|
+
export declare const SDK_VERSION = "0.3.23";
|
|
3
3
|
export type Vec2 = readonly [number, number];
|
|
4
4
|
export type Vec3 = readonly [number, number, number];
|
|
5
5
|
export type Vec4 = readonly [number, number, number, number];
|
|
@@ -249,6 +249,10 @@ export interface ClothVisualOptions {
|
|
|
249
249
|
castShadow?: boolean;
|
|
250
250
|
receiveShadow?: boolean;
|
|
251
251
|
}
|
|
252
|
+
export interface ClothBodyCollider {
|
|
253
|
+
readonly id: string;
|
|
254
|
+
readonly entity: string;
|
|
255
|
+
}
|
|
252
256
|
export interface ClothDefinition {
|
|
253
257
|
id: string;
|
|
254
258
|
points: readonly Vec3[];
|
|
@@ -271,6 +275,10 @@ export interface ClothDefinition {
|
|
|
271
275
|
visual?: ClothVisualOptions;
|
|
272
276
|
/** Discrete non-adjacent particle self-contact; defaults to true for new cloth. */
|
|
273
277
|
selfCollisions?: boolean;
|
|
278
|
+
/** Sphere triangle-surface contacts; defaults to true for new cloth. */
|
|
279
|
+
surfaceCollisions?: boolean;
|
|
280
|
+
/** Optional live dynamic sphere bindings; sampled after the physics phase, one-way only. */
|
|
281
|
+
bodyColliders?: readonly ClothBodyCollider[];
|
|
274
282
|
}
|
|
275
283
|
export interface ClothContact {
|
|
276
284
|
tick: number;
|
|
@@ -284,11 +292,19 @@ export interface ClothSelfContact {
|
|
|
284
292
|
secondParticle: number;
|
|
285
293
|
penetration: number;
|
|
286
294
|
}
|
|
295
|
+
export interface ClothSurfaceContact {
|
|
296
|
+
tick: number;
|
|
297
|
+
triangle: number;
|
|
298
|
+
colliderId: string;
|
|
299
|
+
penetration: number;
|
|
300
|
+
barycentric: [number, number, number];
|
|
301
|
+
}
|
|
287
302
|
export interface ClothState {
|
|
288
303
|
tick: number;
|
|
289
304
|
simulatedSeconds: number;
|
|
290
305
|
disposed: boolean;
|
|
291
306
|
selfCollisions: boolean;
|
|
307
|
+
surfaceCollisions: boolean;
|
|
292
308
|
positions: Vec3[];
|
|
293
309
|
velocities: Vec3[];
|
|
294
310
|
triangles: ClothTriangle[];
|
|
@@ -298,6 +314,7 @@ export interface ClothState {
|
|
|
298
314
|
}[];
|
|
299
315
|
contacts: ClothContact[];
|
|
300
316
|
selfContacts: ClothSelfContact[];
|
|
317
|
+
surfaceContacts: ClothSurfaceContact[];
|
|
301
318
|
maxDistanceError: number;
|
|
302
319
|
resources: {
|
|
303
320
|
particles: number;
|
package/lib/sdk/index.js
CHANGED
|
@@ -15,14 +15,31 @@ function planarUvs(points) {
|
|
|
15
15
|
return fail("VISUAL_UV", "Initial points need two nonzero projection axes or explicit UVs.");
|
|
16
16
|
return points.map(point => [(point[first] - min[first]) / width, (point[second] - min[second]) / height]);
|
|
17
17
|
}
|
|
18
|
-
function createEntry(input, fixedDt, materials, restoredSelfMode) {
|
|
18
|
+
function createEntry(input, fixedDt, materials, restoredSelfMode, restoredSurfaceMode) {
|
|
19
19
|
const definition = cloneSerializable(input);
|
|
20
20
|
if (!definition || !validId(definition.id))
|
|
21
21
|
return fail("ID", "Expected a nonempty id of at most 128 characters.");
|
|
22
22
|
if (definition.autoStep !== undefined && typeof definition.autoStep !== "boolean")
|
|
23
23
|
return fail("AUTO_STEP", "autoStep must be boolean.");
|
|
24
|
+
if (definition.bodyColliders !== undefined) {
|
|
25
|
+
if (!Array.isArray(definition.bodyColliders) || definition.bodyColliders.length > 64)
|
|
26
|
+
return fail("BODY_COLLIDER", "bodyColliders must contain at most 64 bindings.");
|
|
27
|
+
if (definition.bodyColliders.length && !definition.autoStep)
|
|
28
|
+
return fail("BODY_COLLIDER", "bodyColliders require autoStep: true.");
|
|
29
|
+
const ids = new Set(), entities = new Set();
|
|
30
|
+
for (const binding of definition.bodyColliders) {
|
|
31
|
+
if (!binding || !validId(binding.id) || !validId(binding.entity) || ids.has(binding.id) || entities.has(binding.entity))
|
|
32
|
+
return fail("BODY_COLLIDER", "Body bindings require unique nonempty ids and entities.");
|
|
33
|
+
ids.add(binding.id);
|
|
34
|
+
entities.add(binding.entity);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
24
37
|
if (definition.selfCollisions !== undefined && typeof definition.selfCollisions !== "boolean")
|
|
25
38
|
return fail("SELF_COLLISION", "selfCollisions must be boolean.");
|
|
39
|
+
if (definition.surfaceCollisions !== undefined && typeof definition.surfaceCollisions !== "boolean")
|
|
40
|
+
return fail("SURFACE_COLLISION", "surfaceCollisions must be boolean.");
|
|
41
|
+
if (restoredSurfaceMode !== undefined && definition.surfaceCollisions !== undefined && definition.surfaceCollisions !== restoredSurfaceMode)
|
|
42
|
+
return fail("SNAPSHOT", "Surface collision configuration does not match the saved solver.");
|
|
26
43
|
if (restoredSelfMode !== undefined && definition.selfCollisions !== undefined && definition.selfCollisions !== restoredSelfMode)
|
|
27
44
|
return fail("SNAPSHOT", "Self collision configuration does not match the saved solver.");
|
|
28
45
|
if (definition.fixedDt !== undefined && definition.fixedDt !== fixedDt)
|
|
@@ -39,7 +56,7 @@ function createEntry(input, fixedDt, materials, restoredSelfMode) {
|
|
|
39
56
|
visual.uvs.some(uv => !Array.isArray(uv) || uv.length !== 2 || !uv.every(value => typeof value === "number" && Number.isFinite(value) && Math.abs(value) <= 1e6))))
|
|
40
57
|
return fail("VISUAL_UV", "UVs must contain one finite 2D coordinate per particle.");
|
|
41
58
|
}
|
|
42
|
-
const solver = createCloth({ ...definition, fixedDt, ...(restoredSelfMode === undefined ? {} : { selfCollisions: restoredSelfMode }) });
|
|
59
|
+
const solver = createCloth({ ...definition, fixedDt, ...(restoredSelfMode === undefined ? {} : { selfCollisions: restoredSelfMode }), ...(restoredSurfaceMode === undefined ? {} : { surfaceCollisions: restoredSurfaceMode }) });
|
|
43
60
|
try {
|
|
44
61
|
const uvs = definition.visual ? (definition.visual.uvs ? definition.visual.uvs.map(uv => [uv[0], uv[1]]) : planarUvs(definition.points)) : [];
|
|
45
62
|
return { definition, solver, uvs };
|
|
@@ -62,7 +79,10 @@ function parseSnapshot(value, fixedDt, materials) {
|
|
|
62
79
|
const savedSelfMode = item.state?.config?.selfCollisions;
|
|
63
80
|
if (savedSelfMode !== undefined && typeof savedSelfMode !== "boolean")
|
|
64
81
|
fail("SNAPSHOT", "Invalid self collision configuration.");
|
|
65
|
-
const
|
|
82
|
+
const savedSurfaceMode = item.state?.config?.surfaceCollisions;
|
|
83
|
+
if (savedSurfaceMode !== undefined && typeof savedSurfaceMode !== "boolean")
|
|
84
|
+
fail("SNAPSHOT", "Invalid surface collision configuration.");
|
|
85
|
+
const entry = createEntry(item.definition, fixedDt, materials, savedSelfMode ?? false, savedSurfaceMode ?? false);
|
|
66
86
|
if (entries.has(entry.definition.id)) {
|
|
67
87
|
entry.solver.dispose();
|
|
68
88
|
fail("SNAPSHOT", "Duplicate cloth id.");
|
|
@@ -97,7 +117,19 @@ export const createClothPlugin = context => {
|
|
|
97
117
|
if (entries.size >= 256)
|
|
98
118
|
return fail("CAPACITY", "At most 256 cloths per host.");
|
|
99
119
|
const entry = createEntry(definition, hostDt, materials);
|
|
100
|
-
|
|
120
|
+
try {
|
|
121
|
+
const bridge = entry.definition.bodyColliders?.length ? context.getRopePhysicsBridge?.() : undefined;
|
|
122
|
+
for (const binding of entry.definition.bodyColliders ?? []) {
|
|
123
|
+
if (!context.world.isAlive(binding.entity))
|
|
124
|
+
return fail("BODY_COLLIDER", `Body '${binding.entity}' is not a live entity.`);
|
|
125
|
+
bridge?.sampleSphere(binding.entity);
|
|
126
|
+
}
|
|
127
|
+
entries.set(entry.definition.id, entry);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
entry.solver.dispose();
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
101
133
|
},
|
|
102
134
|
step(id, ticks) { const entry = get(id); if (entry.definition.autoStep)
|
|
103
135
|
return fail("AUTO_STEP", `Cloth '${id}' is automatically stepped.`); return entry.solver.step(ticks); },
|
|
@@ -128,10 +160,26 @@ export const createClothPlugin = context => {
|
|
|
128
160
|
try {
|
|
129
161
|
const automatic = [...entries.values()].filter(entry => entry.definition.autoStep)
|
|
130
162
|
.sort((a, b) => a.definition.id < b.definition.id ? -1 : a.definition.id > b.definition.id ? 1 : 0);
|
|
131
|
-
for (const
|
|
132
|
-
const snapshot = solver.snapshot();
|
|
133
|
-
committed.push({ solver, snapshot });
|
|
134
|
-
|
|
163
|
+
for (const entry of automatic) {
|
|
164
|
+
const snapshot = entry.solver.snapshot();
|
|
165
|
+
committed.push({ solver: entry.solver, snapshot });
|
|
166
|
+
const bindings = entry.definition.bodyColliders ?? [];
|
|
167
|
+
if (bindings.length) {
|
|
168
|
+
const bridge = context.getRopePhysicsBridge?.() ?? fail("BODY_COLLIDER", "Body bindings require the physics plugin.");
|
|
169
|
+
const manual = snapshot.colliders.filter(collider => !bindings.some(binding => binding.id === collider.id));
|
|
170
|
+
const sampled = [];
|
|
171
|
+
const live = [];
|
|
172
|
+
for (const binding of bindings) {
|
|
173
|
+
if (!context.world.isAlive(binding.entity))
|
|
174
|
+
continue;
|
|
175
|
+
const sphere = bridge.sampleSphere(binding.entity);
|
|
176
|
+
sampled.push({ kind: "sphere", id: binding.id, center: [...sphere.center], radius: sphere.radius });
|
|
177
|
+
live.push(binding);
|
|
178
|
+
}
|
|
179
|
+
entry.definition = { ...entry.definition, bodyColliders: live };
|
|
180
|
+
entry.solver.setColliders([...manual, ...sampled]);
|
|
181
|
+
}
|
|
182
|
+
entry.solver.step();
|
|
135
183
|
}
|
|
136
184
|
}
|
|
137
185
|
catch (error) {
|
package/lib/simulation/cloth.js
CHANGED
|
@@ -96,6 +96,35 @@ function topology(points, input, bending) {
|
|
|
96
96
|
add(edge.opposite[0], edge.opposite[1]);
|
|
97
97
|
return { triangles, constraints };
|
|
98
98
|
}
|
|
99
|
+
function closestTriangle(point, a, b, c) {
|
|
100
|
+
const ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]], ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]], ap = [point[0] - a[0], point[1] - a[1], point[2] - a[2]];
|
|
101
|
+
const d1 = ab[0] * ap[0] + ab[1] * ap[1] + ab[2] * ap[2], d2 = ac[0] * ap[0] + ac[1] * ap[1] + ac[2] * ap[2];
|
|
102
|
+
if (d1 <= 0 && d2 <= 0)
|
|
103
|
+
return { point: copy(a), barycentric: [1, 0, 0] };
|
|
104
|
+
const bp = [point[0] - b[0], point[1] - b[1], point[2] - b[2]], d3 = ab[0] * bp[0] + ab[1] * bp[1] + ab[2] * bp[2], d4 = ac[0] * bp[0] + ac[1] * bp[1] + ac[2] * bp[2];
|
|
105
|
+
if (d3 >= 0 && d4 <= d3)
|
|
106
|
+
return { point: copy(b), barycentric: [0, 1, 0] };
|
|
107
|
+
const vc = d1 * d4 - d3 * d2;
|
|
108
|
+
if (vc <= 0 && d1 >= 0 && d3 <= 0) {
|
|
109
|
+
const t = d1 / (d1 - d3);
|
|
110
|
+
return { point: [a[0] + ab[0] * t, a[1] + ab[1] * t, a[2] + ab[2] * t], barycentric: [1 - t, t, 0] };
|
|
111
|
+
}
|
|
112
|
+
const cp = [point[0] - c[0], point[1] - c[1], point[2] - c[2]], d5 = ab[0] * cp[0] + ab[1] * cp[1] + ab[2] * cp[2], d6 = ac[0] * cp[0] + ac[1] * cp[1] + ac[2] * cp[2];
|
|
113
|
+
if (d6 >= 0 && d5 <= d6)
|
|
114
|
+
return { point: copy(c), barycentric: [0, 0, 1] };
|
|
115
|
+
const vb = d5 * d2 - d1 * d6;
|
|
116
|
+
if (vb <= 0 && d2 >= 0 && d6 <= 0) {
|
|
117
|
+
const t = d2 / (d2 - d6);
|
|
118
|
+
return { point: [a[0] + ac[0] * t, a[1] + ac[1] * t, a[2] + ac[2] * t], barycentric: [1 - t, 0, t] };
|
|
119
|
+
}
|
|
120
|
+
const va = d3 * d6 - d5 * d4;
|
|
121
|
+
if (va <= 0 && (d4 - d3) >= 0 && (d5 - d6) >= 0) {
|
|
122
|
+
const edge = [c[0] - b[0], c[1] - b[1], c[2] - b[2]], t = (d4 - d3) / ((d4 - d3) + (d5 - d6));
|
|
123
|
+
return { point: [b[0] + edge[0] * t, b[1] + edge[1] * t, b[2] + edge[2] * t], barycentric: [0, 1 - t, t] };
|
|
124
|
+
}
|
|
125
|
+
const denominator = 1 / (va + vb + vc), v = vb * denominator, w = vc * denominator;
|
|
126
|
+
return { point: [a[0] + ab[0] * v + ac[0] * w, a[1] + ab[1] * v + ac[1] * w, a[2] + ab[2] * v + ac[2] * w], barycentric: [1 - v - w, v, w] };
|
|
127
|
+
}
|
|
99
128
|
export function createCloth(input) {
|
|
100
129
|
if (!input || typeof input !== "object" || !Array.isArray(input.points) || input.points.length < 3 || input.points.length > 4096)
|
|
101
130
|
fail("CLOTH_INPUT", "points must contain 3..4096 particles");
|
|
@@ -110,14 +139,14 @@ export function createCloth(input) {
|
|
|
110
139
|
fail("CLOTH_INPUT", "damping must be in [0,100]");
|
|
111
140
|
if (input.bending !== undefined && typeof input.bending !== "boolean")
|
|
112
141
|
fail("CLOTH_INPUT", "bending must be boolean");
|
|
113
|
-
const bending = input.bending ?? true, selfCollisions = input.selfCollisions ?? true, gravity = readVec(input.gravity ?? [0, -9.81, 0], "gravity");
|
|
114
|
-
if (typeof selfCollisions !== "boolean")
|
|
115
|
-
fail("CLOTH_INPUT", "
|
|
142
|
+
const bending = input.bending ?? true, selfCollisions = input.selfCollisions ?? true, surfaceCollisions = input.surfaceCollisions ?? true, gravity = readVec(input.gravity ?? [0, -9.81, 0], "gravity");
|
|
143
|
+
if (typeof selfCollisions !== "boolean" || typeof surfaceCollisions !== "boolean")
|
|
144
|
+
fail("CLOTH_INPUT", "collision options must be boolean");
|
|
116
145
|
const { triangles, constraints } = topology(positions, input.triangles, bending);
|
|
117
146
|
const anchors = readAnchors(input.anchors ?? [], positions.length);
|
|
118
147
|
for (const [index, point] of anchors)
|
|
119
148
|
positions[index] = copy(point);
|
|
120
|
-
return new ClothSolver({ mass, radius, fixedDt, substeps, iterations, damping, gravity, bending, selfCollisions, triangles, constraints }, positions, anchors);
|
|
149
|
+
return new ClothSolver({ mass, radius, fixedDt, substeps, iterations, damping, gravity, bending, selfCollisions, surfaceCollisions, triangles, constraints }, positions, anchors);
|
|
121
150
|
}
|
|
122
151
|
export class ClothSolver {
|
|
123
152
|
config;
|
|
@@ -130,6 +159,7 @@ export class ClothSolver {
|
|
|
130
159
|
colliders = [];
|
|
131
160
|
contacts = [];
|
|
132
161
|
selfContacts = [];
|
|
162
|
+
surfaceContacts = [];
|
|
133
163
|
adjacent;
|
|
134
164
|
constructor(config, positions, anchors) {
|
|
135
165
|
this.config = config;
|
|
@@ -176,7 +206,7 @@ export class ClothSolver {
|
|
|
176
206
|
this.alive();
|
|
177
207
|
if (!Number.isSafeInteger(steps) || steps < 1 || steps > 1000 || this.tickValue + steps > Number.MAX_SAFE_INTEGER)
|
|
178
208
|
fail("CLOTH_INPUT", "steps must be in [1,1000] without tick overflow");
|
|
179
|
-
const before = { positions: this.positions, velocities: this.velocities, forces: this.forces, contacts: this.contacts, selfContacts: this.selfContacts, tick: this.tickValue };
|
|
209
|
+
const before = { positions: this.positions, velocities: this.velocities, forces: this.forces, contacts: this.contacts, selfContacts: this.selfContacts, surfaceContacts: this.surfaceContacts, tick: this.tickValue };
|
|
180
210
|
try {
|
|
181
211
|
for (let i = 0; i < steps; i++)
|
|
182
212
|
this.stepOne();
|
|
@@ -187,6 +217,7 @@ export class ClothSolver {
|
|
|
187
217
|
this.forces = before.forces;
|
|
188
218
|
this.contacts = before.contacts;
|
|
189
219
|
this.selfContacts = before.selfContacts;
|
|
220
|
+
this.surfaceContacts = before.surfaceContacts;
|
|
190
221
|
this.tickValue = before.tick;
|
|
191
222
|
throw error;
|
|
192
223
|
}
|
|
@@ -195,7 +226,7 @@ export class ClothSolver {
|
|
|
195
226
|
stepOne() {
|
|
196
227
|
const { fixedDt, substeps, iterations, mass, gravity, damping, constraints, radius } = this.config;
|
|
197
228
|
const h = fixedDt / substeps, decay = Math.exp(-damping * h);
|
|
198
|
-
const positions = this.positions.map(copy), velocities = this.velocities.map(copy), contacts = new Map(), selfContacts = new Map();
|
|
229
|
+
const positions = this.positions.map(copy), velocities = this.velocities.map(copy), contacts = new Map(), selfContacts = new Map(), surfaceContacts = new Map();
|
|
199
230
|
for (let sub = 0; sub < substeps; sub++) {
|
|
200
231
|
const previous = positions.map(copy);
|
|
201
232
|
for (let i = 0; i < positions.length; i++) {
|
|
@@ -256,6 +287,29 @@ export class ClothSolver {
|
|
|
256
287
|
contacts.set(key, { tick: this.tickValue + 1, particle: i, colliderId: collider.id, penetration: depth });
|
|
257
288
|
}
|
|
258
289
|
}
|
|
290
|
+
if (this.config.surfaceCollisions)
|
|
291
|
+
for (let triangleIndex = 0; triangleIndex < this.config.triangles.length; triangleIndex++) {
|
|
292
|
+
const triangle = this.config.triangles[triangleIndex], a = positions[triangle[0]], b = positions[triangle[1]], c = positions[triangle[2]];
|
|
293
|
+
for (const collider of this.colliders) {
|
|
294
|
+
if (collider.kind !== "sphere")
|
|
295
|
+
continue;
|
|
296
|
+
const closest = closestTriangle(collider.center, a, b, c), delta = [closest.point[0] - collider.center[0], closest.point[1] - collider.center[1], closest.point[2] - collider.center[2]], length = Math.hypot(...delta), depth = collider.radius + radius - length;
|
|
297
|
+
if (depth <= 0)
|
|
298
|
+
continue;
|
|
299
|
+
const normal = length < 1e-12 ? [0, 1, 0] : [delta[0] / length, delta[1] / length, delta[2] / length], weights = [this.anchors.has(triangle[0]) ? 0 : closest.barycentric[0], this.anchors.has(triangle[1]) ? 0 : closest.barycentric[1], this.anchors.has(triangle[2]) ? 0 : closest.barycentric[2]], denominator = weights.reduce((sum, value) => sum + value * value, 0);
|
|
300
|
+
if (denominator === 0)
|
|
301
|
+
continue;
|
|
302
|
+
const correction = depth / denominator;
|
|
303
|
+
for (let vertex = 0; vertex < 3; vertex++)
|
|
304
|
+
for (let axis = 0; axis < 3; axis++)
|
|
305
|
+
positions[triangle[vertex]][axis] += normal[axis] * correction * weights[vertex];
|
|
306
|
+
const key = `${triangleIndex}:${collider.id}`, old = surfaceContacts.get(key);
|
|
307
|
+
if (old)
|
|
308
|
+
old.penetration = Math.max(old.penetration, depth);
|
|
309
|
+
else
|
|
310
|
+
surfaceContacts.set(key, { tick: this.tickValue + 1, triangle: triangleIndex, colliderId: collider.id, penetration: depth, barycentric: closest.barycentric });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
259
313
|
if (this.config.selfCollisions)
|
|
260
314
|
for (let i = 0; i < positions.length; i++)
|
|
261
315
|
for (let j = i + 1; j < positions.length; j++) {
|
|
@@ -294,18 +348,19 @@ export class ClothSolver {
|
|
|
294
348
|
this.forces = positions.map(() => [0, 0, 0]);
|
|
295
349
|
this.contacts = [...contacts.values()];
|
|
296
350
|
this.selfContacts = [...selfContacts.values()];
|
|
351
|
+
this.surfaceContacts = [...surfaceContacts.values()];
|
|
297
352
|
this.tickValue++;
|
|
298
353
|
}
|
|
299
354
|
inspect() {
|
|
300
355
|
if (this.disposed)
|
|
301
|
-
return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: true, selfCollisions: this.config.selfCollisions, positions: [], velocities: [], triangles: [], anchors: [], contacts: [], selfContacts: [], maxDistanceError: 0, resources: { particles: 0, triangles: 0, constraints: 0, anchors: 0, colliders: 0 } };
|
|
356
|
+
return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: true, selfCollisions: this.config.selfCollisions, surfaceCollisions: this.config.surfaceCollisions, positions: [], velocities: [], triangles: [], anchors: [], contacts: [], selfContacts: [], surfaceContacts: [], maxDistanceError: 0, resources: { particles: 0, triangles: 0, constraints: 0, anchors: 0, colliders: 0 } };
|
|
302
357
|
let maxDistanceError = 0;
|
|
303
358
|
for (const [a, b, rest] of this.config.constraints)
|
|
304
359
|
maxDistanceError = Math.max(maxDistanceError, Math.abs(distance(this.positions[a], this.positions[b]) - rest));
|
|
305
|
-
return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: false, selfCollisions: this.config.selfCollisions,
|
|
360
|
+
return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: false, selfCollisions: this.config.selfCollisions, surfaceCollisions: this.config.surfaceCollisions,
|
|
306
361
|
positions: this.positions.map(copy), velocities: this.velocities.map(copy), triangles: this.config.triangles.map(tuple),
|
|
307
362
|
anchors: [...this.anchors].map(([index, position]) => ({ index, position: copy(position) })).sort((a, b) => a.index - b.index),
|
|
308
|
-
contacts: this.contacts.map(contact => ({ ...contact })), selfContacts: this.selfContacts.map(contact => ({ ...contact })), maxDistanceError,
|
|
363
|
+
contacts: this.contacts.map(contact => ({ ...contact })), selfContacts: this.selfContacts.map(contact => ({ ...contact })), surfaceContacts: this.surfaceContacts.map(contact => ({ ...contact })), maxDistanceError,
|
|
309
364
|
resources: { particles: this.positions.length, triangles: this.config.triangles.length, constraints: this.config.constraints.length, anchors: this.anchors.size, colliders: this.colliders.length } };
|
|
310
365
|
}
|
|
311
366
|
snapshot() {
|
|
@@ -313,7 +368,7 @@ export class ClothSolver {
|
|
|
313
368
|
const state = this.inspect();
|
|
314
369
|
return { format: "pelican-cloth-cpu", version: 1, tick: this.tickValue,
|
|
315
370
|
config: { ...this.config, gravity: copy(this.config.gravity), triangles: this.config.triangles.map(tuple), constraints: this.config.constraints.map(tuple) },
|
|
316
|
-
positions: state.positions, velocities: state.velocities, forces: this.forces.map(copy), anchors: state.anchors, colliders: readColliders(this.colliders), contacts: state.contacts, selfContacts: state.selfContacts };
|
|
371
|
+
positions: state.positions, velocities: state.velocities, forces: this.forces.map(copy), anchors: state.anchors, colliders: readColliders(this.colliders), contacts: state.contacts, selfContacts: state.selfContacts, surfaceContacts: state.surfaceContacts };
|
|
317
372
|
}
|
|
318
373
|
restore(input) {
|
|
319
374
|
this.alive();
|
|
@@ -321,7 +376,7 @@ export class ClothSolver {
|
|
|
321
376
|
fail("CLOTH_SNAPSHOT", "unsupported snapshot format or tick");
|
|
322
377
|
const config = input.config, expected = this.config;
|
|
323
378
|
if (!config || config.mass !== expected.mass || config.radius !== expected.radius || config.fixedDt !== expected.fixedDt || config.substeps !== expected.substeps ||
|
|
324
|
-
config.iterations !== expected.iterations || config.damping !== expected.damping || config.bending !== expected.bending || (config.selfCollisions ?? false) !== expected.selfCollisions ||
|
|
379
|
+
config.iterations !== expected.iterations || config.damping !== expected.damping || config.bending !== expected.bending || (config.selfCollisions ?? false) !== expected.selfCollisions || (config.surfaceCollisions ?? false) !== expected.surfaceCollisions ||
|
|
325
380
|
!vec(config.gravity) || config.gravity.some((value, i) => value !== expected.gravity[i]) ||
|
|
326
381
|
!Array.isArray(config.triangles) || JSON.stringify(config.triangles) !== JSON.stringify(expected.triangles) ||
|
|
327
382
|
!Array.isArray(config.constraints) || JSON.stringify(config.constraints) !== JSON.stringify(expected.constraints))
|
|
@@ -357,6 +412,20 @@ export class ClothSolver {
|
|
|
357
412
|
fail("CLOTH_SNAPSHOT", "duplicate self contact");
|
|
358
413
|
selfKeys.add(key);
|
|
359
414
|
}
|
|
415
|
+
const surfaceContacts = input.surfaceContacts ?? [];
|
|
416
|
+
const surfaceKeys = new Set();
|
|
417
|
+
if (!Array.isArray(surfaceContacts) || surfaceContacts.length > this.config.triangles.length * colliders.length || (!this.config.surfaceCollisions && surfaceContacts.length > 0) ||
|
|
418
|
+
surfaceContacts.some(contact => !contact || contact.tick !== input.tick || !Number.isSafeInteger(contact.triangle) || contact.triangle < 0 || contact.triangle >= this.config.triangles.length ||
|
|
419
|
+
!colliders.some(collider => collider.id === contact.colliderId && collider.kind === "sphere") || typeof contact.penetration !== "number" || !Number.isFinite(contact.penetration) || contact.penetration <= 0 ||
|
|
420
|
+
!Array.isArray(contact.barycentric) || contact.barycentric.length !== 3 || !contact.barycentric.every(value => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1) ||
|
|
421
|
+
Math.abs(contact.barycentric[0] + contact.barycentric[1] + contact.barycentric[2] - 1) > 1e-5))
|
|
422
|
+
fail("CLOTH_SNAPSHOT", "invalid surface contact state");
|
|
423
|
+
for (const contact of surfaceContacts) {
|
|
424
|
+
const key = `${contact.triangle}:${contact.colliderId}`;
|
|
425
|
+
if (surfaceKeys.has(key))
|
|
426
|
+
fail("CLOTH_SNAPSHOT", "duplicate surface contact");
|
|
427
|
+
surfaceKeys.add(key);
|
|
428
|
+
}
|
|
360
429
|
this.positions = input.positions.map(copy);
|
|
361
430
|
this.velocities = input.velocities.map(copy);
|
|
362
431
|
this.forces = input.forces.map(copy);
|
|
@@ -364,8 +433,9 @@ export class ClothSolver {
|
|
|
364
433
|
this.colliders = colliders;
|
|
365
434
|
this.contacts = input.contacts.map(contact => ({ ...contact }));
|
|
366
435
|
this.selfContacts = selfContacts.map(contact => ({ ...contact }));
|
|
436
|
+
this.surfaceContacts = surfaceContacts.map(contact => ({ ...contact, barycentric: [...contact.barycentric] }));
|
|
367
437
|
this.tickValue = input.tick;
|
|
368
438
|
}
|
|
369
439
|
dispose() { if (this.disposed)
|
|
370
|
-
return; this.disposed = true; this.positions = []; this.velocities = []; this.forces = []; this.colliders = []; this.contacts = []; this.selfContacts = []; this.anchors.clear(); }
|
|
440
|
+
return; this.disposed = true; this.positions = []; this.velocities = []; this.forces = []; this.colliders = []; this.contacts = []; this.selfContacts = []; this.surfaceContacts = []; this.anchors.clear(); }
|
|
371
441
|
}
|