raylib billboards, advanced use

As raylib is intended for novice programmers (but more than useful enough for advanced users) it’s important not to make assumptions about the end users understanding of the finer points of rendering. With this in mind lets look at just what a “billboard” is.

Billboards are fast, even a low poly tree (for example) could have 40-200 triangles, a billboard uses just 2 triangles, this makes it ideal when you need to render thousands of items, for example a dense forest, or if you use a tiny billboard they are ideal for particles, for example snowflakes, rain etc.

As the name suggests a billboard is flat, if they were to be rendered as if they were just a normal 3D model, you would end up with very thin trees depending what angle the camera is to the billboard. In order to give the impression that the 2D billboard is 3D, a very simple trick is used, basically the billboard is rotated so that it is always directly facing the camera.

In the picture on the left, which shows a number of billboards, looking closely you should be able to see that not all of the billboards are rendered correctly, to explain this we need to look at how the GPU’s render pipeline actually puts a texture on the screen.

Once various transforms (rotations, translation (position) scale, perspective etc) have been done, a fragment of the texture can be rendered to the screen. Each fragment has X and Y coordinates and a Z coordinate (distance to the camera). When a fragment is actually rendered its depth is stored in the depth buffer. However before actually doing this the Z coordinate is compared to the appropriate entry in the depth buffer, if that pixel has already been drawn with a closer depth buffer value, then our new pixel isn’t rendered at all, it’s behind something that is already on the screen. Normally using this method you don’t need to sort every object to be rendered before you can draw them, this works great in almost every circumstance except when we need to deal with transparency (alpha).

Looking at a texture fragment that is say 50% transparent, to accurately determine the finished colour, you need to know the colour of the texture and also the final colour behind the fragment to be rendered. This means you have to sort every item you want to render, this can be complicated by a number of factors and has a not insignificant impact on render speed.

Because even fully transparent pixels are written to the screen, the depth buffer is set, so you end up with the effect pictured, definitely not what we want!

So we need to sort everything then ? Well ideally sure, but fortunately we can cheat if our texture has only fully opaque and fully transparent pixels. In order to do this its easiest just to use a very simple shader (see the right side of the first image)

#version 330

// Input vertex attributes (from vertex shader)
in vec2 fragTexCoord;
in vec4 fragColor;

// Input uniform values
uniform sampler2D texture0;
uniform vec4 colDiffuse;

// Output fragment color
out vec4 finalColor;

void main()
{
    vec4 texelColor = texture(texture0, fragTexCoord);
    if (texelColor.a == 0.0) discard;
    finalColor = texelColor * fragColor * colDiffuse;
}

This fragment shader first looks up the colour of the texture, normally this value would just be output maybe with some effect added like lighting. However our next step in this case, is to look at the transparency of the texture, a zero value is fully transparent which is discarded. The GLSL (OpenGL Shader Language) command discard is worth understanding, here’s a quote from the GLSL language specification.

The discard keyword is only allowed within fragment shaders. It can be used within a fragment shader to abandon the operation on the current fragment. This keyword causes the fragment to be discarded and no updates to any buffers will occur.

So if we find a fully transparent area in the texture, not only is no colour information written but crucially the depth buffer is also not written to. A shader on its own isn’t much help, so to finish here is the rest of the code

 /*
 * Copyright (c) 2019 Chris Camacho (codifies -  http://bedroomcoders.co.uk/)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

#include <stddef.h>

#include "raylib.h"
#include "raymath.h"

#define screenWidth 1280
#define screenHeight 720

int main(void)
{
    //--------------------------------------------------------------------------
    // Initialization
    //--------------------------------------------------------------------------
    InitWindow(screenWidth, screenHeight, "raylib - test");

    // Define the camera to look into our 3d world
    Camera camera = { 0 };
    camera.position = (Vector3){ 0.0f, 2.0f, 8.0f };
    camera.target = (Vector3){ 0.0f, 1.0f, 0.0f };
    camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };
    camera.fovy = 45.0f;

    // textures and shader
    Texture tex = LoadTexture("data/test.png");
    Texture2D bill = LoadTexture("data/billboard.png");
    Shader alphaDiscard = LoadShader(NULL, "data/alphaDiscard.fs");
    
    // a 3D model
    Mesh mesh = GenMeshCube(.7,.7,.7);
    Model model = LoadModelFromMesh(mesh);
    model.materials[0].maps[MAP_DIFFUSE].texture = tex;

    Vector3 ang = { 0 };    // model rotation

    SetTargetFPS(60);
    SetCameraMode(camera, CAMERA_ORBITAL);

    //--------------------------------------------------------------------------
    // Main game loop
    //--------------------------------------------------------------------------
    while (!WindowShouldClose())    // Detect window close button or ESC key
    {
        //----------------------------------------------------------------------
        // Update
        //----------------------------------------------------------------------
        ang = Vector3Add(ang, (Vector3){ 0.01, 0.005, 0.0025} );
        model.transform = MatrixRotateXYZ(ang);
        UpdateCamera(&camera);

        //----------------------------------------------------------------------
        // Draw
        //----------------------------------------------------------------------
        BeginDrawing();

            ClearBackground(RAYWHITE);

            BeginMode3D(camera);

                // lower 3D model
                DrawModel(model, (Vector3){0, 0.5, 0}, 1, WHITE);
                DrawGrid(10, 1.0f);
                
                // blue inner circle
                DrawCircle3D(Vector3Zero(), 1, (Vector3){1, 0, 0}, 90, BLUE);

                // if space key, isn't held down use the shader
                // space key shows default behaviour
                if (!IsKeyDown(KEY_SPACE)) BeginShaderMode(alphaDiscard);
                
                // two circles of billboards
                for (float a=0; a < PI*2; a+=PI/4 ) 
                {
                    DrawBillboard(camera, bill, (Vector3){cos(a), 1, sin(a)}, 1.0f, WHITE);
                    DrawBillboard(camera, bill, (Vector3){cos(a-ang.x*2)*2, 1, sin(a-ang.x*2)*2}, 1.0f, WHITE);
                }
                
                if (!IsKeyDown(KEY_SPACE)) EndShaderMode();

                // invert rotation for second cube
                model.transform = MatrixInvert(model.transform);
                DrawModel(model, (Vector3){0,1.5,0}, 1, WHITE);
                
                // red outer circle
                DrawCircle3D(Vector3Zero(), 2, (Vector3){1,0,0}, 90, RED);
            
            EndMode3D();
            DrawFPS(10, 10);

        EndDrawing();
    }

    //--------------------------------------------------------------------------
    // De-Initialization
    //--------------------------------------------------------------------------

    UnloadModel(model);
    UnloadTexture(tex);
    UnloadTexture(bill);

    CloseWindow();

    return 0;
}

Leave a Reply

Your email address will not be published. Required fields are marked *