1 /**
2     Inochi2D Simple Physics Node
3 
4     Copyright: 
5         Copyright © 2020-2026, Inochi2D Project
6     
7     License:
8         $(LINK2 https://github.com/Inochi2D/inochi2d/blob/main/LICENSE, BSD 2-clause License)
9     
10     Authors:
11         Hoshino Lina
12 */
13 module inochi2d.nodes.legacy.simplephysics;
14 import inochi2d.core.serde;
15 import inochi2d.core.guid;
16 import inochi2d.core.math;
17 import inochi2d.core.phys;
18 import inochi2d.common;
19 import inochi2d;
20 import numem;
21 
22 // dfmt off
23 // Allow disabling legacy node.
24 version (IN_NO_LEGACY) {} else:
25 // dfmt on
26 
27 /**
28     Physics model to use for simple physics
29 */
30 enum PhysicsModel {
31     /**
32         Rigid pendulum
33     */
34     Pendulum = "pendulum",
35 
36     /**
37         Springy pendulum
38     */
39     SpringPendulum = "spring_pendulum",
40 }
41 
42 enum ParamMapMode {
43     AngleLength = "angle_length",
44     XY = "xy",
45     LengthAngle = "length_angle",
46     YX = "yx",
47 }
48 
49 class Pendulum : PhysicsSystem {
50 private:
51 @nogc:
52     SimplePhysics driver;
53     vec2 bob = vec2(0, 0);
54     float angle = 0;
55     float dAngle = 0;
56 
57 protected:
58     override
59     void eval(float t) {
60         setD(angle, dAngle);
61         float lengthRatio = driver.finalGravity / driver.finalLength;
62         float critDamp = 2 * sqrt(lengthRatio);
63         float dd = -lengthRatio * sin(angle);
64         dd -= dAngle * driver.finalAngleDamping * critDamp;
65         setD(dAngle, dd);
66     }
67 
68 public:
69 
70     this(SimplePhysics driver) {
71         this.driver = driver;
72 
73         bob = driver.anchor + vec2(0, driver.finalLength);
74 
75         addVariable(&angle);
76         addVariable(&dAngle);
77     }
78 
79     override
80     void tick(float h) {
81         // Compute the angle against the updated anchor position
82         vec2 dBob = bob - driver.anchor;
83         angle = atan2(-dBob.x, dBob.y);
84 
85         // Run the pendulum simulation in terms of angle
86         super.tick(h);
87 
88         // Update the bob position at the new angle
89         dBob = vec2(-sin(angle), cos(angle));
90         bob = driver.anchor + dBob * driver.finalLength;
91 
92         driver.output = bob;
93     }
94 
95     override
96     void updateAnchor() {
97         bob = driver.anchor + vec2(0, driver.finalLength);
98     }
99 }
100 
101 class SpringPendulum : PhysicsSystem {
102 private:
103 @nogc:
104     SimplePhysics driver;
105 
106     vec2 bob = vec2(0, 0);
107     vec2 dBob = vec2(0, 0);
108 
109 protected:
110 
111     override
112     void eval(float t) {
113         setD(bob, dBob);
114 
115         // These are normalized vs. mass
116         float springKsqrt = driver.finalFrequency * 2 * PI;
117         float springK = springKsqrt ^^ 2;
118 
119         float g = driver.finalGravity;
120         float restLength = driver.finalLength - g / springK;
121 
122         vec2 offPos = bob - driver.anchor;
123         vec2 offPosNorm = offPos.normalized;
124 
125         float lengthRatio = driver.finalGravity / driver.finalLength;
126         float critDampAngle = 2 * sqrt(lengthRatio);
127         float critDampLength = 2 * springKsqrt;
128 
129         float dist = abs(driver.anchor.distance(bob));
130         vec2 force = vec2(0, g);
131         force -= offPosNorm * (dist - restLength) * springK;
132         vec2 ddBob = force;
133 
134         vec2 dBobRot = vec2(
135                 dBob.x * offPosNorm.y + dBob.y * offPosNorm.x,
136                 dBob.y * offPosNorm.y - dBob.x * offPosNorm.x,
137         );
138 
139         vec2 ddBobRot = -vec2(
140                 dBobRot.x * driver.finalAngleDamping * critDampAngle,
141                 dBobRot.y * driver.finalLengthDamping * critDampLength,
142         );
143 
144         vec2 ddBobDamping = vec2(
145                 ddBobRot.x * offPosNorm.y - dBobRot.y * offPosNorm.x,
146                 ddBobRot.y * offPosNorm.y + dBobRot.x * offPosNorm.x,
147         );
148 
149         ddBob += ddBobDamping;
150 
151         setD(dBob, ddBob);
152     }
153 
154 public:
155 
156     this(SimplePhysics driver) {
157         this.driver = driver;
158 
159         bob = driver.anchor + vec2(0, driver.finalLength);
160 
161         addVariable(&bob);
162         addVariable(&dBob);
163     }
164 
165     override
166     void tick(float h) {
167         // Run the spring pendulum simulation
168         super.tick(h);
169 
170         driver.output = bob;
171     }
172 
173     override
174     void updateAnchor() {
175         bob = driver.anchor + vec2(0, driver.finalLength);
176     }
177 }
178 
179 /**
180     Simple Physics Node
181 */
182 @TypeId("SimplePhysics", IN_MAKE_TAG!(0, 0xFF))
183 class SimplePhysics : Node {
184 private:
185 @nogc:
186     GUID paramRef = GUID.nil;
187     PhysicsModel modelType_ = PhysicsModel.Pendulum;
188     Parameter param_;
189     vec2 output;
190 
191 protected:
192     PhysicsSystem system;
193 
194     /**
195         Serializes this node to a DataNode.
196 
197         Params:
198             object =    The DataNode to serialize to.
199     */
200     override
201     void onSerialize(ref DataNode object) {
202         super.onSerialize(object);
203 
204         auto target = paramRef.toString();
205         object["target"] = target[];
206         object["model_type"] = cast(string)modelType_;
207         object["map_mode"] = cast(string)mapMode;
208         object["gravity"] = gravity;
209         object["length"] = length;
210         object["frequency"] = frequency;
211         object["angle_damping"] = angleDamping;
212         object["length_damping"] = lengthDamping;
213         object["output_scale"] = outputScale.serialize();
214         object["local_only"] = localOnly;
215     }
216 
217     /**
218         Deserializes this node from a DataNode.
219 
220         Params:
221             object =    The DataNode to deserialize from.
222             state =     The state of the deserializer.
223     */
224     override
225     void onDeserialize(ref DataNode object, ref ModelState state) {
226         super.onDeserialize(object, state);
227 
228         this.paramRef = object.tryGetGUID(state, "param", "target");
229         object.tryGetRef(state, modelType_, "model_type", PhysicsModel.Pendulum);
230         object.tryGetRef(state, mapMode, "map_mode", ParamMapMode.AngleLength);
231         object.tryGetRef(state, gravity, "gravity", 1.0);
232         object.tryGetRef(state, length, "length", 100);
233         object.tryGetRef(state, frequency, "frequency", 1.0);
234         object.tryGetRef(state, angleDamping, "angle_damping", 0.5);
235         object.tryGetRef(state, lengthDamping, "length_damping", 0.5);
236         object.tryGetRef(state, outputScale, "output_scale", vec2(1, 1));
237         object.tryGetRef(state, localOnly, "local_only", false);
238     }
239 
240     /**
241         Called when the node is to finalize its deserialization from disk.
242 
243         Params:
244             state =     The state of the deserializer.
245     */
246     override
247     void onFinalize(ref ModelState state) {
248         this.param_ = puppet.findParameter(paramRef);
249         this.reset();
250         super.onFinalize(state);
251     }
252 
253     /**
254         Called when the node is to define its properties.
255 
256         Call $(D propList.define) with a quark to do this.
257 
258         Params:
259             propList = The property list to populate.
260     */
261     override void onDefineProperties(ref PropertyStore propList) {
262         super.onDefineProperties(propList);
263 
264         propList.define!float(PROP_LENGTH, 0);
265         propList.define!float(PROP_GRAVITY, 1);
266         propList.define!float(PROP_FREQUENCY, 1);
267         propList.define!float(PROP_ANGLE_DAMPING, 1);
268         propList.define!float(PROP_LENGTH_DAMPING, 1);
269         propList.define!float(PROP_LENGTH_DAMPING, 1);
270         propList.define!float(PROP_OUTPUT_SCALE_X, 1);
271         propList.define!float(PROP_OUTPUT_SCALE_Y, 1);
272 
273         // Define combined overlays.
274         propList.defineOverlay!vec2(PROP_OUTPUT_SCALE_XY, propList.offsetOf(PROP_OUTPUT_SCALE_X));
275     }
276 
277     /**
278         Called during the early update phase of a new frame.
279         
280         Params:
281             drawList =  The drawlist for the active scene.
282     */
283     override
284     void onPreUpdate(DrawList drawList) {
285         super.onPreUpdate(drawList);
286     }
287 
288 public:
289 
290     /**
291         The mapping between physics space and parameter space.
292     */
293     ParamMapMode mapMode = ParamMapMode.AngleLength;
294 
295     /**
296         Whether physics system listens to local transform only.
297     */
298     bool localOnly = false;
299 
300     /**
301         Gravity scale (1.0 = puppet gravity)
302     */
303     float gravity = 1.0;
304 
305     /**
306         Pendulum/spring rest length (pixels)
307     */
308     float length = 100;
309 
310     /**
311         Resonant frequency (Hz)
312     */
313     float frequency = 1;
314 
315     /**
316         Angular damping ratio
317     */
318     float angleDamping = 0.5;
319 
320     /**
321         Length damping ratio
322     */
323     float lengthDamping = 0.5;
324 
325     /**
326         Output scale
327     */
328     vec2 outputScale = vec2(1, 1);
329 
330     /**
331         Previous anchor
332     */
333     vec2 prevAnchor = vec2(0, 0);
334 
335     /**
336         Current anchor
337     */
338     vec2 anchor = vec2(0, 0);
339 
340     /**
341         The parameter that the physics system affects.
342     */
343     @property Parameter param() => param_;
344     @property void param(Parameter p) {
345         this.param_ = p;
346         this.paramRef = param_ ? param_.guid : GUID.nil;
347     }
348 
349     /**
350         The physics model to apply.
351     */
352     @property PhysicsModel modelType() => modelType_;
353     @property void modelType(PhysicsModel t) {
354         modelType_ = t;
355         reset();
356     }
357 
358     /**
359         The affected parameters of the driver.
360     */
361     @property Parameter[] affectedParameters() @nogc => (&param_)[0 .. 1];
362 
363     /**
364         Physics scale.
365     */
366     @property float scale() @nogc => puppet.properties.physicsPixelsPerMeter;
367 
368     /**
369         The final gravity
370     */
371     @property float finalGravity() @nogc => (gravity * props.get!float(PROP_GRAVITY)) * puppet.properties.physicsGravity * this
372         .scale;
373 
374     /**
375         The final length
376     */
377     @property float finalLength() @nogc => length + props.get!float(PROP_LENGTH);
378 
379     /**
380         The final frequency
381     */
382     @property float finalFrequency() @nogc => frequency * props.get!float(PROP_FREQUENCY);
383 
384     /**
385         The final angle damping
386     */
387     @property float finalAngleDamping() @nogc => angleDamping * props.get!float(PROP_ANGLE_DAMPING);
388 
389     /**
390         The final length damping
391     */
392     @property float finalLengthDamping() @nogc => lengthDamping * props.get!float(PROP_LENGTH_DAMPING);
393 
394     /**
395         The final output scale
396     */
397     @property vec2 finalOutputScale() @nogc => outputScale * props.get!vec2(PROP_OUTPUT_SCALE_XY);
398 
399     ~this() {
400         nogc_delete(system);
401     }
402 
403     /**
404         Constructs a new SimplePhysics node
405     */
406     this(Node parent = null) {
407         this(inNewGUID(), parent);
408         this.reset();
409     }
410 
411     /**
412         Constructs a new SimplePhysics node
413     */
414     this(GUID guid, Node parent = null) {
415         super(guid, parent);
416         this.reset();
417     }
418 
419     /**
420         Gets whether the given parameter is affected by
421         this driver.
422 
423         Params:
424             param = The parameter to query.
425         
426         Returns:
427             $(D true) if the parameter is affected by 
428             the driver, $(D false) otherwise.
429     */
430     final
431     bool affectsParameter(ref Parameter param) {
432         foreach (ref Parameter p; this.affectedParameters) {
433             if (p.guid == param.guid)
434                 return true;
435         }
436         return false;
437     }
438 
439     void updateDriver(float delta) {
440 
441         // Timestep is limited to 10 seconds, as if you
442         // Are getting 0.1 FPS, you have bigger issues to deal with.
443         float h = min(delta, 10);
444 
445         updateInputs();
446 
447         // Minimum physics timestep: 0.01s
448         while (h > 0.01) {
449             system.tick(0.01);
450             h -= 0.01;
451         }
452 
453         system.tick(h);
454         updateOutputs();
455     }
456 
457     void updateAnchors() {
458         system.updateAnchor();
459     }
460 
461     void updateInputs() {
462         auto anchorPos = localOnly ?
463             (vec4(localTransform.translation, 1)) : (this.matrix * vec4(0, 0, 0, 1));
464         anchor = vec2(anchorPos.x, anchorPos.y);
465     }
466 
467     void updateOutputs() {
468         if (param is null)
469             return;
470 
471         vec2 oscale = this.finalOutputScale;
472 
473         // Okay, so this is confusing. We want to translate the angle back to local space,
474         // but not the coordinates.
475 
476         // Transform the physics output back into local space.
477         // The origin here is the anchor. This gives us the local angle.
478         auto localPos4 = localOnly ?
479             vec4(output.x, output.y, 0, 1) : (this.matrix.inverse * vec4(output.x, output.y, 0, 1));
480         vec2 localAngle = vec2(localPos4.x, localPos4.y).normalized;
481 
482         // Figure out the relative length. We can work this out directly in global space.
483         auto relLength = output.distance(anchor) / this.finalLength;
484 
485         vec2 paramVal = vec2.zero;
486         switch (mapMode) {
487         case ParamMapMode.XY:
488             auto localPosNorm = localAngle * relLength;
489             paramVal = localPosNorm - vec2(0, 1);
490             paramVal.y = -paramVal.y; // Y goes up for params
491             break;
492         case ParamMapMode.AngleLength:
493             float a = atan2(-localAngle.x, localAngle.y) / PI;
494             paramVal = vec2(a, relLength);
495             break;
496         case ParamMapMode.YX:
497             auto localPosNorm = localAngle * relLength;
498             paramVal = localPosNorm - vec2(0, 1);
499             paramVal.y = -paramVal.y; // Y goes up for params
500             paramVal = vec2(paramVal.y, paramVal.x);
501             break;
502         case ParamMapMode.LengthAngle:
503             float a = atan2(-localAngle.x, localAngle.y) / PI;
504             paramVal = vec2(relLength, a);
505             break;
506         default:
507             break;
508         }
509 
510         if (auto param1d = cast(Parameter1D)param) {
511             auto value = paramVal.x * oscale.x;
512             param1d.pushValue(value);
513             param1d.update();
514         } else if (auto param2d = cast(Parameter2D)param) {
515             auto value = vec2(paramVal.x * oscale.x, paramVal.y * oscale.y);
516             param2d.pushValue(value);
517             param2d.update();
518         }
519     }
520 
521     void reset() {
522         updateInputs();
523 
524         switch (modelType) {
525         case PhysicsModel.Pendulum:
526             system = nogc_new!Pendulum(this);
527             break;
528         case PhysicsModel.SpringPendulum:
529             system = nogc_new!SpringPendulum(this);
530             break;
531         default:
532             break;
533         }
534     }
535 }
536 
537 mixin Register!(SimplePhysics, in_node_registry);
538 // dfmt off
539 
540 
541 
542 
543 //
544 //          QUARKS
545 //
546 
547 mixin RegisterQuarks!();
548 
549 /**
550     Gravity
551 */
552 @propkey("gravity")
553 __gshared immutable(quark) PROP_GRAVITY;
554 
555 /**
556     Length
557 */
558 @propkey("length")
559 __gshared immutable(quark) PROP_LENGTH;
560 
561 /**
562     Frequency
563 */
564 @propkey("frequency")
565 __gshared immutable(quark) PROP_FREQUENCY;
566 
567 /**
568     Angle damping
569 */
570 @propkey("angleDamping")
571 __gshared immutable(quark) PROP_ANGLE_DAMPING;
572 
573 /**
574     Length damping
575 */
576 @propkey("lengthDamping")
577 __gshared immutable(quark) PROP_LENGTH_DAMPING;
578 
579 /**
580     Output scale xy
581 */
582 @propkey("outputScale.xy")
583 __gshared immutable(quark) PROP_OUTPUT_SCALE_XY;
584 
585 /**
586     Output scale x
587 */
588 @propkey("outputScale.x")
589 __gshared immutable(quark) PROP_OUTPUT_SCALE_X;
590 
591 /**
592     Output scale y
593 */
594 @propkey("outputScale.y")
595 __gshared immutable(quark) PROP_OUTPUT_SCALE_Y;