1 /*
2     Inochi2D Simple Physics Node
3 
4     Copyright © 2022, Inochi2D Project
5     Distributed under the 2-Clause BSD License, see LICENSE file.
6 
7     Authors: Asahi Lina
8 */
9 module inochi2d.core.nodes.drivers.simplephysics;
10 private {
11 import inochi2d.core.nodes.drivers;
12 import inochi2d.core.nodes.common;
13 //import inochi2d.core.nodes;
14 import inochi2d.fmt;
15 import inochi2d.core.dbg;
16 //import inochi2d.core;
17 import inochi2d.math;
18 import inochi2d.phys;
19 import inochi2d;
20 import std.exception;
21 import std.algorithm.sorting;
22 import std.stdio;
23 }
24 
25 /**
26     Physics model to use for simple physics
27 */
28 enum PhysicsModel {
29     /**
30         Rigid pendulum
31     */
32     Pendulum = "pendulum",
33 
34     /**
35         Springy pendulum
36     */
37     SpringPendulum = "spring_pendulum",
38 }
39 
40 enum ParamMapMode {
41     AngleLength = "angle_length",
42     XY = "xy",
43 }
44 
45 class Pendulum : PhysicsSystem {
46     SimplePhysics driver;
47 
48 private:
49     vec2 bob = vec2(0, 0);
50     float angle = 0;
51     float dAngle = 0;
52 
53 protected:
54     override
55     void eval(float t) {
56         setD(angle, dAngle);
57         float lengthRatio = driver.getGravity() / driver.getLength();
58         float critDamp = 2 * sqrt(lengthRatio);
59         float dd = -lengthRatio * sin(angle);
60         dd -= dAngle * driver.getAngleDamping() * critDamp;
61         setD(dAngle, dd);
62     }
63 
64 public:
65 
66     this(SimplePhysics driver) {
67         this.driver = driver;
68 
69         bob = driver.anchor + vec2(0, driver.getLength());
70 
71         addVariable(&angle);
72         addVariable(&dAngle);
73     }
74 
75     override
76     void tick(float h) {
77         // Compute the angle against the updated anchor position
78         vec2 dBob = bob - driver.anchor;
79         angle = atan2(-dBob.x, dBob.y);
80 
81         // Run the pendulum simulation in terms of angle
82         super.tick(h);
83 
84         // Update the bob position at the new angle
85         dBob = vec2(-sin(angle), cos(angle));
86         bob = driver.anchor + dBob * driver.getLength();
87 
88         driver.output = bob;
89     }
90 
91     override
92     void drawDebug(mat4 trans = mat4.identity) {
93         vec3[] points = [
94             vec3(driver.anchor.x, driver.anchor.y, 0),
95             vec3(bob.x, bob.y, 0),
96         ];
97 
98         inDbgSetBuffer(points);
99         inDbgLineWidth(3);
100         inDbgDrawLines(vec4(1, 0, 1, 1), trans);
101     }
102 
103     override
104     void updateAnchor() {
105         bob = driver.anchor + vec2(0, driver.getLength());
106     }
107 }
108 
109 class SpringPendulum : PhysicsSystem {
110     SimplePhysics driver;
111 
112 private:
113     vec2 bob = vec2(0, 0);
114     vec2 dBob = vec2(0, 0);
115 
116 protected:
117     override
118     void eval(float t) {
119         setD(bob, dBob);
120 
121         // These are normalized vs. mass
122         float springKsqrt = driver.getFrequency() * 2 * PI;
123         float springK = springKsqrt ^^ 2;
124 
125         float g = driver.getGravity();
126         float restLength = driver.getLength() - g / springK;
127 
128         vec2 offPos = bob - driver.anchor;
129         vec2 offPosNorm = offPos.normalized;
130 
131         float lengthRatio = driver.getGravity() / driver.getLength();
132         float critDampAngle = 2 * sqrt(lengthRatio);
133         float critDampLength = 2 * springKsqrt;
134 
135         float dist = abs(driver.anchor.distance(bob));
136         vec2 force = vec2(0, g);
137         force -= offPosNorm * (dist - restLength) * springK;
138         vec2 ddBob = force;
139 
140         vec2 dBobRot = vec2(
141             dBob.x * offPosNorm.y + dBob.y * offPosNorm.x,
142             dBob.y * offPosNorm.y - dBob.x * offPosNorm.x,
143         );
144 
145         vec2 ddBobRot = -vec2(
146             dBobRot.x * driver.getAngleDamping() * critDampAngle,
147             dBobRot.y * driver.getLengthDamping() * critDampLength,
148         );
149 
150         vec2 ddBobDamping = vec2(
151             ddBobRot.x * offPosNorm.y - dBobRot.y * offPosNorm.x,
152             ddBobRot.y * offPosNorm.y + dBobRot.x * offPosNorm.x,
153         );
154 
155         ddBob += ddBobDamping;
156 
157         setD(dBob, ddBob);
158     }
159 
160 public:
161 
162     this(SimplePhysics driver) {
163         this.driver = driver;
164 
165         bob = driver.anchor + vec2(0, driver.getLength());
166 
167         addVariable(&bob);
168         addVariable(&dBob);
169     }
170 
171     override
172     void tick(float h) {
173         // Run the spring pendulum simulation
174         super.tick(h);
175 
176         driver.output = bob;
177     }
178 
179     override
180     void drawDebug(mat4 trans = mat4.identity) {
181         vec3[] points = [
182             vec3(driver.anchor.x, driver.anchor.y, 0),
183             vec3(bob.x, bob.y, 0),
184         ];
185 
186         inDbgSetBuffer(points);
187         inDbgLineWidth(3);
188         inDbgDrawLines(vec4(1, 0, 1, 1), trans);
189     }
190 
191     override
192     void updateAnchor() {
193         bob = driver.anchor + vec2(0, driver.getLength());
194     }
195 }
196 
197 /**
198     Simple Physics Node
199 */
200 @TypeId("SimplePhysics")
201 class SimplePhysics : Driver {
202 private:
203     this() { }
204 
205     @Name("param")
206     uint paramRef = InInvalidUUID;
207 
208     @Ignore
209     Parameter param_;
210 
211     @Ignore
212     float offsetGravity = 1.0;
213 
214     @Ignore
215     float offsetLength = 0;
216 
217     @Ignore
218     float offsetFrequency = 1;
219 
220     @Ignore
221     float offsetAngleDamping = 0.5;
222 
223     @Ignore
224     float offsetLengthDamping = 0.5;
225 
226     @Ignore
227     vec2 offsetOutputScale = vec2(1, 1);
228 
229 protected:
230     override
231     string typeId() { return "SimplePhysics"; }
232 
233     /**
234         Allows serializing self data (with pretty serializer)
235     */
236     override
237     void serializeSelfImpl(ref InochiSerializer serializer, bool recursive=true) {
238         super.serializeSelfImpl(serializer, recursive);
239         serializer.putKey("param");
240         serializer.serializeValue(paramRef);
241         serializer.putKey("model_type");
242         serializer.serializeValue(modelType_);
243         serializer.putKey("map_mode");
244         serializer.serializeValue(mapMode);
245         serializer.putKey("gravity");
246         serializer.serializeValue(gravity);
247         serializer.putKey("length");
248         serializer.serializeValue(length);
249         serializer.putKey("frequency");
250         serializer.serializeValue(frequency);
251         serializer.putKey("angle_damping");
252         serializer.serializeValue(angleDamping);
253         serializer.putKey("length_damping");
254         serializer.serializeValue(lengthDamping);
255         serializer.putKey("output_scale");
256         outputScale.serialize(serializer);
257         serializer.putKey("local_only");
258         serializer.serializeValue(localOnly);
259     }
260 
261     override
262     SerdeException deserializeFromFghj(Fghj data) {
263         super.deserializeFromFghj(data);
264 
265         if (!data["param"].isEmpty)
266             if (auto exc = data["param"].deserializeValue(this.paramRef)) return exc;
267         if (!data["model_type"].isEmpty)
268             if (auto exc = data["model_type"].deserializeValue(this.modelType_)) return exc;
269         if (!data["map_mode"].isEmpty)
270             if (auto exc = data["map_mode"].deserializeValue(this.mapMode)) return exc;
271         if (!data["gravity"].isEmpty)
272             if (auto exc = data["gravity"].deserializeValue(this.gravity)) return exc;
273         if (!data["length"].isEmpty)
274             if (auto exc = data["length"].deserializeValue(this.length)) return exc;
275         if (!data["frequency"].isEmpty)
276             if (auto exc = data["frequency"].deserializeValue(this.frequency)) return exc;
277         if (!data["angle_damping"].isEmpty)
278             if (auto exc = data["angle_damping"].deserializeValue(this.angleDamping)) return exc;
279         if (!data["length_damping"].isEmpty)
280             if (auto exc = data["length_damping"].deserializeValue(this.lengthDamping)) return exc;
281         if (!data["output_scale"].isEmpty)
282             if (auto exc = outputScale.deserialize(data["output_scale"])) return exc;
283         if (!data["local_only"].isEmpty)
284             if (auto exc = data["local_only"].deserializeValue(this.localOnly)) return exc;
285 
286         return null;
287     }
288 
289 public:
290     PhysicsModel modelType_ = PhysicsModel.Pendulum;
291     ParamMapMode mapMode = ParamMapMode.AngleLength;
292 
293     /**
294         Whether physics system listens to local transform only.
295     */
296     bool localOnly = false;
297 
298     /**
299         Gravity scale (1.0 = puppet gravity)
300     */
301     float gravity = 1.0;
302 
303     /**
304         Pendulum/spring rest length (pixels)
305     */
306     float length = 100;
307 
308     /**
309         Resonant frequency (Hz)
310     */
311     float frequency = 1;
312 
313     /**
314         Angular damping ratio
315     */
316     float angleDamping = 0.5;
317 
318     /**
319         Length damping ratio
320     */
321     float lengthDamping = 0.5;
322     vec2 outputScale = vec2(1, 1);
323 
324     @Ignore
325     vec2 anchor = vec2(0, 0);
326 
327     @Ignore
328     vec2 output;
329 
330     @Ignore
331     PhysicsSystem system;
332 
333     /**
334         Constructs a new SimplePhysics node
335     */
336     this(Node parent = null) {
337         this(inCreateUUID(), parent);
338     }
339 
340     /**
341         Constructs a new SimplePhysics node
342     */
343     this(uint uuid, Node parent = null) {
344         super(uuid, parent);
345         reset();
346     }
347 
348     override
349     void beginUpdate() {
350         super.beginUpdate();
351 
352         offsetGravity = 1;
353         offsetLength = 0;
354         offsetFrequency = 1;
355         offsetAngleDamping = 1;
356         offsetLengthDamping = 1;
357         offsetOutputScale = vec2(1, 1);
358     }
359 
360     override
361     void update() {
362         super.update();
363     }
364 
365     override
366     Parameter[] getAffectedParameters() {
367         if (param_ is null) return [];
368         return [param_];
369     }
370 
371     override
372     void updateDriver() {
373         
374         // Timestep is limited to 10 seconds, as if you
375         // Are getting 0.1 FPS, you have bigger issues to deal with.
376         float h = min(deltaTime(), 10);
377 
378         updateInputs();
379 
380         // Minimum physics timestep: 0.01s
381         while (h > 0.01) {
382             system.tick(0.01);
383             h -= 0.01;
384         }
385 
386         system.tick(h);
387         updateOutputs();
388     }
389 
390     void updateAnchors() {
391         system.updateAnchor();
392     }
393 
394     void updateInputs() {
395 
396         auto anchorPos = localOnly ? 
397             (vec4(transformLocal.translation, 1)) : 
398             (transform.matrix * vec4(0, 0, 0, 1));
399         anchor = vec2(anchorPos.x, anchorPos.y);
400     }
401 
402     void updateOutputs() {
403         if (param is null) return;
404 
405         vec2 oscale = getOutputScale();
406 
407         // Okay, so this is confusing. We want to translate the angle back to local space,
408         // but not the coordinates.
409 
410         // Transform the physics output back into local space.
411         // The origin here is the anchor. This gives us the local angle.
412         auto localPos4 = localOnly ? 
413             vec4(output.x, output.y, 0, 1) : 
414             (transform.matrix.inverse * vec4(output.x, output.y, 0, 1));
415         vec2 localAngle = vec2(localPos4.x, localPos4.y);
416         localAngle.normalize();
417 
418         // Figure out the relative length. We can work this out directly in global space.
419         auto relLength = output.distance(anchor) / getLength();
420 
421         vec2 paramVal;
422         switch (mapMode) {
423             case ParamMapMode.XY:
424                 auto localPosNorm = localAngle * relLength;
425                 paramVal = localPosNorm - vec2(0, 1);
426                 paramVal.y = -paramVal.y; // Y goes up for params
427                 break;
428             case ParamMapMode.AngleLength:
429                 float a = atan2(-localAngle.x, localAngle.y) / PI;
430                 paramVal = vec2(a, relLength);
431                 break;
432             default: assert(0);
433         }
434 
435         param.pushIOffset(vec2(paramVal.x * oscale.x, paramVal.y * oscale.y), ParamMergeMode.Forced);
436         param.update();
437     }
438 
439     override
440     void reset() {
441         updateInputs();
442 
443         switch (modelType) {
444             case PhysicsModel.Pendulum:
445                 system = new Pendulum(this);
446                 break;
447             case PhysicsModel.SpringPendulum:
448                 system = new SpringPendulum(this);
449                 break;
450             default:
451                 assert(0);
452         }
453     }
454 
455     override
456     void finalize() {
457         param_ = puppet.findParameter(paramRef);
458         super.finalize();
459         reset();
460     }
461 
462     override
463     void drawDebug() {
464         system.drawDebug();
465     }
466 
467     Parameter param() {
468         return param_;
469     }
470 
471     void param(Parameter p) {
472         param_ = p;
473         if (p is null) paramRef = InInvalidUUID;
474         else paramRef = p.uuid;
475     }
476 
477     float getScale() {
478         return puppet.physics.pixelsPerMeter;
479     }
480 
481     PhysicsModel modelType() {
482         return modelType_;
483     }
484 
485     void modelType(PhysicsModel t) {
486         modelType_ = t;
487         reset();
488     }
489 
490        override
491     bool hasParam(string key) {
492         if (super.hasParam(key)) return true;
493 
494         switch(key) {
495             case "gravity":
496             case "length":
497             case "frequency":
498             case "angleDamping":
499             case "lengthDamping":
500             case "outputScale.x":
501             case "outputScale.y":
502                 return true;
503             default:
504                 return false;
505         }
506     }
507 
508     override
509     float getDefaultValue(string key) {
510         // Skip our list of our parent already handled it
511         float def = super.getDefaultValue(key);
512         if (!isNaN(def)) return def;
513 
514         switch(key) {
515             case "gravity":
516             case "frequency":
517             case "angleDamping":
518             case "lengthDamping":
519             case "outputScale.x":
520             case "outputScale.y":
521                 return 1;
522             case "length":
523                 return 0;
524             default: return float();
525         }
526     }
527 
528     override
529     bool setValue(string key, float value) {
530         
531         // Skip our list of our parent already handled it
532         if (super.setValue(key, value)) return true;
533 
534         switch(key) {
535             case "gravity":
536                 offsetGravity *= value;
537                 return true;
538             case "length":
539                 offsetLength += value;
540                 return true;
541             case "frequency":
542                 offsetFrequency *= value;
543                 return true;
544             case "angleDamping":
545                 offsetAngleDamping *= value;
546                 return true;
547             case "lengthDamping":
548                 offsetLengthDamping *= value;
549                 return true;
550             case "outputScale.x":
551                 offsetOutputScale.x *= value;
552                 return true;
553             case "outputScale.y":
554                 offsetOutputScale.y *= value;
555                 return true;
556             default: return false;
557         }
558     }
559     
560     override
561     float getValue(string key) {
562         switch(key) {
563             case "gravity":         return offsetGravity;
564             case "length":          return offsetLength;
565             case "frequency":       return offsetFrequency;
566             case "angleDamping":    return offsetAngleDamping;
567             case "lengthDamping":   return offsetLengthDamping;
568             case "outputScale.x":   return offsetOutputScale.x;
569             case "outputScale.y":   return offsetOutputScale.y;
570             default:                return super.getValue(key);
571         }
572     }
573 
574     /// Gets the final gravity
575     float getGravity() { return (gravity * offsetGravity) * puppet.physics.gravity * getScale(); }
576 
577     /// Gets the final length
578     float getLength() { return length + offsetLength; }
579 
580     /// Gets the final frequency
581     float getFrequency() { return frequency * offsetFrequency; }
582 
583     /// Gets the final angle damping
584     float getAngleDamping() { return angleDamping * offsetAngleDamping; }
585 
586     /// Gets the final length damping
587     float getLengthDamping() { return lengthDamping * offsetLengthDamping; }
588 
589     /// Gets the final length damping
590     vec2 getOutputScale() { return outputScale * offsetOutputScale; }
591 }
592 
593 mixin InNode!SimplePhysics;