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.