@driftengine/physics 4.13.1 → 4.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,12 @@
9
9
  *
10
10
  * Allocation-free, and each function a kernel the GPU solver mirrors.
11
11
  */
12
+ /**
13
+ * Every free particle drawn toward its skinned position, keeping `keep` of its offset: the share a
14
+ * substep keeps under the garment's `pull`, `1 / (1 + pull · h²)`. A kinematic particle is already
15
+ * there.
16
+ */
17
+ export declare function applyPull(position: Float32Array, inverseMass: Float32Array, target: Float32Array, keep: number): void;
12
18
  /** A particle further than `maxDistance[i] · scale` from its skinned position is put back on that sphere. */
13
19
  export declare function applyMaxDistance(position: Float32Array, inverseMass: Float32Array, target: Float32Array, maxDistance: Float32Array, scale: number): void;
14
20
  /**
@@ -9,6 +9,22 @@
9
9
  *
10
10
  * Allocation-free, and each function a kernel the GPU solver mirrors.
11
11
  */
12
+ /**
13
+ * Every free particle drawn toward its skinned position, keeping `keep` of its offset: the share a
14
+ * substep keeps under the garment's `pull`, `1 / (1 + pull · h²)`. A kinematic particle is already
15
+ * there.
16
+ */
17
+ export function applyPull(position, inverseMass, target, keep) {
18
+ for (let i = 0; i < inverseMass.length; i++) {
19
+ if (inverseMass[i] === 0)
20
+ continue;
21
+ const at = i * 3;
22
+ for (let k = at; k < at + 3; k++) {
23
+ const t = target[k];
24
+ position[k] = t + (position[k] - t) * keep;
25
+ }
26
+ }
27
+ }
12
28
  /** A particle further than `maxDistance[i] · scale` from its skinned position is put back on that sphere. */
13
29
  export function applyMaxDistance(position, inverseMass, target, maxDistance, scale) {
14
30
  for (let i = 0; i < inverseMass.length; i++) {
@@ -54,7 +54,10 @@ export declare class SkinnedCloth {
54
54
  * each predicted, constrained, limited and turned into velocities.
55
55
  */
56
56
  private stepAt;
57
- /** The limits and the colliders, after the constraints, so a garment pulling into the body loses. */
57
+ /**
58
+ * The pull, the limits and the colliders, after the constraints, so a garment pulling into the body
59
+ * loses. The pull first, so the hard limits have the last word.
60
+ */
58
61
  private limit;
59
62
  /** After a reset, the share of the simulation drawn grows from nothing to all over the blend. */
60
63
  private blend;
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { batchesOf } from './clothBatches.js';
19
19
  import { ClothControl } from './clothControl.js';
20
- import { applyColliders, applyMaxDistance, applyStops } from './clothLimits.js';
20
+ import { applyColliders, applyMaxDistance, applyPull, applyStops } from './clothLimits.js';
21
21
  import { carryParticles } from './clothPose.js';
22
22
  import { exactExp } from './exact.js';
23
23
  import { finishParticles, predictParticles, solveBendingBatch, solveDistanceBatch, solveTethers, } from './skinnedClothSolve.js';
@@ -149,6 +149,8 @@ export class SkinnedCloth {
149
149
  const invH2 = invH * invH;
150
150
  const keep = exactExp(damping * h);
151
151
  const dragKeep = exactExp(drag * h);
152
+ /* A compliant constraint's step toward the skinned place: see `SkinnedClothParameters.pull`. */
153
+ const pullKeep = 1 / (1 + this.parameters.pull * h * h);
152
154
  for (let s = 0; s < substeps; s++) {
153
155
  predictParticles(this.positions, this.substepStart, this.velocities, this.inverseMass, h, keep, dragKeep, gravity, wind);
154
156
  this.distance.lambda.fill(0);
@@ -162,16 +164,21 @@ export class SkinnedCloth {
162
164
  solveTethers(this.positions, tethers.particles, tethers.anchors, tethers.lengths);
163
165
  }
164
166
  }
165
- this.limit();
167
+ this.limit(pullKeep);
166
168
  finishParticles(this.positions, this.substepStart, this.velocities, this.inverseMass, invH);
167
169
  }
168
170
  if (blend > 0)
169
171
  this.blend(blend);
170
172
  }
171
- /** The limits and the colliders, after the constraints, so a garment pulling into the body loses. */
172
- limit() {
173
+ /**
174
+ * The pull, the limits and the colliders, after the constraints, so a garment pulling into the body
175
+ * loses. The pull first, so the hard limits have the last word.
176
+ */
177
+ limit(pullKeep) {
173
178
  const { positions, inverseMass, targets, limits } = this;
174
179
  const { maxDistanceScale, margin } = this.parameters;
180
+ if (pullKeep < 1)
181
+ applyPull(positions, inverseMass, targets.position, pullKeep);
175
182
  if (limits.maxDistance !== undefined) {
176
183
  applyMaxDistance(positions, inverseMass, targets.position, limits.maxDistance, maxDistanceScale);
177
184
  }
@@ -97,6 +97,29 @@ export interface SkinnedClothParameters {
97
97
  readonly damping?: number;
98
98
  /** The same, of the velocity relative to the wind: how fast the air takes the cloth with it. */
99
99
  readonly drag?: number;
100
+ /**
101
+ * How hard each particle is drawn back toward where skinning puts it, as a stiffness: metres a
102
+ * second squared toward its skinned position for every metre it is away from it. Zero, the
103
+ * default, is none. A particle at rest under gravity alone sits `9.81 / pull` metres below its
104
+ * skinned place, whatever the step.
105
+ *
106
+ * **For a garment made to hold a shape gravity would undo**: styled hair held by a few pinned
107
+ * particles at its roots keeps the shape it was made in at rest and swings out of it only as the
108
+ * body moves, then settles back. The hard limits cannot do that: a max distance is a sphere a
109
+ * particle may fall anywhere inside, so hair falls to its edge and hangs.
110
+ *
111
+ * **A stiffness, not a share of the offset kept a second as `damping` is.** Applied first among
112
+ * the limits, after the constraints, each substep keeping `1 / (1 + pull · h²)` of the offset: a
113
+ * compliant constraint's step, so the shape it holds against gravity is one length at every step.
114
+ * A share scaled by the step as `damping` is would hold a sag proportional to the step: hair
115
+ * authored to keep 0.7 of its offset a 1/60 step sags 6.36, 3.49 and 1.83 mm at 1/60, 1/120 and
116
+ * 1/240 that way, where this holds 6.36 mm at all three. A share `k` kept a step at a step `s` is `(1 / k − 1) / s²` here, and keeps
117
+ * exactly `k` at that step. **What it gives up**: one pull for the whole garment, so roots that
118
+ * should hold harder than tips hold them as hard; it is a pull rather than a limit, so a hard blow
119
+ * still takes a particle out of shape for a moment; and with no `damping` a stiff one rings, at a
120
+ * rate a coarse step damps more than a fine one does.
121
+ */
122
+ readonly pull?: number;
100
123
  /**
101
124
  * Metres a second, world space: the air the cloth starts in. Default still air. A garment in a
102
125
  * scene follows the scene's wind a frame at a time, through `setWind`.
@@ -153,6 +176,7 @@ export interface ResolvedClothParameters {
153
176
  readonly gravity: readonly [number, number, number];
154
177
  readonly damping: number;
155
178
  readonly drag: number;
179
+ readonly pull: number;
156
180
  readonly wind: readonly [number, number, number];
157
181
  readonly linearInertia: number;
158
182
  readonly angularInertia: number;
@@ -20,6 +20,7 @@ export function resolveClothParameters(parameters) {
20
20
  gravity: parameters.gravity ?? [0, -9.81, 0],
21
21
  damping: parameters.damping ?? 0,
22
22
  drag: parameters.drag ?? 0,
23
+ pull: parameters.pull ?? 0,
23
24
  wind: parameters.wind ?? [0, 0, 0],
24
25
  linearInertia: Math.min(1, Math.max(0, parameters.linearInertia ?? 1)),
25
26
  angularInertia: Math.min(1, Math.max(0, parameters.angularInertia ?? 1)),
@@ -76,6 +77,10 @@ export function validateClothSetup(setup) {
76
77
  const step = parameters.step ?? 1 / 60;
77
78
  if (!(step > 0))
78
79
  fail(`a step of ${step} seconds`);
80
+ /* A stiffness below zero pushes a particle away from its skinned place rather than toward it. */
81
+ const pull = parameters.pull ?? 0;
82
+ if (!(pull >= 0 && pull < Infinity))
83
+ fail(`a pull of ${pull}: a stiffness, zero or above`);
79
84
  const per = (name, values, width) => {
80
85
  if (values !== undefined && values.length !== count * width) {
81
86
  fail(`${name} has ${values.length} entries for ${count} particles of ${width}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/physics",
3
- "version": "4.13.1",
3
+ "version": "4.13.3",
4
4
  "description": "Collision shapes, spatial queries and swept kinematic collision",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -10,6 +10,27 @@
10
10
  * Allocation-free, and each function a kernel the GPU solver mirrors.
11
11
  */
12
12
 
13
+ /**
14
+ * Every free particle drawn toward its skinned position, keeping `keep` of its offset: the share a
15
+ * substep keeps under the garment's `pull`, `1 / (1 + pull · h²)`. A kinematic particle is already
16
+ * there.
17
+ */
18
+ export function applyPull(
19
+ position: Float32Array,
20
+ inverseMass: Float32Array,
21
+ target: Float32Array,
22
+ keep: number,
23
+ ): void {
24
+ for (let i = 0; i < inverseMass.length; i++) {
25
+ if ((inverseMass[i] as number) === 0) continue;
26
+ const at = i * 3;
27
+ for (let k = at; k < at + 3; k++) {
28
+ const t = target[k] as number;
29
+ position[k] = t + ((position[k] as number) - t) * keep;
30
+ }
31
+ }
32
+ }
33
+
13
34
  /** A particle further than `maxDistance[i] · scale` from its skinned position is put back on that sphere. */
14
35
  export function applyMaxDistance(
15
36
  position: Float32Array,
@@ -18,7 +18,7 @@
18
18
  import { batchesOf } from './clothBatches.ts';
19
19
  import { ClothControl } from './clothControl.ts';
20
20
  import type { ClothDevice } from './clothControl.ts';
21
- import { applyColliders, applyMaxDistance, applyStops } from './clothLimits.ts';
21
+ import { applyColliders, applyMaxDistance, applyPull, applyStops } from './clothLimits.ts';
22
22
  import { carryParticles } from './clothPose.ts';
23
23
  import type { ClothTargets } from './clothTargets.ts';
24
24
  import { exactExp } from './exact.ts';
@@ -218,6 +218,8 @@ export class SkinnedCloth {
218
218
  const invH2 = invH * invH;
219
219
  const keep = exactExp(damping * h);
220
220
  const dragKeep = exactExp(drag * h);
221
+ /* A compliant constraint's step toward the skinned place: see `SkinnedClothParameters.pull`. */
222
+ const pullKeep = 1 / (1 + this.parameters.pull * h * h);
221
223
  for (let s = 0; s < substeps; s++) {
222
224
  predictParticles(
223
225
  this.positions,
@@ -240,16 +242,20 @@ export class SkinnedCloth {
240
242
  solveTethers(this.positions, tethers.particles, tethers.anchors, tethers.lengths);
241
243
  }
242
244
  }
243
- this.limit();
245
+ this.limit(pullKeep);
244
246
  finishParticles(this.positions, this.substepStart, this.velocities, this.inverseMass, invH);
245
247
  }
246
248
  if (blend > 0) this.blend(blend);
247
249
  }
248
250
 
249
- /** The limits and the colliders, after the constraints, so a garment pulling into the body loses. */
250
- private limit(): void {
251
+ /**
252
+ * The pull, the limits and the colliders, after the constraints, so a garment pulling into the body
253
+ * loses. The pull first, so the hard limits have the last word.
254
+ */
255
+ private limit(pullKeep: number): void {
251
256
  const { positions, inverseMass, targets, limits } = this;
252
257
  const { maxDistanceScale, margin } = this.parameters;
258
+ if (pullKeep < 1) applyPull(positions, inverseMass, targets.position, pullKeep);
253
259
  if (limits.maxDistance !== undefined) {
254
260
  applyMaxDistance(
255
261
  positions,
@@ -103,6 +103,29 @@ export interface SkinnedClothParameters {
103
103
  readonly damping?: number;
104
104
  /** The same, of the velocity relative to the wind: how fast the air takes the cloth with it. */
105
105
  readonly drag?: number;
106
+ /**
107
+ * How hard each particle is drawn back toward where skinning puts it, as a stiffness: metres a
108
+ * second squared toward its skinned position for every metre it is away from it. Zero, the
109
+ * default, is none. A particle at rest under gravity alone sits `9.81 / pull` metres below its
110
+ * skinned place, whatever the step.
111
+ *
112
+ * **For a garment made to hold a shape gravity would undo**: styled hair held by a few pinned
113
+ * particles at its roots keeps the shape it was made in at rest and swings out of it only as the
114
+ * body moves, then settles back. The hard limits cannot do that: a max distance is a sphere a
115
+ * particle may fall anywhere inside, so hair falls to its edge and hangs.
116
+ *
117
+ * **A stiffness, not a share of the offset kept a second as `damping` is.** Applied first among
118
+ * the limits, after the constraints, each substep keeping `1 / (1 + pull · h²)` of the offset: a
119
+ * compliant constraint's step, so the shape it holds against gravity is one length at every step.
120
+ * A share scaled by the step as `damping` is would hold a sag proportional to the step: hair
121
+ * authored to keep 0.7 of its offset a 1/60 step sags 6.36, 3.49 and 1.83 mm at 1/60, 1/120 and
122
+ * 1/240 that way, where this holds 6.36 mm at all three. A share `k` kept a step at a step `s` is `(1 / k − 1) / s²` here, and keeps
123
+ * exactly `k` at that step. **What it gives up**: one pull for the whole garment, so roots that
124
+ * should hold harder than tips hold them as hard; it is a pull rather than a limit, so a hard blow
125
+ * still takes a particle out of shape for a moment; and with no `damping` a stiff one rings, at a
126
+ * rate a coarse step damps more than a fine one does.
127
+ */
128
+ readonly pull?: number;
106
129
  /**
107
130
  * Metres a second, world space: the air the cloth starts in. Default still air. A garment in a
108
131
  * scene follows the scene's wind a frame at a time, through `setWind`.
@@ -161,6 +184,7 @@ export interface ResolvedClothParameters {
161
184
  readonly gravity: readonly [number, number, number];
162
185
  readonly damping: number;
163
186
  readonly drag: number;
187
+ readonly pull: number;
164
188
  readonly wind: readonly [number, number, number];
165
189
  readonly linearInertia: number;
166
190
  readonly angularInertia: number;
@@ -183,6 +207,7 @@ export function resolveClothParameters(
183
207
  gravity: parameters.gravity ?? [0, -9.81, 0],
184
208
  damping: parameters.damping ?? 0,
185
209
  drag: parameters.drag ?? 0,
210
+ pull: parameters.pull ?? 0,
186
211
  wind: parameters.wind ?? [0, 0, 0],
187
212
  linearInertia: Math.min(1, Math.max(0, parameters.linearInertia ?? 1)),
188
213
  angularInertia: Math.min(1, Math.max(0, parameters.angularInertia ?? 1)),
@@ -241,6 +266,9 @@ export function validateClothSetup(setup: SkinnedClothSetup): void {
241
266
  }
242
267
  const step = parameters.step ?? 1 / 60;
243
268
  if (!(step > 0)) fail(`a step of ${step} seconds`);
269
+ /* A stiffness below zero pushes a particle away from its skinned place rather than toward it. */
270
+ const pull = parameters.pull ?? 0;
271
+ if (!(pull >= 0 && pull < Infinity)) fail(`a pull of ${pull}: a stiffness, zero or above`);
244
272
  const per = (name: string, values: ArrayLike<number> | undefined, width: number): void => {
245
273
  if (values !== undefined && values.length !== count * width) {
246
274
  fail(`${name} has ${values.length} entries for ${count} particles of ${width}`);