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;