0
\$\begingroup\$

For various reasons I am building a very simple graphics engine. I have a pretty good 2D thing using plain SDL2 and C that essentially boils down to a single "putpixel" function. I can create a window of arbitrary dimensions and draw pixels wherever I want. It's rad.

I would like to expand it to enable me to draw 3D stuff. I have dabbled with OpenGL before and have drawn cubes and triangles so I understand the concepts. For different reasons, I can't use OpenGL currently, and I thought it would be fun anyway to make something "from scratch". And now I'm stuck.

I have a standard cube:

typedef struct {
    float x;
    float y;
    float z;
} vec3;

typedef struct {
    float x;
    float y;
    float z;
    float w;
} vec4;

vec3 mesh[36] = {
    {-1.0f, -1.0f, -1.0f}, {-1.0f, 1.0f, -1.0f}, {1.0f, 1.0f, -1.0f},
    {1.0f, 1.0f, -1.0f}, {1.0f, -1.0f, -1.0f}, {-1.0f, -1.0f, -1.0f},

    {-1.0f, 1.0f, -1.0f}, {-1.0f, 1.0f, 1.0f}, {1.0f, 1.0f, 1.0f},
    {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f, -1.0f}, {-1.0f, 1.0f, -1.0f}, 
... etc

I have a matrix struct:

typedef struct {
    float e00; float e01; float e02; float e03;
    float e10; float e11; float e12; float e13;
    float e20; float e21; float e22; float e23;
    float e30; float e31; float e32; float e33;
} mat4;

I also have a vec4, and some functions to do the basic maths:

vec4 vec3_multiply_mat4(vec3 vector, mat4 matrix); // lazy hardcodes the w as 1
vec4 vec4_multiply_mat4(vec4 vector, mat4 matrix);
mat4 mat4_multiply_mat4(mat4 a, mat4 b);

So. Where I'm stuck - I know that in order to convert my universal device co-ords for my mesh, into pixel coordinates, I need 3 matrices: model, view, projection. I have defined my projection matrix because this seems to be pretty well documented. I'm using a perspective matrix:

projection = perspective(to_radians(90.0f), (float)WIDTH / (float)HEIGHT, 0.1f, 100.0f);

mat4 perspective(float fov, float aspect, float near, float far) {
    float tan_fov = tanf(fov / 2.0f);

    mat4 m = {
        1.0f / (aspect * tan_fov),  0.0f,             0.0f,                                 0.0f,
        0.0f,                       1.0f / tan_fov,   0.0f,                                 0.0f,
        0.0f,                       0.0f,             -( (far + near) / (far - near) ),     -( (2.0f * far * near) / (far - near) ),
        0.0f,                       0.0f,             -1.0f,                                0.0f,
    };


    return m;
}

I'm lost on how to properly define the model (world?) and view (camera?) matrices to properly control how my cube is displayed. For testing, I've set my view matrix to the identity matrix, and my model matrix to scale the cube to half (poking numbers in for a bit got me to this point, and it displays something on the screen).

mat4 model = {
        0.5f,   0.0f,   0.0f,   0.0f,
        0.0f,   0.5f,   0.0f,   0.0f,
        0.0f,   0.0f,   0.5f,   0.0f,
        0.0f,   0.0f,   0.0f,   1.0f,
};

mat4 view = {
        1.0f, 0.0f, 0.0f, 0.0f,
        0.0f, 1.0f, 0.0f, 0.0f,
        0.0f, 0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f,
};

Here's my draw:

void draw(void* arg) {
    pixel_cls(palette[DARK_GRAY]);

    vec4 point;
    for (uint32_t i=0 ; i<36 ; i++) {
        point = vec3_multiply_mat4(mesh[i], model);
        point = vec4_multiply_mat4(point, view);
        point = vec4_multiply_mat4(point, projection);

        point.x += 1.0;
        point.y += 1.0;

        point.x *= 0.5f * WIDTH;
        point.y *= 0.5f * HEIGHT;

        object[i] = point;
    }
    for (uint32_t i=0 ; i<36/3 ; i++) {
        pixel_line(object[i*3].x, object[i*3].y, object[i*3+1].x, object[i*3+1].y, palette[WHITE]);
        pixel_line(object[i*3+1].x, object[i*3+1].y, object[i*3+2].x, object[i*3+2].y, palette[WHITE]);
        pixel_line(object[i*3+2].x, object[i*3+2].y, object[i*3].x, object[i*3].y, palette[WHITE]);
    }
}

Which gets me 2 triangles. It appears to be drawing the cube, but I'm looking directly at one face, so everything essentially lines up.

I don't understand how to define the model & view matrices in a way that allow me to move things around and look from different angles. I also don't understand why I had to hack in some extra math to make it work (I was following a video elsewhere but I'm missing something somewhere) - this should be handled in one of the matrices no?:

point.x += 1.0;
point.y += 1.0;

point.x *= 0.5f * WIDTH;
point.y *= 0.5f * HEIGHT;

I know what I have to do. I just can't quite work out how to do it!

\$\endgroup\$
3
  • \$\begingroup\$ You may find some of the explanation in this previous Q&A helpful \$\endgroup\$
    – DMGregory
    May 10, 2023 at 10:43
  • \$\begingroup\$ @DMGregory thank you. I think I mostly understand the theory explained in that link - I need the matrices, each matrix is responsible for a particular part of the overall view, you multiply them like this. What I'm missing is how that then applies to my cube, and where I want to put it in a world. I can't quite seem to convert the theory into a practical solution. \$\endgroup\$ May 10, 2023 at 10:55
  • \$\begingroup\$ I'm lost on how to properly define the model (world?) and view (camera?) matrices to properly control how my cube is displayed. For the most part, those two matrices can start as identity matrices, which your engine can then use to move the camera or individual objects around. In order to view your cube in front of the camera, you might need to translate one of these matrices (Something like glTranslatef(0, 0, -5)), so that the cube is drawn in front of the camera, instead of on top of it (making it most likely invisible). \$\endgroup\$ May 10, 2023 at 11:32

1 Answer 1

1
\$\begingroup\$

The model matrix converts from object space to world space, so it represents the position, rotation, and scale of the object relative to the world origin.

The view matrix converts from world space to view space, so it represents the position, rotation, and scale of your world relative to the viewpoint.

Put another way, if you create a "camera model matrix" that positions your virtual camera in your world (transforming from camera-relative coordinates to world coordinates), your view matrix is just the inverse of that matrix.

The reason you barely see anything with your current view matrix is that it represents a camera placed at the origin - it's inside the cube.

Try picking some point on a hemisphere a distance away from the cube, with the camera's local z axis rotated to point back at the cube (or away from the cube, if you're using the convention that the view space z axis points out of the screen, not into it, which it looks like you are). Make a matrix to represent that position and orientation, then invert it to make your view matrix. You should see something a little more interesting.

this should be handled in one of the matrices no?

No, what you have there is usually handled by another stage of the pipeline. It's conventional to arrange the projection matrix so that, after perspective divide, x and y coordinates of on-screen points are in the range -1 to +1, with 0 in the middle of the viewport. That makes it very easy to clip/cull off-screen triangles, since the clipping planes are always x = ±1, y = ±1, regardless of the size or aspect ratio of the screen/viewport. When we want to convert to pixel coordinates in the range 0 to pixelWidth and pixelHeight, we need to add one, halve, and multiply by the viewport size on that axis, as you're doing here. When using a rendering API, this part is handled automatically between clipping and rasterization, so we don't need to bake it into our matrices.

\$\endgroup\$
3
  • \$\begingroup\$ thank you for your reply. So I've been playing with it a bit more and I've rotated my cube with the model matrix and I can see that it's drawing it all. Then I added some translations to the view matrix, but they don't work properly. Adding a negative value to the z axis should move the camera away from the cube, so it should get smaller? but nothing changes at all. I'm not sure where the z component gets mixed into the x and y? \$\endgroup\$ May 10, 2023 at 13:01
  • \$\begingroup\$ It happens in the perspective divide, which is currently missing from your code. After multiplying your vector by the projection matrix, divide the resulting xyz by the resulting w. \$\endgroup\$
    – DMGregory
    May 10, 2023 at 13:06
  • \$\begingroup\$ ah hah, ok, so that makes things work a little bit better. I've done this ` point.x /= point.w; point.y /= point.w; point.z /= point.w;` \$\endgroup\$ May 10, 2023 at 13:17

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .