Graphics demo#
Requirements:
In this tutorial we will:
Create a two-link pendulum multibody
Attach a primitive geometry to a body
Attach a 3D mesh geometry to a body
Setup web-based 3d visualization
Render to a file
import atexit
import numpy as np
from typing import cast
from pathlib import Path
from IPython.display import display, Image
import Karana.Core as kc
import Karana.Math as km
import Karana.Frame as kf
import Karana.Dynamics as kd
import Karana.Dynamics.SOADyn_types as kdt
import Karana.Scene as ks
import Karana.KUtils as ku
import Karana.Models as kmdl
resources = Path().absolute().parent / "resources"
dodecahedron_file = str(resources / "dodecahedron.obj")
Setup a two-link pendulum multibody#
def createMbody() -> kd.Multibody:
"""Create the Multibody.
Returns
-------
Multibody
The newly created multibody.
"""
fc = kf.FrameContainer("root")
mb = kd.Multibody("mb", fc)
# inertia used for both bodies
spI = km.SpatialInertia(2.0, np.zeros(3), np.diag([3, 2, 1]))
# inboard to joint transform used for both bodies
ib2j = km.HomTran()
# body to joint transform used for both bodies
b2j = km.HomTran(np.array([0, 0, 1]))
# setup body 1
bd1 = kd.PhysicalBody("bd1", mb)
bd1.setSpatialInertia(spI)
bd1_hge = kd.PhysicalHinge(cast(kd.PhysicalBody, mb.virtualRoot()), bd1, kd.HingeType.REVOLUTE)
bd1.setBodyToJointTransform(b2j)
bd1_subhge = cast(kd.PinSubhinge, bd1_hge.subhinge(0))
bd1_subhge.setUnitAxis(np.array([0, 1, 0]))
# setup body 2
bd2 = kd.PhysicalBody("bd2", mb)
bd2.setSpatialInertia(spI)
bd2_hge = kd.PhysicalHinge(bd1, bd2, kd.HingeType.REVOLUTE)
bd2_hge.onode().setBodyToNodeTransform(ib2j)
bd2.setBodyToJointTransform(b2j)
bd2_subhge = cast(kd.PinSubhinge, bd2_hge.subhinge(0))
bd2_subhge.setUnitAxis(np.array([0, 1, 0]))
mb.ensureHealthy()
# zero out hinge data (coords, velocities, accelerations, forces)
dark = np.zeros(mb.subhingeCoordData().nU())
mb.subhingeCoordData().setQ(dark)
mb.subhingeCoordData().setU(dark)
mb.subhingeCoordData().setUdot(dark)
mb.subhingeCoordData().setT(dark)
mb.setUniformGravAccel(np.zeros(3))
assert kc.allReady()
return mb
Create the multibody and lookup important objects#
mb = createMbody()
# get the first body
bd1 = mb.getBody("bd1")
# get the second body
bd2 = mb.getBody("bd2")
Geometry setup#
We create 3d geometries with associated material properties and attach them to bodies. This is commonly useful for collision detection and 3d visualization.
cleanup_graphics, web_scene = mb.setupGraphics(port=0, wait_for_clients=1)
proxy_scene = mb.getScene()
# create a ball geometry to be used for both bodies
ball_geom = ks.SphereGeometry(0.1)
# load a dodecahedron geometry from file
importer = ks.AssimpImporter()
d12_geom = importer.importFrom(dodecahedron_file).geometries[0]
# create red and blue materials to color the bodies
mat_info = ks.PhysicalMaterialInfo()
mat_info.color = ks.Color.BLUE
blue = ks.PhysicalMaterial(mat_info)
mat_info.color = ks.Color.RED
red = ks.PhysicalMaterial(mat_info)
# create a ks.ProxyScenePart and attach it to the body
bd1_part = ks.ProxyScenePart("bd1_part", scene=proxy_scene, geometry=ball_geom, material=blue)
bd1_part.attachTo(bd1)
bd2_part = ks.ProxyScenePart("bd2_part", scene=proxy_scene, geometry=d12_geom, material=red)
bd2_part.attachTo(bd2)
bd2_part.setScale(0.2)
# create some unattached objects (implicitly attached to the root frame)
root_scene_node = ks.ProxySceneNode("root_scene_node", scene=proxy_scene)
del red, blue, mat_info, d12_geom, ball_geom
Setup camera angle and add ornamental parts#
# position the viewpoint camera of the visualization
web_scene.defaultCamera().pointCameraAt([0.4, 4, -0.9], [0, 0, -1], [0, 0, 1])
# visualize axes for important frames
root_scene_node.graphics().showAxes(0.5)
bd1_part.graphics().showAxes(0.5)
bd2_part.graphics().showAxes(0.5)
# add a trail to track the motion of body 2
bd2_part.graphics().trail()
proxy_scene.update()
# set up state propagator
sp = kd.StatePropagator(mb, km.IntegratorType.CVODE)
integ = sp.getIntegrator()
# Create a UniformGravity model, and set it's gravitational acceleration.
ug = kmdl.Gravity("grav_model", sp, kmdl.UniformGravity("uniform_gravity"), mb)
ug.getGravityInterface().setGravity(np.array([0, 0, -9.81]), 0.0, kmdl.OutputUpdateType.PRE_HOP)
del ug
# Makes sure the visualization scene is updated after each state change.
kmdl.UpdateProxyScene("update_proxy_scene", sp, proxy_scene)
# sync the simulation time with real-time.
kmdl.SyncRealTime("sync_real_time", sp, 1.0)
Register callback for end of each step#
def post_step_fn(t, x):
"""Print out the time and state.
Parameters
----------
t : float
The current time.
x : NDArray[np.float64]
The current state.
"""
print(f"t = {float(integ.getTime())/1e9}s; x = {integ.getX()}")
sp.fns.post_hop_fns["update_and_info"] = post_step_fn
Initialize state and time#
x_init = np.array([0.5, -0.5, 0.0, 0.0])
t_init = np.timedelta64(0, "ns")
sp.setTime(t_init)
sp.setState(x_init)
Register event to limit step size#
h = np.timedelta64(int(1e7), "ns")
t = kd.TimedEvent("hop_size", h, lambda _: None, False)
t.period = h
# register after time has been initialized
sp.registerTimedEvent(t)
del t
print(f"t = {float(integ.getTime())/1e9}s; x = {integ.getX()}")
Run the simulation#
The advanceTo time below can be increased to lengthen the simulation
sp.advanceTo(2.0)
web_scene.renderToFile("render.png")
display(Image("render.png"))
Cleanup#
def cleanup():
"""Cleanup the simulation."""
global integ, bd1, bd2, bd1_part, bd2_part, root_scene_node, web_scene, proxy_scene
del integ, bd1, bd2, bd1_part, bd2_part, root_scene_node, web_scene, proxy_scene
kc.discard(sp)
cleanup_graphics()
fc = mb.frameContainer()
kc.discard(mb)
kc.discard(fc)
atexit.register(cleanup)
Summary#
You now know how to add visual geometries to a simulation and render to a file.