Open Dynamics Engine Integration

From K-3D

Jump to: navigation, search

References

Wendy Langer created a detailed proposal for ODE integration for GSOC 2008. Although Wendy ultimately decided not to pursue her proposal, any realistic attempt at producing a useful capability will have to address all of the points that Wendy discussed:

Following are some additional links and ideas around dynamics in-general.

Prototypes

Paulo Teotonio Sobrinho

Paul has been working on a prototype, creating some great animations and sample scripts:

Tim Shead

Tim has created two simpler examples, to explore the statefulness of simulation systems in-general. To give them a try, you will need the following:

External State

  • Download the sample external state document, ode_external_state.k3d.
  • After loading the document, you will see a typical K-3D environment containing a single polygonal cube.
  • Hit "play" and the cube will begin moving "down", accelerating due to the force of gravity.
  • Rewind the animation, and the cube returns to its starting position.
  • "Scrub" the animation timeline slider back-and-forth, noting that the simulation runs whenever time moves forward, but "resets" whenever time moves backwards.
  • Select the "Simulation" node in the Node List Panel. It is a NullOutputScript that contains the (one-and-only) script that runs the simulation. Note that it has a user property "Time" that is connected to the document TimeSource, and a "Gravity" property that controls the force of gravity. Try altering the force of gravity and re-running the simulation.
  • Select the "Simulation Body" node in the Node List Panel. It is a FrozenTransformation node that is being used for multiple purposes:
    • It is connected to the MeshInstance that displays the polygonal cube, allowing the simulation to alter the cube's transformation matrix.
    • It provides user property storage for physical parameters of the cube that are used by the simulation, in this case "Mass".
    • It provides user property storage for the simulation state of the cube between frames, in this case "Linear Velocity".
  • Re-run the simulation while the "Simulation Body" properties are visible in the Node Properties Panel ... note that the "Linear Velocity" property is updated as the simulation runs.
  • To add additional physical objects to the scene, you will have to:
    • Create a new object.
    • Add a FrozenTransformation node to the new object's transformation input.
    • Add scalar "mass" and vector3 "linear_velocity" user properties to the FrozenTransformation.
    • Make sure that the object's mass is non-zero.
    • Re-run the simulation.
  • Select the "Simulation" node and view the simulation script:
#python

import k3d
import ode

def is_physical_node(node):
	if node.factory().name() != "FrozenTransformation":
		return False

	if not node.has_property("mass"):
		return False

	if not node.has_property("linear_velocity"):
		return False

	return True

if "last_time" not in locals():
	last_time = Node.time

world = ode.World()
world.setGravity(Node.gravity)

physical_nodes = {}

for node in Document.nodes():
	if not is_physical_node(node):
		continue

	mass = ode.Mass()
	mass.setSphereTotal(node.mass, 1.0)
	
	body = ode.Body(world)
	body.setMass(mass)
	body.setPosition(node.output_matrix * k3d.point3(0, 0, 0))
	body.setLinearVel(node.linear_velocity)

	physical_nodes[node] = body

delta_time = Node.time - last_time
last_time = Node.time

if delta_time > 0:
	world.step(delta_time)

	for node,body in physical_nodes.items():
		resting_position = node.input_matrix * k3d.point3(0, 0, 0)
		current_position = node.output_matrix * k3d.point3(0, 0, 0)
		new_position = body.getPosition()
		delta_position = k3d.point3(new_position[0], new_position[1], new_position[2]) - resting_position
		
		node.matrix = k3d.translate3(delta_position)
		node.linear_velocity = body.getLinearVel()

else:
	for node in physical_nodes:
		node.matrix = k3d.identity3()
		node.linear_velocity = k3d.vector3(0, 0, 0)
  • Note the important characteristics of the script:
    • "Physical" nodes are simply nodes that provide a set of required properties (mass, etc). This was a practical decision to get the prototype working with a minimum of effort, but other approaches are possible: physical nodes could also be identified using metadata, C++ interfaces, or new node types.
      • Creating physical nodes in this way requires an unacceptable level of user effort, we assume that it won't be necessary in the long-term, either because new UI code makes it easy, or a new node type is created that already has the correct properties.
    • The script essentially recreates the entire simulation from scratch each time it executes, using data stored in user properties from the previous execution. So a more realistic example would have to handle a larger set of simulation states, including angular velocity, etc.
    • This approach is simple but inefficient, since the script must iterate over every node in the document for every simulation step. A better approach would be to cache the simulation data internally between simulation steps.
    • Internal caching will likely be required to store contact groups anyway.
    • In the general case, it should be possible to have multiple unrelated simulations in a single document, so we likely need a way to explicitly assign physical nodes to a simulation, just as we explicitly assign "visible" nodes and lights to render engines. Unfortunately, there's just no way to express that in scripting currently.
    • The script keeps track of the "last" time so it can compute the time delta for each simulation step, and adjust its behavior whenever time moves backward.
    • As a general observation, NullOutputScript is unlike any other filter in the Visualization Pipeline, because it is not demand-driven. Stated differently, NullOutputScript executes immediately whenever one of its inputs changes. In general, this is inefficient and can lead to infinite loops in execution. We assume that a C++ ODE capability will be correctly demand-driven, i.e. that a physics-enabled transformation node will "request" its state from a central transformation node only when that information is needed.

Internal State

  • Download the sample internal state document, ode_internal_state.k3d.
  • Nearly everything works as before, except that the "Linear Velocity" property in the physical node sets the initial velocity of the node at the beginning of the simulation, rather than caching the simulation state.
#python

import k3d
import ode

# Define what constitutes a "physical" node ...
def is_physical_node(node):
	if node.factory().name() != "FrozenTransformation":
		return False

	if not node.has_property("mass"):
		return False

	return True

# Setup internal state ...
if "last_time" not in locals():
	last_time = Node.time

if "world" not in locals():
	world = ode.World()

if "physical_nodes" not in locals():
	physical_nodes = {}

if "initialize" not in locals():
	initialize = True

# Identify "physical" nodes ...
if initialize:
	initialize = False
	for node in Document.nodes():
		if not is_physical_node(node):
			continue

		if node not in physical_nodes:
			body = ode.Body(world)
			body.setPosition(node.output_matrix * k3d.point3(0, 0, 0))

			if node.has_property("linear_velocity"):
				body.setLinearVel(node.linear_velocity)
			else:
				body.setLinearVel((0, 0, 0))

			physical_nodes[node] = body

# Handle the next time-step ...
delta_time = Node.time - last_time
last_time = Node.time

# If we're moving forward in time, run the next simulation step ...
if delta_time > 0:
	for node,body in physical_nodes.items():
		mass = ode.Mass()
		mass.setSphereTotal(node.mass, 1.0)

		body.setMass(mass)

	world.setGravity(Node.gravity)
	world.step(delta_time)

	for node,body in physical_nodes.items():
		resting_position = node.input_matrix * k3d.point3(0, 0, 0)
		new_position = body.getPosition()
		delta_position = k3d.point3(new_position[0], new_position[1], new_position[2]) - resting_position
		
		node.matrix = k3d.translate3(delta_position)

# If we're moving backward in time, reset all nodes to their resting state ...
else:
	for node,body in physical_nodes.items():
		node.matrix = k3d.identity3()

		body.setPosition(node.output_matrix * k3d.point3(0, 0, 0))

		if node.has_property("linear_velocity"):
			body.setLinearVel(node.linear_velocity)
		else:
			body.setLinearVel((0, 0, 0))
  • Note that the simulation is much faster, because it no-longer scans the entire set of document nodes for each simulation time step.
    • On the down-side, the script only "recognizes" physical nodes the first time it's run. This would not be an issue for C++ components using node collection properties.
    • Similarly, the script will likely cause a segfault if a physical node is deleted. Again, node collection properties handle this easily.
  • Further, the simulation state is persistent across time-steps, so state doesn't have to be cached and retrieved within individual nodes.

Internal State With Collision Handling

  • Download the sample collision handling document, ode_internal_state_collision.k3d
  • Note that there are two boxes that collide, sliding along an (invisible) plane.
  • This version of the script handles rotation as well as translation between K-3D and ODE.