A billboard rendering quest

Someone on reddit had a problem with rendering billboards in raylib. The code looked correct, so I wanted to try it out and thought I could just do this as a blog post - I have updated my blogging software to support compiling C code to WebAssembly, so I can just write the code here and see if it works:

  • 💾
  1 #include <raylib.h>
  2 #include "preferred_size.h"
  3 #include <math.h>
  4 
  5 int main()
  6 {
  7   int screenWidth, screenHeight;
  8   GetPreferredSize(&screenWidth, &screenHeight);
  9   InitWindow(screenWidth, screenHeight, "Billboard rendering");
 10   SetTargetFPS(30);
 11 
 12   // Define the camera to look into our 3d world
 13   Camera camera = {0};
 14   camera.position = (Vector3){5.0f, 8.0f, 5.0f}; // Camera position
 15   camera.target = (Vector3){0.0f, 0.5f, 0.0f};   // Camera looking at point
 16   camera.up = (Vector3){0.0f, 1.0f, 0.0f};       // Camera up vector (rotation towards target)
 17   camera.fovy = 45.0f;                           // Camera field-of-view Y
 18   camera.projection = CAMERA_PERSPECTIVE;        // Camera projection type
 19 
 20   Texture2D bill = LoadTexture("data/sprite.png");
 21   Vector3 billPositionStatic = {0.0f, 2.0f, 0.0f};
 22 
 23   while (!WindowShouldClose())
 24   {
 25     if (IsPaused())
 26     {
 27       // canvas is not visible in browser - do nothing
 28       continue;
 29     }
 30     BeginDrawing();
 31     ClearBackground(DARKGREEN);
 32     camera.position.x = sinf(GetTime() * 0.25f) * 5.0f;
 33     if (IsMouseButtonDown(MOUSE_LEFT_BUTTON))
 34     {
 35       camera.position.y += GetMouseDelta().y * 0.05f;
 36     }
 37     
 38     BeginMode3D(camera);
 39     DrawGrid(4, 1.0f);
 40     DrawCube((Vector3){0.0f, 0.0f, 0.0f}, 1.0f, 1.0f, 1.0f, BLUE);
 41     for (float z = -10.0f; z <= 10.0f; z += 2.0f)
 42     {
 43       Vector3 billPosition = billPositionStatic;
 44       billPosition.z = z;
 45       // a little hopping
 46       billPosition.y = fabsf(sinf(GetTime() * 5.0f + z) * 0.25f);
 47       DrawBillboard(camera, bill, billPosition, 2.0f, WHITE);
 48     }
 49     EndMode3D();
 50     EndDrawing();
 51   }
 52 
 53   CloseWindow();
 54 
 55   return 0;
 56 }
  1 #include "raylib.h"
  2 #include "preferred_size.h"
  3 
  4 // Since the canvas size is not known at compile time, we need to query it at runtime;
  5 // the following platform specific code obtains the canvas size and we will use this
  6 // size as the preferred size for the window at init time. We're ignoring here the
  7 // possibility of the canvas size changing during runtime - this would require to
  8 // poll the canvas size in the game loop or establishing a callback to be notified
  9 
 10 #ifdef PLATFORM_WEB
 11 #include <emscripten.h>
 12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
 13 
 14 void GetPreferredSize(int *screenWidth, int *screenHeight)
 15 {
 16   double canvasWidth, canvasHeight;
 17   emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
 18   *screenWidth = (int)canvasWidth;
 19   *screenHeight = (int)canvasHeight;
 20   TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
 21 }
 22 
 23 int IsPaused()
 24 {
 25   const char *js = "(function(){\n"
 26   "  var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
 27   "  var rect = canvas.getBoundingClientRect();\n"
 28   "  var isVisible = (\n"
 29   "    rect.top >= 0 &&\n"
 30   "    rect.left >= 0 &&\n"
 31   "    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
 32   "    rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
 33   "  );\n"
 34   "  return isVisible ? 0 : 1;\n"
 35   "})()";
 36   return emscripten_run_script_int(js);
 37 }
 38 
 39 #else
 40 void GetPreferredSize(int *screenWidth, int *screenHeight)
 41 {
 42   *screenWidth = 600;
 43   *screenHeight = 240;
 44 }
 45 int IsPaused()
 46 {
 47   return 0;
 48 }
 49 #endif
  1 #ifndef PREFERRED_SIZE_H
  2 #define PREFERRED_SIZE_H
  3 
  4 void GetPreferredSize(int *screenWidth, int *screenHeight);
  5 int IsPaused();
  6 
  7 #endif

Click and drag on the canvas to move the camera up and down.

When the camera angle is steep, the billboards are not facing the camera anymore, just like the user on reddit described.

I suspect that the billboard function needs an up vector to work as desired, so let's calculate the up vector from the camera position and target; note that the new parts are marked with a green background color:

  • 💾
  1 #include <raylib.h>
  2 #include "preferred_size.h"
  3 #include <math.h>
4 #include <raymath.h>
5 6 int main() 7 { 8 int screenWidth, screenHeight; 9 GetPreferredSize(&screenWidth, &screenHeight); 10 InitWindow(screenWidth, screenHeight, "Billboard rendering"); 11 SetTargetFPS(30); 12 13 // Define the camera to look into our 3d world 14 Camera camera = {0}; 15 camera.position = (Vector3){5.0f, 8.0f, 5.0f}; // Camera position 16 camera.target = (Vector3){0.0f, 0.5f, 0.0f}; // Camera looking at point 17 camera.up = (Vector3){0.0f, 1.0f, 0.0f}; // Camera up vector (rotation towards target) 18 camera.fovy = 45.0f; // Camera field-of-view Y 19 camera.projection = CAMERA_PERSPECTIVE; // Camera projection type 20 21 Texture2D bill = LoadTexture("data/sprite.png"); 22 Vector3 billPositionStatic = {0.0f, 2.0f, 0.0f}; 23 24 while (!WindowShouldClose()) 25 { 26 if (IsPaused()) 27 { 28 // canvas is not visible in browser - do nothing 29 continue; 30 } 31 BeginDrawing(); 32 ClearBackground(DARKGREEN); 33 camera.position.x = sinf(GetTime() * 0.25f) * 5.0f; 34 if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) 35 { 36 camera.position.y += GetMouseDelta().y * 0.05f; 37 } 38 39 BeginMode3D(camera); 40 DrawGrid(4, 1.0f);
41 DrawCube((Vector3){0.0f, 0.0f, 0.0f}, 0.25f, 0.25f, 0.25f, BLUE); 42 43 Rectangle source = { 0.0f, 0.0f, (float)bill.width, (float)bill.height }; 44 45 Vector3 forward = Vector3Subtract(camera.target, camera.position); 46 Vector3 up = { 0.0f, 1.0f, 0.0f }; 47 Vector3 right = Vector3CrossProduct(up, forward); 48 up = Vector3CrossProduct(forward, right); 49 up = Vector3Normalize(up); 50 float scale = 2.0f; 51 Vector2 size = (Vector2) { scale*fabsf((float)source.width/source.height), scale }; 52 53 DrawLine3D((Vector3){0.0f, 0.0f, 0.0f}, Vector3Scale(forward, 2.0f), RED); 54 DrawLine3D((Vector3){0.0f, 0.0f, 0.0f}, Vector3Scale(up, 2.0f), GREEN); 55 DrawLine3D((Vector3){0.0f, 0.0f, 0.0f}, Vector3Scale(right, 2.0f), BLUE); 56
57 for (float z = -10.0f; z <= 10.0f; z += 2.0f) 58 { 59 Vector3 billPosition = billPositionStatic;
60 billPosition.z = z; 61 billPosition.x = 2.0f;
62 // a little hopping 63 billPosition.y = fabsf(sinf(GetTime() * 5.0f + z) * 0.25f);
64 DrawBillboard(camera, bill, billPosition, scale, WHITE); 65 66 billPosition.x = -2.0f; 67 DrawBillboardPro(camera, bill, source, billPosition, up, size, Vector2Scale(size, 0.5), 0.0f, WHITE);
68 } 69 EndMode3D(); 70 EndDrawing(); 71 } 72 73 CloseWindow(); 74 75 return 0; 76 }
  1 #include "raylib.h"
  2 #include "preferred_size.h"
  3 
  4 // Since the canvas size is not known at compile time, we need to query it at runtime;
  5 // the following platform specific code obtains the canvas size and we will use this
  6 // size as the preferred size for the window at init time. We're ignoring here the
  7 // possibility of the canvas size changing during runtime - this would require to
  8 // poll the canvas size in the game loop or establishing a callback to be notified
  9 
 10 #ifdef PLATFORM_WEB
 11 #include <emscripten.h>
 12 EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target, double *width, double *height);
 13 
 14 void GetPreferredSize(int *screenWidth, int *screenHeight)
 15 {
 16   double canvasWidth, canvasHeight;
 17   emscripten_get_element_css_size("#" CANVAS_NAME, &canvasWidth, &canvasHeight);
 18   *screenWidth = (int)canvasWidth;
 19   *screenHeight = (int)canvasHeight;
 20   TraceLog(LOG_INFO, "preferred size for %s: %d %d", CANVAS_NAME, *screenWidth, *screenHeight);
 21 }
 22 
 23 int IsPaused()
 24 {
 25   const char *js = "(function(){\n"
 26   "  var canvas = document.getElementById(\"" CANVAS_NAME "\");\n"
 27   "  var rect = canvas.getBoundingClientRect();\n"
 28   "  var isVisible = (\n"
 29   "    rect.top >= 0 &&\n"
 30   "    rect.left >= 0 &&\n"
 31   "    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n"
 32   "    rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n"
 33   "  );\n"
 34   "  return isVisible ? 0 : 1;\n"
 35   "})()";
 36   return emscripten_run_script_int(js);
 37 }
 38 
 39 #else
 40 void GetPreferredSize(int *screenWidth, int *screenHeight)
 41 {
 42   *screenWidth = 600;
 43   *screenHeight = 240;
 44 }
 45 int IsPaused()
 46 {
 47   return 0;
 48 }
 49 #endif
  1 #ifndef PREFERRED_SIZE_H
  2 #define PREFERRED_SIZE_H
  3 
  4 void GetPreferredSize(int *screenWidth, int *screenHeight);
  5 int IsPaused();
  6 
  7 #endif

The billboards on the left are now facing the camera correctly. So in order for the billboards to be perfectly camera facing, the up vector needs to be provided as well. The up vector can be calculated from the camera looking direction:

  1 // the forward direction of the camera (look direction)
  2 Vector3 forward = Vector3Subtract(camera.target, camera.position);
  3 
  4 // the up vector we start with - but this up vector is not orthogonal to the forward vector
  5 Vector3 up = { 0.0f, 1.0f, 0.0f };
  6 
  7 // compute the right vector using the cross product of the up and forward vector
  8 // this vector is orthogonal to the forward vector
  9 Vector3 right = Vector3CrossProduct(up, forward);
 10 
 11 // compute the up vector using the cross product of the forward and right vector
 12 // the result is orthogonal to the forward and right vector, so it's now pointing up in 
 13 // the orientation of the camera itself
 14 up = Vector3CrossProduct(forward, right);
 15 
 16 // normalize the up vector so it's unit length
 17 up = Vector3Normalize(up);

I hope this clarifies how to use the billboard rendering function in raylib.

On a side note: The billboard function does a few of these calculations internally as well. We could use the up / forward / right vector to build a matrix instead and draw the billboards this way. A more efficient way would be to use a vertex shader to do this on the GPU.

🍪