reze-engine 0.33.2 → 0.35.0

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,6 +10,30 @@ import { findContacts, type ContactPool } from "./contact"
10
10
  // integrate; the parent class syncs them from bones around the step. The
11
11
  // solver pass runs on all bodies — kinematic ones have invMass = 0 and
12
12
  // act as anchors.
13
+ /**
14
+ * Air movement across the whole world.
15
+ *
16
+ * Applied as an acceleration alongside gravity, which is the same shape MMD's
17
+ * own wind plugins take and keeps it free: gravity and wind are summed once per
18
+ * substep, so the per-body predict loop is untouched. Terminal velocity comes
19
+ * out of the existing per-body damping rather than a drag model — PMX authors
20
+ * already tune that damping to get the hang they want, and a second, hidden
21
+ * drag term would fight it.
22
+ */
23
+ export interface WindOptions {
24
+ /** Direction the air travels. Normalised on assignment; a zero vector is off. */
25
+ direction: Vec3
26
+ /** Acceleration along it, in gravity's units — the built-in gravity is 98. */
27
+ strength: number
28
+ /** Gust depth, clamped to 0–1. 0 is a steady breeze; 1 swings between still
29
+ * and double. It is a clamp rather than a free scalar because past 1 the
30
+ * swing goes negative and the wind blows backwards on every other beat,
31
+ * which is never what a caller meant by "more turbulent". */
32
+ turbulence?: number
33
+ /** Gusts per second. */
34
+ frequency?: number
35
+ }
36
+
13
37
  export class World {
14
38
  readonly gravity: Vec3
15
39
  solverIterations = 10
@@ -20,6 +44,17 @@ export class World {
20
44
  private linDampFactor: Float32Array | null = null
21
45
  private angDampFactor: Float32Array | null = null
22
46
 
47
+ // Wind, resolved to a direction × strength at construction of the options so
48
+ // the step loop only multiplies by a gust scalar.
49
+ private windX = 0
50
+ private windY = 0
51
+ private windZ = 0
52
+ private windTurbulence = 0
53
+ private windFrequency = 0.35
54
+ /** Advances with simulated time, not wall time, so a scrubbed or exported
55
+ * take gusts identically to a live one. */
56
+ private windClock = 0
57
+
23
58
  constructor(gravity: Vec3) {
24
59
  this.gravity = new Vec3(gravity.x, gravity.y, gravity.z)
25
60
  }
@@ -30,6 +65,37 @@ export class World {
30
65
  this.gravity.z = g.z
31
66
  }
32
67
 
68
+ setWind(wind: WindOptions | null): void {
69
+ // Shape first, and unconditionally: bailing out early on a zero strength
70
+ // used to leave turbulence and frequency holding whatever a previous call
71
+ // set, so raising the strength again resurrected settings the caller had
72
+ // since replaced.
73
+ this.windTurbulence = Math.min(1, Math.max(0, wind?.turbulence ?? 0))
74
+ this.windFrequency = Math.max(0, wind?.frequency ?? 0.35)
75
+
76
+ const d = wind?.direction
77
+ const len = d ? Math.hypot(d.x, d.y, d.z) : 0
78
+ if (!wind || !d || len < 1e-9 || wind.strength === 0) {
79
+ this.windX = this.windY = this.windZ = 0
80
+ return
81
+ }
82
+ const s = wind.strength / len
83
+ this.windX = d.x * s
84
+ this.windY = d.y * s
85
+ this.windZ = d.z * s
86
+ }
87
+
88
+ getWind(): WindOptions | null {
89
+ const strength = Math.hypot(this.windX, this.windY, this.windZ)
90
+ if (strength === 0) return null
91
+ return {
92
+ direction: new Vec3(this.windX / strength, this.windY / strength, this.windZ / strength),
93
+ strength,
94
+ turbulence: this.windTurbulence,
95
+ frequency: this.windFrequency,
96
+ }
97
+ }
98
+
33
99
  step(
34
100
  store: RigidBodyStore,
35
101
  constraints: SixDofSpringConstraint[],
@@ -49,9 +115,24 @@ export class World {
49
115
  const adamp = store.angularDamping
50
116
  const invMass = store.invMass
51
117
 
52
- const gx = this.gravity.x
53
- const gy = this.gravity.y
54
- const gz = this.gravity.z
118
+ // Gravity and wind are the same kind of term, so they are summed here and
119
+ // the predict loop below never learns wind exists.
120
+ let gx = this.gravity.x
121
+ let gy = this.gravity.y
122
+ let gz = this.gravity.z
123
+ if (this.windX !== 0 || this.windY !== 0 || this.windZ !== 0) {
124
+ this.windClock += dt
125
+ let gust = 1
126
+ if (this.windTurbulence > 0 && this.windFrequency > 0) {
127
+ // Two incommensurate sines: a single one is a metronome, and real gusts
128
+ // do not repeat on a bar line. Stays within 1 ± turbulence.
129
+ const t = this.windClock * this.windFrequency * Math.PI * 2
130
+ gust = 1 + this.windTurbulence * 0.5 * (Math.sin(t) + Math.sin(t * 0.37 + 1.3))
131
+ }
132
+ gx += this.windX * gust
133
+ gy += this.windY * gust
134
+ gz += this.windZ * gust
135
+ }
55
136
 
56
137
  // 1. Predict — gravity + damping. The pow form (vs the linear
57
138
  // 1−damping·dt approximation) stays stable at high PMX damping