Search code examples
c++algorithmvectorgeometryintersection

determine the face of a cube intersected by a ray in a Minecraft-like game?


I am developing a Minecraft-like game and I have a cube vertex-buffer

float positions[] = {
    // front face       
    0.0f, 0.0f, 1.0f,
    1.0f, 0.0f, 1.0f,
    1.0f, 1.0f, 1.0f,
    0.0f, 1.0f, 1.0f,
    // back face
    0.0f, 0.0f, 0.0f,
    0.0f, 1.0f, 0.0f,
    1.0f, 1.0f, 0.0f,
    1.0f, 0.0f, 0.0f,
    // top face         
    0.0f, 1.0f, 0.0f,
    0.0f, 1.0f, 1.0f,
    1.0f, 1.0f, 1.0f,
    1.0f, 1.0f, 0.0f,
    // bottom face      
    0.0f, 0.0f, 0.0f,
    1.0f, 0.0f, 0.0f,
    1.0f, 0.0f, 1.0f,
    0.0f, 0.0f, 1.0f,
    // right face       
    1.0f, 0.0f, 0.0f,
    1.0f, 1.0f, 0.0f,
    1.0f, 1.0f, 1.0f,
    1.0f, 0.0f, 1.0f,
    // left face        
    0.0f, 0.0f, 0.0f,
    0.0f, 0.0f, 1.0f,
    0.0f, 1.0f, 1.0f,
    0.0f, 1.0f, 0.0f,
};

I am using these vertex positions in global positioning, meaning the first block will be (0,0,0) and the one that is on its right will be (1,0,0).

I am casting a ray from the player position, and I want to find the face that intersects with the cube. I have the cube that intersected with the ray, but not the face. Could you guys please help me implement this function?

glm::vec3 IntersectCube(const glm::vec3& rayOrigin, const glm::vec3& rayDir, const glm::vec3& blockPos)

Solution

  • let's take it in steps so you can understand how to implement it on your own since i will demonstrate it with DirectX types and you will have to port it to OpenGL (i believe DirectX::XMFLOAT3 equals to glm::vec3).

    We have an Eye Position, which is the origin of the ray (rayOrigin), we have the direction vector of the ray (rayDir), and we have the block we are intersecting (or in your case, its origin position blockPos)

    there are numerous algorithms to calculate intersecting points on the cube, i use a simple and efficient one here created by Tavian Barnes (tavianator). the basic principle behind this algorithm is that you continually clip the ray by three pair of parallel planes (the cube faces if you will) and the remainder is the intersection (if it exists). We start off by assuming that rayDir is a slope and calculate its inverse, so we can turn divisions into multiplication:

    DirectX::XMFLOAT3 _inv_slope = { 1.0f / rayDir.x, 1.0f / rayDir.y, 1.0f / rayDir.z };
    

    then we setup our cube's two bounding points (two points which define our cube, i.e. in your case, one would be the block's origin blockPos, and one would be the opposite corner blockPos + 1.0f in all axes

    DirectX::XMFLOAT3 _bbox_min = blockPos;
    DirectX::XMFLOAT3 _bbox_max = { _bbox_min.x + SOLID_BLOCK_SIZE, _bbox_min.y + SOLID_BLOCK_SIZE, _bbox_min.z + SOLID_BLOCK_SIZE };
    

    SOLID_BLOCK_SIZE is the length of one of your cube's dimensions, which is 1.0f in your case. then we clip the ray by three pair of planes parallel to cube's sides:

    float tx1 = (_bbox_min.x - rayOrigin.x) * _inv_slope.x;
    float tx2 = (_bbox_max.x - rayOrigin.x) * _inv_slope.x;
    
    tmin = min(tx1, tx2);
    tmax = max(tx1, tx2);
    
    float ty1 = (_bbox_min.y - rayOrigin.y) * _inv_slope.y;
    float ty2 = (_bbox_max.y - rayOrigin.y) * _inv_slope.y;
    
    tmin = max(tmin, min(ty1, ty2));
    tmax = min(tmax, max(ty1, ty2));
    
    
    float tz1 = (_bbox_min.z - rayOrigin.z) * _inv_slope.z;
    float tz2 = (_bbox_max.z - rayOrigin.z) * _inv_slope.z;
    
    tmin = max(tmin, min(tz1, tz2));
    tmax = min(tmax, max(tz1, tz2));
    

    the tmin and tmax values give you the near and far intersection values. now we find the exact point in which the ray intersects with cube by multiplying the direction vector by the calculated tmin value and adding it to the ray's origin:

    DirectX::XMFLOAT3 _pos = {};
    _pos.x = (tmin * rayDir.x) + rayOrigin.x;
    _pos.y = (tmin * rayDir.y) + rayOrigin.y;
    _pos.z = (tmin * rayDir.z) + rayOrigin.z;
    

    now if you have previously translated your cube to origin, you can straight away take the biggest component as the face you are hitting, or if not, you have to implement something hacky like this:

    if (abs(_pos.x - blockPos.x) < 0.00001f) //left (-x) face
    {
        //return left face enum, or return the calcualtedposition
        //i myself return a modified block origin for a new block to be made
         blockPos.x -= SOLID_BLOCK_SIZE;
         return blockPos;
    }
    
    if (abs(_pos.x - blockPos.x) > 0.99990) //right (+x) face
    {
         blockPos.x += SOLID_BLOCK_SIZE;
         return blockPos;
    }
    
    if (abs(_pos.y - blockPos.y) < 0.00001f) // bot (-y) face
    {
        blockPos.y -= SOLID_BLOCK_SIZE;
        return blockPos;
    }
    
    if (abs(_pos.y - blockPos.y) > 0.99990f) // top (+y) face
    {
        blockPos.y += SOLID_BLOCK_SIZE;
        return blockPos;
    }
    
    if (abs(_pos.z - blockPos.z) < 0.00001f) // front (-z) face
    {
        blockPos.z -= SOLID_BLOCK_SIZE;
        return blockPos;
    }
    
    if (abs(_pos.z - blockPos.z) > 0.99990f) // back (+z) face
    {
        blockPos.z += SOLID_BLOCK_SIZE;
        return blockPos;
    }
    

    In the above code,i calculate the distance from the intersecting point to each face, if the distance is close enough, the point is on that face. This last part is obviously not that clean or consolidated but it does the job, you can also elect to take into account the scenarios in which the ray exactly hits the edges of the cube, which i have avoided addressing by using the float value margins in the comparisons above.

    above algorithm in action