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 import inochi2d.core.nodes.drivers;
11 import inochi2d.core.nodes.common;
12 import inochi2d.core.nodes;
13 import inochi2d.fmt;
14 import inochi2d.core.dbg;
15 import inochi2d.core;
16 import inochi2d.math;
17 import inochi2d.phys;
18 import inochi2d;
19 import std.exception;
20 import std.algorithm.sorting;
21 import std.stdio;
22 
23 /**
24     Physics model to use for simple physics
25 */
26 enum PhysicsModel {
27     /**
28         Rigid pendulum
29     */
30     Pendulum = "pendulum",
31 
32     /**
33         Springy pendulum
34     */
35     SpringPendulum = "spring_pendulum",
36 }
37 
38 enum ParamMapMode {
39     AngleLength = "angle_length",
40     XY = "xy",
41 }
42 
43 class Pendulum : PhysicsSystem {
44     SimplePhysics driver;
45 
46 private:
47     vec2 bob = vec2(0, 0);
48     float angle = 0;
49     float dAngle = 0;
50 
51 protected:
52     override
53     void eval(float t) {
54         setD(angle, dAngle);
55         float lengthRatio = driver.getGravity() / driver.length;
56         float critDamp = 2 * sqrt(lengthRatio);
57         float dd = -lengthRatio * sin(angle);
58         dd -= dAngle * driver.angleDamping * critDamp;
59         setD(dAngle, dd);
60     }
61 
62 public:
63 
64     this(SimplePhysics driver) {
65         this.driver = driver;
66 
67         bob = driver.anchor + vec2(0, driver.length);
68 
69         addVariable(&angle);
70         addVariable(&dAngle);
71     }
72 
73     override
74     void tick(float h) {
75         // Compute the angle against the updated anchor position
76         vec2 dBob = bob - driver.anchor;
77         angle = atan2(-dBob.x, dBob.y);
78 
79         // Run the pendulum simulation in terms of angle
80         super.tick(h);
81 
82         // Update the bob position at the new angle
83         dBob = vec2(-sin(angle), cos(angle));
84         bob = driver.anchor + dBob * driver.length;
85 
86         driver.output = bob;
87     }
88 
89     override
90     void drawDebug(mat4 trans = mat4.identity) {
91         vec3[] points = [
92             vec3(driver.anchor.x, driver.anchor.y, 0),
93             vec3(bob.x, bob.y, 0),
94         ];
95 
96         inDbgSetBuffer(points);
97         inDbgLineWidth(3);
98         inDbgDrawLines(vec4(1, 0, 1, 1), trans);
99     }
100 }
101 
102 class SpringPendulum : PhysicsSystem {
103     SimplePhysics driver;
104 
105 private:
106     vec2 bob = vec2(0, 0);
107     vec2 dBob = vec2(0, 0);
108 
109 protected:
110     override
111     void eval(float t) {
112         setD(bob, dBob);
113 
114         // These are normalized vs. mass
115         float springKsqrt = driver.frequency * 2 * PI;
116         float springK = springKsqrt ^^ 2;
117 
118         float g = driver.getGravity();
119         float restLength = driver.length - g / springK;
120 
121         vec2 offPos = bob - driver.anchor;
122         vec2 offPosNorm = offPos.normalized;
123 
124         float lengthRatio = driver.getGravity() / driver.length;
125         float critDampAngle = 2 * sqrt(lengthRatio);
126         float critDampLength = 2 * springKsqrt;
127 
128         float dist = abs(driver.anchor.distance(bob));
129         vec2 force = vec2(0, g);
130         force -= offPosNorm * (dist - restLength) * springK;
131         vec2 ddBob = force;
132 
133         vec2 dBobRot = vec2(
134             dBob.x * offPosNorm.y + dBob.y * offPosNorm.x,
135             dBob.y * offPosNorm.y - dBob.x * offPosNorm.x,
136         );
137 
138         vec2 ddBobRot = -vec2(
139             dBobRot.x * driver.angleDamping * critDampAngle,
140             dBobRot.y * driver.lengthDamping * critDampLength,
141         );
142 
143         vec2 ddBobDamping = vec2(
144             ddBobRot.x * offPosNorm.y - dBobRot.y * offPosNorm.x,
145             ddBobRot.y * offPosNorm.y + dBobRot.x * offPosNorm.x,
146         );
147 
148         ddBob += ddBobDamping;
149 
150         setD(dBob, ddBob);
151     }
152 
153 public:
154 
155     this(SimplePhysics driver) {
156         this.driver = driver;
157 
158         bob = driver.anchor + vec2(0, driver.length);
159 
160         addVariable(&bob);
161         addVariable(&dBob);
162     }
163 
164     override
165     void tick(float h) {
166         // Run the spring pendulum simulation
167         super.tick(h);
168 
169         driver.output = bob;
170     }
171 
172     override
173     void drawDebug(mat4 trans = mat4.identity) {
174         vec3[] points = [
175             vec3(driver.anchor.x, driver.anchor.y, 0),
176             vec3(bob.x, bob.y, 0),
177         ];
178 
179         inDbgSetBuffer(points);
180         inDbgLineWidth(3);
181         inDbgDrawLines(vec4(1, 0, 1, 1), trans);
182     }
183 }
184 
185 /**
186     Simple Physics Node
187 */
188 @TypeId("SimplePhysics")
189 class SimplePhysics : Driver {
190 private:
191     this() { }
192 
193     @Name("param")
194     uint paramRef = InInvalidUUID;
195 
196     @Ignore
197     Parameter param_;
198 
199 protected:
200     override
201     string typeId() { return "SimplePhysics"; }
202 
203     /**
204         Allows serializing self data (with pretty serializer)
205     */
206     override
207     void serializeSelf(ref InochiSerializer serializer) {
208         super.serializeSelf(serializer);
209         serializer.putKey("param");
210         serializer.serializeValue(paramRef);
211         serializer.putKey("model_type");
212         serializer.serializeValue(modelType_);
213         serializer.putKey("map_mode");
214         serializer.serializeValue(mapMode);
215         serializer.putKey("gravity");
216         serializer.serializeValue(gravity);
217         serializer.putKey("length");
218         serializer.serializeValue(length);
219         serializer.putKey("frequency");
220         serializer.serializeValue(frequency);
221         serializer.putKey("angle_damping");
222         serializer.serializeValue(angleDamping);
223         serializer.putKey("length_damping");
224         serializer.serializeValue(lengthDamping);
225         serializer.putKey("output_scale");
226         outputScale.serialize(serializer);
227     }
228 
229     /**
230         Allows serializing self data (with compact serializer)
231     */
232     override
233     void serializeSelf(ref InochiSerializerCompact serializer) {
234         super.serializeSelf(serializer);
235         serializer.putKey("param");
236         serializer.serializeValue(paramRef);
237         serializer.putKey("model_type");
238         serializer.serializeValue(modelType_);
239         serializer.putKey("map_mode");
240         serializer.serializeValue(mapMode);
241         serializer.putKey("gravity");
242         serializer.serializeValue(gravity);
243         serializer.putKey("length");
244         serializer.serializeValue(length);
245         serializer.putKey("frequency");
246         serializer.serializeValue(frequency);
247         serializer.putKey("angle_damping");
248         serializer.serializeValue(angleDamping);
249         serializer.putKey("length_damping");
250         serializer.serializeValue(lengthDamping);
251         serializer.putKey("output_scale");
252         outputScale.serialize(serializer);
253     }
254 
255     override
256     SerdeException deserializeFromFghj(Fghj data) {
257         super.deserializeFromFghj(data);
258 
259         if (!data["param"].isEmpty)
260             if (auto exc = data["param"].deserializeValue(this.paramRef)) return exc;
261         if (!data["model_type"].isEmpty)
262             if (auto exc = data["model_type"].deserializeValue(this.modelType_)) return exc;
263         if (!data["map_mode"].isEmpty)
264             if (auto exc = data["map_mode"].deserializeValue(this.mapMode)) return exc;
265         if (!data["gravity"].isEmpty)
266             if (auto exc = data["gravity"].deserializeValue(this.gravity)) return exc;
267         if (!data["length"].isEmpty)
268             if (auto exc = data["length"].deserializeValue(this.length)) return exc;
269         if (!data["frequency"].isEmpty)
270             if (auto exc = data["frequency"].deserializeValue(this.frequency)) return exc;
271         if (!data["angle_damping"].isEmpty)
272             if (auto exc = data["angle_damping"].deserializeValue(this.angleDamping)) return exc;
273         if (!data["length_damping"].isEmpty)
274             if (auto exc = data["length_damping"].deserializeValue(this.lengthDamping)) return exc;
275         if (!data["output_scale"].isEmpty)
276             if (auto exc = outputScale.deserialize(data["output_scale"])) return exc;
277 
278         return null;
279     }
280 
281 public:
282     PhysicsModel modelType_ = PhysicsModel.Pendulum;
283     ParamMapMode mapMode = ParamMapMode.AngleLength;
284 
285     /**
286         Gravity scale (1.0 = puppet gravity)
287     */
288     float gravity = 1.0;
289 
290     /**
291         Pendulum/spring rest length (pixels)
292     */
293     float length = 100;
294 
295     /**
296         Resonant frequency (Hz)
297     */
298     float frequency = 1;
299 
300     /**
301         Angular damping ratio
302     */
303     float angleDamping = 0.5;
304 
305     /**
306         Length damping ratio
307     */
308     float lengthDamping = 0.5;
309     vec2 outputScale = vec2(1, 1);
310 
311     @Ignore
312     vec2 anchor = vec2(0, 0);
313 
314     @Ignore
315     vec2 rest;
316 
317     @Ignore
318     vec2 output;
319 
320     @Ignore
321     PhysicsSystem system;
322 
323     /**
324         Constructs a new SimplePhysics node
325     */
326     this(Node parent = null) {
327         this(inCreateUUID(), parent);
328     }
329 
330     /**
331         Constructs a new SimplePhysics node
332     */
333     this(uint uuid, Node parent = null) {
334         super(uuid, parent);
335         reset();
336     }
337 
338     override
339     void beginUpdate() {
340         super.beginUpdate();
341     }
342 
343     override
344     void update() {
345         super.update();
346     }
347 
348     override
349     Parameter[] getAffectedParameters() {
350         if (param_ is null) return [];
351         return [param_];
352     }
353 
354     override
355     void updateDriver() {
356         float h = deltaTime();
357 
358         updateInputs();
359 
360         // Minimum physics timestep: 0.01s
361         while (h > 0.01) {
362             system.tick(0.01);
363             h -= 0.01;
364         }
365 
366         system.tick(h);
367         updateOutputs();
368     };
369 
370     void updateInputs() {
371         auto anchorPos = transform.matrix * vec4(0, 0, 0, 1);
372         anchor = vec2(anchorPos.x, anchorPos.y);
373         auto restPos = transform.matrix * vec4(0, length, 0, 1);
374         rest = vec2(restPos.x, restPos.y);
375     }
376 
377     void updateOutputs() {
378         if (param is null) return;
379 
380         auto localPos4 = transform.matrix.inverse * vec4(output.x, output.y, 0, 1);
381         vec2 localPos = vec2(localPos4.x, localPos4.y);
382         vec2 localPosNorm = localPos / length;
383 
384         vec2 paramVal;
385         switch (mapMode) {
386             case ParamMapMode.XY:
387                 paramVal = localPosNorm - vec2(0, 1);
388                 paramVal.y = -paramVal.y; // Y goes up for params
389                 break;
390             case ParamMapMode.AngleLength:
391                 float a = atan2(-localPosNorm.x, localPosNorm.y) / PI;
392                 float d = localPosNorm.distance(vec2(0, 0));
393                 paramVal = vec2(a, d);
394                 break;
395             default: assert(0);
396         }
397 
398         param.value = vec2(paramVal.x * outputScale.x, paramVal.y * outputScale.y);
399         param.update();
400     }
401 
402     override
403     void reset() {
404         updateInputs();
405 
406         switch (modelType) {
407             case PhysicsModel.Pendulum:
408                 system = new Pendulum(this);
409                 break;
410             case PhysicsModel.SpringPendulum:
411                 system = new SpringPendulum(this);
412                 break;
413             default:
414                 assert(0);
415         }
416     }
417 
418     override
419     void finalize() {
420         param_ = puppet.findParameter(paramRef);
421         super.finalize();
422         reset();
423     }
424 
425     override
426     void drawDebug() {
427         system.drawDebug();
428     }
429 
430     Parameter param() {
431         return param_;
432     }
433 
434     void param(Parameter p) {
435         param_ = p;
436         if (p is null) paramRef = InInvalidUUID;
437         else paramRef = p.uuid;
438     }
439 
440     float getScale() {
441         return puppet.physics.pixelsPerMeter;
442     }
443 
444     float getGravity() {
445         return gravity * puppet.physics.gravity * getScale();
446     }
447 
448     PhysicsModel modelType() {
449         return modelType_;
450     }
451 
452     void modelType(PhysicsModel t) {
453         modelType_ = t;
454         reset();
455     }
456 }
457 
458 mixin InNode!SimplePhysics;