@driftengine/physics 4.13.3 → 4.13.5

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.
@@ -10,11 +10,12 @@
10
10
  * Allocation-free, and each function a kernel the GPU solver mirrors.
11
11
  */
12
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.
13
+ * Every free particle drawn toward its skinned position under the garment's `pull`: `stiffness` is
14
+ * `pull · h²`, each particle's weighted by `weights` (all 1 where null), and a particle keeps
15
+ * `1 / (1 + w · stiffness)` of its offset. A kinematic particle is already there, and one weighted
16
+ * 0 is left as it is: `t + (p − t)` is not `p` in floats.
16
17
  */
17
- export declare function applyPull(position: Float32Array, inverseMass: Float32Array, target: Float32Array, keep: number): void;
18
+ export declare function applyPull(position: Float32Array, inverseMass: Float32Array, target: Float32Array, weights: Float32Array | null, stiffness: number): void;
18
19
  /** A particle further than `maxDistance[i] · scale` from its skinned position is put back on that sphere. */
19
20
  export declare function applyMaxDistance(position: Float32Array, inverseMass: Float32Array, target: Float32Array, maxDistance: Float32Array, scale: number): void;
20
21
  /**
@@ -10,14 +10,19 @@
10
10
  * Allocation-free, and each function a kernel the GPU solver mirrors.
11
11
  */
12
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.
13
+ * Every free particle drawn toward its skinned position under the garment's `pull`: `stiffness` is
14
+ * `pull · h²`, each particle's weighted by `weights` (all 1 where null), and a particle keeps
15
+ * `1 / (1 + w · stiffness)` of its offset. A kinematic particle is already there, and one weighted
16
+ * 0 is left as it is: `t + (p − t)` is not `p` in floats.
16
17
  */
17
- export function applyPull(position, inverseMass, target, keep) {
18
+ export function applyPull(position, inverseMass, target, weights, stiffness) {
18
19
  for (let i = 0; i < inverseMass.length; i++) {
19
20
  if (inverseMass[i] === 0)
20
21
  continue;
22
+ const pull = weights === null ? stiffness : weights[i] * stiffness;
23
+ if (!(pull > 0))
24
+ continue;
25
+ const keep = 1 / (1 + pull);
21
26
  const at = i * 3;
22
27
  for (let k = at; k < at + 3; k++) {
23
28
  const t = target[k];
@@ -24,6 +24,7 @@ export declare class SkinnedCloth {
24
24
  private readonly control;
25
25
  private readonly targets;
26
26
  private readonly limits;
27
+ private readonly pullWeights;
27
28
  private readonly colliders;
28
29
  /** The air the cloth drags toward, metres a second: the set-up's until `setWind` says otherwise. */
29
30
  private readonly wind;
@@ -51,6 +51,7 @@ export class SkinnedCloth {
51
51
  control;
52
52
  targets;
53
53
  limits;
54
+ pullWeights;
54
55
  colliders;
55
56
  /** The air the cloth drags toward, metres a second: the set-up's until `setWind` says otherwise. */
56
57
  wind;
@@ -70,6 +71,7 @@ export class SkinnedCloth {
70
71
  : solvable('bending', bending.quads, 4, bending.rest, bending.compliance, this.inverseMass, bending.batches);
71
72
  this.tethers = setup.tethers ?? null;
72
73
  this.limits = setup.limits ?? {};
74
+ this.pullWeights = setup.pullWeights ?? null;
73
75
  this.colliders = setup.colliders?.length ?? 0;
74
76
  /* Made once, so a step builds nothing: the control calls back into the private step. */
75
77
  const device = {
@@ -150,7 +152,7 @@ export class SkinnedCloth {
150
152
  const keep = exactExp(damping * h);
151
153
  const dragKeep = exactExp(drag * h);
152
154
  /* A compliant constraint's step toward the skinned place: see `SkinnedClothParameters.pull`. */
153
- const pullKeep = 1 / (1 + this.parameters.pull * h * h);
155
+ const pullStep = this.parameters.pull * h * h;
154
156
  for (let s = 0; s < substeps; s++) {
155
157
  predictParticles(this.positions, this.substepStart, this.velocities, this.inverseMass, h, keep, dragKeep, gravity, wind);
156
158
  this.distance.lambda.fill(0);
@@ -164,7 +166,7 @@ export class SkinnedCloth {
164
166
  solveTethers(this.positions, tethers.particles, tethers.anchors, tethers.lengths);
165
167
  }
166
168
  }
167
- this.limit(pullKeep);
169
+ this.limit(pullStep);
168
170
  finishParticles(this.positions, this.substepStart, this.velocities, this.inverseMass, invH);
169
171
  }
170
172
  if (blend > 0)
@@ -174,11 +176,12 @@ export class SkinnedCloth {
174
176
  * The pull, the limits and the colliders, after the constraints, so a garment pulling into the body
175
177
  * loses. The pull first, so the hard limits have the last word.
176
178
  */
177
- limit(pullKeep) {
179
+ limit(pullStep) {
178
180
  const { positions, inverseMass, targets, limits } = this;
179
181
  const { maxDistanceScale, margin } = this.parameters;
180
- if (pullKeep < 1)
181
- applyPull(positions, inverseMass, targets.position, pullKeep);
182
+ if (pullStep > 0) {
183
+ applyPull(positions, inverseMass, targets.position, this.pullWeights, pullStep);
184
+ }
182
185
  if (limits.maxDistance !== undefined) {
183
186
  applyMaxDistance(positions, inverseMass, targets.position, limits.maxDistance, maxDistanceScale);
184
187
  }
@@ -114,10 +114,10 @@ export interface SkinnedClothParameters {
114
114
  * A share scaled by the step as `damping` is would hold a sag proportional to the step: hair
115
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
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.
117
+ * exactly `k` at that step. `SkinnedClothSetup.pullWeights` weighs it particle by particle, so a
118
+ * strap can be held at its root and hang free at its tip. **What it gives up**: it is a pull
119
+ * rather than a limit, so a hard blow still takes a particle out of shape for a moment; and with
120
+ * no `damping` a stiff one rings, at a rate a coarse step damps more than a fine one does.
121
121
  */
122
122
  readonly pull?: number;
123
123
  /**
@@ -166,6 +166,14 @@ export interface SkinnedClothSetup {
166
166
  readonly inverseBind?: Float32Array;
167
167
  readonly limits?: ClothLimits;
168
168
  readonly colliders?: readonly ClothCollider[];
169
+ /**
170
+ * One a particle, 0 to 1: how much of the garment's `pull` draws it toward its skinned place,
171
+ * each substep keeping `1 / (1 + w · pull · h²)` of its offset. Absent, every particle takes the
172
+ * whole pull. **For a garment held unevenly**: a strap held at its root and free at its tip, a
173
+ * styled fringe held nearly whole while the body of a coat swings free. A weight of 0 is no pull
174
+ * at all, which is a free particle, not a kinematic one: `inverseMass` is what pins.
175
+ */
176
+ readonly pullWeights?: Float32Array;
169
177
  }
170
178
  /** The parameters with every default filled in. */
171
179
  export interface ResolvedClothParameters {
@@ -96,6 +96,13 @@ export function validateClothSetup(setup) {
96
96
  per('limits.frontstop', setup.limits?.frontstop, 2);
97
97
  per('limits.thickness', setup.limits?.thickness, 1);
98
98
  per('limits.selfCollision', setup.limits?.selfCollision, 1);
99
+ per('pullWeights', setup.pullWeights, 1);
100
+ const weights = setup.pullWeights;
101
+ for (let i = 0; weights !== undefined && i < weights.length; i++) {
102
+ const w = weights[i];
103
+ if (!(w >= 0 && w <= 1))
104
+ fail(`pullWeights[${i}] is ${w}: a share of the pull, 0 to 1`);
105
+ }
99
106
  if ((setup.joints === undefined) !== (setup.weights === undefined))
100
107
  fail('joints without weights, or weights without joints');
101
108
  if ((setup.joints2 === undefined) !== (setup.weights2 === undefined))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/physics",
3
- "version": "4.13.3",
3
+ "version": "4.13.5",
4
4
  "description": "Collision shapes, spatial queries and swept kinematic collision",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -11,18 +11,23 @@
11
11
  */
12
12
 
13
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.
14
+ * Every free particle drawn toward its skinned position under the garment's `pull`: `stiffness` is
15
+ * `pull · h²`, each particle's weighted by `weights` (all 1 where null), and a particle keeps
16
+ * `1 / (1 + w · stiffness)` of its offset. A kinematic particle is already there, and one weighted
17
+ * 0 is left as it is: `t + (p − t)` is not `p` in floats.
17
18
  */
18
19
  export function applyPull(
19
20
  position: Float32Array,
20
21
  inverseMass: Float32Array,
21
22
  target: Float32Array,
22
- keep: number,
23
+ weights: Float32Array | null,
24
+ stiffness: number,
23
25
  ): void {
24
26
  for (let i = 0; i < inverseMass.length; i++) {
25
27
  if ((inverseMass[i] as number) === 0) continue;
28
+ const pull = weights === null ? stiffness : (weights[i] as number) * stiffness;
29
+ if (!(pull > 0)) continue;
30
+ const keep = 1 / (1 + pull);
26
31
  const at = i * 3;
27
32
  for (let k = at; k < at + 3; k++) {
28
33
  const t = target[k] as number;
@@ -86,6 +86,7 @@ export class SkinnedCloth {
86
86
  private readonly control: ClothControl;
87
87
  private readonly targets: ClothTargets;
88
88
  private readonly limits: ClothLimits;
89
+ private readonly pullWeights: Float32Array | null;
89
90
  private readonly colliders: number;
90
91
  /** The air the cloth drags toward, metres a second: the set-up's until `setWind` says otherwise. */
91
92
  private readonly wind: [number, number, number];
@@ -122,6 +123,7 @@ export class SkinnedCloth {
122
123
  );
123
124
  this.tethers = setup.tethers ?? null;
124
125
  this.limits = setup.limits ?? {};
126
+ this.pullWeights = setup.pullWeights ?? null;
125
127
  this.colliders = setup.colliders?.length ?? 0;
126
128
  /* Made once, so a step builds nothing: the control calls back into the private step. */
127
129
  const device: ClothDevice = {
@@ -219,7 +221,7 @@ export class SkinnedCloth {
219
221
  const keep = exactExp(damping * h);
220
222
  const dragKeep = exactExp(drag * h);
221
223
  /* A compliant constraint's step toward the skinned place: see `SkinnedClothParameters.pull`. */
222
- const pullKeep = 1 / (1 + this.parameters.pull * h * h);
224
+ const pullStep = this.parameters.pull * h * h;
223
225
  for (let s = 0; s < substeps; s++) {
224
226
  predictParticles(
225
227
  this.positions,
@@ -242,7 +244,7 @@ export class SkinnedCloth {
242
244
  solveTethers(this.positions, tethers.particles, tethers.anchors, tethers.lengths);
243
245
  }
244
246
  }
245
- this.limit(pullKeep);
247
+ this.limit(pullStep);
246
248
  finishParticles(this.positions, this.substepStart, this.velocities, this.inverseMass, invH);
247
249
  }
248
250
  if (blend > 0) this.blend(blend);
@@ -252,10 +254,12 @@ export class SkinnedCloth {
252
254
  * The pull, the limits and the colliders, after the constraints, so a garment pulling into the body
253
255
  * loses. The pull first, so the hard limits have the last word.
254
256
  */
255
- private limit(pullKeep: number): void {
257
+ private limit(pullStep: number): void {
256
258
  const { positions, inverseMass, targets, limits } = this;
257
259
  const { maxDistanceScale, margin } = this.parameters;
258
- if (pullKeep < 1) applyPull(positions, inverseMass, targets.position, pullKeep);
260
+ if (pullStep > 0) {
261
+ applyPull(positions, inverseMass, targets.position, this.pullWeights, pullStep);
262
+ }
259
263
  if (limits.maxDistance !== undefined) {
260
264
  applyMaxDistance(
261
265
  positions,
@@ -120,10 +120,10 @@ export interface SkinnedClothParameters {
120
120
  * A share scaled by the step as `damping` is would hold a sag proportional to the step: hair
121
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
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.
123
+ * exactly `k` at that step. `SkinnedClothSetup.pullWeights` weighs it particle by particle, so a
124
+ * strap can be held at its root and hang free at its tip. **What it gives up**: it is a pull
125
+ * rather than a limit, so a hard blow still takes a particle out of shape for a moment; and with
126
+ * no `damping` a stiff one rings, at a rate a coarse step damps more than a fine one does.
127
127
  */
128
128
  readonly pull?: number;
129
129
  /**
@@ -173,6 +173,14 @@ export interface SkinnedClothSetup {
173
173
  readonly inverseBind?: Float32Array;
174
174
  readonly limits?: ClothLimits;
175
175
  readonly colliders?: readonly ClothCollider[];
176
+ /**
177
+ * One a particle, 0 to 1: how much of the garment's `pull` draws it toward its skinned place,
178
+ * each substep keeping `1 / (1 + w · pull · h²)` of its offset. Absent, every particle takes the
179
+ * whole pull. **For a garment held unevenly**: a strap held at its root and free at its tip, a
180
+ * styled fringe held nearly whole while the body of a coat swings free. A weight of 0 is no pull
181
+ * at all, which is a free particle, not a kinematic one: `inverseMass` is what pins.
182
+ */
183
+ readonly pullWeights?: Float32Array;
176
184
  }
177
185
 
178
186
  /** The parameters with every default filled in. */
@@ -284,6 +292,12 @@ export function validateClothSetup(setup: SkinnedClothSetup): void {
284
292
  per('limits.frontstop', setup.limits?.frontstop, 2);
285
293
  per('limits.thickness', setup.limits?.thickness, 1);
286
294
  per('limits.selfCollision', setup.limits?.selfCollision, 1);
295
+ per('pullWeights', setup.pullWeights, 1);
296
+ const weights = setup.pullWeights;
297
+ for (let i = 0; weights !== undefined && i < weights.length; i++) {
298
+ const w = weights[i] as number;
299
+ if (!(w >= 0 && w <= 1)) fail(`pullWeights[${i}] is ${w}: a share of the pull, 0 to 1`);
300
+ }
287
301
  if ((setup.joints === undefined) !== (setup.weights === undefined))
288
302
  fail('joints without weights, or weights without joints');
289
303
  if ((setup.joints2 === undefined) !== (setup.weights2 === undefined))