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;