# ==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]]