Puhomir logo

Home / Oil-Rig

Card rendering

The first and most important part of the "graphics prototyping stage" is to draw the playing cards. At this point I'm trying to answer the question of how the cards will be rendered in the first place, not necessarily how should the final playing cards look like.

At first I did the naive thing, which is - I made a 3D model for a card in Blender and exported the UV map to draw over it. While it makes sense in the real world for a card to be a 3d model, in this case it's an overkill. There's no reason for a card to have 6 faces when the edge is barely visible even if you focus on it. The bigger problem however, is that the card back-face texture will take up 50% of all texture space. It's incredibly wasteful.

First attempt at card texturing

The second solution was to conditionally render either back-face or front-face of the card, depending on the viewing angle and card angle. This is more complex than the 3D model solution but it saves me a lot of space and allows me to easily use a texture atlas for card art. At this same time I also created the first card atlas, where the card back-face is the last image.

First image texture atlas

There's also no need to have the card back-face in the same texture atlas as card front-face. In reality the back-face of the card is something that will be rendered rarely, so I decided to optimize for the card-front face, by moving the back-face texture to another atlas. This also allows me to have more than 1 texture for the card back-face.

Technically speaking, there are N different sets of UV-coordinates, where N is the number of cards within 1 texture atlas, each pointing to the different sub-texture in the texture atlas. Card mesh is just a quad which only has one face, so when viewed from behind it becomes invisible because of the back-face culling.

I can use that to my advantage and instead use 2 quads for a card, 1 for the front-face and 1 for the back-face. The quads have the same position and rotation, but are facing opposite directions. Meaning when the front-face quad is visible, the back-face will be culled and vice versa. Furthermore each card mesh (quad) can have it's own material which points to the correct texture so I don't have to re-map the material conditionally.

This is much simpler, and it's the reason why I decided to go forward with it. Right now, there are 40 cards in one atlas, here's an example of the "debug art" card atlas, where you can clearly see the moment I realized I'm spending too much time on throwaway art:

Image texture atlas v2

Right now, I use rl.DrawMesh() to draw every card face with its own material and transformation like so:

transform := rl.MatrixScale(scale_x, scale_y, scale_z);
transform = rl.MatrixMultiply(transform, rl.MatrixRotateXYZ(rotation));
transform = rl.MatrixMultiply(transform,
    rl.MatrixTranslate(
        position.x,
        position.y,
        position.z
    )
);

rl.DrawMesh(mesh, material, transform);

Finally, I must be able to display information such as card cost, directly on the card itself. The way I do this is by dynamically drawing that information on an off-screen texture - Raylib RenderTexture. Then I make another mesh which will sample from that texture and be rendered in-front of the card front-face mesh to display that information.

For instance, let's draw a number for each cell of the card info texture atlas:

begin_render_texture(card_info);
for y: 0..4 {
    for x: 0..7 {
        rl.DrawRectangleRec(
            .{
                x * 256.0 + (96-32),
                y * 392.0 + (128-32),
                128.0,
                128.0
            },
            rl.Color.{ 200, 200, 0, 255 }
        );
        str := tprint("%\0", y * 8 + x);
        rl.DrawText(
            str.data,
            cast(s32, x * 256 + 112),
            cast(s32, y * 392 + 164),
            50,
            rl.Color.{ 0, 0, 0, 255 }
        );
    }
}

The resulting texture will look like this:

Card information texture.

This combined with everything else makes for a decent start to my card renderer:

Card rendering in-game

Even though this can be optimized, I will not do that until I have figured out rendering for the entire scene. Because at that point there will be more to consider and any optimization route which I take now will most likely be unfit in that scenario.