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 => (¶m_)[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;