pelican-engine 0.3.18 → 0.3.19
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 +8 -0
- package/distribution.json +11 -11
- package/docs/STATUS.md +2 -2
- package/docs/api/REFERENCE.md +104 -94
- package/docs/api/index.json +110 -95
- package/docs/npm/GAME-AUTHOR.md +8 -0
- package/engine.manifest.json +1 -1
- package/lib/sdk/index.d.ts +11 -1
- package/lib/sdk/index.js +1 -1
- package/lib/simulation/cloth.js +41 -10
- package/package.json +1 -1
package/docs/npm/GAME-AUTHOR.md
CHANGED
|
@@ -233,6 +233,14 @@ 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
|
+
New cloth also resolves discrete self-contact between non-adjacent particles by
|
|
237
|
+
projecting movable particles apart; shared triangle edges are excluded.
|
|
238
|
+
`selfCollisions` defaults to true for new cloth and is reported by
|
|
239
|
+
`ctx.cloth.inspect(id)`. Set it to false for particle-only cloth. Self-contact
|
|
240
|
+
state is included in snapshots. This is particle self-contact, not triangle
|
|
241
|
+
interior/edge collision, continuous collision, friction or a guarantee against
|
|
242
|
+
penetration. It has not been runtime or stability validated.
|
|
243
|
+
|
|
236
244
|
The CPU solver uses edge distance constraints and an optional distance-based
|
|
237
245
|
bending approximation (`bending: true` by default). Sampled planes and spheres
|
|
238
246
|
only move cloth particles; they do not receive reactions. Contacts do not cover
|
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.19";
|
|
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];
|
|
@@ -269,6 +269,8 @@ export interface ClothDefinition {
|
|
|
269
269
|
position: Vec3;
|
|
270
270
|
}[];
|
|
271
271
|
visual?: ClothVisualOptions;
|
|
272
|
+
/** Discrete non-adjacent particle self-contact; defaults to true for new cloth. */
|
|
273
|
+
selfCollisions?: boolean;
|
|
272
274
|
}
|
|
273
275
|
export interface ClothContact {
|
|
274
276
|
tick: number;
|
|
@@ -276,10 +278,17 @@ export interface ClothContact {
|
|
|
276
278
|
colliderId: string;
|
|
277
279
|
penetration: number;
|
|
278
280
|
}
|
|
281
|
+
export interface ClothSelfContact {
|
|
282
|
+
tick: number;
|
|
283
|
+
firstParticle: number;
|
|
284
|
+
secondParticle: number;
|
|
285
|
+
penetration: number;
|
|
286
|
+
}
|
|
279
287
|
export interface ClothState {
|
|
280
288
|
tick: number;
|
|
281
289
|
simulatedSeconds: number;
|
|
282
290
|
disposed: boolean;
|
|
291
|
+
selfCollisions: boolean;
|
|
283
292
|
positions: Vec3[];
|
|
284
293
|
velocities: Vec3[];
|
|
285
294
|
triangles: ClothTriangle[];
|
|
@@ -288,6 +297,7 @@ export interface ClothState {
|
|
|
288
297
|
position: Vec3;
|
|
289
298
|
}[];
|
|
290
299
|
contacts: ClothContact[];
|
|
300
|
+
selfContacts: ClothSelfContact[];
|
|
291
301
|
maxDistanceError: number;
|
|
292
302
|
resources: {
|
|
293
303
|
particles: number;
|
package/lib/sdk/index.js
CHANGED
package/lib/simulation/cloth.js
CHANGED
|
@@ -110,12 +110,14 @@ export function createCloth(input) {
|
|
|
110
110
|
fail("CLOTH_INPUT", "damping must be in [0,100]");
|
|
111
111
|
if (input.bending !== undefined && typeof input.bending !== "boolean")
|
|
112
112
|
fail("CLOTH_INPUT", "bending must be boolean");
|
|
113
|
-
const bending = input.bending ?? true, gravity = readVec(input.gravity ?? [0, -9.81, 0], "gravity");
|
|
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", "selfCollisions must be boolean");
|
|
114
116
|
const { triangles, constraints } = topology(positions, input.triangles, bending);
|
|
115
117
|
const anchors = readAnchors(input.anchors ?? [], positions.length);
|
|
116
118
|
for (const [index, point] of anchors)
|
|
117
119
|
positions[index] = copy(point);
|
|
118
|
-
return new ClothSolver({ mass, radius, fixedDt, substeps, iterations, damping, gravity, bending, triangles, constraints }, positions, anchors);
|
|
120
|
+
return new ClothSolver({ mass, radius, fixedDt, substeps, iterations, damping, gravity, bending, selfCollisions, triangles, constraints }, positions, anchors);
|
|
119
121
|
}
|
|
120
122
|
export class ClothSolver {
|
|
121
123
|
config;
|
|
@@ -127,12 +129,15 @@ export class ClothSolver {
|
|
|
127
129
|
forces;
|
|
128
130
|
colliders = [];
|
|
129
131
|
contacts = [];
|
|
132
|
+
selfContacts = [];
|
|
133
|
+
adjacent;
|
|
130
134
|
constructor(config, positions, anchors) {
|
|
131
135
|
this.config = config;
|
|
132
136
|
this.positions = positions;
|
|
133
137
|
this.anchors = anchors;
|
|
134
138
|
this.velocities = positions.map(() => [0, 0, 0]);
|
|
135
139
|
this.forces = positions.map(() => [0, 0, 0]);
|
|
140
|
+
this.adjacent = new Set(config.triangles.flatMap(([a, b, c]) => [`${Math.min(a, b)}:${Math.max(a, b)}`, `${Math.min(b, c)}:${Math.max(b, c)}`, `${Math.min(c, a)}:${Math.max(c, a)}`]));
|
|
136
141
|
}
|
|
137
142
|
alive() { if (this.disposed)
|
|
138
143
|
fail("CLOTH_DISPOSED", "cloth has been disposed"); }
|
|
@@ -171,7 +176,7 @@ export class ClothSolver {
|
|
|
171
176
|
this.alive();
|
|
172
177
|
if (!Number.isSafeInteger(steps) || steps < 1 || steps > 1000 || this.tickValue + steps > Number.MAX_SAFE_INTEGER)
|
|
173
178
|
fail("CLOTH_INPUT", "steps must be in [1,1000] without tick overflow");
|
|
174
|
-
const before = { positions: this.positions, velocities: this.velocities, forces: this.forces, contacts: this.contacts, tick: this.tickValue };
|
|
179
|
+
const before = { positions: this.positions, velocities: this.velocities, forces: this.forces, contacts: this.contacts, selfContacts: this.selfContacts, tick: this.tickValue };
|
|
175
180
|
try {
|
|
176
181
|
for (let i = 0; i < steps; i++)
|
|
177
182
|
this.stepOne();
|
|
@@ -181,6 +186,7 @@ export class ClothSolver {
|
|
|
181
186
|
this.velocities = before.velocities;
|
|
182
187
|
this.forces = before.forces;
|
|
183
188
|
this.contacts = before.contacts;
|
|
189
|
+
this.selfContacts = before.selfContacts;
|
|
184
190
|
this.tickValue = before.tick;
|
|
185
191
|
throw error;
|
|
186
192
|
}
|
|
@@ -189,7 +195,7 @@ export class ClothSolver {
|
|
|
189
195
|
stepOne() {
|
|
190
196
|
const { fixedDt, substeps, iterations, mass, gravity, damping, constraints, radius } = this.config;
|
|
191
197
|
const h = fixedDt / substeps, decay = Math.exp(-damping * h);
|
|
192
|
-
const positions = this.positions.map(copy), velocities = this.velocities.map(copy), contacts = new Map();
|
|
198
|
+
const positions = this.positions.map(copy), velocities = this.velocities.map(copy), contacts = new Map(), selfContacts = new Map();
|
|
193
199
|
for (let sub = 0; sub < substeps; sub++) {
|
|
194
200
|
const previous = positions.map(copy);
|
|
195
201
|
for (let i = 0; i < positions.length; i++) {
|
|
@@ -250,6 +256,29 @@ export class ClothSolver {
|
|
|
250
256
|
contacts.set(key, { tick: this.tickValue + 1, particle: i, colliderId: collider.id, penetration: depth });
|
|
251
257
|
}
|
|
252
258
|
}
|
|
259
|
+
if (this.config.selfCollisions)
|
|
260
|
+
for (let i = 0; i < positions.length; i++)
|
|
261
|
+
for (let j = i + 1; j < positions.length; j++) {
|
|
262
|
+
if (this.adjacent.has(`${i}:${j}`))
|
|
263
|
+
continue;
|
|
264
|
+
const a = positions[i], b = positions[j], dx = a[0] - b[0], dy = a[1] - b[1], dz = a[2] - b[2], length = Math.hypot(dx, dy, dz), depth = 2 * radius - length;
|
|
265
|
+
if (depth <= 0)
|
|
266
|
+
continue;
|
|
267
|
+
const normal = length < 1e-12 ? [1, 0, 0] : [dx / length, dy / length, dz / length];
|
|
268
|
+
const wa = this.anchors.has(i) ? 0 : 1, wb = this.anchors.has(j) ? 0 : 1;
|
|
269
|
+
if (wa + wb === 0)
|
|
270
|
+
continue;
|
|
271
|
+
const correction = depth / (wa + wb);
|
|
272
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
273
|
+
a[axis] += normal[axis] * correction * wa;
|
|
274
|
+
b[axis] -= normal[axis] * correction * wb;
|
|
275
|
+
}
|
|
276
|
+
const key = `${i}:${j}`, old = selfContacts.get(key);
|
|
277
|
+
if (old)
|
|
278
|
+
old.penetration = Math.max(old.penetration, depth);
|
|
279
|
+
else
|
|
280
|
+
selfContacts.set(key, { tick: this.tickValue + 1, firstParticle: i, secondParticle: j, penetration: depth });
|
|
281
|
+
}
|
|
253
282
|
}
|
|
254
283
|
for (let i = 0; i < positions.length; i++) {
|
|
255
284
|
if (this.anchors.has(i))
|
|
@@ -264,18 +293,19 @@ export class ClothSolver {
|
|
|
264
293
|
this.velocities = velocities;
|
|
265
294
|
this.forces = positions.map(() => [0, 0, 0]);
|
|
266
295
|
this.contacts = [...contacts.values()];
|
|
296
|
+
this.selfContacts = [...selfContacts.values()];
|
|
267
297
|
this.tickValue++;
|
|
268
298
|
}
|
|
269
299
|
inspect() {
|
|
270
300
|
if (this.disposed)
|
|
271
|
-
return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: true, positions: [], velocities: [], triangles: [], anchors: [], contacts: [], maxDistanceError: 0, resources: { particles: 0, triangles: 0, constraints: 0, anchors: 0, colliders: 0 } };
|
|
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 } };
|
|
272
302
|
let maxDistanceError = 0;
|
|
273
303
|
for (const [a, b, rest] of this.config.constraints)
|
|
274
304
|
maxDistanceError = Math.max(maxDistanceError, Math.abs(distance(this.positions[a], this.positions[b]) - rest));
|
|
275
|
-
return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: false,
|
|
305
|
+
return { tick: this.tickValue, simulatedSeconds: this.tickValue * this.config.fixedDt, disposed: false, selfCollisions: this.config.selfCollisions,
|
|
276
306
|
positions: this.positions.map(copy), velocities: this.velocities.map(copy), triangles: this.config.triangles.map(tuple),
|
|
277
307
|
anchors: [...this.anchors].map(([index, position]) => ({ index, position: copy(position) })).sort((a, b) => a.index - b.index),
|
|
278
|
-
contacts: this.contacts.map(contact => ({ ...contact })), maxDistanceError,
|
|
308
|
+
contacts: this.contacts.map(contact => ({ ...contact })), selfContacts: this.selfContacts.map(contact => ({ ...contact })), maxDistanceError,
|
|
279
309
|
resources: { particles: this.positions.length, triangles: this.config.triangles.length, constraints: this.config.constraints.length, anchors: this.anchors.size, colliders: this.colliders.length } };
|
|
280
310
|
}
|
|
281
311
|
snapshot() {
|
|
@@ -283,7 +313,7 @@ export class ClothSolver {
|
|
|
283
313
|
const state = this.inspect();
|
|
284
314
|
return { format: "pelican-cloth-cpu", version: 1, tick: this.tickValue,
|
|
285
315
|
config: { ...this.config, gravity: copy(this.config.gravity), triangles: this.config.triangles.map(tuple), constraints: this.config.constraints.map(tuple) },
|
|
286
|
-
positions: state.positions, velocities: state.velocities, forces: this.forces.map(copy), anchors: state.anchors, colliders: readColliders(this.colliders), contacts: state.contacts };
|
|
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 };
|
|
287
317
|
}
|
|
288
318
|
restore(input) {
|
|
289
319
|
this.alive();
|
|
@@ -291,7 +321,7 @@ export class ClothSolver {
|
|
|
291
321
|
fail("CLOTH_SNAPSHOT", "unsupported snapshot format or tick");
|
|
292
322
|
const config = input.config, expected = this.config;
|
|
293
323
|
if (!config || config.mass !== expected.mass || config.radius !== expected.radius || config.fixedDt !== expected.fixedDt || config.substeps !== expected.substeps ||
|
|
294
|
-
config.iterations !== expected.iterations || config.damping !== expected.damping || config.bending !== expected.bending ||
|
|
324
|
+
config.iterations !== expected.iterations || config.damping !== expected.damping || config.bending !== expected.bending || (config.selfCollisions ?? false) !== expected.selfCollisions ||
|
|
295
325
|
!vec(config.gravity) || config.gravity.some((value, i) => value !== expected.gravity[i]) ||
|
|
296
326
|
!Array.isArray(config.triangles) || JSON.stringify(config.triangles) !== JSON.stringify(expected.triangles) ||
|
|
297
327
|
!Array.isArray(config.constraints) || JSON.stringify(config.constraints) !== JSON.stringify(expected.constraints))
|
|
@@ -320,8 +350,9 @@ export class ClothSolver {
|
|
|
320
350
|
this.anchors = anchors;
|
|
321
351
|
this.colliders = colliders;
|
|
322
352
|
this.contacts = input.contacts.map(contact => ({ ...contact }));
|
|
353
|
+
this.selfContacts = (input.selfContacts ?? []).map(contact => ({ ...contact }));
|
|
323
354
|
this.tickValue = input.tick;
|
|
324
355
|
}
|
|
325
356
|
dispose() { if (this.disposed)
|
|
326
|
-
return; this.disposed = true; this.positions = []; this.velocities = []; this.forces = []; this.colliders = []; this.contacts = []; this.anchors.clear(); }
|
|
357
|
+
return; this.disposed = true; this.positions = []; this.velocities = []; this.forces = []; this.colliders = []; this.contacts = []; this.selfContacts = []; this.anchors.clear(); }
|
|
327
358
|
}
|