Technology Demo

Physics (Collision) Demo

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Spot for images

Intro

This demo will show you how to implement some basic collision physics by having blocks that fall from the sky onto the plane below.

Creating a Physics World

In order to implement physics, we must first set up a world for the physics to occur in. You can set it up so that it coincides with a graphical representation of terrain, but the physics world itself is invisible.

The first step is to define the physics world. Luster's built in physics world takes a table of options, and here we set the type (dynamic) and we also define the gravity.

-- Create the physics world
self.world = luster.physics.World({
type = luster.physics.World.DYNAMICS,
gravity = luster.Vector3(0, -10, 0)  })

The physics world simply defines the physics laws that will apply to all of the bodies that it contains. Next, we'll want to make a physics body that represents the "ground" that our cubes will be falling onto. To create a body for the physics world, we need a node and a shape. The node doesn't do anything except fill one of the arguments for adding a body to the physics world, and the shape is the invisible shape that the physics will be applied to when the world is activated.

-- Create the plane node
self.planeNode = luster.display.Node()
self:addChild(self.planeNode)

The shape part of the body is, in this case, a flat plane. A terrain shape can also be used if your project has a terrain, and is called with TerrainShape instead of StaticPlaneShape and takes the same arguments. For this demo, a [[Luster Player Procedural Meshes Demo|procedural mesh]] was used to create a visible representation of the plane - the y for each point on the plane was -1, and this is reflected in the plane shape, as we want our cubes to look as though they are hitting the ground.

The static plane shape takes two arguments: a vector representing the normal, and a constant that tells the plane how far along the vector that it needs to move. Since the y of our procedural mesh is -1, we'll want the static plane shape to match this.

-- Create the plane shape
self.planeShape = luster.physics.StaticPlaneShape(luster.Vector3(0,1,0),-1)

To add the plane body to the physics world, we need to define the body using the node, shape, and the table of options that define the type, mass, and inertia. Using 0 for the mass makes the object static and unmovable, which we want since this shape is meant to represent the ground.

-- Create the plane body
self.planeBody = luster.physics.Body(self.planeNode, self.planeShape, {
   type = luster.physics.Body.DYNAMIC,
   mass = 0,
   inertia = luster.Vector3(0, 0, 0)})
self.world:addBody(self.planeBody)

The final step in setting up the physics world is to activate it. In this demo, activate was moved to a separate method so that when the space bar is pressed, the physics are enabled. Activating is one line of code that takes the frame rate of the physics world, which doesn't have to match the frame rate of the project.

self.world:activate(100)

Creating an Object with Physics

Creating a cube with physics is very similar to creating the plane body, except with a few more options. To begin, create a cube as shown in previous tutorials, and then use the following code to add physics to the shape we've just created.

We set the cube's shape to be a new box shape and give it a vector that corresponds to its scale. The scale for this cube was set to .002, meaning that in units (what would be used in the vector) we would want the number to be .2. This creates an invisible shape that is the exact size of the cube, and this is used in physics interactions. Next, we store the calculated inertia in a local inertia variable, using the same number that will be used for the mass. The inertia is necessary if we want our cube to interact with other cubes, rather than just the plane.

self.cube.shape = luster.physics.BoxShape(luster.Vector3(.2, .2, .2))
local inertia = self.cube.shape:calculateInertia(1)

Here we set up the body of the cube, similar to the body we used for the plane earlier. The body takes the cube, the cube's shape, and a table of options as its arguments. Here, the type is once again dynamic, the mass is 1 as decided earlier, the inertia becomes the earlier calculated inertia and we set a friction variable as well. Then, we simply add the new physics body into the physics world so that it can start interacting with other objects.

self.cube.body = luster.physics.Body(self.cube, self.cube.shape, {
   type = luster.physics.Body.DYNAMIC,
   mass = 1,
   inertia = inertia,
   friction = 0.75	})
self.world:addBody(self.cube.body)

Mouse Interactions

For this demo, a mouse plane was used to be able to click a spot on the screen and have a cube appear there. For this demo, several of the earlier functions, such as creating a cube, were moved to their own methods. The cube creation was moved to a method called createCube, which took a Vector3 called "pos" (short for position) as an argument, and the only thing it changed was that the position of the cube became the given position.

As mentioned earlier, the activation of the physics world was also moved to a separate method so that when the space bar is pressed, the physics activate. Also, when the mouse is clicked, a cube is created at a position provided by the next method, onMouseMove.

To see where the mouse tip is in the world, we create a mouse plane that we'll test to see if there is interaction between it and the mouse.

self.mousePlane = luster.Plane(luster.Vector3(0, 0, 1), 0)

We also set the camera's position to be a bit different.

self.camera.position = luster.Vector3(0,.7,5)

Next, we test on every mouse move to see if the mouse tip is hitting the mouse plane that we created earlier. Here, a local variable ray is created, and a ray is sent out from the camera at the place where the mouse tip is.

The local variable success and dist are then defined to be the result of the ray intersecting the mouse plane. If the mouse tip is over the mouse plane, then this function will return a boolean (true) and the distance between the tip and the plane.

The if statements says that if the mouse tip is indeed over the mouse plane, then the variable cubePosition will be set to the point at dist.

function Root:onMouseMove()
   local ray = self.camera:getCameraToViewportRay(luster.Mouse.x/stage.width, luster.Mouse.y/stage.height)
   local success, dist = ray:intersects(self.mousePlane)
   if success then
      self.cubePosition = ray:getPoint(dist)
   end
end

Upon a mouseDown event, a cube will be created using the self.cubePosition as the position, so a cube will be created directly under the mouse.