• Hey Guest! Ever feel like entering a Game Jam, but the time limit is always too much pressure? We get it... You lead a hectic life and dedicating 3 whole days to make a game just doesn't work for you! So, why not enter the GMC SLOW JAM? Take your time! Kick back and make your game over 4 months! Interested? Then just click here!

Asset - Scripts ColMesh - 3D Collisions Made Easy!

Brylle

Member
Im a 20 yr old BSIT student from the Philippines and i'm joining the game maker contest for Opera. My intention is only that i want to request that you allow non licensed game maker studio or using the free license to download the project files since i followed your smf guide and it was all free i thought this was free too but it is for licensed users only. I hope you reply because the deadline is getting near and if i win you will not only get shout outs or credits thank you sir:cool:
 

j123

Member
If I use this in a commercial project for console/PC/Mac, what credits need to be placed in the credits of the project?
 

TheSnidr

Heavy metal viking dentist
GMC Elder
New update is live!
I'm working on a video showing off how to use this system, and figured some modernization was in order.

colmesh.displaceCapsule now returns a struct instead of an array. It always returns a struct, and if the capsule didn't collide it doesn't alter the input coords, so now you can do this to move your player out of level geometry:
Code:
//Avoid ground
var col = levelColmesh.displaceCapsule(x, y, z, xup, yup, zup, radius, height, slopeAngle, executeColfunc);
x = col.x;
y = col.y;
z = col.z;
ground = col.ground;
The getDeltaMatrix has been moved from colmesh() to the new struct that is returned. Good to know if you're working with moving objects!

Ray casting is now done like this:
Code:
var ray = colmesh.castRay(x1, y1, z1, x2, y2, z2, executeRayFunc);
if (is_struct(ray))
{
    hitX = ray.x;
    hitY = ray.y;
    hitZ = ray.z;
}
 

TheSnidr

Heavy metal viking dentist
GMC Elder
New update is live!
THIS UPDATE WILL NOT BE ABLE TO LOAD OLD CACHES
The format has changed, and the loader hasn't been made backwards compatible. I'd like to emphasize that this may also happen for future updates, so don't depend too much on the saving/loading system.

The changes:
There's a new constructor function called colmesh_collider_capsule, which creates a struct that can be used to avoid a ColMesh. This is a different workflow from before, where you made the ColMesh itself displace the capsule. Also, you can assign collision groups to solid objects, and a collision mask for the collider, so that it can only collide with the groups you give it. This was inspired by one of Dragonite's later videos. So a typical collision check now looks as follows:
Code:
//Create event
//colmesh_collider_capsule(x, y, z, xup, yup, zup, radius, height, slopeAngle, precision, mask);
collider = new colmesh_collider_capsule(x, y, z, 0, 0, 1, radius, height, 40, 3, cmGroupSolid | cmGroupColTrigger);
//The collider will now not slide if the slope beneath it is less than 40 degrees.
//Precision defines how many repetitions the collision system is allowed to do. A precision of 0 corresponds to setting the old argument "fast" to true. A precision of 3 will make the capsule avoid geometry up to three times, but if it's confident the capsule is in the correct position it will exit earlier.
//The capsule will only check for collisions with geometry in the solid group and the trigger group
You can add more collision groups yourself in the colmesh script. The default groups are the following:
Code:
#macro cmGroupSolid 1
#macro cmGroupColTrigger 2
#macro cmGroupRayTrigger 4
All objects that can potentially displace a capsule must at least be in the solid group. In addition, you can add more groups on your own. Let's say for example you want to make some walls that enemies can walk through, but not the player. I'll give this group the obnoxiously long name of cmGroupSolidForEnemies:
Code:
#macro cmGroupSolidForEnemies 8 //Notice how each group has a sequential power of two. This is important! Only powers of two can be used for the groups
Now, every time you add shapes to the level, you can assign groups to them. You combine groups by using the binary or, |, as you can see:
Code:
levelColmesh.addShape(new colmesh_sphere(x, y, z, 100, cmGroupSolid | cmGroupSolidForEnemies));
levelColmesh.addShape(new colmesh_sphere(x + 200, y, z, 100, cmGroupSolid));
If you give the player a mask of (cmGroupSolid) and the enemies a mask of (cmGroupSolidForEnemies), the player will avoid both of the shapes that were just added to the colmesh, while the enemies will only avoid the first one.

Getting the delta matrix from when the collider is standing on a moving object is now also a static method of the collider.

Executing collision functions is no longer done directly in the collision script. Instead, it is performed after the fact by calling the following function:
Code:
collider.executeColFunc(); //Whatever arguments you give this function will be passed on to the custom collision function
This will loop through all the objects the player actually collided with, and execute their custom collision functions if they have one. This now also applies to triangle meshes, making it a lot easier to give triangel meshes custom collision responses, like playing different sounds when walking on grass compared to walking on metal.

Casting rays has also changed, and has been optimized quite a bit. The biggest thing to note is that the ray casting script now always will return a struct, so there's no point in doing if (is_struct(ray)) on the result.
The returned struct has a variable called "hit" that is true or false depending on whether or not it hit anything. If the ray didn't hit anything, its x,y,z values will be at the endpoint of the ray.
Executing the ray trigger functions is done similarly to collision trigger functions, by calling the following function:
Code:
ray.executeRayFunc(); //The arguments given to this function are passed onto the custom trigger function
You can load triangle meshes the same way you're used to, by using colmesh.addMesh(path), but you can also now save your meshes as separate structs. Whenever you add the mesh struct to a colmesh, all its triangles are transformed and added to the colmesh individually, essentially copying the mesh over to the colmesh. The triangles will however still reference back to the original mesh struct, letting you assign it custom collision and ray functions, and custom collision groups. Use like this:
Code:
//Load the mesh
//colmesh_mesh(name, mesh, matrix*, group*)
mesh = new colmesh_mesh("Tree", "Tree.obj");

//Add multiple copies of the mesh to the colmesh, giving it a randomized matrix each time
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
    colmesh.addMesh(mesh, M); //Add the mesh to the colmesh, using the randomized matrix to transform it first
    //Note that meshes added this way cannot be deleted from the colmesh! There is no way to reference each "instance" of the mesh, the mesh is basically baked into the colmesh.
}

//Now, every time something collides with this mesh, do something
var colFunc = function()
{
    //Do something when something collides with the tree mesh
}
//setTrigger(solid, colFunc*, rayFunc*)
mesh.setTrigger(true, colFunc);
However, adding meshes by baking their transforms and copying over all the geometry is wasteful memory-wise. You are essentially duplicating the entire mesh every time. It's useful in some situations, but if you have lots of trees in a forest, it might be better to add the trees by reference instead of copying them all. This is where the dynamic objects come in handy. The dynamic is a container that transforms the collider from one space to another, performs collision checks, and then transforms back. It requires a couple of more steps to get it working:
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.
}
I hope this update fixes the problems some users have reported. If you encounter any bugs, please give me a heads-up so I can fix it!
 
Last edited:
First off I want to say that this is fantastic and thank you. Attempting to use this in a project I am working on but I am having trouble with one spot. Can't seem to get addTriangle collisions to work. Project has an in game level creator similar in style to the old Duke Build engine(except not raycast). And the sector floor/ceiling polygons are triangulated when the room is altered. I am able to use levelColmesh addDynamic new colmesh_block and rotate the matrix for the walls. But I wouldn't be able to use the square block shape for oddly shaped floor and ceiling polygons. What I am attempting is to add triangles to the collision mesh when the room is triangulated. I tried to feed it an array of x y z points, xyz[8], by passing an array like this levelColmesh.addTriangle(xyz). But the collisions don't seem to register. Am I doing it wrong? Or do I have to create a mesh from an array of buffers and use addMesh?
 
Last edited:

TheSnidr

Heavy metal viking dentist
GMC Elder
First off I want to say that this is fantastic and thank you. Attempting to use this in a project I am working on but I am having trouble with one spot. Can't seem to get addTriangle collisions to work. Project has an in game level creator similar in style to the old Duke Build engine(except not raycast). And the sector floor/ceiling polygons are triangulated when the room is altered. I am able to use levelColmesh addDynamic new colmesh_block and rotate the matrix for the walls. But I wouldn't be able to use the square block shape for oddly shaped floor and ceiling polygons. What I am attempting is to add triangles to the collision mesh when the room is triangulated. I tried to feed it an array of x y z points, xyz[8], by passing an array like this levelColmesh.addTriangle(xyz). But the collisions don't seem to register. Am I doing it wrong? Or do I have to create a mesh from an array of buffers and use addMesh?
This actually helped uncover a bug! Triangles that do not belong to a mesh accidentally reference the colmesh as their parent, and thus end up using the colmesh's functions for getting size and regions. This is very wrong!
I've instead added a default mesh parent that stand-alone triangles are added to, which fixes the problem. So in order to fix it, you'll need to do the following:
Add this line to the top of colmesh.gml:
Code:
global.ColMeshDefaultParent = new colmesh_mesh();
And replace addTriangle with this:
Code:
/// @func addTriangle(V[9], parent*)
    static addTriangle = function(V, parent = global.ColMeshDefaultParent)
    {
        //Construct normal vector
        var nx = (V[4] - V[1]) * (V[8] - V[2]) - (V[5] - V[2]) * (V[7] - V[1]);
        var ny = (V[5] - V[2]) * (V[6] - V[0]) - (V[3] - V[0]) * (V[8] - V[2]);
        var nz = (V[3] - V[0]) * (V[7] - V[1]) - (V[4] - V[1]) * (V[6] - V[0]);
        var l = point_distance_3d(0, 0, 0, nx, ny, nz);
        if (l <= 0){return false;}
        var tri = array_create(13);
        array_copy(tri, 0, V, 0, 9);
        tri[9]  = nx / l;
        tri[10] = ny / l;
        tri[11] = nz / l;
        tri[12] = parent ?? global.ColMeshDefaultParent;
        addShape(tri);
        return tri;
    }
I'll push an update to the marketplace soon.

With this little fix, you can add triangles like this:
levelColmesh.addTriangle([x1, y1, z1, x2, y2, z2, x3, y3, z3]);
 
Hello TheSnidr,
Thank you for creating 3D collision system. I've run up against a brick wall in my project, and I think this is the perfect fix.
I have several large complex vertex buffers as TriangleLists. Can I convert these to collision meshes with your system, then use raycasting (rayclicking?) to determine the x,y point on the model the mouse is over? You mention being able to create a collision mesh from buffers and .obj files, I'd just like to double check vertex buffers would work too.

Thank you for any help you can offer,
- Glen G.
 

TheSnidr

Heavy metal viking dentist
GMC Elder
Heyo Glen G! Yes, it works with vertex buffers too, you just have to convert them to regular buffers first with buffer_create_from_vertex_buffer. It also needs the correct format though:
Code:
3D position, 3x4 bytes
3D normal, 3x4 bytes
UV coords, 2x4 bytes
Colour, 4 bytes
 

57E

Member
Hi,
I found one dodgy case on collision response:
If a moving capsule (the player) moves against a flat quad just right it starts to either shake in place or gets stuck in place on nothing, depending on the angle of contact.
I think it tries to move the capsule to the center of the triangle you collide with?
It won't let the capsule clip through the wall or get it permanently stuck as it plops it out as soon as your contact angle changes enough.
You can try this easily in the demo 1 by rubbing against the camera facing wall of the orange house on the right.

Even though it is more noticeable on first person view (imagine looking for secret walls in Doom) It's not excatly game breaking.

But it only ever seems to happen when the collision is between a triangle and the side of the moving capsule. The capsules top and bottom parts seem to glide along the models perfectly without any jittering or sticking into any seams.
So I thought I'd point it out in case you have some insight on if anything could be done about it.


Also at least in the Colmesh example I downloaded through the My Library in GMS2, the "Demo 5" appears to be broken. It only shows the player capsule and one triangle against the sky and nothing else.
 

kamiyasi

Member
Is this out of date? I've opened the marketplace example and get an issue with unknown function createLookatMatrix not being used outside the create event of oColmeshDemo1.
 

TheSnidr

Heavy metal viking dentist
GMC Elder
Oh, oops, that's a leftover from some testing I did recently. Just remove that from oColmeshDemo1's create event. I'll upload a fix right away. Thanks for notifying me!
 

kamiyasi

Member
Oh, oops, that's a leftover from some testing I did recently. Just remove that from oColmeshDemo1's create event. I'll upload a fix right away. Thanks for notifying me!
Thanks! Also, currently the part
GML:
if (!levelColmesh.load("Demo1Cache.cm"))
is commented out so that the levelColmesh.save, addmesh, and subdivide functions always execute. How is the .load function meant to work? If I remove the comment, the player just falls through the world.
 

TheSnidr

Heavy metal viking dentist
GMC Elder
Thanks! Also, currently the part
GML:
if (!levelColmesh.load("Demo1Cache.cm"))
is commented out so that the levelColmesh.save, addmesh, and subdivide functions always execute. How is the .load function meant to work? If I remove the comment, the player just falls through the world.
Ah, another blunder on my part. The save function tries to write the subdivision section key as a float, yet it is stored as a string. I've fixed and reuploaded to the marketplace, thanks for notifying me. Now the save and load functions should work.
 

TheSnidr

Heavy metal viking dentist
GMC Elder
So the entire thing has been rewriten! Download link to beta version 2.0.8 can be found in the first post. Please note that the guide in the first post still uses the function names of the first version. I've also recently made some video updates to my 3D Gravity Platformer video series where I show how I use the new rewritten ColMesh:
 

Heretic

Member
I came across this video on YouTube and it looked awesome so I've been trying to get it to work today. It's just giving me this error but I'm probably doing it all wrong.

ERROR in
action number 1
of Create Event
for object obj_player:

trying to index a variable which is not an array
at gml_Script_cm_list_displace (line 50) - var pri = CM_GET_PRIORITY(object, collider, mask);
############################################################################################
gml_Script_cm_list_displace (line 50)
gml_Script_cm_displace (line 30) - return CM_DISPLACE(object, collider, mask);
gml_Object_obj_player_Create_0 (line 43) - cm_displace(global.mesh, collider);
So I've added the colmesh to the object which handles my terrain:

GML:
global.mesh = cm_list();
matrix = matrix_build(x,y,z,0,0,90,12.8,12.8,12.8);
cm_add(global.mesh, cm_load_obj("terrain.obj", matrix, true));
And I made the collider in my players create event:

GML:
collider = cm_collider(x, y, z, 0, 0, 1, 10, height, 35, 5);
cm_displace(global.mesh, collider);
x = collider[CM.X];
y = collider[CM.Y];
z = collider[CM.Z];
ground = collider[CM.GROUND];
Where am I going wrong to cause this error? Any advice would be greatly appreciated
 

TheSnidr

Heavy metal viking dentist
GMC Elder
I came across this video on YouTube and it looked awesome so I've been trying to get it to work today. It's just giving me this error but I'm probably doing it all wrong.



So I've added the colmesh to the object which handles my terrain:

GML:
global.mesh = cm_list();
matrix = matrix_build(x,y,z,0,0,90,12.8,12.8,12.8);
cm_add(global.mesh, cm_load_obj("terrain.obj", matrix, true));
And I made the collider in my players create event:

GML:
collider = cm_collider(x, y, z, 0, 0, 1, 10, height, 35, 5);
cm_displace(global.mesh, collider);
x = collider[CM.X];
y = collider[CM.Y];
z = collider[CM.Z];
ground = collider[CM.GROUND];
Where am I going wrong to cause this error? Any advice would be greatly appreciated
Looks like the obj loader failed and returned -1. Which is not an array. Make sure the string is correct. Is terrain.obj placed in a folder perhaps?
 

Heretic

Member
Thanks for your response Snidr. The object string is correct and I was getting the the same result with other models. I've been having a look through the scripts and I seem to be getting some errors here. But this is also the case in your gravity platformer project but that runs just fine so I'm really not sure.

cm.png

EDIT: This does seem to be a problem with loading the model. I tried simply replacing your model in the demo project with mine and I get the error in there too. It's an OBJ model created in Blender and it displays fine if I use the OBJ importer script that Dragonite created. I have no idea why Colmesh doesn't seem to like it.
 
Last edited:

FireflyX91

Member
EDIT: This does seem to be a problem with loading the model. I tried simply replacing your model in the demo project with mine and I get the error in there too. It's an OBJ model created in Blender and it displays fine if I use the OBJ importer script that Dragonite created. I have no idea why Colmesh doesn't seem to like it.
I did the opposite to you and replaced the model in my project with the demo model. But if I use my model I'm getting the same error you're experiencing. I can't understand why it would reject one OBJ file but not another. So have you had any luck getting yours to work yet?
 

TheSnidr

Heavy metal viking dentist
GMC Elder
Thank you. The problem was that the model contains triangles with zero area, and trying to add those resulted in false being returned from the cm_triangle function. I've added a check that makes sure the value returned from cm_triangle actually contains a triangle. I'll update the link in the first post, but here's the updated cm_load_obj function:
Code:
// Script assets have changed for v2.3.0 see
// https://help.yoyogames.com/hc/en-us/articles/360005277377 for more information
function cm_load_obj(filename, matrix = undefined, singlesided = true, smoothnormals = false, group = 0)
{
    static read_face = function(faceList, str) 
    {
        gml_pragma("forceinline");
        str = string_delete(str, 1, string_pos(" ", str))
        if (string_char_at(str, string_length(str)) == " ")
        {
            //Make sure the string doesn't end with an empty space
            str = string_copy(str, 0, string_length(str) - 1);
        }
        var triNum = string_count(" ", str);
        var vertString = array_create(triNum + 1);
        for (var i = 0; i < triNum; i ++)
        {
            //Add vertices in a triangle fan
            vertString[i] = string_copy(str, 1, string_pos(" ", str));
            str = string_delete(str, 1, string_pos(" ", str));
        }
        vertString[i--] = str;
        while i--
        {
            for (var j = 2; j >= 0; j --)
            {
                var vstr = vertString[(i + j) * (j > 0)];
                var v = 0, n = 0;
                //If the vertex contains a position, texture coordinate and normal
                if string_count("/", vstr) == 2 and string_count("//", vstr) == 0
                {
                    v = abs(real(string_copy(vstr, 1, string_pos("/", vstr) - 1)));
                    vstr = string_delete(vstr, 1, string_pos("/", vstr));
                    n = abs(real(string_delete(vstr, 1, string_pos("/", vstr))));
                }
                //If the vertex contains a position and a texture coordinate
                else if string_count("/", vstr) == 1
                {
                    v = abs(real(string_copy(vstr, 1, string_pos("/", vstr) - 1)));
                }
                //If the vertex only contains a position
                else if (string_count("/", vstr) == 0)
                {
                    v = abs(real(vstr));
                }
                //If the vertex contains a position and normal
                else if string_count("//", vstr) == 1
                {
                    vstr = string_replace(vstr, "//", "/");
                    v = abs(real(string_copy(vstr, 1, string_pos("/", vstr) - 1)));
                    n = abs(real(string_delete(vstr, 1, string_pos("/", vstr))));
                }
                ds_list_add(faceList, [v-1, n-1]);
            }
        }
    }
    static read_line = function(str) 
    {
        gml_pragma("forceinline");
        str = string_delete(str, 1, string_pos(" ", str));
        var retNum = string_count(" ", str) + 1;
        var ret = array_create(retNum);
        for (var i = 0; i < retNum; i ++)
        {
            var pos = string_pos(" ", str);
            if (pos == 0)
            {
                pos = string_length(str);
                ret[i] = real(string_copy(str, 1, pos)); 
                break;
            }
            ret[i] = real(string_copy(str, 1, pos)); 
            str = string_delete(str, 1, pos);
        }
        return ret;
    }
    var file = file_text_open_read(filename);
    if (file == -1){cm_debug_message("Failed to load model " + string(filename)); return cm_list();}
    cm_debug_message("Script cm_mesh_load_from_obj: Loading obj file " + string(filename));

    //Create the necessary lists
    var V = ds_list_create();
    var N = ds_list_create();
    var F = ds_list_create();

    //Read .obj as textfile
    var str, type;
    while !file_text_eof(file)
    {
        str = string_replace_all(file_text_read_string(file),"  "," ");
        //Different types of information in the .obj starts with different headers
        switch string_copy(str, 1, string_pos(" ", str)-1)
        {
            //Load vertex positions
            case "v":
                ds_list_add(V, read_line(str));
                break;
            //Load vertex normals
            case "vn":
                ds_list_add(N, read_line(str));
                break;
            //Load faces
            case "f":
                read_face(F, str);
                break;
        }
        file_text_readln(file);
    }
    file_text_close(file);

    //Loop through the loaded information and generate a model
    var v, v1, v2, v3, n1, n2, n3, l1, l2, l3, vn;
    var vertNum = ds_list_size(F);
    var mesh = array_create(CM_LIST_NUM + (vertNum div 3));
    mesh[CM_ARGS_LIST.TYPE] = CM_OBJECTS.LIST;
    mesh[CM_ARGS_LIST.NEGATIVESIZE] = - (vertNum div 3);
    var triInd = CM_LIST_NUM;
    if (is_array(matrix))
    {
        for (var f = 0; f < vertNum; f += 3)
        {
            //Add the vertex to the model buffer
            if (smoothnormals)
            {
                vn = F[| f];
                v1 = V[| vn[0]];
                n1 = N[| vn[1]];
                if !is_array(v1){v1 = [0, 0, 0];}
                if !is_array(n1)
                {
                    f -= 3;
                    smoothnormals = false;
                    cm_debug_message("Error in function cm_load_obj: Trying to load an OBJ with smooth normals, but the OBJ format does not contain normals. Loading with flat normals instead.");
                    continue;
                }
                v1 = matrix_transform_vertex(matrix, v1[0], v1[1], v1[2]);
                n1 = matrix_transform_vertex(matrix, n1[0], n1[1], n1[2], 0);
                l1 = point_distance_3d(0, 0, 0, n1[0], n1[1], n1[2]);
                n1[0] /= l1; n1[1] /= l1; n1[2] /= l1;
                
                vn = F[| f+1];
                v2 = V[| vn[0]];
                n2 = N[| vn[1]];
                if !is_array(v2){v2 = [0, 0, 0];}
                if !is_array(n2){n2 = [0, 0, 1];}
                v2 = matrix_transform_vertex(matrix, v2[0], v2[1], v2[2]);
                n2 = matrix_transform_vertex(matrix, n2[0], n2[1], n2[2], 0);
                l2 = point_distance_3d(0, 0, 0, n2[0], n2[1], n2[2]);
                n2[0] /= l2; n2[1] /= l2; n2[2] /= l2;
                
                vn = F[| f+2];
                v3 = V[| vn[0]];
                n3 = N[| vn[1]];
                if !is_array(v3){v3 = [0, 0, 0];}
                if !is_array(n3){n3 = [0, 0, 1];}
                v3 = matrix_transform_vertex(matrix, v3[0], v3[1], v3[2]);
                n3 = matrix_transform_vertex(matrix, n3[0], n3[1], n3[2], 0);
                l3 = point_distance_3d(0, 0, 0, n3[0], n3[1], n3[2]);
                n3[0] /= l3; n3[1] /= l3; n3[2] /= l3;
        
                var tri = cm_triangle(singlesided, v1[0], v1[1], v1[2], v2[0], v2[1], v2[2], v3[0], v3[1], v3[2], n1, n2, n3, group);
                if (is_array(tri))
                {
                    mesh[triInd++] = tri;
                }
            }
            else
            {
                v = V[| F[| f][0]];
                if !is_array(v){v = [0, 0, 0];}
                v1 = matrix_transform_vertex(matrix, v[0], v[1], v[2]);
                v = V[| F[| f+1][0]];
                if !is_array(v){v = [0, 0, 0];}
                v2 = matrix_transform_vertex(matrix, v[0], v[1], v[2]);
                v = V[| F[| f+2][0]];
                if !is_array(v){v = [0, 0, 0];}
                v3 = matrix_transform_vertex(matrix, v[0], v[1], v[2]);
        
                var tri = cm_triangle(singlesided, v1[0], v1[1], v1[2], v2[0], v2[1], v2[2], v3[0], v3[1], v3[2], group);
                if (is_array(tri))
                {
                    mesh[triInd++] = tri;
                }
            }
        }
    }
    else
    {
        for (var f = 0; f < vertNum; f += 3)
        {
            //Add the vertex to the model buffer
            if (smoothnormals)
            {
                vn = F[| f];
                v1 = V[| vn[0]];
                n1 = N[| vn[1]];
                if !is_array(v1){v1 = [0, 0, 0];}
                if !is_array(n1)
                {
                    f -= 3;
                    smoothnormals = false;
                    continue;
                }
                
                vn = F[| f+1];
                v2 = V[| vn[0]];
                n2 = N[| vn[1]];
                if !is_array(v2){v2 = [0, 0, 0];}
                if !is_array(n2){n2 = [0, 0, 1];}
                
                vn = F[| f+2];
                v3 = V[| vn[0]];
                n3 = N[| vn[1]];
                if !is_array(v3){v3 = [0, 0, 0];}
                if !is_array(n3){n3 = [0, 0, 1];}
        
                var tri = cm_triangle(singlesided, v1[0], v1[1], v1[2], v2[0], v2[1], v2[2], v3[0], v3[1], v3[2], n1, n2, n3, group);
                if (is_array(tri))
                {
                    mesh[triInd++] = tri;
                }
            }
            else
            {
                v1 = V[| F[| f][0]];
                if !is_array(v1){v1 = [0, 0, 0];}
                v2 = V[| F[| f+1][0]];
                if !is_array(v2){v2 = [0, 0, 0];}
                v3 = V[| F[| f+2][0]];
                if !is_array(v3){v3 = [0, 0, 0];}
        
                var tri = cm_triangle(singlesided, v1[0], v1[1], v1[2], v2[0], v2[1], v2[2], v3[0], v3[1], v3[2], group);
                if (is_array(tri))
                {
                    mesh[triInd++] = tri;
                }
            }
        }
    }
    ds_list_destroy(F);
    ds_list_destroy(V);
    cm_debug_message("Script cm_mesh_load_from_obj: Successfully loaded obj " + string(filename));
    array_resize(mesh, triInd);
    return mesh;
}
 

FireflyX91

Member
That's great, I'm glad you sorted that. But there seems to be a new error now and it's caused by the same model.

ERROR in
action number 1
of Create Event
for object obj_player:

Push :: Execution Error - Variable Index [1404] out of range [1404] - -15.argument0(93,1404)
at gml_Script_cm_list_displace (line 39) - var object = list[i++];
############################################################################################
gml_Script_cm_list_displace (line 39)
gml_Script_cm_displace (line 30) - return CM_DISPLACE(object, collider, mask);
gml_Object_obj_player_Create_0 (line 60) - cm_displace(levelColmesh, collider);
 

TheSnidr

Heavy metal viking dentist
GMC Elder
Right, that's still the OBJ importer. This should fix that error:
Code:
// Script assets have changed for v2.3.0 see
// https://help.yoyogames.com/hc/en-us/articles/360005277377 for more information
function cm_load_obj(filename, matrix = undefined, singlesided = true, smoothnormals = false, group = 0)
{
    static read_face = function(faceList, str)
    {
        gml_pragma("forceinline");
        str = string_delete(str, 1, string_pos(" ", str))
        if (string_char_at(str, string_length(str)) == " ")
        {
            //Make sure the string doesn't end with an empty space
            str = string_copy(str, 0, string_length(str) - 1);
        }
        var triNum = string_count(" ", str);
        var vertString = array_create(triNum + 1);
        for (var i = 0; i < triNum; i ++)
        {
            //Add vertices in a triangle fan
            vertString[i] = string_copy(str, 1, string_pos(" ", str));
            str = string_delete(str, 1, string_pos(" ", str));
        }
        vertString[i--] = str;
        while i--
        {
            for (var j = 2; j >= 0; j --)
            {
                var vstr = vertString[(i + j) * (j > 0)];
                var v = 0, n = 0;
                //If the vertex contains a position, texture coordinate and normal
                if string_count("/", vstr) == 2 and string_count("//", vstr) == 0
                {
                    v = abs(real(string_copy(vstr, 1, string_pos("/", vstr) - 1)));
                    vstr = string_delete(vstr, 1, string_pos("/", vstr));
                    n = abs(real(string_delete(vstr, 1, string_pos("/", vstr))));
                }
                //If the vertex contains a position and a texture coordinate
                else if string_count("/", vstr) == 1
                {
                    v = abs(real(string_copy(vstr, 1, string_pos("/", vstr) - 1)));
                }
                //If the vertex only contains a position
                else if (string_count("/", vstr) == 0)
                {
                    v = abs(real(vstr));
                }
                //If the vertex contains a position and normal
                else if string_count("//", vstr) == 1
                {
                    vstr = string_replace(vstr, "//", "/");
                    v = abs(real(string_copy(vstr, 1, string_pos("/", vstr) - 1)));
                    n = abs(real(string_delete(vstr, 1, string_pos("/", vstr))));
                }
                ds_list_add(faceList, [v-1, n-1]);
            }
        }
    }
    static read_line = function(str)
    {
        gml_pragma("forceinline");
        str = string_delete(str, 1, string_pos(" ", str));
        var retNum = string_count(" ", str) + 1;
        var ret = array_create(retNum);
        for (var i = 0; i < retNum; i ++)
        {
            var pos = string_pos(" ", str);
            if (pos == 0)
            {
                pos = string_length(str);
                ret[i] = real(string_copy(str, 1, pos));
                break;
            }
            ret[i] = real(string_copy(str, 1, pos));
            str = string_delete(str, 1, pos);
        }
        return ret;
    }
    var file = file_text_open_read(filename);
    if (file == -1){cm_debug_message("Failed to load model " + string(filename)); return cm_list();}
    cm_debug_message("Script cm_mesh_load_from_obj: Loading obj file " + string(filename));

    //Create the necessary lists
    var V = ds_list_create();
    var N = ds_list_create();
    var F = ds_list_create();

    //Read .obj as textfile
    var str, type;
    while !file_text_eof(file)
    {
        str = string_replace_all(file_text_read_string(file),"  "," ");
        //Different types of information in the .obj starts with different headers
        switch string_copy(str, 1, string_pos(" ", str)-1)
        {
            //Load vertex positions
            case "v":
                ds_list_add(V, read_line(str));
                break;
            //Load vertex normals
            case "vn":
                ds_list_add(N, read_line(str));
                break;
            //Load faces
            case "f":
                read_face(F, str);
                break;
        }
        file_text_readln(file);
    }
    file_text_close(file);

    //Loop through the loaded information and generate a model
    var v, v1, v2, v3, n1, n2, n3, l1, l2, l3, vn;
    var vertNum = ds_list_size(F);
    var mesh = array_create(CM_LIST_NUM + (vertNum div 3));
    mesh[CM_ARGS_LIST.TYPE] = CM_OBJECTS.LIST;
    var triInd = CM_LIST_NUM;
    if (is_array(matrix))
    {
        for (var f = 0; f < vertNum; f += 3)
        {
            //Add the vertex to the model buffer
            if (smoothnormals)
            {
                vn = F[| f];
                v1 = V[| vn[0]];
                n1 = N[| vn[1]];
                if !is_array(v1){v1 = [0, 0, 0];}
                if !is_array(n1)
                {
                    f -= 3;
                    smoothnormals = false;
                    cm_debug_message("Error in function cm_load_obj: Trying to load an OBJ with smooth normals, but the OBJ format does not contain normals. Loading with flat normals instead.");
                    continue;
                }
                v1 = matrix_transform_vertex(matrix, v1[0], v1[1], v1[2]);
                n1 = matrix_transform_vertex(matrix, n1[0], n1[1], n1[2], 0);
                l1 = point_distance_3d(0, 0, 0, n1[0], n1[1], n1[2]);
                n1[0] /= l1; n1[1] /= l1; n1[2] /= l1;
               
                vn = F[| f+1];
                v2 = V[| vn[0]];
                n2 = N[| vn[1]];
                if !is_array(v2){v2 = [0, 0, 0];}
                if !is_array(n2){n2 = [0, 0, 1];}
                v2 = matrix_transform_vertex(matrix, v2[0], v2[1], v2[2]);
                n2 = matrix_transform_vertex(matrix, n2[0], n2[1], n2[2], 0);
                l2 = point_distance_3d(0, 0, 0, n2[0], n2[1], n2[2]);
                n2[0] /= l2; n2[1] /= l2; n2[2] /= l2;
               
                vn = F[| f+2];
                v3 = V[| vn[0]];
                n3 = N[| vn[1]];
                if !is_array(v3){v3 = [0, 0, 0];}
                if !is_array(n3){n3 = [0, 0, 1];}
                v3 = matrix_transform_vertex(matrix, v3[0], v3[1], v3[2]);
                n3 = matrix_transform_vertex(matrix, n3[0], n3[1], n3[2], 0);
                l3 = point_distance_3d(0, 0, 0, n3[0], n3[1], n3[2]);
                n3[0] /= l3; n3[1] /= l3; n3[2] /= l3;
       
                var tri = cm_triangle(singlesided, v1[0], v1[1], v1[2], v2[0], v2[1], v2[2], v3[0], v3[1], v3[2], n1, n2, n3, group);
                if (is_array(tri))
                {
                    mesh[triInd++] = tri;
                }
            }
            else
            {
                v = V[| F[| f][0]];
                if !is_array(v){v = [0, 0, 0];}
                v1 = matrix_transform_vertex(matrix, v[0], v[1], v[2]);
                v = V[| F[| f+1][0]];
                if !is_array(v){v = [0, 0, 0];}
                v2 = matrix_transform_vertex(matrix, v[0], v[1], v[2]);
                v = V[| F[| f+2][0]];
                if !is_array(v){v = [0, 0, 0];}
                v3 = matrix_transform_vertex(matrix, v[0], v[1], v[2]);
       
                var tri = cm_triangle(singlesided, v1[0], v1[1], v1[2], v2[0], v2[1], v2[2], v3[0], v3[1], v3[2], group);
                if (is_array(tri))
                {
                    mesh[triInd++] = tri;
                }
            }
        }
    }
    else
    {
        for (var f = 0; f < vertNum; f += 3)
        {
            //Add the vertex to the model buffer
            if (smoothnormals)
            {
                vn = F[| f];
                v1 = V[| vn[0]];
                n1 = N[| vn[1]];
                if !is_array(v1){v1 = [0, 0, 0];}
                if !is_array(n1)
                {
                    f -= 3;
                    smoothnormals = false;
                    continue;
                }
               
                vn = F[| f+1];
                v2 = V[| vn[0]];
                n2 = N[| vn[1]];
                if !is_array(v2){v2 = [0, 0, 0];}
                if !is_array(n2){n2 = [0, 0, 1];}
               
                vn = F[| f+2];
                v3 = V[| vn[0]];
                n3 = N[| vn[1]];
                if !is_array(v3){v3 = [0, 0, 0];}
                if !is_array(n3){n3 = [0, 0, 1];}
       
                var tri = cm_triangle(singlesided, v1[0], v1[1], v1[2], v2[0], v2[1], v2[2], v3[0], v3[1], v3[2], n1, n2, n3, group);
                if (is_array(tri))
                {
                    mesh[triInd++] = tri;
                }
            }
            else
            {
                v1 = V[| F[| f][0]];
                if !is_array(v1){v1 = [0, 0, 0];}
                v2 = V[| F[| f+1][0]];
                if !is_array(v2){v2 = [0, 0, 0];}
                v3 = V[| F[| f+2][0]];
                if !is_array(v3){v3 = [0, 0, 0];}
       
                var tri = cm_triangle(singlesided, v1[0], v1[1], v1[2], v2[0], v2[1], v2[2], v3[0], v3[1], v3[2], group);
                if (is_array(tri))
                {
                    mesh[triInd++] = tri;
                }
            }
        }
    }
    ds_list_destroy(F);
    ds_list_destroy(V);
    cm_debug_message("Script cm_mesh_load_from_obj: Successfully loaded obj " + string(filename));
    array_resize(mesh, triInd);
    mesh[CM_ARGS_LIST.NEGATIVESIZE] = CM_LIST_NUM - triInd;
    return mesh;
}
 

FireflyX91

Member
Yep! That's sorted it and I got everything up and running. Much more efficient than my current system of placing separate height objects.

Thank you for creating and sharing this. I'll be sure to credit you if I ever finish the projectđź‘Ť
 

FireflyX91

Member

Just thought I'd mention that if the matrix is scaled up then the capsule will pass through the mesh at random points. I'd just get around it by scaling the models in blender but it might be something worth looking at.
 

TheSnidr

Heavy metal viking dentist
GMC Elder

Just thought I'd mention that if the matrix is scaled up then the capsule will pass through the mesh at random points. I'd just get around it by scaling the models in blender but it might be something worth looking at.
The demo uses an octree for the level. The octree has a bounding box, which is defined when the octree is created. Objects cannot be added outside of the bounding box. It'll work again if you increase the size of the bounding box accordingly, or switch to a spatial hash.
 

TheSnidr

Heavy metal viking dentist
GMC Elder
Ah, my bad. That amount of scaling does push it outside the boundary box and it's obvious now you point that out.
I thought about this, and realized it doesn't have to be this way. So I've updated the octrees so that they expand outwards as well as subdivide as objects are added. This makes working with octrees (and quadtrees) pretty much the same as working with spatial hashes, except, the trees can have variable region sizes. The cm_octree function now only needs a single argument, which is the minimum region size. It also has an optional argument for the maximum number of objects per region. It will subdivide until one of these conditions is met.
I've also renamed the internal enums slightly, removing "_ARGS" from the enums describing the objects.
Here's the latest version: https://www.dropbox.com/s/4qw9su9f8rtoefd/ColMeshV2.0.14.yymps?dl=0
 
Top