ColMesh - 3D Collisions Made Easy!
License
The ColMesh system is licensed under a CreativeCommons Attribution 4.0 International License.
This means you are free to use it in both personal and commercial projects, free of charge.
Appropriate credit is required.
What is a ColMesh?
A ColMesh is a collection of 3D primitives and triangle meshes against which you can cast rays and do collision checks. It is basically an easy-to-use 3D collision system for GMS 2.3.
What does it do?
It will push your player out of level geometry.
It handles slope calculations for you so that your player doesn’t slide down slopes.
It lets you move platforms around and gives you everything you need to make sure your player moves the same way.
It lets you cast rays so that your player can shoot bullets or laser beams, and by casting a ray from the player’s previous coordinate to the new coordinate before doing collision checking, you can make sure the player never falls through level geometry.
How does it do it?
It’s all GML, so it should work on any platform. Any object that should check for collisions against level geometry must be represented by either a sphere or a capsule.
The level can be subdivided into smaller regions so that your player only has to check for collisions with nearby primitives.
For the actual collisions it uses this thing, you might have heard of it, it's called math.
But why would I make a 3D game in GameMaker?
That’s entirely up to you. There are better tools out there, with branch standard 3D physics dlls and the like.
Only use GM for 3D games if you really wanna use GM for 3D games
And why did you make this tool?
Because I really wanna use GM for 3D games.
What you see here is a ColMesh that has been placed inside itself, causing a recursive effect. Notice how collisions even works properly against the smaller ColMeshes
Okay!
With that out of the way, let’s get into how you can actually use this tool.
Download it from here:
Join Discord channel
Executable demo
Version 1 on the Marketplace
Version 2.0.14 download
The zip contains an executable demo that has been compiled with the YYC. Since all the calculations are done CPU-side, the collision system sees a massive boost when compiling with YYC compared to VM.
Getting started
No initialization necessary, just import the ColMesh scripts, and it’s ready to be used.
Create a new ColMesh like this:
This creates an empty ColMesh. Now populate it with some geometry! There are a bunch of primitives that can be used.
For now, let’s just add a single triangle mesh by giving it the path to an OBJ file:
Now it is already possible to perform collision checks and cast rays against this ColMesh, but if it contains a lot of geometry, that will be very slow.
It’s a better idea to subdivide it into smaller regions first! In order to subdivide a model, you need to supply the desired region size. A region
will always be an axis-aligned bounding cube, and regionSize is the side length of this cube. The smaller the region size, the less geometry will be in
each region, but the more regions and more memory usage there will be. Finding the balance between efficiency and memory usage is its own art!
But in short, subdivide your model like this, and then you’re ready to collide with it:
Colliding with the ColMesh
A popular way of doing 2D collisions in GameMaker is to basically step towards the target position, checking for collisions at each step and exiting the loop if it finds a collision.
In contrast, the ColMesh system requires you to move your collider first, and then displace it out of the level geometry.
In the player's create event, you can create a collider capsule. Only capsule colliders are supported at this time. Give the capsule some properties like radius and height, an up vector, and the maximum slope angle that can be walked on without sliding down:
SlopeAngle is given in degrees. If set to 0, the capsule will slide on all surfaces. Setting it to 90 degrees will break your collisions, divide by zero and possibly end the universe. But anything between 0 and 90 will make your player not slide.
There are a couple of optional arguments to this constructor function as well:
[precision]: The maximum number of times to repeat collision calculations for this capsule. If the algorithm is sure it has found the optimal place of the capsule it will exit early. This is by default set to 1, which means it will first sort the shapes it collides with, and then perform the collisions that displace the capsule the most first. Setting precision to 0 will skip the sorting step, providing faster collisions at the cost of precision. You can set the precision to 10 if you'd like, and it'll still run mostly the same, but the algorithm will have more room to adjust the position of the capsule.
[mask]: This is an advanced feature letting you define which collision groups this capsule should check for collisions with. It'll compare the mask of the collider to the group of each shape, and only perform collisions if they match.
In the player's step event, we can make the player move while avoiding the colmesh. I like using verlet integration for player movement, this is shown in all the included demos. Assuming you've already made your player move this step, this is how you can then make it avoid the colmesh:
And that's it, now the player is being displaced out of the colmesh!
How to add primitives
At the moment, these are the supported primitives that can be put into a ColMesh, in addition to triangle meshes:
addShape returns the handle of the new shape. In addition, all shape constructor functions have an optional argument [group], which lets you define the collision groups of this shape. More on this later.
If you want your primitives to move, you should add what’s called a dynamic. What’s that? Well, just keep reading…
Dynamic shapes
If you want your objects to move, you should add them as “dynamics”. A dynamic is a container for a primitive, or even a whole ColMesh, that can be moved around inside the ColMesh.
Adding new dynamics to a ColMesh is a pretty similar process to adding regular primitives:
Notice how the cube itself is added to local coordinates (0, 0, 0), and then its world-space position is stored in a matrix called M. This will center the cube at (x, y, z),
and if you apply any rotations, it will rotate around its own center. If you had done it the other way, adding the cube to (x, y, z) and setting the matrix coordinates to (0, 0, 0),
the pivot point of the cube will be at the world-space (0, 0, 0), which in most cases will not make sense.
Here’s how you can make it move. This little piece of code will make the cube rotate around the worldspace Y axis:
Now the cube is rotating, but the player will not rotate with it just yet. We need a way to figure out how the cube the player is standing on has moved, rotated, or been scaled since the previous step.
And for that, we have this useful function:
This will transform the player’s coordinates in the same manner that the cube it’s standing on has been transformed, making the player move and rotate with the cube.
Dynamics can also be used to store more complex objects by reference, instead of adding all their subshapes to the colmesh every time. Say for example you want to create a forest with a bunch of trees that all have the same collision shape, only rotated and scaled slightly differently. In this case, you can make another colmesh for just the tree, subdivide it as usual, and then add it by reference to the level colmesh with a dynamic:
Casting rays
You can cast rays at the ColMesh using ColMesh.castRay(x1, y1, z1, x2, y2, z2). This function will always return a struct (whereas in previous versions it would only return a struct if the ray hit anything).
The returned struct is pretty similar to what you get when displacing a capsule out of the ColMesh.
ray.hit is true if there was an intersection, or false if not
ray.x/y/z is the point of intersection
ray.nx/ny/nz is the normal at the point of intersection
You’ll often want to cast a ray from the player’s previous position to its current position, just to make sure it never passes through level geometry. Like this:
This piece of code assumes that the player is spherical. It doesn’t entirely make sense for a capsule though… We may need a smarter solution for a capsule.
We can use the speed vector of the player to figure out the best point along the capsule for which to perform a ray cast. If the player is moving downwards, it makes the most sense to cast from its feet. If it’s moving upwards, we’ll most likely want to prevent its head from going through the ceiling. As such, we can slide the origin of the ray along the capsule like this:
A ray cast should usually be followed by displacing the capsule properly out of the level geometry with collider.avoid(colmesh)
Trigger objects
So far we've only covered solid objects.
But a 3D world also contains non-solid objects, for example collectible objects like coins and powerups, and trigger objects like doors and portals.
You can add this kind of trigger objects as follows:
Notice how you have two optional arguments here. These allow you to control what should happen when the player collides with the shape, or when it is hit by a ray.
GMS 2.3 allows you to pass functions by reference. That means that you can create custom functions that should be executed instead of the regular collision response.
Demo 1 showcases how you can add collectible coins to a 3D level. The coins will send their collision shape a function that will increment global.coins by 1, play a sound, delete the coin object and remove the shape from the ColMesh.
The code for this looks like this:
There has also been added a new optional argument to colmesh.displaceCapsule, that allows you to turn on or off collision functions. When turned off, the custom collision functions are not executed.
This is useful for objects that should only collide with the world and not trigger game objects.
Non-solid objects are NOT saved when using colmesh.save or colmesh.writeToBuffer.
Saving and loading
You’ll typically want to precompute your ColMeshes. Generating and subdividing a ColMesh can be a slow process, so luckily you can easily save and load them to and from buffers.
If you don’t want to include the precomputed ColMeshes, you could generate them the first time they’re needed, and then cache them in local files like this:
If your game should be able to run in HTML, you can’t use the ColMesh.save and load functions. Instead, you should save and load a buffer asynchronously, and use ColMesh.writeToBuffer and ColMesh.readFromBuffer.
Planned features
These are some shapes that are planned:
Thank you!
Feedback is always appreciated.
Snidr
License
The ColMesh system is licensed under a CreativeCommons Attribution 4.0 International License.
This means you are free to use it in both personal and commercial projects, free of charge.
Appropriate credit is required.
What is a ColMesh?
A ColMesh is a collection of 3D primitives and triangle meshes against which you can cast rays and do collision checks. It is basically an easy-to-use 3D collision system for GMS 2.3.
What does it do?
It will push your player out of level geometry.
It handles slope calculations for you so that your player doesn’t slide down slopes.
It lets you move platforms around and gives you everything you need to make sure your player moves the same way.
It lets you cast rays so that your player can shoot bullets or laser beams, and by casting a ray from the player’s previous coordinate to the new coordinate before doing collision checking, you can make sure the player never falls through level geometry.
How does it do it?
It’s all GML, so it should work on any platform. Any object that should check for collisions against level geometry must be represented by either a sphere or a capsule.
The level can be subdivided into smaller regions so that your player only has to check for collisions with nearby primitives.
For the actual collisions it uses this thing, you might have heard of it, it's called math.
But why would I make a 3D game in GameMaker?
That’s entirely up to you. There are better tools out there, with branch standard 3D physics dlls and the like.
Only use GM for 3D games if you really wanna use GM for 3D games
And why did you make this tool?
Because I really wanna use GM for 3D games.
What you see here is a ColMesh that has been placed inside itself, causing a recursive effect. Notice how collisions even works properly against the smaller ColMeshes
Okay!
With that out of the way, let’s get into how you can actually use this tool.
Download it from here:
Join Discord channel
Executable demo
Version 1 on the Marketplace
Version 2.0.14 download
The zip contains an executable demo that has been compiled with the YYC. Since all the calculations are done CPU-side, the collision system sees a massive boost when compiling with YYC compared to VM.
Getting started
No initialization necessary, just import the ColMesh scripts, and it’s ready to be used.
Create a new ColMesh like this:
Code:
levelColmesh = new colmesh();
For now, let’s just add a single triangle mesh by giving it the path to an OBJ file:
Code:
levelColmesh.addMesh(“ColMesh Demo/Demo1Level.obj”);
It’s a better idea to subdivide it into smaller regions first! In order to subdivide a model, you need to supply the desired region size. A region
will always be an axis-aligned bounding cube, and regionSize is the side length of this cube. The smaller the region size, the less geometry will be in
each region, but the more regions and more memory usage there will be. Finding the balance between efficiency and memory usage is its own art!
But in short, subdivide your model like this, and then you’re ready to collide with it:
Code:
var regionSize = 120; //120 is a magic number I chose that fit well for my player size and level complexity. It may have to be different for your game!
levelColmesh.subdivide(regionSize);
Colliding with the ColMesh
A popular way of doing 2D collisions in GameMaker is to basically step towards the target position, checking for collisions at each step and exiting the loop if it finds a collision.
In contrast, the ColMesh system requires you to move your collider first, and then displace it out of the level geometry.
In the player's create event, you can create a collider capsule. Only capsule colliders are supported at this time. Give the capsule some properties like radius and height, an up vector, and the maximum slope angle that can be walked on without sliding down:
Code:
collider = new colmesh_collider_capsule(x, y, z, xup, yup, zup, radius, height, slopeAngle);
There are a couple of optional arguments to this constructor function as well:
[precision]: The maximum number of times to repeat collision calculations for this capsule. If the algorithm is sure it has found the optimal place of the capsule it will exit early. This is by default set to 1, which means it will first sort the shapes it collides with, and then perform the collisions that displace the capsule the most first. Setting precision to 0 will skip the sorting step, providing faster collisions at the cost of precision. You can set the precision to 10 if you'd like, and it'll still run mostly the same, but the algorithm will have more room to adjust the position of the capsule.
[mask]: This is an advanced feature letting you define which collision groups this capsule should check for collisions with. It'll compare the mask of the collider to the group of each shape, and only perform collisions if they match.
In the player's step event, we can make the player move while avoiding the colmesh. I like using verlet integration for player movement, this is shown in all the included demos. Assuming you've already made your player move this step, this is how you can then make it avoid the colmesh:
Code:
//Make sure the collider has the same coordinates as the player
collider.x = x;
collider.y = y;
collider.z = z;
//Make the collider avoid the colmesh
collider.avoid(levelColmesh);
//Copy the coordinates of the collider back to the player
x = collider.x;
y = collider.y;
z = collider.z;
How to add primitives
At the moment, these are the supported primitives that can be put into a ColMesh, in addition to triangle meshes:
- Sphere
- Axis-aligned cube
- Block (can have any orientation and non-uniform scaling, but not shear)
- Capsule
- Cylinder
- Torus
- Disk
Code:
levelColmesh.addShape(new colmesh_sphere(x, y, z, radius));
levelColmesh.addShape(new colmesh_cube(x, y, z, sideLength));
levelColmesh.addShape(new colmesh_block(colmesh_matrix_build(x, y, z, 0, 0, 0, xSize / 2, ySize / 2, zSize / 2))); //The cube will be twice as big as the given scale
levelColmesh.addShape(new colmesh_capsule(x, y, z, xup, yup, xup, radius, height));
levelColmesh.addShape(new colmesh_cylinder(x, y, z, xup, yup, xup, radius, height));
levelColmesh.addShape(new colmesh_torus(x, y, z, xup, yup, xup, majorRadius, minorRadius));
levelColmesh.addShape(new colmesh_disk(x, y, z, xup, yup, xup, majorRadius, minorRadius));
If you want your primitives to move, you should add what’s called a dynamic. What’s that? Well, just keep reading…
Dynamic shapes
If you want your objects to move, you should add them as “dynamics”. A dynamic is a container for a primitive, or even a whole ColMesh, that can be moved around inside the ColMesh.
Adding new dynamics to a ColMesh is a pretty similar process to adding regular primitives:
Code:
M = colmesh_matrix_build(x, y, z, 0, 0, 0, 1, 1, 1);
dynamic = levelColmesh.addDynamic(new colmesh_cube(0, 0, 0, size), M);
and if you apply any rotations, it will rotate around its own center. If you had done it the other way, adding the cube to (x, y, z) and setting the matrix coordinates to (0, 0, 0),
the pivot point of the cube will be at the world-space (0, 0, 0), which in most cases will not make sense.
Here’s how you can make it move. This little piece of code will make the cube rotate around the worldspace Y axis:
Code:
M = colmesh_matrix_build(x, y, z, 0, current_time / 50, 0, 1, 1, 1);
shape.setMatrix(M, true);
And for that, we have this useful function:
Code:
collider.avoid(levelColmesh);
var D = collider.getDeltaMatrix();
if (is_array(D))
{
var P = matrix_transform_vertex(D, x, y, z);
x = P[0];
y = P[1];
z = P[2];
}
Dynamics can also be used to store more complex objects by reference, instead of adding all their subshapes to the colmesh every time. Say for example you want to create a forest with a bunch of trees that all have the same collision shape, only rotated and scaled slightly differently. In this case, you can make another colmesh for just the tree, subdivide it as usual, and then add it by reference to the level colmesh with a dynamic:
Code:
//Create a colmesh for just the tree mesh, and subdivide it
treeColmesh= new colmesh();
treeColmesh.addMesh("Tree.obj");
treeColmesh.subdivide(regionSize);
//Add multiple dynamics referencing the treecolmesh to the level
repeat 10
{
var M = matrix_build(random(room_width), random(room_height), 0, 0, 0, random(360), 1, 1, 1); //Build a matrix for a random position in the room
var shape = levelColmesh.addDynamic(treeColmesh, M);
//Now the tree colmesh has been added by reference, instead of being copied over. Another useful fact here is that now each tree can have their own trigger functions.
}
You can cast rays at the ColMesh using ColMesh.castRay(x1, y1, z1, x2, y2, z2). This function will always return a struct (whereas in previous versions it would only return a struct if the ray hit anything).
The returned struct is pretty similar to what you get when displacing a capsule out of the ColMesh.
ray.hit is true if there was an intersection, or false if not
ray.x/y/z is the point of intersection
ray.nx/ny/nz is the normal at the point of intersection
You’ll often want to cast a ray from the player’s previous position to its current position, just to make sure it never passes through level geometry. Like this:
Code:
//Cast a short-range ray from the previous position to the current position to avoid going through geometry
ray = levelColmesh.castRay(prevX, prevY, prevZ, x, y, z);
if (ray.hit)
{
//If the ray hit anything, move the player to just outside the point of intersection, and then perform capsule collision as usual
x = ray.x + ray.nx * .1;
y = ray.y + ray.ny * .1;
z = ray.z + ray.nz * .1;
}
We can use the speed vector of the player to figure out the best point along the capsule for which to perform a ray cast. If the player is moving downwards, it makes the most sense to cast from its feet. If it’s moving upwards, we’ll most likely want to prevent its head from going through the ceiling. As such, we can slide the origin of the ray along the capsule like this:
Code:
var d = height * (.5 + .5 * sign(xup * (x - prevX) + yup * (y - prevY) + zup * (z - prevZ)));
var dx = xup * d;
var dy = yup * d;
var dz = zup * d;
ray = levelColmesh.castRay(prevX + dx, prevY + dy, prevZ + dz, x + dx, y + dy, z + dz);
if (ray.hit)
{
//If the ray hit anything, move the player to just outside the point of intersection, and then perform capsule collision as usual
x = ray.x + ray.nx * .1;
y = ray.y + ray.ny * .1;
z = ray.z + ray.nz * .1;
}
Trigger objects
So far we've only covered solid objects.
But a 3D world also contains non-solid objects, for example collectible objects like coins and powerups, and trigger objects like doors and portals.
You can add this kind of trigger objects as follows:
Code:
shape = levelColmesh.addTrigger(shape, solid, colFunc*, rayFunc*);
GMS 2.3 allows you to pass functions by reference. That means that you can create custom functions that should be executed instead of the regular collision response.
Demo 1 showcases how you can add collectible coins to a 3D level. The coins will send their collision shape a function that will increment global.coins by 1, play a sound, delete the coin object and remove the shape from the ColMesh.
The code for this looks like this:
Code:
//Create a collision function for the coin, telling it to destroy itself and remove its shape from the level ColMesh
colFunc = function()
{
global.coins ++; //Increment the global variable "coins"
instance_destroy(); //This will destroy the current instance of oCoin
levelColmesh.removeShape(shape); //"shape" is oCoin's shape variable. Remove it from the ColMesh
audio_play_sound(sndCoin, 0, false); //Play coin pickup sound
}
//Create a spherical collision shape for the coin
shape = new colmesh_sphere(x, y, z, radius)
//Add the coin to the ColMesh as a trigger
//The collision function will be executed if the player collides with the coin, using colmesh.displaceCapsule.
levelColmesh.addTrigger(shape, colFunc);
This is useful for objects that should only collide with the world and not trigger game objects.
Non-solid objects are NOT saved when using colmesh.save or colmesh.writeToBuffer.
Saving and loading
You’ll typically want to precompute your ColMeshes. Generating and subdividing a ColMesh can be a slow process, so luckily you can easily save and load them to and from buffers.
If you don’t want to include the precomputed ColMeshes, you could generate them the first time they’re needed, and then cache them in local files like this:
Code:
levelColmesh = new colmesh();
if !(levelColmesh.load("ColMeshCache.cm"))
{
//If a cache does not exist, generate a colmesh from an OBJ file, subdivide it, and save a cache
levelColmesh.addMesh("Level.obj");
levelColmesh.subdivide(130);
levelColmesh.save("ColMeshCache.cm");
}
Planned features
These are some shapes that are planned:
- Halfpipe/partial hollow cylinder
- Bowl/partial hollow sphere
- Planar heightmap
- Spherical heightmap
- Other kinds of heightmaps?
- Subtractive shapes, so that you can “cut away” parts of primitives.
- Physics simulations
- Combining ColMesh and SMF to create ragdolls
Thank you!
Feedback is always appreciated.
Snidr
Last edited: