# ==Raylib== for 3D
<p class="doc-sub">// status: budding</p>
[Raylib](https://www.raylib.com/) is Ramon Santamaria's "videogame programming without the fuss" library — a thin C layer over OpenGL (desktop 2.1/3.3/4.3, ES 2.0/3.0, or WebGL) that gets a triangle on screen in about ten lines. I used it in [[Raym - Interactive Terrain Generation with Marching Cubes|Raym]] to focus on the algorithm rather than the boilerplate, and these are the bits of its 3D side worth writing down.
# Why pick raylib for 3D
- **Header-only-ish C** — single `raylib.h`, links against a static/shared lib. No build-system archaeology.
- **Batteries included** — windowing, input, audio, file I/O, image loading, a math lib (`raymath.h`), and a GUI (`raygui.h`) all in one.
- **Readable internals** — the code is genuinely small and well-commented. When you hit a weird behaviour, reading the source is faster than searching for docs.
- **Portability** — same code compiles to desktop, web (emscripten), Android, and RPi.
Where it stops being the right tool: anything that needs a modern GPU pipeline (compute shaders are GL-4.3-only and the bindings are minimal), large-scene culling, or more than a handful of dynamic lights. You'll graduate to [[OpenGL - learning log|raw OpenGL]] or [[Vulkan - learning log|Vulkan]] the moment you need it.
# The hello-cube
```c
#include "raylib.h"
int main(void) {
InitWindow(1280, 720, "hello 3d");
SetTargetFPS(60);
Camera3D camera = {
.position = { 6.0f, 6.0f, 6.0f },
.target = { 0.0f, 0.0f, 0.0f },
.up = { 0.0f, 1.0f, 0.0f },
.fovy = 45.0f,
.projection = CAMERA_PERSPECTIVE,
};
while (!WindowShouldClose()) {
UpdateCamera(&camera, CAMERA_ORBITAL);
BeginDrawing();
ClearBackground(RAYWHITE);
BeginMode3D(camera);
DrawCube((Vector3){0,0,0}, 2,2,2, MAROON);
DrawCubeWires((Vector3){0,0,0}, 2,2,2, BLACK);
DrawGrid(10, 1.0f);
EndMode3D();
DrawFPS(10, 10);
EndDrawing();
}
InitWindow(...); // forgot to close — let Raylib clean up
CloseWindow();
return 0;
}
```
Everything inside `BeginMode3D(camera)` / `EndMode3D()` uses the 3D pipeline. Outside, it's 2D and UI.
# Cameras
`Camera3D` is a plain struct. Five built-in modes via `UpdateCamera(&camera, mode)`:
- `CAMERA_FREE` — fly-through with mouse + WASD + E/Q.
- `CAMERA_ORBITAL` — auto-orbits the target. Great for demos.
- `CAMERA_FIRST_PERSON` — mouselook + WASD, stays on ground.
- `CAMERA_THIRD_PERSON` — follows a target with mouse orbit.
- `CAMERA_CUSTOM` — do nothing, you drive it yourself.
For anything real, bypass `UpdateCamera` and move position/target yourself — the built-in modes are prototype-grade. The camera exposes its matrices via `GetCameraMatrix` and `GetCameraProjectionMatrix` when you need them.
# What to draw
Two families of API sit on top of the same mesh pipeline:
- **Immediate-mode primitives** — `DrawCube`, `DrawSphere`, `DrawCylinder`, `DrawPlane`, `DrawLine3D`, `DrawRay`, `DrawBoundingBox`, `DrawGrid`. Good for debug viz, placeholder geometry, gizmos. Each call generates a mesh on the fly and draws it.
- **Meshes & models** — persistent GPU buffers. `Mesh`, `Material`, and `Model` structs. This is what you want for anything drawn more than once.
`DrawGrid` and `DrawRay` are the two I keep reaching for while debugging. Any time a 3D coordinate seems off, dropping a `DrawSphere` at that point answers it faster than a printf.
# Meshes
A `Mesh` is a flat struct of GPU buffers:
```c
typedef struct Mesh {
int vertexCount;
int triangleCount;
float *vertices; // positions (xyz)
float *texcoords; // (uv)
float *normals; // (xyz)
float *colors; // optional (rgba)
unsigned short *indices; // optional
// ... boneIds, boneWeights, tangents, texcoords2, animVertices, animNormals ...
unsigned int vaoId;
unsigned int vboId[7];
} Mesh;
```
`UploadMesh(&mesh, dynamic)` copies CPU-side arrays to GPU buffers. `UpdateMeshBuffer(mesh, i, data, size, offset)` patches one buffer for dynamic geometry. `UnloadMesh(mesh)` frees both sides.
Built-in generators for the usual primitives:
```c
Mesh sphere = GenMeshSphere(1.0f, 32, 32);
Mesh torus = GenMeshTorus(0.3f, 1.0f, 32, 24);
Mesh knot = GenMeshKnot(1.0f, 2.0f, 32, 24);
Mesh plane = GenMeshPlane(10, 10, 10, 10); // subdivided
Mesh hf = GenMeshHeightmap(imgHeight, (Vector3){16,4,16});
Mesh cubicmap = GenMeshCubicmap(imgMap, (Vector3){1,1,1});
```
`GenMeshHeightmap` and `GenMeshCubicmap` are the "terrain in one line" shortcuts — the first turns a grayscale image into a plane with displaced Y, the second turns a black-and-white image into blocky stacked cubes. For anything organic, generate your own vertex arrays and call `UploadMesh`.
# Models & materials
A `Model` bundles one or more meshes with materials and an optional skeleton:
```c
Model model = LoadModel("castle.glb");
model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = LoadTexture("castle_diffuse.png");
DrawModel(model, (Vector3){0,0,0}, 1.0f, WHITE);
UnloadModel(model);
```
Supported formats: OBJ, IQM (skeletal animation), glTF/glb (the recommended one today), VOX (MagicaVoxel), and M3D. glTF is the best supported and the one to author into.
A `Material` is a default-shader + up to 12 texture `maps`:
| Map | Slot |
|-----|------|
| `MATERIAL_MAP_DIFFUSE` / `ALBEDO` | base colour |
| `MATERIAL_MAP_METALNESS` | metallic |
| `MATERIAL_MAP_NORMAL` | normal map |
| `MATERIAL_MAP_ROUGHNESS` | roughness |
| `MATERIAL_MAP_OCCLUSION` | AO |
| `MATERIAL_MAP_EMISSION` | emissive |
| `MATERIAL_MAP_HEIGHT` | height/parallax |
| `MATERIAL_MAP_CUBEMAP` | environment |
| `MATERIAL_MAP_IRRADIANCE` | diffuse IBL |
| `MATERIAL_MAP_PREFILTER` | specular IBL |
| `MATERIAL_MAP_BRDF` | split-sum LUT |
The stock shader uses only diffuse + the light count you enable. For real [[Physically Based Rendering|PBR]] you ship a custom shader and plug the remaining maps in yourself. The [raylib-extras PBR example](https://github.com/raylib-extras) has a good reference implementation.
# Shaders
`Shader` is a vertex+fragment program pair loaded from files:
```c
Shader sh = LoadShader("shaders/mine.vs", "shaders/mine.fs");
int locTime = GetShaderLocation(sh, "uTime");
model.materials[0].shader = sh;
// in the draw loop
float t = (float)GetTime();
SetShaderValue(sh, locTime, &t, SHADER_UNIFORM_FLOAT);
DrawModel(model, ...);
```
Locations you get for free (Raylib fills these in if you name your uniforms the conventional way):
- `mvp` — full MVP matrix.
- `matModel`, `matView`, `matProjection` — components.
- `viewPos` — camera position in world space.
- `colDiffuse` — the tint passed to `DrawModel`.
- `texture0`..`texture2` — up to three samplers.
Pass `NULL` for either stage to use the built-in one:
```c
Shader sh = LoadShader(NULL, "shaders/fragment.fs"); // default VS, custom FS
```
Cross-platform tip: Raylib picks GLSL version for you (`#version 330` desktop, `100` or `300 es` web). Use `LoadShader`'s string-loading cousins (`LoadShaderFromMemory`) and `#if defined(GRAPHICS_API_OPENGL_ES)` guards if you target web.
# Lighting — `rlights.h`
Raylib doesn't ship built-in lighting. The canonical sample adds `examples/shaders/rlights.h`, a small helper that plugs Blinn-Phong (or PBR, in the PBR sample) into a forward shader with up to four lights:
```c
#include "rlights.h"
Shader sh = LoadShader("shaders/lighting.vs", "shaders/lighting.fs");
sh.locs[SHADER_LOC_VECTOR_VIEW] = GetShaderLocation(sh, "viewPos");
int ambientLoc = GetShaderLocation(sh, "ambient");
float ambient[4] = { 0.1f, 0.1f, 0.1f, 1.0f };
SetShaderValue(sh, ambientLoc, ambient, SHADER_UNIFORM_VEC4);
Light lights[MAX_LIGHTS] = {
CreateLight(LIGHT_POINT, (Vector3){-2,1, 2}, Vector3Zero(), YELLOW, sh),
CreateLight(LIGHT_DIRECTIONAL, (Vector3){ 2,2,-2}, Vector3Zero(), WHITE, sh),
};
```
For serious lighting you graduate to your own pipeline (see [[Deferred vs forward rendering]]). `rlights.h` is "demo quality" but perfect for small projects.
# Textures & render targets
```c
Texture2D tex = LoadTexture("grass.png");
Image img = LoadImage("grass.png"); // CPU-side
RenderTexture2D rt = LoadRenderTexture(1920, 1080); // FBO
```
Render-to-texture is the gateway to post-processing:
```c
BeginTextureMode(rt);
ClearBackground(BLACK);
BeginMode3D(camera);
DrawModel(model, ...);
EndMode3D();
EndTextureMode();
BeginDrawing();
ClearBackground(BLACK);
BeginShaderMode(postFX);
DrawTextureRec(rt.texture,
(Rectangle){0, 0, rt.texture.width, -rt.texture.height}, // Y flip!
(Vector2){0, 0}, WHITE);
EndShaderMode();
EndDrawing();
```
The negative `height` is the Y-flip for the flipped FBO coordinate system — easy to forget and produces an "everything is upside down" bug that baffles for longer than it should.
# Instancing
`DrawMeshInstanced` takes an array of transforms and a custom shader that reads them from `instanceTransform` (a `mat4` attribute at location 12 by default):
```c
#define N 10000
Matrix *transforms = MemAlloc(sizeof(Matrix) * N);
for (int i = 0; i < N; i++)
transforms[i] = MatrixTranslate(randX(), 0, randZ());
Shader instanceSh = LoadShader("shaders/instance.vs", "shaders/default.fs");
instanceSh.locs[SHADER_LOC_MATRIX_MVP] = GetShaderLocation(instanceSh, "mvp");
instanceSh.locs[SHADER_LOC_MATRIX_MODEL] = GetShaderLocationAttrib(instanceSh, "instanceTransform");
Material mat = LoadMaterialDefault();
mat.shader = instanceSh;
DrawMeshInstanced(cubeMesh, mat, transforms, N);
```
The instance VS multiplies by `instanceTransform` before MVP. Raylib uploads the transforms into a VBO each call — for fully static instance sets you'll want your own persistent SSBO, at which point you're dropping into rlgl.
# rlgl — the layer below
`rlgl.h` is Raylib's ==OpenGL abstraction layer==: a thin wrapper that presents an immediate-mode interface (`rlBegin`, `rlVertex3f`, `rlColor4ub`, `rlEnd`) and batches draws internally. It is how `DrawCube` is implemented, and how you drop below the friendly API when you need to:
- Build custom meshes vertex-by-vertex without touching OpenGL directly.
- Push/pop matrix state (`rlPushMatrix`, `rlRotatef`, `rlTranslatef`) à la old GL.
- Manage your own VBOs/VAOs/SSBOs (`rlLoadVertexBuffer`, `rlLoadVertexArray`).
- Dispatch compute shaders (`rlLoadComputeShaderProgram`, `rlComputeShaderDispatch`) on GL 4.3+.
When you need a modern feature Raylib doesn't expose at the top level, rlgl is almost always where it lives. If that fails, you can mix raw OpenGL into a raylib program — the GL context is just there.
## Custom mesh via rlgl
The cleaner path is usually: build float arrays, call `UploadMesh`, done. When you need immediate-mode for viz:
```c
rlBegin(RL_TRIANGLES);
rlColor4ub(255, 0, 0, 255);
rlVertex3f(0, 0, 0);
rlVertex3f(1, 0, 0);
rlVertex3f(0, 1, 0);
rlEnd();
```
Raylib's internal batcher flushes these when the primitive/type/texture changes, or when `EndMode3D`/`EndDrawing` runs. Keep all your immediate-mode calls contiguous per material to avoid unnecessary flushes.
# Skyboxes
Standard cube-map skybox — the `examples/models/models_skybox.c` sample is the reference:
1. Load 6 cube-face images, combine into a cubemap texture with `LoadTextureCubemap`.
2. Create a unit cube mesh and assign a material with a custom shader that samples the cubemap.
3. Draw it first with depth-writes disabled (`rlDisableDepthMask`), strip the translation from the view matrix so it stays centred on the camera.
HDR equirectangular → cubemap conversion is also one example away, using a `LoadShader` pass and an FBO.
# Things that tripped me up
- **Y axis on render textures** — `DrawTextureRec` with `height = -texture.height` is the fix. The Raylib coordinate system is Y-up but FBO textures are stored Y-down.
- **`DrawSphere` is slow** — it generates and uploads a mesh every frame. Use `DrawModel` on a preloaded sphere if you have more than a handful.
- **Material slot 0 is special** — `model.materials[0]` always exists, and most tutorials assume it. Multi-material models have the per-mesh material index in `model.meshMaterial[i]`.
- **`LoadModel` mutates paths** — it loads referenced textures relative to the model file. Leave them alongside the `.glb` or prepare to fix up paths manually.
- **Shader uniforms must be set while bound** — `SetShaderValue` binds the shader implicitly, but cache the locations with `GetShaderLocation` once, not per-frame.
- **Y-up vs Y-down in glTF** — both coordinates conventions appear in the wild. A flipped normal map usually means your tangent space disagrees with the asset's.
- **`BeginMode3D` resets state** — blending, depth test, face culling. Set them _after_ `BeginMode3D`, not before.
- **Triple-buffered immediate mode** — rlgl keeps three internal vertex buffers to avoid CPU/GPU stalls. You can bump the per-buffer capacity at compile time (`RL_DEFAULT_BATCH_BUFFER_ELEMENTS`) if you outgrow the default 8k vertices.
- **Compute shaders are desktop-only** — GL 4.3+ required, so they won't run in the browser. For cross-platform compute-ish work, you're stuck doing it CPU-side or waiting for WebGPU support.
# Ecosystem
- **[raygui](https://github.com/raysan5/raygui)** — immediate-mode UI in the same spirit.
- **[raylib-extras](https://github.com/raylib-extras)** — community extras: PBR, particle systems, glTF tooling, camera controllers.
- **[rres](https://github.com/raysan5/rres)** — Raylib's resource packaging format.
- **raymath.h** — the built-in math lib. Quaternions, matrices, SLERP, everything you'd want.
# References
- [raylib.com](https://www.raylib.com/) — docs and API cheatsheet.
- [raylib examples](https://github.com/raysan5/raylib/tree/master/examples/models) — the `models/` and `shaders/` folders are the most useful.
- _Raylib cheatsheet_ — one-page API reference on the site.
- Ramon Santamaria's talks on the philosophy and internals of Raylib.
---
Back to [[Index|Notes]] · see also [[Raym - Interactive Terrain Generation with Marching Cubes|Raym]] · [[OpenGL - learning log]] · [[Physically Based Rendering]]