Archive for December, 2018

Monkeying Around with mojo3d: wrapping physics behaviours

Sunday, December 16th, 2018

This post continues on from Monkeying Around with mojo3d: physics behaviours.

As always, the intention of this post is that you scroll through the code at the same time, in order to see what each section is doing.

Note that this time, I have moved the PlaneBehaviour class into its own file, to more accurately reflect how a real-world project would be organised.

Download the updated code and media below before continuing:

IslandDemo source and media [4.6 MB; note that this is NOT a secure server, but the zip file contains only source code — text — and the 3D media. Run it through your virus scanner for peace of mind!]

Open up both island.monkey2 and planebehaviour.monkey2 in the Ted2Go editor, and double-click the island.monkey2 tab to set it as build file; a litte + sign will appear in the tab to make this clear. Making island.monkey2 the build file means that even if you’re tweaking the PlaneBehaviour class, you can just hit Ted2Go’s Run command (F5 or the rocket icon) to build island.monkey2.

You can double-click the tab again to de-select island.monkey2 as the build file. When no file is set as the build file, whichever tab you hit Run from will be treated as the build file.

Splitting into separate files makes the main file much simpler, and makes jumping between the PlaneBehaviour definition and the main code a simple switch between tabs, rather than scrolling up and down between class and main code.

Importing Code

Note that the main code file now features this line near the top to import the PlaneBehaviour file, making the PlaneBehaviour class accessible to island.monkey2:

#Import "planebehaviour"

You can specify the full name, like so, if you prefer:

#Import "planebehaviour.monkey2"

 

Before we proceed, a ‘Mea Culpa’…

In the last post, I ended by saying:

In the real world, you probably wouldn’t want to have the collider and rigid body setup carried out in the main body of the code, but instead do this within the OnStart method of the Behaviour so as to encapsulate all of the plane’s data.

As the New method of Behaviour only allows the passing-in of an entity parameter, this can make things a little tricky, so I’ll address my solution to this in the next post.

While the first line is true, the second line is… well, an embarrassing mistake!

I had thought that the Behaviour class’s New method could only take the default entity:Entity parameter listed in the Monkey2 documentation.

However, PlaneBehaviour extends Behaviour, and we can in fact define our own overridden New methods!

What this means, in short, is that we can easily pass in any parameters we want to PlaneBehaviour, and therefore set everything up inside the PlaneBehaviour class, instead of partly in the main program and partly inside PlaneBehaviour (without the trickery I had intended to demonstrate!).

The Old

Previously, the plane was set up outside the PlaneBehaviour class, in the main program (within OnCreateWindow), like so:


		Local plane_size:Float					= 16.83 * 0.5
		Local plane_box:Boxf					= New Boxf (-plane_size, -plane_size, -plane_size, plane_size, plane_size, plane_size)
		
		Local plane_model:Model					= Model.Load ("asset::1397 Jet_gltf_3B3Pa6BHXn1_fKZwaiJPXpf\1397 Jet.gltf")

			plane_model.Mesh.FitVertices (plane_box)
			plane_model.Mesh.Rotate (0, 180, 0)
			
			plane_model.Move (0.0, 5.0, 0.0)

		' Cull hidden tris...
		
		For Local mat:Material = Eachin plane_model.Materials
			mat.CullMode = CullMode.Back	
		Next

		' Physics setup...
		
		Local plane_collider:SphereCollider		= plane_model.AddComponent <SphereCollider> ()

			plane_collider.Radius				= plane_size * 0.6
		
		Local plane_body:RigidBody				= plane_model.AddComponent <RigidBody> ()

			plane_body.Mass						= 10954.0
			plane_body.Restitution				= 0.5
			plane_body.AngularDamping			= 0.9
			plane_body.LinearDamping			= 0.1
			plane_body.Friction					= 0.0

		plane									= New PlaneBehaviour (plane_model)

This is not ideal, as it places all of the technical setup in the main body of the code, reducing the simplicity of setting up another plane, or several planes, as you have to define a collider and rigid body every time. When this complexity is all wrapped-up into the class, setting up a new plane can be greatly simplified, requiring nothing more than a simple call to New PlaneBehaviour.

The New

In this revised version, the setup code simply loads the model and sets up some basic values for physics processing:


		Local plane_model:Model					= Model.Load ("asset::1397 Jet_gltf_3B3Pa6BHXn1_fKZwaiJPXpf\1397 Jet.gltf")

		' Core values for physics...

		Local mass:Float						= 10954.0
		Local roll_rate:Float					= 550000.0
		Local pitch_rate:Float					= 250000.0
	
		' And pass plane_model into PlaneBehaviour, where physics
		' will be applied...
		
		plane = New PlaneBehaviour (plane_model, mass, roll_rate, pitch_rate)
		

The model is loaded and then a few core values are set up — mass, roll rate and pitch rate (technically single elements of the torques that will be applied to specific axes).

Finally, New PlaneBehaviour is called, passing in:

  • plane_model
    the model loaded and set up earlier;
  • mass
    the mass, defined above as 10954 kg;
  • roll_rate
    the roll rate (element of torque to be applied to the z-axis);
  • pitch_rate
    the pitch rate (element of torque to be applied to the x-axis).

These values are arbitrary — just the values I chose to pass in — but you could easily amend this to suit yourself.

For example, if you decided to simply hard-code the mass, roll rate and pitch rate as fixed values inside the class, you could remove these elements (from both the call here, and the PlaneBehaviour New definition) and simply set them inside PlaneBehaviour’s New method.

The idea here is simply to demonstrate the ability to pass in changeable values to New PlaneBehaviour.

In a professional setup, other programmers might be calling your New PlaneBehaviour method to set up enemy aircraft, for example.

If you’ve hard-coded these values inside PlaneBehaviour, anyone (including you!) trying to tweak these for different types of enemy plane would have to edit the PlaneBehaviour class, affecting all other planes in the game! By allowing these values to be passed in per-plane, they can be defined by the calling programmer without affecting any other aircraft behaviours.

Down inside PlaneBehaviour, we’ve gone from this:


	Method New (entity:Entity)
		
		Super.New (entity)
		AddInstance ()

	End
	

… to something that looks much more complex, but is in fact much the same code as before, just moved from outside the class to inside the class:


	Method New (entity:Entity, mass:Float, roll_rate:Float, pitch_rate:Float)
		
		Super.New (entity)
		
		AddInstance ()

		Local plane_size:Float					= 16.83 * 0.5
		Local plane_box:Boxf					= New Boxf (-plane_size, -plane_size, -plane_size, plane_size, plane_size, plane_size)
	
		' Get a Model-specific handle to the entity:
		
		Local plane_model:Model					= Cast <Model> (entity)
		
		plane_model.Mesh.FitVertices (plane_box)
		plane_model.Mesh.Rotate (0, 180, 0)
			
		plane_model.Move (0.0, 5.0, 0.0)

		' Cull hidden tris...
		
		For Local mat:Material = Eachin plane_model.Materials
			mat.CullMode = CullMode.Back	
		Next

		' Physics setup...

		Local plane_collider:SphereCollider		= entity.AddComponent <SphereCollider> ()

			plane_collider.Radius				= plane_size * 0.6
		
		Local plane_body:RigidBody				= entity.AddComponent <RigidBody> ()

			plane_body.Mass						= mass
			plane_body.Restitution				= 0.5
			plane_body.AngularDamping			= 0.9
			plane_body.LinearDamping			= 0.75
			plane_body.Friction					= 0.0

		roll_torque								= roll_rate
		pitch_torque							= pitch_rate
		
	End
	

We’re now loading and adjusting the model here in PlaneBehaviour.New and doing setup of the physics collider and rigid body where they really belong. (The code remains much the same as it was before, when it was defined in OnCreateWindow, but is now properly encapsulated within the class.)

The first two lines remain as before, creating a Behaviour from the entity (plane model), and placing it into the scene; just treat these as necessary boilerplate lines, as mentioned last time — copy and paste!

We’ve then moved all the loading and positioning code into the class itself, setting up the plane’s size, bounding box and adjusting the mesh to fit, then positioning the plane at its start point.

One line that will almost certainly need explanation is this, just after the size and bounding box definition:

Local plane_model:Model					= Cast <Model> (entity)

Behaviour requires that an Entity object be passed in, Entity being the ‘lowest common denominator’ class of 3D object.

All 3D objects in mojo3d are variations of the Entity class: Model, Light and Camera are all extended versions of class Entity (each of these extends the Entity class), but each has its own domain-specific functionalities, some of which would include:

  • Model: Holds a mesh and materials to be applied to the mesh;
  • Light: Holds a direction, range and colour;
  • Camera: Holds near and far ranges, field of view, ability to render to a Canvas, etc.

Entity, on the other hand, gives us the core positioning and rotation methods used by Model, Light and Camera, as well as many other basic functionality.

Because Behaviour.New only gives us a reference to an entity (not a Model, or a Camera, for example), we need to ‘cast’ the entity to the specific class of entity we expect to operate upon; in this case, a Model.

We create a local variable to hold this Model reference:

Local plane_model:Model ...

… and call Cast (entity) with the modifier to receive the type of entity required:

... Cast <Model> (entity)

The plane_model variable therefore now holds a reference to the entity upon which we can call Model-specific methods.

After setting up the model, we cull any backfacing triangles, as before, and then set up the collider and rigid body — again, as before.

Lastly, the renamed roll_torque and pitch_torque fields (which are still technically misnamed!) are set up from the roll_rate and pitch_rate parameters passed in.

In Summary

  • Use the extended Behaviour’s New method to get the entity (usually a model*) and any required data into the class, and set up your model and its physics collider and rigid body. (There is certainly a case to be made for leaving the model adjustment outside of the class, but this will be down to your own judgement.)* Note that there’s no reason you can’t pass in other types of entity, such as a camera you’d like to be physically controlled.
  • Use OnStart to do any initial startup processing; in this case, we get a handy typing shortcut to the physics body and set the plane’s initial impulse force.
  • Use OnUpdate to process any custom updates, such as applying player input forces to the entity, as happens here.

Other Tweaks

  • The rigid body’s LinearDamping value has been tweaked to 0.75; previously, in a tight turn, the plane’s rotation appeared unrealistically tight. This looks a little better.
  • The plane’s collider visualisation (the semi-transparent sphere you could enable) has been removed.
  • Support functions have been to aerialcamera, which is the only place they’re used.

This post has probably been rather technical, but just look at the code itself and cross-reference here for anything you don’t quite ‘get’!

In the next post, I’ll demonstrate the power of Behaviours a little more clearly by adding bullets, created as Behaviours, placed in the world and then left to their own devices, with no additional management code necessary!

Monkeying Around with mojo3d: physics behaviours

Tuesday, December 11th, 2018

This post continues on from Monkeying Around with mojo3d: arcade plane physics.

island

Download the code and media below before continuing, and open up island.monkey2 in the default Monkey2 editor, Ted2Go:

IslandDemo source and media [4.6 MB; note that this is NOT a secure server, but the zip file contains only source code — text — and the 3D media. Run it through your virus scanner for peace of mind!]

Again, the intention of this post is that you scroll through the code at the same time, in order to see what each section is doing.

Tidying up the previous post

This time, we will look at control of the plane, via the PlaneBehaviour class… but let’s quickly get the application’s remaining OnRender method out of the way:


	Method OnRender (canvas:Canvas) Override
	
		If Keyboard.KeyHit (Key.Escape) Then App.Terminate ()
		
		RequestRender ()
		scene.Update ()
		
		camera.Update (plane)
		camera.Render (canvas)
		
		canvas.DrawText ("FPS: " + App.FPS, 0, 0)
		canvas.DrawText ("Y: " + plane.Entity.Y, 0, 20)
		
	End
	

Pretty simple — it checks if the Escape key has been hit, and exits the application if so.

It also asks mojo (the graphics engine) to render a new frame when it’s ready to do so.

The most important line for us here, which we’ll come back to in a sec, is:


		scene.Update ()

There follows a call to the AerialCamera’s update method, to which we pass the PlaneBehaviour object, and, from this, AerialCamera will obtain the entity and rigid body information necessary to visually track the plane. (This is irrelevant to the point of the post, and specific to the custom AerialCamera class.)

Finally, there’s a call to render the scene from this camera; the remaining two lines simply print some text on-screen.

Going back to scene.Update, this is in fact where all the physics magic happens!

Without scene.Update, no physics processing occurs, so it’s vital you call this if you want your physics objects to do anything.

(Note that the scene handle was set up at the very start of the OnCreateWindow method.)

Within scene.Update, mojo3d iterates through all of the 3D entities in the scene (models, lights, etc) and asks them to update themselves; it then runs the core physics processing against all rigid bodies in the world (which are typically attached to entities) and everything moves, falls and collides automatically.

That iteration through all scene entities, asking them to update themselves, is where Behaviours come in.

Behaviours

Behaviours provide a way to easily manage all physics-based entities in the scene, without needing to manually track or process them using lists or other collections.

Aside: Note that Monkey2’s Behaviour class, unlike its Color class, uses the non-US spelling!

For physics-based entities, you’ll generally create and position an entity (a model, camera, or whatever), attach a collider and rigid body and leave scene.Update to process and move the body within the scene, according to gravity or any other forces, such as collisions between objects.

If you want to then apply forces manually, you’ll need a way to track and manage all of these objects, such as a globally-accessible list, which would then also require manual iteration in order to visit each object and apply any relevant forces.

Not with Behaviours!

These are automatically managed by scene.Update; you just need to define a Behaviour class for any given object type and during scene.Update the Behaviour’s OnUpdate method will be called.

It means you can simply place a physics-based entity in the world and leave it to its own devices, with no additional management code.

Here’s the plane’s Behaviour class in full (minus comments):


Class PlaneBehaviour Extends Behaviour
	
	Property RollRate:Float ()
		Return roll_rate
	End
	
	Property PitchRate:Float ()
		Return pitch_rate
	End
	
	Field plane_body:RigidBody

	Field roll_rate:Float	= 500000.0
	Field pitch_rate:Float	= 200000.0

	Field throttle:Float	= 1000000.0
	
	Method New (entity:Entity)
		
		Super.New (entity)
		AddInstance ()

	End
	
	Method OnStart () Override

		plane_body = Entity.GetComponent <RigidBody> ()

		plane_body.ApplyImpulse (Entity.Basis * New Vec3f (0.0, 0.0, 500000.0))

	End
	
	Method OnUpdate (elapsed:Float) Override

		plane_body.ApplyForce (Entity.Basis * New Vec3f (0.0, 0.0, throttle))
		
		If Keyboard.KeyDown (Key.A)
			throttle = throttle + 10000.0
		Endif
		
		If Keyboard.KeyDown (Key.Z)
			throttle = throttle - 10000.0
		Endif
		
		If Keyboard.KeyDown (Key.Left)
			plane_body.ApplyTorque (Entity.Basis * New Vec3f (0.0, 0.0, RollRate))
		Endif

		If Keyboard.KeyDown (Key.Right)
			plane_body.ApplyTorque (Entity.Basis * New Vec3f (0.0, 0.0, -RollRate))
		Endif

		If Keyboard.KeyDown (Key.Up)
			plane_body.ApplyTorque (Entity.Basis * New Vec3f (PitchRate, 0.0, 0.0))
		Endif

		If Keyboard.KeyDown (Key.Down)
			plane_body.ApplyTorque (Entity.Basis * New Vec3f (-PitchRate, 0.0, 0.0))
		Endif
		
	End
	
End

Taking away the blank lines, that’s only about 40 lines of code; in fact, we could remove a further 6 lines of unnecessary code due to the properties, which I’ve just realised aren’t really doing anything useful here! The working lines of code here therefore total no more than 35 lines.

So, let’s take a closer look.

The properties here are redundant, frankly having been added too early on, and so the later references to both RollRate and PitchRate can simply be replaced with roll_rate and pitch_rate respectively.


Class PlaneBehaviour Extends Behaviour
	
	Property RollRate:Float ()
		Return roll_rate
	End
	
	Property PitchRate:Float ()
		Return pitch_rate
	End
	
	Field plane_body:RigidBody

	Field roll_rate:Float	= 500000.0
	Field pitch_rate:Float	= 200000.0

	Field throttle:Float	= 1000000.0
	
...

We hold a reference to the plane’s RigidBody — technically, this is not necessary, but saves a lot of typing later on!

The remaining fields define some hard-coded values later used when applying torques (rotational forces around an axis) and directional forces (those applied along an axis).

(Physicists will quite rightly point out that these fields are mis-named!)

The large values seen here are due to use of a real-world mass value when setting up the rigid body in the previous post, the Jaguar aircraft used as a physics reference weighing in at around 10,000 kg. (With the default mass of 1.0 kg given to a rigid body, you would normally be using values around 1/10,000th of the sizes shown here.)

Next up is the Behaviour’s New method:


	Method New (entity:Entity)
		
		Super.New (entity)
		AddInstance ()

	End
	

This is boilerplate code — just copy and paste! (It technically binds the Behaviour to the entity being passed in — the plane model in this case — and then places it in the scene.)

Note that New is passed in a 3D ‘entity’ upon creation — in this case, the entity is the plane model we loaded earlier. That entity then gains the physics behaviours defined here.

You can see New being called earlier in the code, with the plane model being passed in, after the physics collider and rigid body were set up:


plane = New PlaneBehaviour (plane_model)

Boilerplate out of the way, let’s move towards the interesting stuff!

OnStart

The OnStart method is called the first time scene.Update runs, and allows us to carry out a little initialisation:


	Method OnStart () Override

		' Storing this just for typing convenience...
		
		plane_body = Entity.GetComponent <RigidBody> ()

		' Give it a shove (instantly applied, which is
		' not physically possible), to get it started...
		
		plane_body.ApplyImpulse (Entity.Basis * New Vec3f (0.0, 0.0, 500000.0))

	End
	

Note that we defined the plane_body (RigidBody) field earlier; by assigning the entity’s rigid body here in the startup phase to a handy variable, it means we don’t later need to type the fairly complex GetComponent statement every time we need to access it.

Note: The GetComponent line refers to an Entity; note the capitalisation here. Behaviours hold a reference to the entity they were attached to, accessible via the Entity property.

Entities have a GetComponent method by which you can obtain a handle to any attached ‘components’ such as rigid bodies, and that’s what’s happening here — we’re obtaining a handle to the entity’s rigid body that we set up earlier.

So, from a Behaviour, you can:

  • obtain the entity it applies to, via Entity, and
  • from the resulting entity, obtain the physics body being operated upon, via GetComponent.

This is the magic that means you don’t need to manually track lists of objects in order to work upon them each frame!

Finally, the Start method applies an initial force to the plane’s physics body:


plane_body.ApplyImpulse (Entity.Basis * New Vec3f (0.0, 0.0, 500000.0))

Important! ApplyImpulse is a special case of force application that you shouldn’t normally use.

Forces, importantly, apply over time (physics students will note that force = mass x acceleration, acceleration being a difference in velocity over time). It’s physically impossible for a force to fully apply instantly in the real world.

The intent of this line is to start the plane moving immediately at a desired velocity, so we need the force to apply instantly. ApplyImpulse achieves this, but in most circumstances, where you would be repeatedly applying forces each frame, you should be using ApplyForce.

Being called within OnStart, this line is called only once, and so the impulse force applies only once, instantly applying 500,000 ‘Newtons’ of force along the plane’s z-axis by the time the first frame runs, giving our plane an instant speed.

Generally-speaking, you should use ApplyImpulse only once in order to instantly set the initial speed of an object — a bullet is a good example, though even that is technically not correct in the real world. (In reality, a very large force applies over a very short period of time.)

Note that all other forces in this demo are applied per-frame via ApplyForce or ApplyTorque in order to correctly apply over time.

If you find you need it, there is also an impulse equivalent for torque forces, ApplyTorqueImpulse — you could try adding this in OnStart to give the plane a starting roll rate, for example.

Entity.Basis and forces

Lastly, an important factor here is that reference to Entity.Basis and the 3D vector!

mojo3d’s physics are based upon a subset of the Bullet Physics SDK‘s functionality.

Bullet will apply forces according to the vector supplied (note the x, y and z values within New Vec3f), but always in relation to the orientation of the world itself.

That means that a force with z-value of 10 will always apply to zero degrees ‘north’ within the world, regardless of a physics body’s orientation.

Here’s a view of the plane, looking down from above:

topdown

If we apply a force with z-component of 10, the plane will not move in the intended (red) direction, but in the unintended (yellow) direction, because the force operates in world-space.

To apply the force relative to the plane’s own orientation, we multiply the force by the plane’s ‘basis’.

An entity’s Basis property is just a handy reference to its orientation — how it’s currently rotated in space.

Multiplying the force by the entity’s basis will allow the force to apply along the entity’s own axes; in this case, along its z-axis.

Important: Due to complex mathematical properties — ie. reasons I don’t pretend to understand — you must always place the basis first in this multiplication!

Yes:

Basis * Force Vector

No!

Force Vector * Basis

That’s all you need to know in order to correctly apply a force based upon an entity’s orientation.

Our plane happens to be pointing ‘north’ by default, so is pushed away from the camera along its intended path.

OnUpdate

The OnUpdate method is where the action really happens! As scene.Update iterates through the scene’s entities, it looks for any attached components (a Behaviour is a special case of Component) and calls their OnUpdate methods — and that’s the code we’re defining here.


	Method OnUpdate (elapsed:Float) Override

		' -Scene.GetCurrent ().World.Gravity.Y * 1.0

		plane_body.ApplyForce (Entity.Basis * New Vec3f (0.0, 0.0, throttle))
		
		If Keyboard.KeyDown (Key.A)
			throttle = throttle + 10000.0
		Endif
		
		If Keyboard.KeyDown (Key.Z)
			throttle = throttle - 10000.0
		Endif

...

Ignore the commented-out line referring to gravity.

Each frame, we start by applying the throttle force along the plane’s z-axis. Note again that this force vector is multiplied by the plane entity’s bundle of rotation information, Entity.Basis, to ensure it operates along the plane’s own z-axis and not that of the world.

Next we check for two keys, A and Z, having been pressed, and amend the throttle force up or down by 10,000 Newtons — an amount determined by simple trial-and-error.

Next time this method is called, the new throttle value will apply.

We’re applying this force every frame, so why doesn’t the plane accelerate off out of control? It’s kept in check by the body’s LinearDamping value defined in the previous post. Rotational forces are similarly restricted by AngularDamping. For our purposes, these values therefore have an effect similar to air resistance.

Following this, we check the cursor keys and apply torque — loosely speaking, rotational force — around the relevant axes.

Left and right cursors operate around the z-axis (the nose-to-tail axis of the plane). These are also multiplied by the plane’s rotational Basis information, as they would otherwise operate according to the orientation of the world itself:


		If Keyboard.KeyDown (Key.Left)
			plane_body.ApplyTorque (Entity.Basis * New Vec3f (0.0, 0.0, RollRate))
		Endif

		If Keyboard.KeyDown (Key.Right)
			plane_body.ApplyTorque (Entity.Basis * New Vec3f (0.0, 0.0, -RollRate))
		Endif

And finally, the up and down cursors operate on the plane’s x-axis — the left-to-right axis along the wings:


		If Keyboard.KeyDown (Key.Up)
			plane_body.ApplyTorque (Entity.Basis * New Vec3f (PitchRate, 0.0, 0.0))
		Endif

		If Keyboard.KeyDown (Key.Down)
			plane_body.ApplyTorque (Entity.Basis * New Vec3f (-PitchRate, 0.0, 0.0))
		Endif
		

That’s it! A simple physics-based arcade plane!

(The remainder of the code consists of nothing more than a few support functions, most, if not all, of which should really be moved into the AerialCamera class where they’re used!)

In the real world, you probably wouldn’t want to have the collider and rigid body setup carried out in the main body of the code, but instead do this within the OnStart method of the Behaviour so as to encapsulate all of the plane’s data.

As the New method of Behaviour only allows the passing-in of an entity parameter, this can make things a little tricky, so I’ll address my solution to this in the next post.

Happy flying!

Monkeying Around with mojo3d: arcade plane physics

Sunday, December 9th, 2018

This is a very basic demo that uses physics to create a simple arcade-oriented plane you can fly around an island. It uses two CC0-licensed models from Google Poly and looks something like this:

island

Download the code and media below before continuing, and open up island.monkey2 in the default Monkey2 editor, Ted2Go:

IslandDemo source and media [4.6 MB; note that this is NOT a secure server, but the zip file contains only source code — text — and the 3D media. Run it through your virus scanner for peace of mind!]

The controls are the cursor keys, plus A and Z for throttle. You can fly around the island and bounce off it, nothing more involved than that.

Note that the demo doesn’t implement proper flight dynamics, so you can’t stall, and basically will fly in a straight line, with minimal impact from gravity, unless actively controlling it. You can even fly backwards if you slow down enough! While it won’t make flight simulator fans happy, it is almost enough to create a Pilotwings-style arcade game. With some effort, simple stall behaviour could no doubt be implemented.

The intention of this post is that you scroll through the code at the same time, in order to see what each section is doing.

Importing modules

The code starts by importing a few standard monkey2 modules: std, mojo, mojo3d, and mojo3d-loaders (the latter to allow loading of the unaltered GLTF-format 3D models straight from Google Poly). It then declares that we are ‘using’ them, in order to avoid typing full paths to functions, etc. (So instead of mojo3d.Model, for instance, we can just type Model.)


#Import "<std>"
#Import "<mojo>"
#Import "<mojo3d>"
#Import "<mojo3d-loaders>"

#Import "aerialcamera"
#Import "assets/"

Using std..
Using mojo..
Using mojo3d..

#Import “aerialcamera” here refers to a single external .monkey2 file, importing the AerialCamera class defined there — this has been separated out as it’s rather complex and made the code appear too cluttered, and isn’t relevant to the core point of this post. (It’s also rather hacky.)

The IslandDemo class

Next comes the main IslandDemo class, which implements the core application. In Monkey2, mojo3d applications extend the Window class and define the various built-in methods required by the Window class.

Note that the program starts in the Main function, which has been moved to the end of the code in this case, as it’s pretty uninteresting, simply setting up the core application and calling IslandDemo.New, followed by App.Run to begin.

We start out with a few constants and necessary fields:


Class IslandDemo Extends Window

' Set this to False if it runs slowly on low-end devices!

Const GROUND_SHADOWS:Bool        = True

Const WINDOW_WIDTH:Int            = 640
Const WINDOW_HEIGHT:Int            = 480
Const WINDOW_FLAGS:WindowFlags    = WindowFlags.Resizable

'    Const WINDOW_WIDTH:Int            = 1920
'    Const WINDOW_HEIGHT:Int            = 1080
'    Const WINDOW_FLAGS:WindowFlags    = WindowFlags.Fullscreen

Field scene:Scene
Field camera:AerialCamera
Field plane:PlaneBehaviour

...

If you find the application runs slowly on a low-end device, such as a typical laptop, try changing GROUND_SHADOWS to False — the extensive self-shadowing can cause performance problems on such devices.

You can comment-out the existing window width/height/flags values and uncomment those below to create a full-screen 1080p display.

We also define a few fields for the mojo3d scene, our aerial camera and a set of physics behaviours for the plane.

The New method simply sets up the application’s window.

Scene setup

Next up is the OnCreateWindow method, which is where media gets loaded and everything is set up ready for the game to start. We start with the 3D scene:


	Method OnCreateWindow () Override

		' ---------------------------------------------------------------------
		' Scene setup:
		' ---------------------------------------------------------------------
		' Gets a reference to the mojo3d default scene and sets a few options...
		' ---------------------------------------------------------------------
		
		scene									= Scene.GetCurrent ()

			scene.ClearColor					= New Color (0.2, 0.6, 1.0)
			scene.AmbientLight					= scene.ClearColor * 0.25
			scene.FogColor						= scene.ClearColor
			scene.FogNear						= 128
			scene.FogFar						= 2048

...

Here we set up the basic scene parameters, obtaining a handle to the default scene for ease of reference, then setting up the background colour, ambient light colour, fog colour and where the fog starts and ends relative to the camera.

The camera

Next up is setting up a handle to an aerial camera, which simply follows the plane around the scene. AerialCamera is a class defined in aerialcamera.monkey2 and just makes for a more interesting view of the scene than attaching a fixed camera. Its implementation isn’t relevant here, and frankly even I don’t fully understand how it works… and I wrote it! (Good old trial and error.)

The Sun

Then we set up a simple light, tell it to cast shadows in the scene (this will then affect any entity whose CastsShadow property is True). It’s angled to always cast a diagonal light upon the scene.

The default light type is ‘directional’, which means that it casts a universal wall of light across everything in the scene, regardless of where it’s positioned. Just think of it as the Sun!


		' ---------------------------------------------------------------------
		' Light setup:
		' ---------------------------------------------------------------------
		' Creates a light representing the Sun. The default light type is directional,
		' a universal 'wall' of light pointing in a given direction...
		' ---------------------------------------------------------------------
		
		Local light:Light						= New Light

			light.CastsShadow					= True
	
			light.Rotate (45, 45, 0)
		
...

The ground

Following this is the ground setup:


		' ---------------------------------------------------------------------
		' Ground model and physics setup:
		' ---------------------------------------------------------------------
		' Loads an external model and adds a physics collider and rigid body...
		' ---------------------------------------------------------------------
		
		Local ground_size:Float					= 4096 * 0.5
		Local ground_box:Boxf					= New Boxf (-ground_size, -ground_size * 0.5, -ground_size, ground_size, 0, ground_size)
		Local ground_model:Model				= Model.Load ("asset::model_gltf_6G3x4Sgg6iX_7QCCWe9sgpb\model.gltf")'CreateBox( groundBox,1,1,1,groundMaterial )

			ground_model.CastsShadow			= GROUND_SHADOWS
			
			ground_model.Mesh.FitVertices (ground_box, False)
			
			For Local mat:Material = Eachin ground_model.Materials
				mat.CullMode = CullMode.Back	
			Next

		Local ground_collider:MeshCollider		= ground_model.AddComponent <MeshCollider> ()

			ground_collider.Mesh				= ground_model.Mesh
		
		Local ground_body:RigidBody				= ground_model.AddComponent <RigidBody> ()

			ground_body.Mass					= 0
	
...

Breaking this down, we are loading a model, located within a sub-folder of the assets folder and scaling it to fit within a given volume:


		Local ground_size:Float					= 4096 * 0.5
		Local ground_box:Boxf					= New Boxf (-ground_size, -ground_size * 0.5, -ground_size, ground_size, 0, ground_size)
		Local ground_model:Model				= Model.Load ("asset::model_gltf_6G3x4Sgg6iX_7QCCWe9sgpb\model.gltf")'CreateBox( groundBox,1,1,1,groundMaterial )

			ground_model.CastsShadow			= GROUND_SHADOWS
			
			ground_model.Mesh.FitVertices (ground_box, False)
			
			For Local mat:Material = Eachin ground_model.Materials
				mat.CullMode = CullMode.Back	
			Next

...

The size of the ground, 4096 metres by 4096 metres, was decided by simply reloading and trying different values until the buildings looked ‘about right’ relative to the plane. (The ground model by default appears very small within the scene.)

A Boxf object (a set of bounds defining a box, using floating-point values) defines the area it will fit within. The size is halved when it’s defined, as we scale in each direction, therefore half to the left, half to the right, and so on.

Model.Load is used to load the .gltf-format model (note that this will only work if mojo3d-loaders is imported). The “asset::” prefix refers to the assets folder.

Then we set up shadows according to the True/False status of the GROUND_SHADOWS constant defined earlier and fit the vertices (3D points) of the model’s Mesh (the raw 3D data) to our box.

The False parameter of FitVertices allows us to override the default aspect ratios of the mesh.

We then iterate through the model’s materials — groupings of colours and/or textures — and enable backface-culling. (From memory, the model had front- and back-facing triangles — or perhaps it was just the plane, and I applied it to both!)

The ground’s physics

Next, we set up the ground’s physics:


		Local ground_collider:MeshCollider		= ground_model.AddComponent <MeshCollider> ()

			ground_collider.Mesh				= ground_model.Mesh
		
		Local ground_body:RigidBody				= ground_model.AddComponent <RigidBody> ()

			ground_body.Mass					= 0
	
...

Important: To enable physics on an entity, we need a collider, which defines the shape used for collision detection, and a rigid body, which defines the parameters used to physically control the entity.

In this case, the type of collider is MeshCollider, which uses the raw triangles of the underlying mesh. This can be slow and should be avoided where possible in favour of rougher approximations, such as spheres and cylinders. However, with a terrain like this, nothing else will do.

Because they are so ‘expensive’, mesh colliders will only work for static objects in the scene.

We point the mesh collider’s Mesh field to the model’s own mesh, from which it will be automatically set up.

We also set up the ground’s rigid body, which holds its motion and collision response parameters. In this case, all we need is to set the mass to zero — this means that the object will be fixed in place, its position unaffected by gravity, collisions, etc.

Note that both the collider and the rigid body are ‘added’ to the entity (the ground model) as ‘components’ — like bolting on physics responses to a model that would otherwise have to be manually-controlled and would simply pass through other models.

The plane

Next up, the plane itself!


		Local plane_size:Float					= 16.83 * 0.5
		Local plane_box:Boxf					= New Boxf (-plane_size, -plane_size, -plane_size, plane_size, plane_size, plane_size)
		
		Local plane_model:Model					= Model.Load ("asset::1397 Jet_gltf_3B3Pa6BHXn1_fKZwaiJPXpf\1397 Jet.gltf")

			plane_model.Mesh.FitVertices (plane_box)
			plane_model.Mesh.Rotate (0, 180, 0)
			
			plane_model.Move (0.0, 5.0, 0.0)

		' Cull hidden tris...
		
		For Local mat:Material = Eachin plane_model.Materials
			mat.CullMode = CullMode.Back	
		Next

...

Again, we create a box within the plane will be fitted. In this case, I’ve used the wingspan of a real-world plane (the SEPECAT Jaguar) to define the widest point of the box, and (unlike the ground box) have left out the last parameter to FitVertices, which is True by default, in order to retain the model’s aspect ratios automatically. (This means it won’t look ‘squished’ in any direction, even if I don’t know the correct values for each.)

We load the plane and scale to fit within its box.

However! This model faces towards the camera when loaded, so the plane would be flying backwards. For reasons too complex to go into, simply rotating the model (the collection of 3D mesh, texture and other data) won’t work with the rigid body — the plane initially flew backwards.

Near the end of the source code is a class extension (“Class Mesh Extension”) that implements a way to rotate the raw 3D mesh while retaining the orientation of the model. Hopefully something of this nature will be added to mojo3d in the future!

The model is then positioned at its starting point. In this case, it was probably unnecessary to do so, the code to move upwards by 5 units left in place in error, but it’s important to note that you need to position a model correctly prior to adding any physics colliders or rigid bodies. You can’t manually re-position them after creation without causing problems with the physics processing.

Again, backface culling is enabled throughout the model’s materials. (If you’re creating your own models, you can avoid creating front-and-back triangles in your modeller, and avoid this step, but for models created by others you can’t control this, other than manually-editing the model yourself.)

The plane’s physics

Similar to the ground’s physics setup, we create a collider and a rigid body:


		' Physics setup...
		
		Local plane_collider:SphereCollider		= plane_model.AddComponent <SphereCollider> ()

			plane_collider.Radius				= plane_size * 0.6
		
		Local plane_body:RigidBody				= plane_model.AddComponent <RigidBody> ()

			plane_body.Mass						= 10954.0
			plane_body.Restitution				= 0.5
			plane_body.AngularDamping			= 0.9
			plane_body.LinearDamping			= 0.5
			plane_body.Friction					= 0.0

...

In this case, the collider is a SphereCollider, and we set its Radius parameter according to what we need. (This is simply the radius at which it will collide with other physics objects.)

The plane’s body is more interesting, as this time it’s not a static body with mass of 0, but intended to be fully dynamic and affected by physical forces and collisions.

The mass here is set to the real-world mass of the Jaguar, measured in kilograms. This isn’t strictly necessary: originally the plane flew with a default mass of 1 kg and controlled in exactly the same way, with the forces that are later applied being scaled down 10,000 times!

It’s more important that dynamic bodies in the scene have correct relative values so that they appear to bounce off each other correctly.

A few other properties are defined:

  • Restitution: This simply defines how bouncy an object is, in a range typically from 0 to 1. A value of 1 will bounce away from another object at exactly the same velocity it hit with; a value of 0.5 will bounce away with half the velocity, etc. Note that if the other body’s restitution is another value, that will also affect the bounce response.
  • AngularDamping: By default, with a value of zero, any rotation forces applied (known as torque) will leave the object rotating forever, until counteracted with a torque in the opposite direction. By setting AngularDamping to higher values (up to 1), you can have the object’s rotation reduce automatically. The plane is very hard to control without damping — try setting it to zero, and some other values in-between!
  • LinearDamping: This works in similar fashion to AngularDamping, but on forces operating directionally (pushing and pulling forces). With LinearDamping set to zero, the object continues in a given direction until counteracted; higher values cause the object to slow down gradually. If you set this to zero, you’ll find that when you turn the plane around, it continues flying in the direction it was travelling (ie. backwards!).
  • Friction: This is used to decide how the object is slowed by grazing another object. It’s set to zero here so that when colliding with the ground, the underlying sphere doesn’t rotate as a result of the collision, as a real-world ball would. Try setting this to 0.5 and clipping the ground — the plane will tumble end-over-end. (More realistic, but less fun while simply flying around.)

Try playing around with all of these values — you can always set them back to those shown above to regain the original behaviour.

(You may also want to play with the pitch_rate, roll_rate and throttle parameters defined in PlaneBehaviour, as these can counteract the effects of changing the above values.)

The plane’s physics behaviour and collision visualisation

Next, we set up the plane model’s physics behaviour — defined in the PlaneBehaviour class — which gives us a way to modify the plane’s movement and physics responses on each frame, and create a simple model for visualisation of the physics sphere collider, by parenting a semi-transparent sphere model, of the same radius as the sphere collider, to the plane model. This is disabled by default — try setting the coll_vis variable to True and running the program. You can tweak plane_collider.Radius (defined earlier) to see how it looks and responds with different radii.

Finally, we hide the mouse pointer from view.


		plane									= New PlaneBehaviour (plane_model)

		' Debug sphere for visibility of collision radius only...
		
		Local coll_vis:Bool = False
		
		If coll_vis

			' Create a sphere model used to represent the collision sphere; hidden by default...
	
			Local plane_collider_vis:Model					= Model.CreateSphere (plane_collider.Radius, 16, 16, New PbrMaterial (Color.White), plane_model)
	
				plane_collider_vis.Alpha					= 0.25
		
		End
		
		' Hide mouse pointer...
		
		Mouse.PointerVisible					= False
		
...

PlaneBehaviour is where the plane’s controls are defined. More on Behaviours in the next post!

Happy flying!