Need Help With 3D Raycasting

Hello!

So I started playing around with GameMaker's 3D functions a few days ago and have been trying to build my own Minecraft clone (original, I know). I've had very few issues thus far, however now I'm stuck. I'm trying to build a system that allows the player to build/break blocks, just like in Minecraft, and I think I've come up with a good system for this, I just don't know how exactly to implement it into my game. Here's what I'm working with:

I have a 2D project that I'm using to test my building/breaking system (pictures below). In this 2D project, a line is drawn that starts at the player and goes on in the direction the player is facing until either 1) it hits a wall, or 2) it reaches it's max. distance (it's like a raycast). If a block is being touched by a raycast line, left click can destroy it and right click can place another block next to it (just like in Minecraft). Here is the project file for this test if you don't mind seeing exactly what I've done. I want to convert this system to my 3D game somehow, which I'm not really sure how to do. I have a basic understanding of how GameMaker's 3D functions work and how to build a game with them, however I just can't figure this out. I've tried adding a Z axis check to my 2D test project which seems to work, I mainly just don't know how I could add this system to the player controller in my 3D game. If someone could point me in the right direction I'd really appreciate it! I'm using GM:S 1.4.

Pics:
Capture 1.PNG Capture 2.PNG capture 3.PNG
 

fishfern

Member
Hi there, I'm actually looking for a fairly similar thing (I think). I've had a look at your project files, and I'm wondering if you could add an extra step to your raycast logic that would check the z value/coords of anything the ray meets. So logic wise, your ray would evaluate something a little like this:
>Does the ray meet an object on the X value?
>Does the ray meet an object on the Y value?
>If yes, does it meet on the Z value?

As for actually computing the Z value, you could possibly pull together a z check based on the pitch variable of the camera object (or your player character, if the camera and player are the same object). I can't specifically remember where I saw it, but I remember reading that the 'lengthdir_y()' can stand in for a 'lengthdir_z()' function where it doesn't natively exist. That being said, I'd check for confirmation from someone more experienced, as I'm only just starting to learn the ropes of 3D myself, so I could well be wrong on that one.

Hopefully this helps even a little!
 
Hi there, I'm actually looking for a fairly similar thing (I think). I've had a look at your project files, and I'm wondering if you could add an extra step to your raycast logic that would check the z value/coords of anything the ray meets. So logic wise, your ray would evaluate something a little like this:
>Does the ray meet an object on the X value?
>Does the ray meet an object on the Y value?
>If yes, does it meet on the Z value?

As for actually computing the Z value, you could possibly pull together a z check based on the pitch variable of the camera object (or your player character, if the camera and player are the same object). I can't specifically remember where I saw it, but I remember reading that the 'lengthdir_y()' can stand in for a 'lengthdir_z()' function where it doesn't natively exist. That being said, I'd check for confirmation from someone more experienced, as I'm only just starting to learn the ropes of 3D myself, so I could well be wrong on that one.

Hopefully this helps even a little!
Thanks for the reply! That's what I had assumed I would have to do - I was playing around with adding a fake Z value to the 2D player and blocks, and it seemed to work right. Like you said, I just need to figure out how to use that system with the 3D player's pitch/zdir value. And thanks for letting me know about the lengthdir_z() thing - that's really useful!
Feel free to use the project files if you find them useful btw - it's nothing much but maybe it'll help haha.
 

fishfern

Member
Thanks for the reply! That's what I had assumed I would have to do - I was playing around with adding a fake Z value to the 2D player and blocks, and it seemed to work right. Like you said, I just need to figure out how to use that system with the 3D player's pitch/zdir value. And thanks for letting me know about the lengthdir_z() thing - that's really useful!
Feel free to use the project files if you find them useful btw - it's nothing much but maybe it'll help haha.
Oh thanks, that's really generous! I may have a look for some concepts, if that's okay, ray casting is something I've always struggled with, so it's awesome to see something in a language I can understand that isn't pegged to a FPS!

I'll have a look around and see if I can find any more information relating to emulating a lengthdir_z() function, I think I have an example somewhere that I'll see if I can find. I think from memory I had the system applied directly to a pitch value, so it *may* provide some help regarding using your system with a zdir value. :)
 
Oh thanks, that's really generous! I may have a look for some concepts, if that's okay, ray casting is something I've always struggled with, so it's awesome to see something in a language I can understand that isn't pegged to a FPS!

I'll have a look around and see if I can find any more information relating to emulating a lengthdir_z() function, I think I have an example somewhere that I'll see if I can find. I think from memory I had the system applied directly to a pitch value, so it *may* provide some help regarding using your system with a zdir value. :)
No problem! For lengthdir_z() I found this on Reddit:
GML:
///lengthdir_z(val,dir)
return argument0*tan(degtorad(argument1));
I haven't gotten around to testing it yet but a lot of people said it worked. Let me know if you find anything else out - I appreciate you helping me :D
 

fishfern

Member
Hi again,

I've been messing around with some code (to varying levels of success) and I may some new info to add.

I tried taking your existing 2D raycast code and simply applying a third dimension/vector to it, like so:

GML:
///draw_raycast_3D(dir, pitch, useMaxLength, maxLength, visible)

var dir = argument[0]; //the direction of the raycast
var pitch = argument[1]; //the pitch of the raycast
var useMaxLength = argument[2]; //does this raycast line have a max. length?
var maxLength = argument[3]; //if it has a max. length, what is the max. length?
var vis = argument[4]; //is this raycast line visible (for debugging)

if (useMaxLength) {
    for (var i = 0; i < maxLength; i ++) {
        var lx = x + lengthdir_x(i, dir); //lx = x endpoint of raycast
        var ly = y + lengthdir_y(i, dir); //ly = y endpoint of raycast
        var lz = (z+height) + lengthdir_z(i, pitch); //lz = z endpoint of raycast
        
        if place_meeting_ext(lx,ly,lz,obj_par_solid){
            break; //The collisions here seem to be more than a tad messy, so I'm not sure
            //place_meeting_ext is totally accurate for this application.
        }
        
    }
}


//Draw the raycast line (if vis = true)
if (vis) {
    draw_set_color(c_lime);
    d3d_primitive_begin(pr_linelist);
    d3d_vertex(x,y,z+height);
    d3d_vertex(lx,ly,lz);
    d3d_primitive_end();
    
    
    if place_meeting_ext(lx,ly,lz,obj_par_solid){
        draw_set_color(c_red);
        var radius = 1;
        d3d_draw_ellipsoid(lx - radius, ly - radius, lz - radius, lx + radius, ly + radius, lz + radius, tex_red, 1, 1, 8);
    }
    
    draw_set_colour(c_white);
}
This was reasonably effective at throwing up (and drawing) a 3D ray cast. At the moment though the ray can sometimes miss some object collisions, or cause some false collisions, so I think I may have some errors/issues with the 'place_meeting_ext()' script.

I found that the lengthdir_z() code you suggested caused some slightly effects, as I found that a) my pitch needed to be inverted, and b) there was some strange goings on, where the angle of the ray changed at different amounts depending on the current camera pitch. In the end I tried swapping it out for the code below and it (appears) to work fine:


Code:
///lengthdir_z(length, dir)

//return argument0*tan(degtorad(argument1)); //This cause some odd effects

return lengthdir_y(argument[0],argument[1]);
That being said, the strangeness of 'argument0*tan(degtorad(argument1));' could possibly be due to how the 3D is set up in my own project, so you may find it works fine for you!

As a quite explanation, I often have 'z+height' in places, as this allows for the height and z location of objects to be taken into consideration. In my case the camera object is off the ground a tad, and some objects float, so it's necessary to account for the differing heights and z locations of objects.

Hopefully this helps a tad, and I love to hear if you find anything more out!
 

Yal

🍋 *lemon noises*
GMC Elder
In 3D, the X, Y and Z components of a vector depend on two angles, the angle in the XY plane (called phi) and the up/down angle (called theta). For instance, a vector facing straight up will always have zero X and Y components no matter the phi angle.

With theta defined as zero for the XY plane and Z positive upwards, you'd get something like this:
GML:
///euler_to_cartesian(radius,phi,theta)
delta_x = radius*lengthdir_x(1,theta)*lengthdir_x(1,phi);
delta_y = radius*lengthdir_x(1,theta)*lengthdir_y(1,phi);
delta_z =-radius*lengthdir_y(1,theta);
 

fishfern

Member
In 3D, the X, Y and Z components of a vector depend on two angles, the angle in the XY plane (called phi) and the up/down angle (called theta). For instance, a vector facing straight up will always have zero X and Y components no matter the phi angle.

With theta defined as zero for the XY plane and Z positive upwards, you'd get something like this:
GML:
///euler_to_cartesian(radius,phi,theta)
delta_x = radius*lengthdir_x(1,theta)*lengthdir_x(1,phi);
delta_y = radius*lengthdir_x(1,theta)*lengthdir_y(1,phi);
delta_z =-radius*lengthdir_y(1,theta);
Oh wow, that's great to know! Just to clarify (I'm new to 3D and want to make sure I'm on the right track), when you say phi is the angle on the XY plane, and theta is on the up/down, does that mean phi would literally be an objects dir/direction/rotation, and the theta would be it's 'pitch'?

And (pre-apologising for what is no doubt a really stupid question), what aspect of a raycast code would this replace/fulfil? Would the point at (delta_x,delta_y,delta_z) be the position you'd check for a collision?

Seriously though, thanks so much for explaining that about phi and theta, I've seen those terms thrown around a bit, but I've never managed to actually grasp what they meant!
 
Thank you both for your replies! As @fishfern said that is very helpful (I'm also a 3D noob and still trying to figure out concepts like that, too).

I hadn't seen that either of you replied before I tried testing out a new system, but luckily I still managed to get raycasts working in 3D! It's, admittedly, not optimized the best it could be, but hey, it's something (I also re-did the whole system I originally had created lol). You can download the project file here. I basically made the player shoot an invisible bullet every 2 frames (terribly optimized, like I said), and if these bullets hit a block it will "highlight" the block it hits. If a block is highlighted, you can break it with left click. I haven't gotten around to making a proper block placing system but I'm going to try over the next few days. I found a place_meeting_ext() script as well as a 3D instance_place() script online which did the trick. A video of the system can also be found here.

Hope it helps!
 

fishfern

Member
Wow, the example you included is super neat! It runs smoothly on my end, though I get what you're saying, I'm not entirely sure it would scale too well! That being said, I played with your code for a little while and have had (some) success at implementing the same logic (that is, the movement of the 'bullet' objects) into a raycast script. With any luck, that should seriously improve the optimisation and make things a bit more scalable. I wasn't able to get your 'hovering' mechanic working, but I didn't spend too long pulling that side of things apart.


There does appear to be a couple of strange bugs with collisions, as the ray appears to register collisions well below the z-value of objects to a certain height. The ray also appears to penetrate into objects to some degree. I'll pop the code in below, which I ran from the draw event of the obj_chararacter (as it has some drawing processes for debugging).

GML:
///raycast_test_3D(direction, pitch, distance,visible?)

var dir = argument[0]; //the direction of the raycast
var pitch = argument[1]; //the pitch of the raycast
var dist = argument[2]; //the max distance of the raycast
var vis = argument[3]; //is this raycast line visible (for debugging)

        //-------------------------
        //---xto,yto,zto method----
        //Set xto, yto, zto
        n_z = z + height;
        n_xto = xto - x;
        n_yto = yto - y;
        n_zto = zto - z - height;
        n_x = x;
        n_y = y;
        
        var range = 0;
        var maxrange = 300;
    
    do {
        n_x += n_xto;
        n_y += n_yto;
        n_z += n_zto;
        range ++;
        }
    until (range >= maxrange || d3d_place_meeting(n_x, n_y, n_z, par_solid));
        lx = n_x;
        ly = n_y;
        lz = n_z;
        //-------------------------
        
    if (vis) {
    draw_set_color(c_lime);
    d3d_primitive_begin(pr_linelist);
    d3d_vertex(x,y,z+height);
    d3d_vertex(lx,ly,lz);
    d3d_primitive_end();
    
    
    if d3d_place_meeting(n_x, n_y, n_z, par_solid) {
        draw_set_color(c_red);
        var radius = 6;
        d3d_draw_ellipsoid(lx - radius, ly - radius, lz - radius, lx + radius, ly + radius, lz + radius, tex_red, 1, 1, 8);
        
    }
    
    draw_set_colour(c_white);
}

The variable names and things are a little off-kilter (as it was a close translation from your objects instance 'n'), and there's some variable doubling in there (such as converting 'n_x' into 'lx' and so on), but I figured I'd try and iterate this as quickly as possible. Hopefully this helps a little!
 

Yal

🍋 *lemon noises*
GMC Elder
Oh wow, that's great to know! Just to clarify (I'm new to 3D and want to make sure I'm on the right track), when you say phi is the angle on the XY plane, and theta is on the up/down, does that mean phi would literally be an objects dir/direction/rotation, and the theta would be it's 'pitch'?
Yes, exactly. Phi corresponds 1:1 to GM's direction. A pitch (theta) of +90 degrees would mean z-upwards, and so on. Though there's one caveat: standard spherical coordinates (also known as "Euler angles") define theta=0 as fully upwards and theta=180 as downwards, but I find theta = 0 --> z = 0 ("natural Euler angles") to be much easier to deal with... but keep this in mind if you're gonna get math formulas for some 3D vector stuff into your game so you don't go around 'comparing apples and oranges' with different coordinate systems without noticing.
And (pre-apologising for what is no doubt a really stupid question), what aspect of a raycast code would this replace/fulfil? Would the point at (delta_x,delta_y,delta_z) be the position you'd check for a collision?
That code turns a radius / angle vector into an x / y / z vector. To do a raycast, you'd just add the x/y/z deltas to the x/y/z coordinates each step and then abort when there's a collision. (Note that it's basically pseudocode since I didn't use argument* for the arguments or the new function-with-defined-argument-names syntax from GMS2.3 so you gotta fix that before you can use it directly)
 

fishfern

Member
Yes, exactly. Phi corresponds 1:1 to GM's direction. A pitch (theta) of +90 degrees would mean z-upwards, and so on. Though there's one caveat: standard spherical coordinates (also known as "Euler angles") define theta=0 as fully upwards and theta=180 as downwards, but I find theta = 0 --> z = 0 ("natural Euler angles") to be much easier to deal with... but keep this in mind if you're gonna get math formulas for some 3D vector stuff into your game so you don't go around 'comparing apples and oranges' with different coordinate systems without noticing.

That code turns a radius / angle vector into an x / y / z vector. To do a raycast, you'd just add the x/y/z deltas to the x/y/z coordinates each step and then abort when there's a collision. (Note that it's basically pseudocode since I didn't use argument* for the arguments or the new function-with-defined-argument-names syntax from GMS2.3 so you gotta fix that before you can use it directly)

Thanks so much for explaining that, it makes so much sense! I'm going to need to do a bit of reading of Euler angles, from the sound of things!

Thanks for the heads up regarding the pseudocode, I figured as much was able to integrate it into the script that steps through with minimal issues. I did notice that on my end there seems to be a bug with how the ray casts on the z axis; it seems that as the camera pitches downwards, the ray angles upwards slightly, and when I pitch upwards, it angles downward. It's not acting as if the pitch was inverted, because it still moves with the pitch of the mouse, however it seems that the further I 'look' away from the 'natural position'/horizon/forwards position, the more pronounced this bug becomes.

Thanks so much for your insight once again!
 

Yal

🍋 *lemon noises*
GMC Elder
How do you set the camera projection? A lot of the example code just use a hardcoded 0,0,1 for the up vector, and that makes the camera inaccurate for extreme up/down angles.
 

fishfern

Member
Oh, you're exactly right, my xup,yup, and zup are 0,0,1 respectively. If I were to correct the up vector, do you have any tips for how I could do it? I can't seem to find any information on how to calculate an accurate up vector.

Thank you so much for your help!
 

Yal

🍋 *lemon noises*
GMC Elder
do you have any tips for how I could do it? I can't seem to find any information on how to calculate an accurate up vector.
Rotate the default up vector the same way you rotate other stuff? I.e. if you find the point to aim the camera towards using certain set of rotations, apply the same rotations to the up vector and you'll get the "up direction" corresponding to that forwards direction.
 

fishfern

Member
Rotate the default up vector the same way you rotate other stuff? I.e. if you find the point to aim the camera towards using certain set of rotations, apply the same rotations to the up vector and you'll get the "up direction" corresponding to that forwards direction.
Ohh right! I'm still more than a little lost (it's been years since I have studied maths in depth), but from what you're saying, is the 'up' vector paired to camera, rather than 'absolute' within the space? Apologies for being super dense with this, something about cameras, and the trigonometry involved makes my head spin. I may be (massively) over simplifying things, but would/should I essentially be looking for my 'zup' to equate to something akin to my zto angle + 90?
 

Yal

🍋 *lemon noises*
GMC Elder
The up vector is the direction that's up for the camera. You give it two points when setting up the projection (if this hasn't changed in GMS2, at least): from and to x/y/z. This is used for two things: computing the "fowards vector" (which is the to point minus the from point) and also placing the camera at the from point. The thing is, you can still rotate the camera freely along the forwards vector while still facing the "to" point:
1592749249358.png


Having a separate "up" vector removes this ambiguity. Chances are the code that sets up the projection matrix behind the scenes just assumes the up vector is perpendicular to the forwards vector, and if it isn't (e.g. if you use 0,0,1 when having the camera angled from the XY plane) you get slight corruptions in the result.
 

fishfern

Member
The up vector is the direction that's up for the camera. You give it two points when setting up the projection (if this hasn't changed in GMS2, at least): from and to x/y/z. This is used for two things: computing the "fowards vector" (which is the to point minus the from point) and also placing the camera at the from point. The thing is, you can still rotate the camera freely along the forwards vector while still facing the "to" point:

Having a separate "up" vector removes this ambiguity. Chances are the code that sets up the projection matrix behind the scenes just assumes the up vector is perpendicular to the forwards vector, and if it isn't (e.g. if you use 0,0,1 when having the camera angled from the XY plane) you get slight corruptions in the result.
Right, that makes sense! I (think) I'm starting to get it?

In my game, the camera doesn't need to roll, so it sounds as though I should be leaving xup and yup as '0', though from what you have said, it sounds like I need to calculate my zup as perpendicular to my zto variable (I think)? At the moment my variables zto and zup look like this:

Camera_Query_1.png

Which projects a functional camera (i.e no errors or crazy display issues), however it still seems to be displaying the raycast strangely at higher/lower camera pitches. It's not hugely pronounced, but it does have reduce the intuitiveness of actually playing it.
 
Top