0xc3 1 miesiąc temu
commit
b2a78d0568
58 zmienionych plików z 6858 dodań i 0 usunięć
  1. 6 0
      README.md
  2. 11 0
      ols.json
  3. 169 0
      src/main.odin
  4. 16 0
      third-party/r3d-odin/LICENSE
  5. 94 0
      third-party/r3d-odin/examples/animation.odin
  6. 58 0
      third-party/r3d-odin/examples/basic.odin
  7. 88 0
      third-party/r3d-odin/examples/billboards.odin
  8. 109 0
      third-party/r3d-odin/examples/bloom.odin
  9. 88 0
      third-party/r3d-odin/examples/decal.odin
  10. 127 0
      third-party/r3d-odin/examples/dof.odin
  11. 91 0
      third-party/r3d-odin/examples/instanced.odin
  12. 155 0
      third-party/r3d-odin/examples/kinematics.odin
  13. 93 0
      third-party/r3d-odin/examples/lights.odin
  14. 113 0
      third-party/r3d-odin/examples/particles.odin
  15. 77 0
      third-party/r3d-odin/examples/pbr.odin
  16. 84 0
      third-party/r3d-odin/examples/probe.odin
  17. 101 0
      third-party/r3d-odin/examples/resize.odin
  18. BIN
      third-party/r3d-odin/examples/resources/images/decal.png
  19. BIN
      third-party/r3d-odin/examples/resources/images/decal_normal.png
  20. BIN
      third-party/r3d-odin/examples/resources/images/placeholder.png
  21. BIN
      third-party/r3d-odin/examples/resources/images/spritesheet.png
  22. BIN
      third-party/r3d-odin/examples/resources/images/tree.png
  23. BIN
      third-party/r3d-odin/examples/resources/models/CesiumMan.glb
  24. BIN
      third-party/r3d-odin/examples/resources/models/DamagedHelmet.glb
  25. BIN
      third-party/r3d-odin/examples/resources/panorama/indoor.png
  26. BIN
      third-party/r3d-odin/examples/resources/panorama/sky.png
  27. 26 0
      third-party/r3d-odin/examples/resources/shaders/material.glsl
  28. 8 0
      third-party/r3d-odin/examples/resources/shaders/screen.glsl
  29. 80 0
      third-party/r3d-odin/examples/shader.odin
  30. 90 0
      third-party/r3d-odin/examples/skybox.odin
  31. 103 0
      third-party/r3d-odin/examples/sprite.odin
  32. 86 0
      third-party/r3d-odin/examples/sun.odin
  33. 74 0
      third-party/r3d-odin/examples/transparency.odin
  34. BIN
      third-party/r3d-odin/r3d/linux/libr3d.a
  35. 118 0
      third-party/r3d-odin/r3d/r3d_ambient_map.odin
  36. 136 0
      third-party/r3d-odin/r3d/r3d_animation.odin
  37. 285 0
      third-party/r3d-odin/r3d/r3d_animation_player.odin
  38. 324 0
      third-party/r3d-odin/r3d/r3d_core.odin
  39. 131 0
      third-party/r3d-odin/r3d/r3d_cubemap.odin
  40. 98 0
      third-party/r3d-odin/r3d/r3d_decal.odin
  41. 249 0
      third-party/r3d-odin/r3d/r3d_draw.odin
  42. 335 0
      third-party/r3d-odin/r3d/r3d_environment.odin
  43. 104 0
      third-party/r3d-odin/r3d/r3d_importer.odin
  44. 96 0
      third-party/r3d-odin/r3d/r3d_instance.odin
  45. 376 0
      third-party/r3d-odin/r3d/r3d_kinematics.odin
  46. 566 0
      third-party/r3d-odin/r3d/r3d_lighting.odin
  47. 471 0
      third-party/r3d-odin/r3d/r3d_material.odin
  48. 283 0
      third-party/r3d-odin/r3d/r3d_mesh.odin
  49. 392 0
      third-party/r3d-odin/r3d/r3d_mesh_data.odin
  50. 134 0
      third-party/r3d-odin/r3d/r3d_model.odin
  51. 31 0
      third-party/r3d-odin/r3d/r3d_platform.odin
  52. 214 0
      third-party/r3d-odin/r3d/r3d_probe.odin
  53. 126 0
      third-party/r3d-odin/r3d/r3d_screen_shader.odin
  54. 130 0
      third-party/r3d-odin/r3d/r3d_skeleton.odin
  55. 106 0
      third-party/r3d-odin/r3d/r3d_surface_shader.odin
  56. 126 0
      third-party/r3d-odin/r3d/r3d_utils.odin
  57. 80 0
      third-party/r3d-odin/r3d/r3d_visibility.odin
  58. BIN
      third-party/r3d-odin/r3d/windows/libr3d.a

+ 6 - 0
README.md

@@ -0,0 +1,6 @@
+# game-templ-odin
+
+distrobox:
+```
+odin run src -o:none -debug -collection:huginn=src -collection:third-party=third-party -extra-linker-flags:"-L$HOME/.local/opt/odin/vendor/raylib/linux -Wl,-rpath,$HOME/.local/opt/odin/vendor/raylib/linux"
+```

+ 11 - 0
ols.json

@@ -0,0 +1,11 @@
+{
+  "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json",
+  "collections": [
+    { "name": "huginn", "path": "src" },
+    { "name": "third-party", "path": "third-party" }
+  ],
+  "enable_semantic_tokens": true,
+  "enable_document_symbols": true,
+  "enable_hover": true,
+  "enable_snippets": true
+}

+ 169 - 0
src/main.odin

@@ -0,0 +1,169 @@
+package main
+
+import "core:math"
+
+import "third-party:r3d-odin/r3d"
+import rl "vendor:raylib"
+
+GRAVITY :: -15.0
+MOVE_SPEED :: 5.0
+JUMP_FORCE :: 8.0
+
+capsule_center :: proc(caps: r3d.Capsule) -> rl.Vector3 {
+	return (caps.start + caps.end) * 0.5
+}
+
+box_center :: proc(box: rl.BoundingBox) -> rl.Vector3 {
+	return (box.min + box.max) * 0.5
+}
+
+main :: proc() {
+	rl.InitWindow(2560, 1440, "[Dev] Sandbox")
+	defer rl.CloseWindow()
+	rl.SetTargetFPS(60)
+	rl.SetWindowState({.VSYNC_HINT, .WINDOW_RESIZABLE})
+
+	r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+	defer r3d.Close()
+	r3d.SetTextureFilter(.ANISOTROPIC_8X)
+
+	sky := r3d.GenCubemapSky(4096, r3d.CUBEMAP_SKY_BASE)
+	ambient := r3d.GenAmbientMap(sky, {.ILLUMINATION, .REFLECTION})
+	env := r3d.GetEnvironment()
+	env.background.sky = sky
+	env.ambient._map = ambient
+
+	light := r3d.CreateLight(.DIR)
+	r3d.SetLightDirection(light, {-1, -1, -1})
+	r3d.SetLightRange(light, 16.0)
+	r3d.SetLightActive(light, true)
+	r3d.EnableShadow(light)
+	r3d.SetShadowDepthBias(light, 0.005)
+
+	// Load materials
+	baseAlbedo := r3d.LoadAlbedoMap(
+		"third-party/r3d-odin/examples/resources/images/placeholder.png",
+		rl.WHITE,
+	)
+
+	groundMat := r3d.GetDefaultMaterial()
+	groundMat.uvScale = {250.0, 250.0}
+	groundMat.albedo = baseAlbedo
+
+	slopeMat := r3d.GetDefaultMaterial()
+	slopeMat.albedo.color = {255, 255, 0, 255}
+	slopeMat.albedo.texture = baseAlbedo.texture
+
+	// Ground
+	groundMesh := r3d.GenMeshPlane(1000, 1000, 1, 1)
+	defer r3d.UnloadMesh(groundMesh)
+	groundBox: rl.BoundingBox = {
+		min = {-500, -1, -500},
+		max = {500, 0, 500},
+	}
+
+	// Slope obstacle
+	slopeMeshData := r3d.GenMeshDataSlope(2, 2, 2, {0, 1, -1})
+	defer r3d.UnloadMeshData(slopeMeshData)
+	slopeMesh := r3d.LoadMesh(.TRIANGLES, slopeMeshData, nil, .STATIC_MESH)
+	defer r3d.UnloadMesh(slopeMesh)
+	slopeTransform := rl.MatrixTranslate(0, 1, 5)
+
+	// Player capsule
+	capsule: r3d.Capsule = {
+		start  = {0, 0.5, 0},
+		end    = {0, 1.5, 0},
+		radius = 0.5,
+	}
+	capsMesh := r3d.GenMeshCapsule(0.5, 1.0, 64, 32)
+	defer r3d.UnloadMesh(capsMesh)
+	velocity: rl.Vector3 = {0, 0, 0}
+
+	// Camera
+	cameraAngle: f32 = 0.0
+	cameraPitch: f32 = 30.0
+	camera: rl.Camera3D = {
+		position = {0, 5, 5},
+		target   = capsule_center(capsule),
+		up       = {0, 1, 0},
+		fovy     = 60,
+	}
+
+	rl.DisableCursor()
+
+	for !rl.WindowShouldClose() {
+		dt := rl.GetFrameTime()
+
+		// Camera rotation
+		mouseDelta := rl.GetMouseDelta()
+		cameraAngle -= mouseDelta.x * 0.15
+		cameraPitch = clamp(cameraPitch + mouseDelta.y * 0.15, -7.5, 80.0)
+
+		// Movement input relative to camera
+		dx := i32(rl.IsKeyDown(.A)) - i32(rl.IsKeyDown(.D))
+		dz := i32(rl.IsKeyDown(.W)) - i32(rl.IsKeyDown(.S))
+
+		moveInput: rl.Vector3 = {0, 0, 0}
+		if dx != 0 || dz != 0 {
+			angleRad := cameraAngle * rl.DEG2RAD
+			right := rl.Vector3{math.cos_f32(angleRad), 0, -math.sin_f32(angleRad)}
+			forward := rl.Vector3{math.sin_f32(angleRad), 0, math.cos_f32(angleRad)}
+			moveInput = rl.Vector3Normalize(right * f32(dx) + forward * f32(dz))
+		}
+
+		// Check grounded
+		isGrounded :=
+			r3d.IsCapsuleGroundedBox(capsule, 0.01, groundBox, nil) ||
+			r3d.IsCapsuleGroundedMesh(capsule, 0.3, slopeMeshData, slopeTransform, nil)
+
+		// Jump and apply gravity
+		if isGrounded && rl.IsKeyPressed(.SPACE) do velocity.y = JUMP_FORCE
+		if !isGrounded do velocity.y += GRAVITY * dt
+		else if velocity.y < 0 do velocity.y = 0
+
+		// Calculate total movement
+		movement := moveInput * MOVE_SPEED * dt
+		movement.y = velocity.y * dt
+
+		// Apply movement with collision
+		movement = r3d.SlideCapsuleMesh(capsule, movement, slopeMeshData, slopeTransform, nil)
+		capsule.start = capsule.start + movement
+		capsule.end = capsule.end + movement
+
+		// Ground clamp
+		if capsule.start.y < 0.5 {
+			correction := 0.5 - capsule.start.y
+			capsule.start.y += correction
+			capsule.end.y += correction
+			velocity.y = 0
+		}
+
+		// Update camera position
+		target := capsule_center(capsule)
+		pitchRad := cameraPitch * rl.DEG2RAD
+		angleRad := cameraAngle * rl.DEG2RAD
+		camera.position = {
+			target.x - math.sin_f32(angleRad) * math.cos_f32(pitchRad) * 5.0,
+			target.y + math.sin_f32(pitchRad) * 5.0,
+			target.z - math.cos_f32(angleRad) * math.cos_f32(pitchRad) * 5.0,
+		}
+		camera.target = target
+
+		rl.BeginDrawing()
+		rl.ClearBackground(rl.BLACK)
+		r3d.Begin(camera)
+		r3d.DrawMeshPro(slopeMesh, slopeMat, slopeTransform)
+		r3d.DrawMesh(groundMesh, groundMat, {0, 0, 0}, 1.0)
+		r3d.DrawMesh(capsMesh, r3d.GetDefaultMaterial(), capsule_center(capsule), 1.0)
+		r3d.End()
+		rl.DrawFPS(10, 10)
+		rl.DrawText(
+			isGrounded ? "GROUNDED" : "AIRBORNE",
+			10,
+			rl.GetScreenHeight() - 30,
+			20,
+			isGrounded ? rl.LIME : rl.YELLOW,
+		)
+		rl.EndDrawing()
+	}
+}

+ 16 - 0
third-party/r3d-odin/LICENSE

@@ -0,0 +1,16 @@
+Copyright (c) 2026 Le Juez Victor
+
+This software is provided "as-is", without any express or implied warranty. In no event 
+will the authors be held liable for any damages arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose, including commercial 
+applications, and to alter it and redistribute it freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not claim that you 
+  wrote the original software. If you use this software in a product, an acknowledgment 
+  in the product documentation would be appreciated but is not required.
+
+  2. Altered source versions must be plainly marked as such, and must not be misrepresented
+  as being the original software.
+
+  3. This notice may not be removed or altered from any source distribution.

+ 94 - 0
third-party/r3d-odin/examples/animation.odin

@@ -0,0 +1,94 @@
+package animation
+
+import rl "vendor:raylib"
+import r3d "../r3d"
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - Animation example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D with FXAA
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+    r3d.SetAntiAliasing(.FXAA)
+
+    // Setup environment sky
+    cubemap := r3d.LoadCubemap("./resources/panorama/indoor.png", .AUTO_DETECT)
+    env := r3d.GetEnvironment()
+    env.background.skyBlur = 0.3
+    env.background.energy = 0.6
+    env.background.sky = cubemap
+
+    // Setup environment ambient
+    ambientMap := r3d.GenAmbientMap(cubemap, {.ILLUMINATION})
+    env.ambient._map = ambientMap
+    env.ambient.energy = 0.25
+
+    // Setup tonemapping
+    env.tonemap.mode = .FILMIC
+    env.tonemap.exposure = 1.5
+
+    // Generate a ground plane and load the animated model
+    plane := r3d.GenMeshPlane(10, 10, 1, 1)
+    model := r3d.LoadModel("./resources/models/CesiumMan.glb")
+
+    // Load animations
+    modelAnims := r3d.LoadAnimationLib("./resources/models/CesiumMan.glb")
+    modelPlayer := r3d.LoadAnimationPlayer(model.skeleton, modelAnims)
+
+    // Setup animation playing
+    r3d.SetAnimationWeight(&modelPlayer, 0, 1.0)
+    r3d.SetAnimationLoop(&modelPlayer, 0, true)
+    r3d.PlayAnimation(&modelPlayer, 0)
+
+    // Create model instances
+    instances := r3d.LoadInstanceBuffer(4, {.POSITION})
+    positions := cast([^]rl.Vector3)r3d.MapInstances(instances, {.POSITION})
+    for z in 0..<2 {
+        for x in 0..<2 {
+            positions[z*2 + x] = {f32(x) - 0.5, 0, f32(z) - 0.5}
+        }
+    }
+    r3d.UnmapInstances(instances, {.POSITION})
+
+    // Setup lights with shadows
+    light := r3d.CreateLight(.DIR)
+    r3d.SetLightDirection(light, {-1.0, -1.0, -1.0})
+    r3d.SetLightActive(light, true)
+    r3d.SetLightRange(light, 10.0)
+    r3d.EnableShadow(light)
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {0, 1.5, 3.0},
+        target = {0, 0.75, 0.0},
+        up = {0, 1, 0},
+        fovy = 60
+    }
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        delta := rl.GetFrameTime()
+
+        rl.UpdateCamera(&camera, rl.CameraMode.ORBITAL)
+        r3d.UpdateAnimationPlayer(&modelPlayer, delta)
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.RAYWHITE)
+            r3d.Begin(camera)
+                r3d.DrawMesh(plane, r3d.GetDefaultMaterial(), {0, 0, 0}, 1.0)
+                r3d.DrawAnimatedModel(model, modelPlayer, {0, 0, 0}, 1.25)
+                r3d.DrawAnimatedModelInstanced(model, modelPlayer, instances, 4)
+            r3d.End()
+        rl.EndDrawing()
+    }
+
+    // Cleanup
+    r3d.UnloadAnimationPlayer(modelPlayer)
+    r3d.UnloadAnimationLib(modelAnims)
+    r3d.UnloadModel(model, true)
+    r3d.UnloadMesh(plane)
+}

+ 58 - 0
third-party/r3d-odin/examples/basic.odin

@@ -0,0 +1,58 @@
+package basic
+
+import rl "vendor:raylib"
+import r3d "../r3d"
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - Basic example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+
+    // Create meshes
+    plane := r3d.GenMeshPlane(1000, 1000, 1, 1)
+    sphere := r3d.GenMeshSphere(0.5, 64, 64)
+    material := r3d.GetDefaultMaterial()
+
+    // Setup environment
+    env := r3d.GetEnvironment()
+    env.ambient.color = {10, 10, 10, 255}
+
+    // Create light
+    light := r3d.CreateLight(.SPOT)
+    r3d.LightLookAt(light, {0, 10, 5}, {0, 0, 0})
+    r3d.SetLightActive(light, true)
+    r3d.EnableShadow(light)
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {0, 2, 2},
+        target = {0, 0, 0},
+        up = {0, 1, 0},
+        fovy = 60
+    }
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        rl.UpdateCamera(&camera, rl.CameraMode.ORBITAL)
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.RAYWHITE)
+
+            r3d.Begin(camera)
+                r3d.DrawMesh(plane, material, {0, -0.5, 0}, 1.0)
+                r3d.DrawMesh(sphere, material, {0, 0, 0}, 1.0)
+            r3d.End()
+
+        rl.EndDrawing()
+    }
+
+    // Cleanup
+    r3d.UnloadMesh(sphere)
+    r3d.UnloadMesh(plane)
+}

+ 88 - 0
third-party/r3d-odin/examples/billboards.odin

@@ -0,0 +1,88 @@
+package billboards
+
+import rl "vendor:raylib"
+import r3d "../r3d"
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - Billboards example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+    r3d.SetTextureFilter(.POINT)
+
+    // Set background/ambient color
+    env := r3d.GetEnvironment()
+    env.background.color = {102, 191, 255, 255}
+    env.ambient.color = {10, 19, 25, 255}
+    env.tonemap.mode = .FILMIC
+
+    // Create ground mesh and material
+    meshGround := r3d.GenMeshPlane(200, 200, 1, 1)
+    defer r3d.UnloadMesh(meshGround)
+    matGround := r3d.GetDefaultMaterial()
+    matGround.albedo.color = rl.GREEN
+
+    // Create billboard mesh and material
+    meshBillboard := r3d.GenMeshQuad(1.0, 1.0, 1, 1, {0.0, 0.0, 1.0})
+    defer r3d.UnloadMesh(meshBillboard)
+    meshBillboard.shadowCastMode = .ON_DOUBLE_SIDED
+
+    matBillboard := r3d.GetDefaultMaterial()
+    defer r3d.UnloadMaterial(matBillboard)
+    matBillboard.albedo = r3d.LoadAlbedoMap("./resources/images/tree.png", rl.WHITE)
+    matBillboard.billboardMode = .Y_AXIS
+
+    // Create transforms for instanced billboards
+    instances := r3d.LoadInstanceBuffer(64, {.POSITION, .SCALE})
+    positions := cast([^]rl.Vector3)r3d.MapInstances(instances, {.POSITION})
+    scales := cast([^]rl.Vector3)r3d.MapInstances(instances, {.SCALE})
+    for i in 0..<64 {
+        scaleFactor := f32(rl.GetRandomValue(25, 50)) / 10.0
+        scales[i] = {scaleFactor, scaleFactor, 1.0}
+        positions[i] = {
+            f32(rl.GetRandomValue(-100, 100)),
+            scaleFactor * 0.5,
+            f32(rl.GetRandomValue(-100, 100)),
+        }
+    }
+    r3d.UnmapInstances(instances, {.POSITION, .SCALE})
+
+    // Setup directional light with shadows
+    light := r3d.CreateLight(.DIR)
+    r3d.SetLightDirection(light, {-1, -1, -1})
+    r3d.SetShadowDepthBias(light, 0.01)
+    r3d.EnableShadow(light)
+    r3d.SetLightActive(light, true)
+    r3d.SetLightRange(light, 32.0)
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {0, 5, 0},
+        target = {0, 5, -1},
+        up = {0, 1, 0},
+        fovy = 60,
+    }
+
+    // Capture mouse
+    rl.DisableCursor()
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        rl.UpdateCamera(&camera, rl.CameraMode.FREE)
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.RAYWHITE)
+
+            r3d.Begin(camera)
+                r3d.DrawMesh(meshGround, matGround, {0, 0, 0}, 1.0)
+                r3d.DrawMeshInstanced(meshBillboard, matBillboard, instances, 64)
+            r3d.End()
+
+        rl.EndDrawing()
+    }
+}

+ 109 - 0
third-party/r3d-odin/examples/bloom.odin

@@ -0,0 +1,109 @@
+package bloom
+
+import rl "vendor:raylib"
+import "core:math"
+import r3d "../r3d"
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - Bloom example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+
+    // Setup bloom and tonemapping
+    env := r3d.GetEnvironment()
+    env.tonemap.mode = .ACES
+    env.bloom.mode = .MIX
+    env.bloom.levels = 1.0
+
+    // Set background
+    env.background.color = rl.BLACK
+
+    // Create cube mesh and material
+    cube := r3d.GenMeshCube(1.0, 1.0, 1.0)
+    defer r3d.UnloadMesh(cube)
+    material := r3d.GetDefaultMaterial()
+    hueCube: f32 = 0.0
+    material.emission.color = rl.ColorFromHSV(hueCube, 1.0, 1.0)
+    material.emission.energy = 1.0
+    material.albedo.color = rl.BLACK
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {0, 3.5, 5},
+        target   = {0, 0, 0},
+        up       = {0, 1, 0},
+        fovy     = 60,
+    }
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        delta := rl.GetFrameTime()
+        rl.UpdateCamera(&camera, rl.CameraMode.ORBITAL)
+
+        // Change cube color
+        if rl.IsKeyDown(.C) {
+            hueCube = math.wrap(hueCube + 45.0 * delta, 360)
+            material.emission.color = rl.ColorFromHSV(hueCube, 1.0, 1.0)
+        }
+
+        // Adjust bloom parameters
+        env := r3d.GetEnvironment()
+        
+        intensityDir := i32(is_key_down_delay(.RIGHT)) - i32(is_key_down_delay(.LEFT))
+        adjust_bloom_param(&env.bloom.intensity, intensityDir, 0.01, 0.0, math.F32_MAX)
+
+        radiusDir := i32(is_key_down_delay(.UP)) - i32(is_key_down_delay(.DOWN))
+        adjust_bloom_param(&env.bloom.filterRadius, radiusDir, 0.1, 0.0, math.F32_MAX)
+
+        levelDir := i32(rl.IsMouseButtonDown(.RIGHT)) - i32(rl.IsMouseButtonDown(.LEFT))
+        adjust_bloom_param(&env.bloom.levels, levelDir, 0.01, 0.0, 1.0)
+
+        // Cycle bloom mode
+        if rl.IsKeyPressed(.SPACE) {
+            env.bloom.mode = r3d.Bloom((int(env.bloom.mode) + 1) % (int(r3d.Bloom.SCREEN) + 1))
+        }
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.RAYWHITE)
+
+            r3d.Begin(camera)
+                r3d.DrawMesh(cube, material, {0, 0, 0}, 1.0)
+            r3d.End()
+
+            // Draw bloom info
+            draw_text_right(rl.TextFormat("Mode: %s", get_bloom_mode_name()), 10, 20, rl.LIME)
+            draw_text_right(rl.TextFormat("Intensity: %.2f", env.bloom.intensity), 40, 20, rl.LIME)
+            draw_text_right(rl.TextFormat("Filter Radius: %.2f", env.bloom.filterRadius), 70, 20, rl.LIME)
+            draw_text_right(rl.TextFormat("Levels: %.2f", env.bloom.levels), 100, 20, rl.LIME)
+
+        rl.EndDrawing()
+    }
+}
+
+is_key_down_delay :: proc(key: rl.KeyboardKey) -> bool {
+    return rl.IsKeyPressedRepeat(key) || rl.IsKeyPressed(key)
+}
+
+get_bloom_mode_name :: proc() -> cstring {
+    modes := [?]cstring{"Disabled", "Mix", "Additive", "Screen"}
+    env := r3d.GetEnvironment()
+    mode := int(env.bloom.mode)
+    return mode >= 0 && mode < len(modes) ? modes[mode] : "Unknown"
+}
+
+draw_text_right :: proc(text: cstring, y: i32, fontSize: i32, color: rl.Color) {
+    width := rl.MeasureText(text, fontSize)
+    rl.DrawText(text, rl.GetScreenWidth() - width - 10, y, fontSize, color)
+}
+
+adjust_bloom_param :: proc(param: ^f32, direction: i32, step: f32, min: f32, max: f32) {
+    if direction != 0 {
+        param^ = clamp(param^ + f32(direction) * step, min, max)
+    }
+}

+ 88 - 0
third-party/r3d-odin/examples/decal.odin

@@ -0,0 +1,88 @@
+package decal
+
+import rl "vendor:raylib"
+import r3d "../r3d"
+import "core:math"
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - Decal example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+
+    // Create meshes
+    plane := r3d.GenMeshPlane(5.0, 5.0, 1, 1)
+    defer r3d.UnloadMesh(plane)
+    sphere := r3d.GenMeshSphere(0.5, 64, 64)
+    defer r3d.UnloadMesh(sphere)
+    cylinder := r3d.GenMeshCylinder(0.5, 0.5, 1, 64)
+    defer r3d.UnloadMesh(cylinder)
+
+    material := r3d.GetDefaultMaterial()
+    defer r3d.UnloadMaterial(material)
+    material.albedo.color = rl.GRAY
+
+    // Create decal
+    decal := r3d.DECAL_BASE
+    defer r3d.UnloadDecalMaps(decal)
+    r3d.SetTextureFilter(.BILINEAR)
+    decal.albedo = r3d.LoadAlbedoMap("./resources/images/decal.png", rl.WHITE)
+    decal.normal = r3d.LoadNormalMap("./resources/images/decal_normal.png", 1.0)
+    decal.normalThreshold = 45.0
+    decal.fadeWidth = 20.0
+
+    // Create data for instanced drawing
+    instances := r3d.LoadInstanceBuffer(3, {.POSITION})
+    positions := cast([^]rl.Vector3)r3d.MapInstances(instances, {.POSITION})
+    positions[0] = {-1.25, 0, 1}
+    positions[1] = {0, 0, 1}
+    positions[2] = {1.25, 0, 1}
+    r3d.UnmapInstances(instances, {.POSITION})
+
+    // Setup environment
+    env := r3d.GetEnvironment()
+    env.ambient.color = {10, 10, 10, 255}
+
+    // Create light
+    light := r3d.CreateLight(.DIR)
+    r3d.SetLightDirection(light, {0.5, -1, -0.5})
+    r3d.SetShadowDepthBias(light, 0.005)
+    r3d.EnableShadow(light)
+    r3d.SetLightActive(light, true)
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {0, 3, 3},
+        target = {0, 0, 0},
+        up = {0, 1, 0},
+        fovy = 60,
+    }
+
+    // Capture mouse
+    rl.DisableCursor()
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        rl.UpdateCamera(&camera, rl.CameraMode.FREE)
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.RAYWHITE)
+
+            r3d.Begin(camera)
+                r3d.DrawMesh(plane, material, {0, 0, 0}, 1.0)
+                r3d.DrawMesh(sphere, material, {-1, 0.5, -1}, 1.0)
+                r3d.DrawMeshEx(cylinder, material, {1, 0.5, -1}, rl.QuaternionFromEuler(0, 0, math.PI/2), {1, 1, 1})
+             
+                r3d.DrawDecal(decal, {-1, 1, -1}, 1.0)
+                r3d.DrawDecalEx(decal, {1, 0.5, -0.5}, rl.QuaternionFromEuler(math.PI/2, 0, 0), {1.25, 1.25, 1.25})
+                r3d.DrawDecalInstanced(decal, instances, 3)
+            r3d.End()
+
+        rl.EndDrawing()
+    }
+}

+ 127 - 0
third-party/r3d-odin/examples/dof.odin

@@ -0,0 +1,127 @@
+package dof
+
+import rl "vendor:raylib"
+import "core:math/rand"
+import "core:fmt"
+import r3d "../r3d"
+
+X_INSTANCES :: 10
+Y_INSTANCES :: 10
+INSTANCE_COUNT :: X_INSTANCES * Y_INSTANCES
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - DoF example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D with FXAA
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+    r3d.SetAntiAliasing(.FXAA)
+
+    // Configure depth of field and background
+    env := r3d.GetEnvironment()
+    env.background.color = rl.BLACK
+    env.dof.mode = .ENABLED
+    env.dof.focusPoint = 2.0
+    env.dof.focusScale = 3.0
+    env.dof.maxBlurSize = 20.0
+
+    // Create directional light
+    light := r3d.CreateLight(.DIR)
+    r3d.SetLightDirection(light, {0, -1, 0})
+    r3d.SetLightActive(light, true)
+
+    // Create sphere mesh and default material
+    meshSphere := r3d.GenMeshSphere(0.2, 64, 64)
+    defer r3d.UnloadMesh(meshSphere)
+    matDefault := r3d.GetDefaultMaterial()
+
+    // Generate instance matrices and colors
+    spacing: f32 = 0.5
+    offsetX := (X_INSTANCES * spacing) / 2.0
+    offsetZ := (Y_INSTANCES * spacing) / 2.0
+    idx := 0
+    instances := r3d.LoadInstanceBuffer(INSTANCE_COUNT, {.POSITION, .COLOR})
+    defer r3d.UnloadInstanceBuffer(instances)
+    positions := cast([^]rl.Vector3)r3d.MapInstances(instances, {.POSITION})
+    colors := cast([^]rl.Color)r3d.MapInstances(instances, {.COLOR})
+    for x in 0..<X_INSTANCES {
+        for y in 0..<Y_INSTANCES {
+            positions[idx] = {f32(x) * spacing - offsetX, 0, f32(y) * spacing - offsetZ}
+            colors[idx] = {u8(rand.uint32() % 256), u8(rand.uint32() % 256), u8(rand.uint32() % 256), 255}
+            idx += 1
+        }
+    }
+    r3d.UnmapInstances(instances, {.POSITION, .COLOR})
+
+    // Setup camera
+    camDefault: rl.Camera3D = {
+        position = {0, 2, 2},
+        target = {0, 0, 0},
+        up = {0, 1, 0},
+        fovy = 60,
+    }
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        delta := rl.GetFrameTime()
+
+        // Rotate camera
+        rotation := rl.MatrixRotate(camDefault.up, 0.1 * delta)
+        view := camDefault.position - camDefault.target
+        view = rl.Vector3Transform(view, rotation)
+        camDefault.position = camDefault.target + view
+
+        // Adjust DoF based on mouse
+        mousePos := rl.GetMousePosition()
+        focusPoint := 0.5 + (5.0 - (mousePos.y / f32(rl.GetScreenHeight())) * 5.0)
+        focusScale := 0.5 + (5.0 - (mousePos.x / f32(rl.GetScreenWidth())) * 5.0)
+        env := r3d.GetEnvironment()
+        env.dof.focusPoint = focusPoint
+        env.dof.focusScale = focusScale
+
+        mouseWheel := rl.GetMouseWheelMove()
+        if mouseWheel != 0.0 {
+            env.dof.maxBlurSize = env.dof.maxBlurSize + mouseWheel * 0.1
+        }
+
+        if rl.IsKeyPressed(.F1) {
+            if r3d.GetOutputMode() == .DOF {
+                r3d.SetOutputMode(.SCENE)
+            } else {
+                r3d.SetOutputMode(.DOF)
+            }
+        }
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.BLACK)
+
+            // Render scene
+            r3d.Begin(camDefault)
+                r3d.DrawMeshInstanced(meshSphere, matDefault, instances, INSTANCE_COUNT)
+            r3d.End()
+
+            // Display DoF values
+            dofText := fmt.ctprintf(
+                "Focus Point: %.2f\nFocus Scale: %.2f\nMax Blur Size: %.2f\nDebug Mode: %v",
+                env.dof.focusPoint, env.dof.focusScale,
+                env.dof.maxBlurSize, r3d.GetOutputMode() == .DOF,
+            )
+            rl.DrawText(dofText, 10, 30, 20, {255, 255, 255, 127})
+
+            // Display instructions
+            rl.DrawText(
+                "F1: Toggle Debug Mode\nScroll: Adjust Max Blur Size\nMouse Left/Right: Shallow/Deep DoF\nMouse Up/Down: Adjust Focus Point Depth",
+                300, 10, 20, {255, 255, 255, 127},
+            )
+
+            // Display FPS
+            fpsText := fmt.ctprintf("FPS: %d", rl.GetFPS())
+            rl.DrawText(fpsText, 10, 10, 20, {255, 255, 255, 127})
+
+        rl.EndDrawing()
+    }
+}

+ 91 - 0
third-party/r3d-odin/examples/instanced.odin

@@ -0,0 +1,91 @@
+package instanced
+
+import rl "vendor:raylib"
+import r3d "../r3d"
+
+INSTANCE_COUNT :: 1000
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - Instanced rendering example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+
+    // Set ambient light
+    env := r3d.GetEnvironment()
+    env.ambient.color = rl.DARKGRAY
+
+    // Create cube mesh and default material
+    mesh := r3d.GenMeshCube(1, 1, 1)
+    defer r3d.UnloadMesh(mesh)
+    material := r3d.GetDefaultMaterial()
+    defer r3d.UnloadMaterial(material)
+
+    // Generate random transforms and colors for instances
+    instances := r3d.LoadInstanceBuffer(INSTANCE_COUNT, {.POSITION, .ROTATION, .SCALE, .COLOR})
+    defer r3d.UnloadInstanceBuffer(instances)
+    positions := cast([^]rl.Vector3)r3d.MapInstances(instances, {.POSITION})
+    rotations := cast([^]rl.Quaternion)r3d.MapInstances(instances, {.ROTATION})
+    scales := cast([^]rl.Vector3)r3d.MapInstances(instances, {.SCALE})
+    colors := cast([^]rl.Color)r3d.MapInstances(instances, {.COLOR})
+
+    for i in 0..<INSTANCE_COUNT
+    {
+        positions[i] = {
+            f32(rl.GetRandomValue(-50000, 50000)) / 1000,
+            f32(rl.GetRandomValue(-50000, 50000)) / 1000,
+            f32(rl.GetRandomValue(-50000, 50000)) / 1000,
+        }
+        rotations[i] = rl.QuaternionFromEuler(
+            f32(rl.GetRandomValue(-314000, 314000)) / 100000,
+            f32(rl.GetRandomValue(-314000, 314000)) / 100000,
+            f32(rl.GetRandomValue(-314000, 314000)) / 100000,
+        )
+        scales[i] = {
+            f32(rl.GetRandomValue(100, 2000)) / 1000,
+            f32(rl.GetRandomValue(100, 2000)) / 1000,
+            f32(rl.GetRandomValue(100, 2000)) / 1000,
+        }
+        colors[i] = rl.ColorFromHSV(
+            f32(rl.GetRandomValue(0, 360000)) / 1000, 1.0, 1.0,
+        )
+    }
+
+    r3d.UnmapInstances(instances, {.POSITION, .ROTATION, .SCALE, .COLOR})
+
+    // Setup directional light
+    light := r3d.CreateLight(.DIR)
+    r3d.SetLightDirection(light, {0, -1, 0})
+    r3d.SetLightActive(light, true)
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {0, 2, 2},
+        target = {0, 0, 0},
+        up = {0, 1, 0},
+        fovy = 60,
+    }
+
+    // Capture mouse
+    rl.DisableCursor()
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        rl.UpdateCamera(&camera, rl.CameraMode.FREE)
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.RAYWHITE)
+
+            r3d.Begin(camera)
+                r3d.DrawMeshInstanced(mesh, material, instances, INSTANCE_COUNT)
+            r3d.End()
+
+            rl.DrawFPS(10, 10)
+        rl.EndDrawing()
+    }
+}

+ 155 - 0
third-party/r3d-odin/examples/kinematics.odin

@@ -0,0 +1,155 @@
+package kinematics
+
+import rl "vendor:raylib"
+import "core:math"
+import r3d "../r3d"
+
+GRAVITY :: -15.0
+MOVE_SPEED :: 5.0
+JUMP_FORCE :: 8.0
+
+capsule_center :: proc(caps: r3d.Capsule) -> rl.Vector3 {
+    return (caps.start + caps.end) * 0.5
+}
+
+box_center :: proc(box: rl.BoundingBox) -> rl.Vector3 {
+    return (box.min + box.max) * 0.5
+}
+
+main :: proc() {
+    rl.InitWindow(800, 450, "[r3d] - Kinematics Example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+    r3d.SetTextureFilter(.ANISOTROPIC_8X)
+
+    sky := r3d.GenCubemapSky(4096, r3d.CUBEMAP_SKY_BASE)
+    ambient := r3d.GenAmbientMap(sky, {.ILLUMINATION, .REFLECTION})
+    env := r3d.GetEnvironment()
+    env.background.sky = sky
+    env.ambient._map = ambient
+
+    light := r3d.CreateLight(.DIR)
+    r3d.SetLightDirection(light, {-1, -1, -1})
+    r3d.SetLightRange(light, 16.0)
+    r3d.SetLightActive(light, true)
+    r3d.EnableShadow(light)
+    r3d.SetShadowDepthBias(light, 0.005)
+
+    // Load materials
+    baseAlbedo := r3d.LoadAlbedoMap("./resources/images/placeholder.png", rl.WHITE)
+
+    groundMat := r3d.GetDefaultMaterial()
+    groundMat.uvScale = {250.0, 250.0}
+    groundMat.albedo = baseAlbedo
+
+    slopeMat := r3d.GetDefaultMaterial()
+    slopeMat.albedo.color = {255, 255, 0, 255}
+    slopeMat.albedo.texture = baseAlbedo.texture
+
+    // Ground
+    groundMesh := r3d.GenMeshPlane(1000, 1000, 1, 1)
+    defer r3d.UnloadMesh(groundMesh)
+    groundBox: rl.BoundingBox = {min = {-500, -1, -500}, max = {500, 0, 500}}
+
+    // Slope obstacle
+    slopeMeshData := r3d.GenMeshDataSlope(2, 2, 2, {0, 1, -1})
+    defer r3d.UnloadMeshData(slopeMeshData)
+    slopeMesh := r3d.LoadMesh(.TRIANGLES, slopeMeshData, nil, .STATIC_MESH)
+    defer r3d.UnloadMesh(slopeMesh)
+    slopeTransform := rl.MatrixTranslate(0, 1, 5)
+
+    // Player capsule
+    capsule: r3d.Capsule = {start = {0, 0.5, 0}, end = {0, 1.5, 0}, radius = 0.5}
+    capsMesh := r3d.GenMeshCapsule(0.5, 1.0, 64, 32)
+    defer r3d.UnloadMesh(capsMesh)
+    velocity: rl.Vector3 = {0, 0, 0}
+
+    // Camera
+    cameraAngle: f32 = 0.0
+    cameraPitch: f32 = 30.0
+    camera: rl.Camera3D = {
+        position = {0, 5, 5},
+        target = capsule_center(capsule),
+        up = {0, 1, 0},
+        fovy = 60,
+    }
+
+    rl.DisableCursor()
+
+    for !rl.WindowShouldClose()
+    {
+        dt := rl.GetFrameTime()
+
+        // Camera rotation
+        mouseDelta := rl.GetMouseDelta()
+        cameraAngle -= mouseDelta.x * 0.15
+        cameraPitch = clamp(cameraPitch + mouseDelta.y * 0.15, -7.5, 80.0)
+
+        // Movement input relative to camera
+        dx := i32(rl.IsKeyDown(.A)) - i32(rl.IsKeyDown(.D))
+        dz := i32(rl.IsKeyDown(.W)) - i32(rl.IsKeyDown(.S))
+        
+        moveInput: rl.Vector3 = {0, 0, 0}
+        if dx != 0 || dz != 0 {
+            angleRad := cameraAngle * rl.DEG2RAD
+            right := rl.Vector3{math.cos_f32(angleRad), 0, -math.sin_f32(angleRad)}
+            forward := rl.Vector3{math.sin_f32(angleRad), 0, math.cos_f32(angleRad)}
+            moveInput = rl.Vector3Normalize(right * f32(dx) + forward * f32(dz))
+        }
+
+        // Check grounded
+        isGrounded := r3d.IsCapsuleGroundedBox(capsule, 0.01, groundBox, nil) ||
+                      r3d.IsCapsuleGroundedMesh(capsule, 0.3, slopeMeshData, slopeTransform, nil)
+
+        // Jump and apply gravity
+        if isGrounded && rl.IsKeyPressed(.SPACE) do velocity.y = JUMP_FORCE
+        if !isGrounded do velocity.y += GRAVITY * dt
+        else if velocity.y < 0 do velocity.y = 0
+
+        // Calculate total movement
+        movement := moveInput * MOVE_SPEED * dt
+        movement.y = velocity.y * dt
+
+        // Apply movement with collision
+        movement = r3d.SlideCapsuleMesh(capsule, movement, slopeMeshData, slopeTransform, nil)
+        capsule.start = capsule.start + movement
+        capsule.end = capsule.end + movement
+
+        // Ground clamp
+        if capsule.start.y < 0.5 {
+            correction := 0.5 - capsule.start.y
+            capsule.start.y += correction
+            capsule.end.y += correction
+            velocity.y = 0
+        }
+
+        // Update camera position
+        target := capsule_center(capsule)
+        pitchRad := cameraPitch * rl.DEG2RAD
+        angleRad := cameraAngle * rl.DEG2RAD
+        camera.position = {
+            target.x - math.sin_f32(angleRad) * math.cos_f32(pitchRad) * 5.0,
+            target.y + math.sin_f32(pitchRad) * 5.0,
+            target.z - math.cos_f32(angleRad) * math.cos_f32(pitchRad) * 5.0,
+        }
+        camera.target = target
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.BLACK)
+            r3d.Begin(camera)
+                r3d.DrawMeshPro(slopeMesh, slopeMat, slopeTransform)
+                r3d.DrawMesh(groundMesh, groundMat, {0, 0, 0}, 1.0)
+                r3d.DrawMesh(capsMesh, r3d.GetDefaultMaterial(), capsule_center(capsule), 1.0)
+            r3d.End()
+            rl.DrawFPS(10, 10)
+            rl.DrawText(
+                isGrounded ? "GROUNDED" : "AIRBORNE",
+                10, rl.GetScreenHeight() - 30, 20,
+                isGrounded ? rl.LIME : rl.YELLOW,
+            )
+        rl.EndDrawing()
+    }
+}

+ 93 - 0
third-party/r3d-odin/examples/lights.odin

@@ -0,0 +1,93 @@
+package lights
+
+import rl "vendor:raylib"
+import "core:math/rand"
+import r3d "../r3d"
+
+NUM_LIGHTS :: 128
+GRID_SIZE :: 100
+
+randf :: proc(min: f32, max: f32) -> f32 {
+    return min + (max - min) * rand.float32()
+}
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - Many lights example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+
+    // Set ambient light
+    env := r3d.GetEnvironment()
+    env.background.color = rl.BLACK
+    env.ambient.color = {10, 10, 10, 255}
+
+    // Create plane and cube meshes
+    plane := r3d.GenMeshPlane(100, 100, 1, 1)
+    defer r3d.UnloadMesh(plane)
+    cube := r3d.GenMeshCube(0.5, 0.5, 0.5)
+    defer r3d.UnloadMesh(cube)
+    material := r3d.GetDefaultMaterial()
+
+    // Allocate transforms for all spheres
+    instances := r3d.LoadInstanceBuffer(GRID_SIZE * GRID_SIZE, {.POSITION})
+    defer r3d.UnloadInstanceBuffer(instances)
+    positions := cast([^]rl.Vector3)r3d.MapInstances(instances, {.POSITION})
+    for x in -50..<50 {
+        for z in -50..<50 {
+            positions[(z+50)*GRID_SIZE + (x+50)] = {f32(x) + 0.5, 0, f32(z) + 0.5}
+        }
+    }
+    r3d.UnmapInstances(instances, {.POSITION})
+
+    // Create lights
+    lights: [NUM_LIGHTS]r3d.Light
+    for i in 0..<NUM_LIGHTS {
+        lights[i] = r3d.CreateLight(.OMNI)
+        r3d.SetLightPosition(lights[i], {randf(-50.0, 50.0), randf(1.0, 5.0), randf(-50.0, 50.0)})
+        r3d.SetLightColor(lights[i], rl.ColorFromHSV(randf(0.0, 360.0), 1.0, 1.0))
+        r3d.SetLightRange(lights[i], randf(8.0, 16.0))
+        r3d.SetLightActive(lights[i], true)
+    }
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {0, 10, 10},
+        target = {0, 0, 0},
+        up = {0, 1, 0},
+        fovy = 60,
+    }
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        rl.UpdateCamera(&camera, rl.CameraMode.ORBITAL)
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.RAYWHITE)
+
+            // Draw scene
+            r3d.Begin(camera)
+                r3d.DrawMesh(plane, material, {0, -0.25, 0}, 1.0)
+                r3d.DrawMeshInstanced(cube, material, instances, GRID_SIZE*GRID_SIZE)
+            r3d.End()
+
+            // Optionally show lights shapes
+            if rl.IsKeyDown(.F) {
+                rl.BeginMode3D(camera)
+                    for i in 0..<NUM_LIGHTS {
+                        r3d.DrawLightShape(lights[i])
+                    }
+                rl.EndMode3D()
+            }
+
+            rl.DrawFPS(10, 10)
+            rl.DrawText("Press 'F' to show the lights", 10, rl.GetScreenHeight()-34, 24, rl.BLACK)
+
+        rl.EndDrawing()
+    }
+}

+ 113 - 0
third-party/r3d-odin/examples/particles.odin

@@ -0,0 +1,113 @@
+package particles
+
+import rl "vendor:raylib"
+import "core:math"
+import r3d "../r3d"
+
+MAX_PARTICLES :: 4096
+
+Particle :: struct {
+    pos: rl.Vector3,
+    vel: rl.Vector3,
+    life: f32,
+}
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - Particles example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+
+    // Set environment
+    env := r3d.GetEnvironment()
+    env.background.color = {4, 4, 4, 255}
+    env.bloom.mode = .ADDITIVE
+
+    // Generate a gradient as emission texture for our particles
+    image := rl.GenImageGradientRadial(64, 64, 0.0, rl.WHITE, rl.BLACK)
+    texture := rl.LoadTextureFromImage(image)
+    defer rl.UnloadTexture(texture)
+    rl.UnloadImage(image)
+
+    // Generate a quad mesh for our particles
+    mesh := r3d.GenMeshQuad(0.25, 0.25, 1, 1, {0, 0, 1})
+    defer r3d.UnloadMesh(mesh)
+
+    // Setup particle material
+    material := r3d.GetDefaultMaterial()
+    defer r3d.UnloadMaterial(material)
+    material.billboardMode = .FRONT
+    material.blendMode = .ADDITIVE
+    material.albedo.texture = r3d.GetBlackTexture()
+    material.emission.color = {255, 0, 0, 255}
+    material.emission.texture = texture
+    material.emission.energy = 1.0
+
+    // Create particle instance buffer
+    instances := r3d.LoadInstanceBuffer(MAX_PARTICLES, {.POSITION})
+    defer r3d.UnloadInstanceBuffer(instances)
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {-7, 7, -7},
+        target = {0, 1, 0},
+        up = {0, 1, 0},
+        fovy = 60.0,
+        projection = .PERSPECTIVE,
+    }
+
+    // CPU buffer for storing particles
+    particles: [MAX_PARTICLES]Particle
+    positions: [MAX_PARTICLES]rl.Vector3
+    particleCount: i32 = 0
+
+    for !rl.WindowShouldClose()
+    {
+        dt := rl.GetFrameTime()
+        rl.UpdateCamera(&camera, rl.CameraMode.ORBITAL)
+
+        // Spawn particles
+        for i in 0..<10 {
+            if particleCount < MAX_PARTICLES {
+                angle := f32(rl.GetRandomValue(0, 360)) * rl.DEG2RAD
+                particles[particleCount].pos = {0, 0, 0}
+                particles[particleCount].vel = {
+                    math.cos_f32(angle) * f32(rl.GetRandomValue(20, 40)) / 10.0,
+                    f32(rl.GetRandomValue(60, 80)) / 10.0,
+                    math.sin_f32(angle) * f32(rl.GetRandomValue(20, 40)) / 10.0,
+                }
+                particles[particleCount].life = 1.0
+                particleCount += 1
+            }
+        }
+
+        // Update particles
+        alive: i32 = 0
+        for i in 0..<particleCount {
+            particles[i].vel.y -= 9.81 * dt
+            particles[i].pos.x += particles[i].vel.x * dt
+            particles[i].pos.y += particles[i].vel.y * dt
+            particles[i].pos.z += particles[i].vel.z * dt
+            particles[i].life -= dt * 0.5
+            if particles[i].life > 0 {
+                positions[alive] = particles[i].pos
+                particles[alive] = particles[i]
+                alive += 1
+            }
+        }
+        particleCount = alive
+
+        r3d.UploadInstances(instances, {.POSITION}, 0, particleCount, raw_data(positions[:]))
+
+        rl.BeginDrawing()
+            r3d.Begin(camera)
+                r3d.DrawMeshInstanced(mesh, material, instances, particleCount)
+            r3d.End()
+            rl.DrawFPS(10, 10)
+        rl.EndDrawing()
+    }
+}

+ 77 - 0
third-party/r3d-odin/examples/pbr.odin

@@ -0,0 +1,77 @@
+package pbr
+
+import rl "vendor:raylib"
+import r3d "../r3d"
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - PBR example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+    r3d.SetAntiAliasing(.FXAA)
+
+    // Setup environment sky
+    cubemap := r3d.LoadCubemap("./resources/panorama/indoor.png", .AUTO_DETECT)
+    defer r3d.UnloadCubemap(cubemap)
+    env := r3d.GetEnvironment()
+    env.background.skyBlur = 0.775
+    env.background.sky = cubemap
+
+    // Setup environment ambient
+    ambientMap := r3d.GenAmbientMap(cubemap, {.ILLUMINATION, .REFLECTION})
+    defer r3d.UnloadAmbientMap(ambientMap)
+    env.ambient._map = ambientMap
+
+    // Setup bloom
+    env.bloom.mode = .MIX
+    env.bloom.intensity = 0.02
+
+    // Setup tonemapping
+    env.tonemap.mode = .FILMIC
+    env.tonemap.exposure = 1.0
+    env.tonemap.white = 4.0
+
+    // Load model
+    r3d.SetTextureFilter(.ANISOTROPIC_4X)
+    model := r3d.LoadModel("./resources/models/DamagedHelmet.glb")
+    defer r3d.UnloadModel(model, true)
+    modelMatrix := rl.Matrix(1)
+    modelScale: f32 = 1.0
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {0, 0, 2.5},
+        target = {0, 0, 0},
+        up = {0, 1, 0},
+        fovy = 60,
+    }
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        // Update model scale with mouse wheel
+        modelScale = clamp(modelScale + rl.GetMouseWheelMove() * 0.1, 0.25, 2.5)
+
+        // Rotate model with left mouse button
+        if rl.IsMouseButtonDown(.LEFT) {
+            mouseDelta := rl.GetMouseDelta()
+            pitch := (mouseDelta.y * 0.005) / modelScale
+            yaw   := (mouseDelta.x * 0.005) / modelScale
+            rotate := rl.MatrixRotateXYZ({pitch, yaw, 0.0})
+            modelMatrix = rotate * modelMatrix
+        }
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.RAYWHITE)
+            r3d.Begin(camera)
+                scale := rl.MatrixScale(modelScale, modelScale, modelScale)
+                transform := scale * modelMatrix
+                r3d.DrawModelPro(model, transform)
+            r3d.End()
+        rl.EndDrawing()
+    }
+}

+ 84 - 0
third-party/r3d-odin/examples/probe.odin

@@ -0,0 +1,84 @@
+package probe
+
+import rl "vendor:raylib"
+import "core:math"
+import r3d "../r3d"
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - Probe example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+
+    // Setup environment sky
+    cubemap := r3d.LoadCubemap("./resources/panorama/indoor.png", .AUTO_DETECT)
+    defer r3d.UnloadCubemap(cubemap)
+    env := r3d.GetEnvironment()
+    env.background.skyBlur = 0.3
+    env.background.sky = cubemap
+
+    // Setup environment ambient
+    ambientMap := r3d.GenAmbientMap(cubemap, {.ILLUMINATION, .REFLECTION})
+    defer r3d.UnloadAmbientMap(ambientMap)
+    env.ambient._map = ambientMap
+
+    // Setup tonemapping
+    env.tonemap.mode = .FILMIC
+
+    // Create meshes
+    plane := r3d.GenMeshPlane(30, 30, 1, 1)
+    defer r3d.UnloadMesh(plane)
+    sphere := r3d.GenMeshSphere(0.5, 64, 64)
+    defer r3d.UnloadMesh(sphere)
+    material := r3d.GetDefaultMaterial()
+
+    // Create light
+    light := r3d.CreateLight(.SPOT)
+    r3d.LightLookAt(light, {0, 10, 5}, {0, 0, 0})
+    r3d.SetLightActive(light, true)
+    r3d.EnableShadow(light)
+
+    // Create probe
+    probe := r3d.CreateProbe({.ILLUMINATION, .REFLECTION})
+    r3d.SetProbePosition(probe, {0, 1, 0})
+    r3d.SetProbeShadows(probe, true)
+    r3d.SetProbeFalloff(probe, 0.5)
+    r3d.SetProbeActive(probe, true)
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {0, 3.0, 6.0},
+        target = {0, 0.5, 0},
+        up = {0, 1, 0},
+        fovy = 60,
+    }
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        rl.UpdateCamera(&camera, rl.CameraMode.ORBITAL)
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.RAYWHITE)
+
+            r3d.Begin(camera)
+
+                material.orm.roughness = 0.5
+                material.orm.metalness = 0.0
+                r3d.DrawMesh(plane, material, {0, 0, 0}, 1.0)
+
+                for i in -1..=1 {
+                    material.orm.roughness = math.abs(f32(i)) * 0.4
+                    material.orm.metalness = 1.0 - math.abs(f32(i))
+                    r3d.DrawMesh(sphere, material, {f32(i) * 3.0, 1.0, 0}, 2.0)
+                }
+
+            r3d.End()
+
+        rl.EndDrawing()
+    }
+}

+ 101 - 0
third-party/r3d-odin/examples/resize.odin

@@ -0,0 +1,101 @@
+package resize
+
+import rl "vendor:raylib"
+import "core:fmt"
+import r3d "../r3d"
+
+get_aspect_mode_name :: proc(mode: r3d.AspectMode) -> cstring {
+    switch mode {
+    case .EXPAND: return "EXPAND"
+    case .KEEP:   return "KEEP"
+    }
+    return "UNKNOWN"
+}
+
+get_upscale_mode_name :: proc(mode: r3d.UpscaleMode) -> cstring {
+    switch mode {
+    case .NEAREST: return "NEAREST"
+    case .LINEAR:  return "LINEAR"
+    case .BICUBIC: return "BICUBIC"
+    case .LANCZOS: return "LANCZOS"
+    }
+    return "UNKNOWN"
+}
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - Resize example")
+    defer rl.CloseWindow()
+    rl.SetWindowState({.WINDOW_RESIZABLE})
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+
+    // Create sphere mesh and materials
+    sphere := r3d.GenMeshSphere(0.5, 64, 64)
+    defer r3d.UnloadMesh(sphere)
+    materials: [5]r3d.Material
+    for i in 0..<5 {
+        materials[i] = r3d.GetDefaultMaterial()
+        materials[i].albedo.color = rl.ColorFromHSV(f32(i) / 5 * 330, 1.0, 1.0)
+    }
+
+    // Setup directional light
+    light := r3d.CreateLight(.DIR)
+    r3d.SetLightDirection(light, {0, 0, -1})
+    r3d.SetLightActive(light, true)
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {0, 2, 2},
+        target = {0, 0, 0},
+        up = {0, 1, 0},
+        fovy = 60,
+    }
+
+    // Current blit state
+    aspect: r3d.AspectMode = .EXPAND
+    upscale: r3d.UpscaleMode = .NEAREST
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        rl.UpdateCamera(&camera, rl.CameraMode.ORBITAL)
+
+        // Toggle aspect keep
+        if rl.IsKeyPressed(.R) {
+            aspect = r3d.AspectMode((int(aspect) + 1) % 2)
+            r3d.SetAspectMode(aspect)
+        }
+
+        // Toggle linear filtering
+        if rl.IsKeyPressed(.F) {
+            upscale = r3d.UpscaleMode((int(upscale) + 1) % 4)
+            r3d.SetUpscaleMode(upscale)
+        }
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.BLACK)
+
+            // Draw spheres
+            r3d.Begin(camera)
+                for i in 0..<5 {
+                    r3d.DrawMesh(sphere, materials[i], {f32(i) - 2, 0, 0}, 1.0)
+                }
+            r3d.End()
+
+            // Draw info
+            rl.DrawText(
+                fmt.ctprintf("Resize mode: %s", get_aspect_mode_name(aspect)),
+                10, 10, 20, rl.RAYWHITE,
+            )
+            rl.DrawText(
+                fmt.ctprintf("Filter mode: %s", get_upscale_mode_name(upscale)),
+                10, 40, 20, rl.RAYWHITE,
+            )
+
+        rl.EndDrawing()
+    }
+}

BIN
third-party/r3d-odin/examples/resources/images/decal.png


BIN
third-party/r3d-odin/examples/resources/images/decal_normal.png


BIN
third-party/r3d-odin/examples/resources/images/placeholder.png


BIN
third-party/r3d-odin/examples/resources/images/spritesheet.png


BIN
third-party/r3d-odin/examples/resources/images/tree.png


BIN
third-party/r3d-odin/examples/resources/models/CesiumMan.glb


BIN
third-party/r3d-odin/examples/resources/models/DamagedHelmet.glb


BIN
third-party/r3d-odin/examples/resources/panorama/indoor.png


BIN
third-party/r3d-odin/examples/resources/panorama/sky.png


+ 26 - 0
third-party/r3d-odin/examples/resources/shaders/material.glsl

@@ -0,0 +1,26 @@
+#pragma usage opaque shadow
+
+uniform sampler2D u_texture;
+uniform float u_time;
+
+flat varying float v_time;
+
+void vertex() {
+    v_time = 0.5 * sin(u_time) + 0.5;
+    POSITION *= 1.0 + v_time;
+}
+
+void fragment() {
+    vec2 uv = TEXCOORD;
+    uv.y += v_time * 0.5;
+
+    const vec3 base_color = vec3(0.5, 0.1, 0.0);
+    const vec3 active_color = vec3(1.0, 0.2, 0.0);
+    vec3 color = mix(base_color, active_color, v_time);
+
+    ALBEDO = color * texture(u_texture, uv).rgb;
+    EMISSION = ALBEDO * v_time * 0.5;
+
+    ROUGHNESS = mix(1.0, 0.25, v_time);
+    METALNESS = mix(0.5, 1.0, v_time);
+}

+ 8 - 0
third-party/r3d-odin/examples/resources/shaders/screen.glsl

@@ -0,0 +1,8 @@
+uniform float u_time;
+
+void fragment() {
+    vec2 uv = TEXCOORD * 2.0 - 1.0;
+    uv.x += sin(uv.y * 10.0 + u_time * 3.0) * 0.01;
+    uv.y += sin(uv.x * 10.0 + u_time * 2.0) * 0.01;
+    COLOR = SampleColor(uv * 0.5 + 0.5);
+}

+ 80 - 0
third-party/r3d-odin/examples/shader.odin

@@ -0,0 +1,80 @@
+package shader
+
+import rl "vendor:raylib"
+import r3d "../r3d"
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - Shader example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+
+    // Setup environment
+    env := r3d.GetEnvironment()
+    env.ambient.color = {10, 10, 10, 255}
+    env.bloom.mode = .ADDITIVE
+
+    // Create meshes
+    plane := r3d.GenMeshPlane(1000, 1000, 1, 1)
+    defer r3d.UnloadMesh(plane)
+    torus := r3d.GenMeshTorus(0.5, 0.1, 32, 16)
+    defer r3d.UnloadMesh(torus)
+
+    // Create material
+    material := r3d.GetDefaultMaterial()
+    material.shader = r3d.LoadSurfaceShader("./resources/shaders/material.glsl")
+    defer r3d.UnloadSurfaceShader(material.shader)
+
+    // Generate a texture for custom sampler
+    image := rl.GenImageChecked(512, 512, 16, 32, rl.WHITE, rl.BLACK)
+    texture := rl.LoadTextureFromImage(image)
+    defer rl.UnloadTexture(texture)
+    rl.UnloadImage(image)
+
+    // Set custom sampler
+    r3d.SetSurfaceShaderSampler(material.shader, "u_texture", texture)
+
+    // Load a screen shader
+    shader := r3d.LoadScreenShader("./resources/shaders/screen.glsl")
+    defer r3d.UnloadScreenShader(shader)
+    shaderPtr := shader
+    r3d.SetScreenShaderChain(&shaderPtr, 1)
+
+    // Create light
+    light := r3d.CreateLight(.SPOT)
+    r3d.LightLookAt(light, {0, 10, 5}, {0, 0, 0})
+    r3d.EnableShadow(light)
+    r3d.SetLightActive(light, true)
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {0, 2, 2},
+        target = {0, 0, 0},
+        up = {0, 1, 0},
+        fovy = 60,
+    }
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        rl.UpdateCamera(&camera, rl.CameraMode.ORBITAL)
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.RAYWHITE)
+
+            time := 2.0 * f32(rl.GetTime())
+            r3d.SetScreenShaderUniform(shader, "u_time", &time)
+            r3d.SetSurfaceShaderUniform(material.shader, "u_time", &time)
+
+            r3d.Begin(camera)
+                r3d.DrawMesh(plane, r3d.GetDefaultMaterial(), {0, -0.5, 0}, 1.0)
+                r3d.DrawMesh(torus, material, {0, 0, 0}, 1.0)
+            r3d.End()
+
+        rl.EndDrawing()
+    }
+}

+ 90 - 0
third-party/r3d-odin/examples/skybox.odin

@@ -0,0 +1,90 @@
+package skybox
+
+import rl "vendor:raylib"
+import r3d "../r3d"
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - Skybox example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+
+    // Create sphere mesh
+    sphere := r3d.GenMeshSphere(0.5, 32, 64)
+    defer r3d.UnloadMesh(sphere)
+
+    // Define procedural skybox parameters
+    skyParams := r3d.CUBEMAP_SKY_BASE
+    skyParams.groundEnergy = 2.0
+    skyParams.skyEnergy = 2.0
+    skyParams.sunEnergy = 2.0
+
+    // Load and generate skyboxes
+    skyProcedural := r3d.GenCubemapSky(512, skyParams)
+    defer r3d.UnloadCubemap(skyProcedural)
+    skyPanorama := r3d.LoadCubemap("./resources/panorama/sky.png", .AUTO_DETECT)
+    defer r3d.UnloadCubemap(skyPanorama)
+
+    // Generate ambient maps
+    ambientProcedural := r3d.GenAmbientMap(skyProcedural, {.ILLUMINATION, .REFLECTION})
+    defer r3d.UnloadAmbientMap(ambientProcedural)
+    ambientPanorama := r3d.GenAmbientMap(skyPanorama, {.ILLUMINATION, .REFLECTION})
+    defer r3d.UnloadAmbientMap(ambientPanorama)
+
+    // Set default sky/ambient maps
+    env := r3d.GetEnvironment()
+    env.background.sky = skyPanorama
+    env.ambient._map = ambientPanorama
+
+    // Set tonemapping
+    env.tonemap.mode = .AGX
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {0, 0, 10},
+        target = {0, 0, 0},
+        up = {0, 1, 0},
+        fovy = 60,
+    }
+
+    // Capture mouse
+    rl.DisableCursor()
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        rl.UpdateCamera(&camera, rl.CameraMode.FREE)
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.RAYWHITE)
+
+            env := r3d.GetEnvironment()
+            if rl.IsMouseButtonPressed(.LEFT) {
+                if env.background.sky.texture == skyPanorama.texture {
+                    env.background.sky = skyProcedural
+                    env.ambient._map = ambientProcedural
+                } else {
+                    env.background.sky = skyPanorama
+                    env.ambient._map = ambientPanorama
+                }
+            }
+
+            // Draw sphere grid
+            r3d.Begin(camera)
+                for x in 0..=8 {
+                    for y in 0..=8 {
+                        material := r3d.GetDefaultMaterial()
+                        material.orm.roughness = rl.Remap(f32(y), 0.0, 8.0, 0.0, 1.0)
+                        material.orm.metalness = rl.Remap(f32(x), 0.0, 8.0, 0.0, 1.0)
+                        r3d.DrawMesh(sphere, material, {f32(x - 4) * 1.25, f32(y - 4) * 1.25, 0.0}, 1.0)
+                    }
+                }
+            r3d.End()
+
+        rl.EndDrawing()
+    }
+}

+ 103 - 0
third-party/r3d-odin/examples/sprite.odin

@@ -0,0 +1,103 @@
+package sprite
+
+import rl "vendor:raylib"
+import "core:math"
+import r3d "../r3d"
+
+get_texcoord_scale_offset :: proc(xFrameCount: int, yFrameCount: int, currentFrame: f32) -> (uvScale: rl.Vector2, uvOffset: rl.Vector2) {
+    uvScale.x = 1.0 / f32(xFrameCount)
+    uvScale.y = 1.0 / f32(yFrameCount)
+
+    frameIndex := int(currentFrame + 0.5) % (xFrameCount * yFrameCount)
+    frameX := frameIndex % xFrameCount
+    frameY := frameIndex / xFrameCount
+
+    uvOffset.x = f32(frameX) * uvScale.x
+    uvOffset.y = f32(frameY) * uvScale.y
+
+    return
+}
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - Sprite example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+    r3d.SetTextureFilter(.POINT)
+
+    // Set background/ambient color
+    env := r3d.GetEnvironment()
+    env.background.color = {102, 191, 255, 255}
+    env.ambient.color = {10, 19, 25, 255}
+    env.tonemap.mode = .FILMIC
+
+    // Create ground mesh and material
+    meshGround := r3d.GenMeshPlane(200, 200, 1, 1)
+    defer r3d.UnloadMesh(meshGround)
+    matGround := r3d.GetDefaultMaterial()
+    matGround.albedo.color = rl.GREEN
+
+    // Create sprite mesh and material
+    meshSprite := r3d.GenMeshQuad(1.0, 1.0, 1, 1, {0, 0, 1})
+    defer r3d.UnloadMesh(meshSprite)
+    meshSprite.shadowCastMode = .ON_DOUBLE_SIDED
+
+    matSprite := r3d.GetDefaultMaterial()
+    defer r3d.UnloadMaterial(matSprite)
+    matSprite.albedo = r3d.LoadAlbedoMap("./resources/images/spritesheet.png", rl.WHITE)
+    matSprite.billboardMode = .Y_AXIS
+
+    // Setup spotlight
+    light := r3d.CreateLight(.SPOT)
+    r3d.LightLookAt(light, {0, 10, 10}, {0, 0, 0})
+    r3d.SetLightRange(light, 64.0)
+    r3d.EnableShadow(light)
+    r3d.SetLightActive(light, true)
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {0, 2, 5},
+        target = {0, 0.5, 0},
+        up = {0, 1, 0},
+        fovy = 45,
+    }
+
+    // Bird data
+    birdPos: rl.Vector3 = {0, 0.5, 0}
+    birdDirX: f32 = 1.0
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        // Update bird position
+        birdPrev := birdPos
+        time := f32(rl.GetTime())
+        birdPos.x = 2.0 * math.sin_f32(time)
+        birdPos.y = 1.0 + math.cos_f32(time * 4.0) * 0.5
+        birdDirX = (birdPos.x - birdPrev.x >= 0.0) ? 1.0 : -1.0
+
+        // Update sprite UVs
+        // We multiply by the sign of the X direction to invert the uvScale.x
+        currentFrame := 10.0 * time
+        matSprite.uvScale, matSprite.uvOffset = get_texcoord_scale_offset(
+            int(4 * birdDirX),
+            1,
+            currentFrame,
+        )
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.RAYWHITE)
+
+            // Draw scene
+            r3d.Begin(camera)
+                r3d.DrawMesh(meshGround, matGround, {0, -0.5, 0}, 1.0)
+                r3d.DrawMesh(meshSprite, matSprite, {birdPos.x, birdPos.y, 0}, 1.0)
+            r3d.End()
+
+        rl.EndDrawing()
+    }
+}

+ 86 - 0
third-party/r3d-odin/examples/sun.odin

@@ -0,0 +1,86 @@
+package sun
+
+import rl "vendor:raylib"
+import r3d "../r3d"
+
+X_INSTANCES :: 50
+Y_INSTANCES :: 50
+INSTANCE_COUNT :: X_INSTANCES * Y_INSTANCES
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - Sun example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+    r3d.SetAntiAliasing(.FXAA)
+
+    // Create meshes and material
+    plane := r3d.GenMeshPlane(1000, 1000, 1, 1)
+    defer r3d.UnloadMesh(plane)
+    sphere := r3d.GenMeshSphere(0.35, 16, 32)
+    defer r3d.UnloadMesh(sphere)
+    material := r3d.GetDefaultMaterial()
+    defer r3d.UnloadMaterial(material)
+
+    // Create transforms for instanced spheres
+    instances := r3d.LoadInstanceBuffer(INSTANCE_COUNT, {.POSITION})
+    defer r3d.UnloadInstanceBuffer(instances)
+    positions := cast([^]rl.Vector3)r3d.MapInstances(instances, {.POSITION})
+    spacing: f32 = 1.5
+    offsetX := (X_INSTANCES * spacing) / 2.0
+    offsetZ := (Y_INSTANCES * spacing) / 2.0
+    idx := 0
+    for x in 0..<X_INSTANCES {
+        for y in 0..<Y_INSTANCES {
+            positions[idx] = {f32(x) * spacing - offsetX, 0, f32(y) * spacing - offsetZ}
+            idx += 1
+        }
+    }
+    r3d.UnmapInstances(instances, {.POSITION})
+
+    // Setup environment
+    skybox := r3d.GenCubemapSky(1024, r3d.CUBEMAP_SKY_BASE)
+    env := r3d.GetEnvironment()
+    env.background.sky = skybox
+
+    ambientMap := r3d.GenAmbientMap(skybox, {.ILLUMINATION, .REFLECTION})
+    env.ambient._map = ambientMap
+
+    // Create directional light with shadows
+    light := r3d.CreateLight(.DIR)
+    r3d.SetLightDirection(light, {-1, -1, -1})
+    r3d.SetLightActive(light, true)
+    r3d.SetLightRange(light, 16.0)
+    r3d.SetShadowSoftness(light, 2.0)
+    r3d.SetShadowDepthBias(light, 0.01)
+    r3d.EnableShadow(light)
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {0, 1, 0},
+        target = {1, 1.25, 1},
+        up = {0, 1, 0},
+        fovy = 60,
+    }
+
+    // Capture mouse
+    rl.DisableCursor()
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        rl.UpdateCamera(&camera, rl.CameraMode.FREE)
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.RAYWHITE)
+            r3d.Begin(camera)
+                r3d.DrawMesh(plane, material, {0, -0.5, 0}, 1.0)
+                r3d.DrawMeshInstanced(sphere, material, instances, INSTANCE_COUNT)
+            r3d.End()
+        rl.EndDrawing()
+    }
+}

+ 74 - 0
third-party/r3d-odin/examples/transparency.odin

@@ -0,0 +1,74 @@
+package transparency
+
+import rl "vendor:raylib"
+import r3d "../r3d"
+
+main :: proc() {
+    // Initialize window
+    rl.InitWindow(800, 450, "[r3d] - Transparency example")
+    defer rl.CloseWindow()
+    rl.SetTargetFPS(60)
+
+    // Initialize R3D
+    r3d.Init(rl.GetScreenWidth(), rl.GetScreenHeight())
+    defer r3d.Close()
+
+    // Create cube model
+    cube := r3d.GenMeshCube(1, 1, 1)
+    defer r3d.UnloadMesh(cube)
+    matCube := r3d.GetDefaultMaterial()
+    matCube.transparencyMode = .ALPHA
+    matCube.albedo.color = {150, 150, 255, 100}
+    matCube.orm.occlusion = 1.0
+    matCube.orm.roughness = 0.2
+    matCube.orm.metalness = 0.2
+
+    // Create plane model
+    plane := r3d.GenMeshPlane(1000, 1000, 1, 1)
+    defer r3d.UnloadMesh(plane)
+    matPlane := r3d.GetDefaultMaterial()
+    matPlane.orm.occlusion = 1.0
+    matPlane.orm.roughness = 1.0
+    matPlane.orm.metalness = 0.0
+
+    // Create sphere model
+    sphere := r3d.GenMeshSphere(0.5, 64, 64)
+    defer r3d.UnloadMesh(sphere)
+    matSphere := r3d.GetDefaultMaterial()
+    matSphere.orm.occlusion = 1.0
+    matSphere.orm.roughness = 0.25
+    matSphere.orm.metalness = 0.75
+
+    // Setup camera
+    camera: rl.Camera3D = {
+        position = {0, 2, 2},
+        target = {0, 0, 0},
+        up = {0, 1, 0},
+        fovy = 60,
+    }
+
+    // Setup lighting
+    env := r3d.GetEnvironment()
+    env.ambient.color = {10, 10, 10, 255}
+    light := r3d.CreateLight(.SPOT)
+    r3d.LightLookAt(light, {0, 10, 5}, {0, 0, 0})
+    r3d.SetLightActive(light, true)
+    r3d.EnableShadow(light)
+
+    // Main loop
+    for !rl.WindowShouldClose()
+    {
+        rl.UpdateCamera(&camera, rl.CameraMode.ORBITAL)
+
+        rl.BeginDrawing()
+            rl.ClearBackground(rl.RAYWHITE)
+
+            r3d.Begin(camera)
+                r3d.DrawMesh(plane, matPlane, {0, -0.5, 0}, 1.0)
+                r3d.DrawMesh(sphere, matSphere, {0, 0, 0}, 1.0)
+                r3d.DrawMesh(cube, matCube, {0, 0, 0}, 1.0)
+            r3d.End()
+
+        rl.EndDrawing()
+    }
+}

BIN
third-party/r3d-odin/r3d/linux/libr3d.a


+ 118 - 0
third-party/r3d-odin/r3d/r3d_ambient_map.odin

@@ -0,0 +1,118 @@
+/* r3d_ambient_map.odin -- R3D Ambient Map Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+/**
+ * @brief Global environment lighting data.
+ *
+ * An ambient map is built from a cubemap (like a skybox)
+ * and preprocessed into two specialized textures:
+ *
+ *  - irradiance:
+ *      Low-frequency lighting used for diffuse IBL.
+ *      Captures soft ambient light from all directions.
+ *
+ *  - prefilter:
+ *      Mipmapped environment used for specular reflections.
+ *      Higher mip levels simulate rougher surfaces.
+ *
+ * Both textures are derived from the same source cubemap,
+ * but serve different shading purposes.
+ */
+AmbientMap :: struct {
+    flags:      AmbientFlags, ///< Components generated for this map
+    irradiance: u32,          ///< Diffuse IBL cubemap (may be 0 if not generated)
+    prefilter:  u32,          ///< Specular prefiltered cubemap (may be 0 if not generated)
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Loads a ambient map from an image file.
+     *
+     * The layout parameter tells how faces are arranged inside the source image.
+     */
+    LoadAmbientMap :: proc(fileName: cstring, layout: CubemapLayout, flags: AmbientFlags) -> AmbientMap ---
+
+    /**
+     * @brief Builds a ambient map from an existing rl.Image.
+     *
+     * Same behavior as R3D_LoadAmbientMap(), but without loading from disk.
+     */
+    LoadAmbientMapFromImage :: proc(image: rl.Image, layout: CubemapLayout, flags: AmbientFlags) -> AmbientMap ---
+
+    /**
+     * @brief Generates an ambient map from a cubemap.
+     *
+     * The source cubemap should usually be an HDR sky/environment.
+     *
+     * Depending on the provided flags, this function:
+     *  - convolves the cubemap into diffuse irradiance
+     *  - builds a mipmapped prefiltered cubemap for reflections
+     *
+     * @param cubemap Source cubemap (environment / sky).
+     * @param flags   Which components to generate (irradiance, reflection, or both).
+     * @return A fully initialized ambient map.
+     */
+    GenAmbientMap :: proc(cubemap: Cubemap, flags: AmbientFlags) -> AmbientMap ---
+
+    /**
+     * @brief Frees the textures used by an ambient map.
+     *
+     * After this call, the ambient map is no longer valid.
+     */
+    UnloadAmbientMap :: proc(ambientMap: AmbientMap) ---
+
+    /**
+     * @brief Rebuilds an existing ambient map from a new cubemap.
+     *
+     * Use this when the environment changes dynamically (time of day,
+     * weather, interior/exterior transitions, etc).
+     *
+     * Only the components enabled in `ambientMap.flags` are regenerated.
+     *
+     * @param ambientMap Existing ambient map to update.
+     * @param cubemap    New cubemap source.
+     */
+    UpdateAmbientMap :: proc(ambientMap: AmbientMap, cubemap: Cubemap) ---
+}
+
+/**
+ * @brief Bit-flags controlling what components are generated.
+ *
+ * - R3D_AMBIENT_ILLUMINATION -> generate diffuse irradiance
+ * - R3D_AMBIENT_REFLECTION   -> generate specular prefiltered map
+ */
+AmbientFlag :: enum u32 {
+    ILLUMINATION = 0,
+    REFLECTION   = 1,
+}
+
+AmbientFlags :: bit_set[AmbientFlag; u32]

+ 136 - 0
third-party/r3d-odin/r3d/r3d_animation.odin

@@ -0,0 +1,136 @@
+/* r3d_animation.odin -- R3D Animation Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+/**
+ * @brief Animation track storing keyframe times and values.
+ *
+ * Represents a single animated property (translation, rotation or scale).
+ * Keys are sampled by time and interpolated at runtime.
+ */
+AnimationTrack :: struct {
+    times:  [^]f32, ///< Keyframe times (sorted, in animation ticks).
+    values: [^]f32, ///< Keyframe values (rl.Vector3 or rl.Quaternion).
+    count:  i32,    ///< Number of keyframes.
+}
+
+/**
+ * @brief Animation channel controlling a single bone.
+ *
+ * Contains animation tracks for translation, rotation and scale.
+ * The sampled tracks are combined to produce the bone local transform.
+ */
+AnimationChannel :: struct {
+    translation: AnimationTrack, ///< Translation track (rl.Vector3).
+    rotation:    AnimationTrack, ///< Rotation track (rl.Quaternion).
+    scale:       AnimationTrack, ///< Scale track (rl.Vector3).
+    boneIndex:   i32,            ///< Index of the affected bone.
+}
+
+/**
+ * @brief Represents a skeletal animation for a model.
+ *
+ * Contains all animation channels required to animate a skeleton.
+ * Each channel corresponds to one bone and defines its transformation
+ * (translation, rotation, scale) over time.
+ */
+Animation :: struct {
+    channels:       [^]AnimationChannel, ///< Array of animation channels, one per animated bone.
+    channelCount:   i32,                 ///< Total number of channels in this animation.
+    ticksPerSecond: f32,                 ///< Playback rate; number of animation ticks per second.
+    duration:       f32,                 ///< Total length of the animation, in ticks.
+    boneCount:      i32,                 ///< Number of bones in the target skeleton.
+    name:           [32]i8,              ///< Animation name (null-terminated string).
+}
+
+/**
+ * @brief Represents a collection of skeletal animations sharing the same skeleton.
+ *
+ * Holds multiple animations that can be applied to compatible models or skeletons.
+ * Typically loaded together from a single 3D model file (e.g., GLTF, FBX) containing several animation clips.
+ */
+AnimationLib :: struct {
+    animations: ^Animation, ///< Array of animations included in this library.
+    count:      i32,        ///< Number of animations contained in the library.
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Loads animations from a model file.
+     * @param filePath Path to the model file containing animations.
+     * @param targetFrameRate Desired frame rate (FPS) for sampling the animations.
+     * @return Pointer to an array of R3D_Animation, or NULL on failure.
+     * @note Free the returned array using R3D_UnloadAnimationLib().
+     */
+    LoadAnimationLib :: proc(filePath: cstring) -> AnimationLib ---
+
+    /**
+     * @brief Loads animations from memory data.
+     * @param data Pointer to memory buffer containing model animation data.
+     * @param size Size of the buffer in bytes.
+     * @param hint Hint on the model format (can be NULL).
+     * @param targetFrameRate Desired frame rate (FPS) for sampling the animations.
+     * @return Pointer to an array of R3D_Animation, or NULL on failure.
+     * @note Free the returned array using R3D_UnloadAnimationLib().
+     */
+    LoadAnimationLibFromMemory :: proc(data: rawptr, size: u32, hint: cstring) -> AnimationLib ---
+
+    /**
+     * @brief Loads animations from an existing importer.
+     * @param importer Importer instance containing animation data.
+     * @return Pointer to an array of R3D_Animation, or NULL on failure.
+     * @note Free the returned array using R3D_UnloadAnimationLib().
+     */
+    LoadAnimationLibFromImporter :: proc(importer: ^Importer) -> AnimationLib ---
+
+    /**
+     * @brief Releases all resources associated with an animation library.
+     * @param animLib Animation library to unload.
+     */
+    UnloadAnimationLib :: proc(animLib: AnimationLib) ---
+
+    /**
+     * @brief Returns the index of an animation by name.
+     * @param animLib Animation library to search.
+     * @param name Name of the animation (case-sensitive).
+     * @return Zero-based index if found, or -1 if not found.
+     */
+    GetAnimationIndex :: proc(animLib: AnimationLib, name: cstring) -> i32 ---
+
+    /**
+     * @brief Retrieves an animation by name.
+     * @param animLib Animation library to search.
+     * @param name Name of the animation (case-sensitive).
+     * @return Pointer to the animation, or NULL if not found.
+     */
+    GetAnimation :: proc(animLib: AnimationLib, name: cstring) -> ^Animation ---
+}
+

+ 285 - 0
third-party/r3d-odin/r3d/r3d_animation_player.odin

@@ -0,0 +1,285 @@
+/* r3d_animation_player.odin -- R3D Animation Player Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+/**
+ * @brief Types of events that an animation player can emit.
+ */
+AnimationEvent :: enum u32 {
+    FINISHED = 0, ///< Animation has finished playing (non-looping).
+    LOOPED   = 1, ///< Animation has completed a loop.
+}
+
+/**
+ * @brief Callback type for receiving animation events.
+ *
+ * @param player Pointer to the animation player emitting the event.
+ * @param eventType Type of the event (finished, looped).
+ * @param animIndex Index of the animation triggering the event.
+ * @param userData Optional user-defined data passed when the callback was registered.
+ */
+AnimationEventCallback :: proc "c" (player: ^AnimationPlayer, eventType: AnimationEvent, animIndex: i32, userData: rawptr)
+
+/**
+ * @brief Describes the playback state of a single animation within a player.
+ *
+ * Tracks the current time, blending weight, speed, play/pause state, and looping behavior.
+ */
+AnimationState :: struct {
+    currentTime: f32,  ///< Current playback time in animation ticks.
+    weight:      f32,  ///< Blending weight; any positive value is valid.
+    speed:       f32,  ///< Playback speed; can be negative for reverse playback.
+    play:        bool, ///< Whether the animation is currently playing.
+    loop:        bool, ///< True to enable looping playback.
+}
+
+// ========================================
+// FORWARD DECLARATIONS
+// ========================================
+AnimationPlayer :: struct {
+    states:        [^]AnimationState,      ///< Array of active animation states, one per animation.
+    animLib:       AnimationLib,           ///< Animation library providing the available animations.
+    skeleton:      Skeleton,               ///< Skeleton to animate.
+    localPose:     [^]rl.Matrix,              ///< Array of bone transforms representing the blended local pose.
+    modelPose:     [^]rl.Matrix,              ///< Array of bone transforms in model space, obtained by hierarchical accumulation.
+    skinBuffer:    [^]rl.Matrix,              ///< Array of final skinning matrices (invBind * modelPose), sent to the GPU.
+    skinTexture:   u32,                    ///< GPU texture ID storing the skinning matrices as a 1D RGBA16F texture.
+    eventCallback: AnimationEventCallback, ///< Callback function to receive animation events.
+    eventUserData: rawptr,                 ///< Optional user data pointer passed to the callback.
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Creates an animation player for a skeleton and animation library.
+     *
+     * Allocates memory for animation states and pose buffers.
+     *
+     * @param skeleton Skeleton to animate.
+     * @param animLib Animation library providing animations.
+     * @return Newly created animation player, or a zeroed struct on failure.
+     */
+    LoadAnimationPlayer :: proc(skeleton: Skeleton, animLib: AnimationLib) -> AnimationPlayer ---
+
+    /**
+     * @brief Releases all resources used by an animation player.
+     *
+     * @param player Animation player to unload.
+     */
+    UnloadAnimationPlayer :: proc(player: AnimationPlayer) ---
+
+    /**
+     * @brief Checks whether an animation player is valid.
+     *
+     * @param player Animation player to check.
+     * @return true if valid, false otherwise.
+     */
+    IsAnimationPlayerValid :: proc(player: AnimationPlayer) -> bool ---
+
+    /**
+     * @brief Returns whether a given animation is currently playing.
+     *
+     * @param player Animation player.
+     * @param animIndex Index of the animation.
+     * @return true if playing, false otherwise.
+     */
+    IsAnimationPlaying :: proc(player: AnimationPlayer, animIndex: i32) -> bool ---
+
+    /**
+     * @brief Starts playback of the specified animation.
+     *
+     * @param player Animation player.
+     * @param animIndex Index of the animation to play.
+     */
+    PlayAnimation :: proc(player: ^AnimationPlayer, animIndex: i32) ---
+
+    /**
+     * @brief Pauses the specified animation.
+     *
+     * @param player Animation player.
+     * @param animIndex Index of the animation to pause.
+     */
+    PauseAnimation :: proc(player: ^AnimationPlayer, animIndex: i32) ---
+
+    /**
+     * @brief Stops the specified animation and clamps its time.
+     *
+     * @param player Animation player.
+     * @param animIndex Index of the animation to stop.
+     */
+    StopAnimation :: proc(player: ^AnimationPlayer, animIndex: i32) ---
+
+    /**
+     * @brief Rewinds the animation to the start or end depending on playback direction.
+     *
+     * @param player Animation player.
+     * @param animIndex Index of the animation to rewind.
+     */
+    RewindAnimation :: proc(player: ^AnimationPlayer, animIndex: i32) ---
+
+    /**
+     * @brief Gets the current playback time of an animation.
+     *
+     * @param player Animation player.
+     * @param animIndex Index of the animation.
+     * @return Current time in animation ticks.
+     */
+    GetAnimationTime :: proc(player: AnimationPlayer, animIndex: i32) -> f32 ---
+
+    /**
+     * @brief Sets the current playback time of an animation.
+     *
+     * @param player Animation player.
+     * @param animIndex Index of the animation.
+     * @param time Time in animation ticks.
+     */
+    SetAnimationTime :: proc(player: ^AnimationPlayer, animIndex: i32, time: f32) ---
+
+    /**
+     * @brief Gets the blending weight of an animation.
+     *
+     * @param player Animation player.
+     * @param animIndex Index of the animation.
+     * @return Current weight.
+     */
+    GetAnimationWeight :: proc(player: AnimationPlayer, animIndex: i32) -> f32 ---
+
+    /**
+     * @brief Sets the blending weight of an animation.
+     *
+     * @param player Animation player.
+     * @param animIndex Index of the animation.
+     * @param weight Blending weight to apply.
+     */
+    SetAnimationWeight :: proc(player: ^AnimationPlayer, animIndex: i32, weight: f32) ---
+
+    /**
+     * @brief Gets the playback speed of an animation.
+     *
+     * @param player Animation player.
+     * @param animIndex Index of the animation.
+     * @return Current speed (may be negative for reverse playback).
+     */
+    GetAnimationSpeed :: proc(player: AnimationPlayer, animIndex: i32) -> f32 ---
+
+    /**
+     * @brief Sets the playback speed of an animation.
+     *
+     * Negative values play the animation backwards. If setting a negative speed
+     * on a stopped animation, consider calling RewindAnimation() to start at the end.
+     *
+     * @param player Animation player.
+     * @param animIndex Index of the animation.
+     * @param speed Playback speed.
+     */
+    SetAnimationSpeed :: proc(player: ^AnimationPlayer, animIndex: i32, speed: f32) ---
+
+    /**
+     * @brief Gets whether the animation is set to loop.
+     *
+     * @param player Animation player.
+     * @param animIndex Index of the animation.
+     * @return True if looping is enabled.
+     */
+    GetAnimationLoop :: proc(player: AnimationPlayer, animIndex: i32) -> bool ---
+
+    /**
+     * @brief Enables or disables looping for the animation.
+     *
+     * @param player Animation player.
+     * @param animIndex Index of the animation.
+     * @param loop True to enable looping.
+     */
+    SetAnimationLoop :: proc(player: ^AnimationPlayer, animIndex: i32, loop: bool) ---
+
+    /**
+     * @brief Advances the time of all active animations.
+     *
+     * Updates all internal animation timers based on speed and delta time.
+     * Does NOT recalculate the skeleton pose.
+     *
+     * @param player Animation player.
+     * @param dt Delta time in seconds.
+     */
+    AdvanceAnimationPlayerTime :: proc(player: ^AnimationPlayer, dt: f32) ---
+
+    /**
+     * @brief Calculates the current blended local pose of the skeleton.
+     *
+     * Interpolates keyframes and blends all active animations according to their weights,
+     * but only computes the local transforms of each bone relative to its parent.
+     * Does NOT advance animation time.
+     *
+     * @param player Animation player whose local pose will be updated.
+     */
+    CalculateAnimationPlayerLocalPose :: proc(player: ^AnimationPlayer) ---
+
+    /**
+     * @brief Calculates the current blended model (global) pose of the skeleton.
+     *
+     * Interpolates keyframes and blends all active animations according to their weights,
+     * but only computes the global transforms of each bone in model space.
+     * This assumes the local pose is already up-to-date.
+     * Does NOT advance animation time.
+     *
+     * @param player Animation player whose model pose will be updated.
+     */
+    CalculateAnimationPlayerModelPose :: proc(player: ^AnimationPlayer) ---
+
+    /**
+     * @brief Calculates the current blended skeleton pose (local and model).
+     *
+     * Interpolates keyframes and blends all active animations according to their weights,
+     * then computes both local and model transforms for the entire skeleton.
+     * Does NOT advance animation time.
+     *
+     * @param player Animation player whose local and model poses will be updated.
+     */
+    CalculateAnimationPlayerPose :: proc(player: ^AnimationPlayer) ---
+
+    /**
+     * @brief Calculates the skinning matrices and uploads them to the GPU.
+     *
+     * @param player Animation player.
+     */
+    UploadAnimationPlayerPose :: proc(player: ^AnimationPlayer) ---
+
+    /**
+     * @brief Updates the animation player: calculates and upload blended pose, then advances time.
+     *
+     * Equivalent to calling R3D_CalculateAnimationPlayerPose() followed by
+     * R3D_UploadAnimationPlayerPose() and R3D_AdvanceAnimationPlayerTime().
+     *
+     * @param player Animation player.
+     * @param dt Delta time in seconds.
+     */
+    UpdateAnimationPlayer :: proc(player: ^AnimationPlayer, dt: f32) ---
+}
+

+ 324 - 0
third-party/r3d-odin/r3d/r3d_core.odin

@@ -0,0 +1,324 @@
+/* r3d_core.odin -- R3D Core Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+/**
+ * @brief Anti-aliasing modes for rendering.
+ */
+AntiAliasing :: enum u32 {
+    DISABLED = 0, ///< Anti-aliasing is disabled. Edges may appear jagged.
+    FXAA     = 1, ///< FXAA is applied. Smooths edges efficiently but may appear blurry.
+}
+
+/**
+ * @brief Aspect ratio handling modes for rendering.
+ */
+AspectMode :: enum u32 {
+    EXPAND = 0, ///< Expands the rendered output to fully fill the target (render texture or window).
+    KEEP   = 1, ///< Preserves the target's aspect ratio without distortion, adding empty gaps if necessary.
+}
+
+/**
+ * @brief Upscaling/filtering methods for rendering output.
+ *
+ * Upscale mode to apply when the output window is larger than the internal render resolution.
+ */
+UpscaleMode :: enum u32 {
+    NEAREST = 0, ///< Nearest-neighbor upscaling: very fast, but produces blocky pixels.
+    LINEAR  = 1, ///< Bilinear upscaling: very fast, smoother than nearest, but can appear blurry.
+    BICUBIC = 2, ///< Bicubic (Catmull-Rom) upscaling: slower, smoother, and less blurry than linear.
+    LANCZOS = 3, ///< Lanczos-2 upscaling: preserves more fine details, but is the most expensive.
+}
+
+/**
+ * @brief Downscaling/filtering methods for rendering output.
+ *
+ * Downscale mode to apply when the output window is smaller than the internal render resolution.
+ */
+DownscaleMode :: enum u32 {
+    NEAREST = 0, ///< Nearest-neighbor downscaling: very fast, but produces aliasing.
+    LINEAR  = 1, ///< Bilinear downscaling: very fast, can serve as a basic form of anti-aliasing (SSAA).
+    BOX     = 2, ///< Box-blur downscaling: uses a simple but effective box blur, slightly more expensive than linear, smooths moiré better.
+}
+
+/**
+ * @brief Defines the buffer to output (render texture or window).
+ * @note Nothing will be output if the requested target has not been created / used.
+ */
+OutputMode :: enum u32 {
+    SCENE    = 0,
+    ALBEDO   = 1,
+    NORMAL   = 2,
+    ORM      = 3,
+    DIFFUSE  = 4,
+    SPECULAR = 5,
+    SSAO     = 6,
+    SSIL     = 7,
+    SSR      = 8,
+    BLOOM    = 9,
+    DOF      = 10,
+}
+
+/**
+ * @brief Specifies the color space for user-provided colors and color textures.
+ *
+ * This enum defines how colors are interpreted for material inputs:
+ * - Surface colors (e.g., albedo or emission tint)
+ * - rl.Color textures (albedo, emission maps)
+ *
+ * Lighting values (direct or indirect light) are always linear and
+ * are not affected by this setting.
+ *
+ * Used with `R3D_SetColorSpace()` to control whether input colors
+ * should be treated as linear or sRGB.
+ */
+ColorSpace :: enum u32 {
+    LINEAR = 0, ///< Linear color space: values are used as-is.
+    SRGB   = 1, ///< sRGB color space: values are converted to linear on load.
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Initializes the rendering engine.
+     *
+     * This function sets up the internal rendering system with the provided resolution.
+     *
+     * @param resWidth Width of the internal resolution.
+     * @param resHeight Height of the internal resolution.
+     *
+     * @return True if the initialization is successful.
+     */
+    Init :: proc(resWidth: i32, resHeight: i32) -> bool ---
+
+    /**
+     * @brief Closes the rendering engine and deallocates all resources.
+     *
+     * This function shuts down the rendering system and frees all allocated memory,
+     * including the resources associated with the created lights.
+     */
+    Close :: proc() ---
+
+    /**
+     * @brief Gets the current internal resolution.
+     *
+     * This function retrieves the current internal resolution being used by the
+     * rendering engine.
+     *
+     * @param width Pointer to store the width of the internal resolution.
+     * @param height Pointer to store the height of the internal resolution.
+     */
+    GetResolution :: proc(width: ^i32, height: ^i32) ---
+
+    /**
+     * @brief Updates the internal resolution.
+     *
+     * This function changes the internal resolution of the rendering engine. Note that
+     * this process destroys and recreates all framebuffers, which may be a slow operation.
+     *
+     * @param width The new width for the internal resolution.
+     * @param height The new height for the internal resolution.
+     *
+     * @warning This function may be slow due to the destruction and recreation of framebuffers.
+     */
+    UpdateResolution :: proc(width: i32, height: i32) ---
+
+    /**
+     * @brief Retrieves the current anti-aliasing mode used for rendering.
+     * @return The currently active R3D_AntiAliasing mode.
+     */
+    GetAntiAliasing :: proc() -> AntiAliasing ---
+
+    /**
+     * @brief Sets the anti-aliasing mode for rendering.
+     * @param mode The desired R3D_AntiAliasing mode.
+     */
+    SetAntiAliasing :: proc(mode: AntiAliasing) ---
+
+    /**
+     * @brief Retrieves the current aspect ratio handling mode.
+     * @return The currently active R3D_AspectMode.
+     */
+    GetAspectMode :: proc() -> AspectMode ---
+
+    /**
+     * @brief Sets the aspect ratio handling mode for rendering.
+     * @param mode The desired R3D_AspectMode.
+     */
+    SetAspectMode :: proc(mode: AspectMode) ---
+
+    /**
+     * @brief Retrieves the current upscaling/filtering method.
+     * @return The currently active R3D_UpscaleMode.
+     */
+    GetUpscaleMode :: proc() -> UpscaleMode ---
+
+    /**
+     * @brief Sets the upscaling/filtering method for rendering output.
+     * @param mode The desired R3D_UpscaleMode.
+     */
+    SetUpscaleMode :: proc(mode: UpscaleMode) ---
+
+    /**
+     * @brief Retrieves the current downscaling mode used for rendering.
+     * @return The currently active R3D_DownscaleMode.
+     */
+    GetDownscaleMode :: proc() -> DownscaleMode ---
+
+    /**
+     * @brief Sets the downscaling mode for rendering output.
+     * @param mode The desired R3D_DownscaleMode.
+     */
+    SetDownscaleMode :: proc(mode: DownscaleMode) ---
+
+    /**
+     * @brief Gets the current output mode.
+     * @return The currently active R3D_OutputMode.
+     */
+    GetOutputMode :: proc() -> OutputMode ---
+
+    /**
+     * @brief Sets the output mode for rendering.
+     * @param mode The R3D_OutputMode to use.
+     * @note Nothing will be output if the requested target has not been created / used.
+     */
+    SetOutputMode :: proc(mode: OutputMode) ---
+
+    /**
+     * @brief Sets the default texture filtering mode.
+     *
+     * This function defines the default texture filter that will be applied to all subsequently
+     * loaded textures, including those used in materials, sprites, and other resources.
+     *
+     * If a trilinear or anisotropic filter is selected, mipmaps will be automatically generated
+     * for the textures, but they will not be generated when using nearest or bilinear filtering.
+     *
+     * The default texture filter mode is `TEXTURE_FILTER_TRILINEAR`.
+     *
+     * @param filter The texture filtering mode to be applied by default.
+     */
+    SetTextureFilter :: proc(filter: rl.TextureFilter) ---
+
+    /**
+     * @brief Set the working color space for user-provided surface colors and color textures.
+     *
+     * Defines how all *color inputs* should be interpreted:
+     * - surface colors provided in materials (e.g. albedo/emission tints)
+     * - color textures such as albedo and emission maps
+     *
+     * When set to sRGB, these values are converted to linear before shading.
+     * When set to linear, values are used as-is.
+     *
+     * This does NOT affect lighting inputs (direct or indirect light),
+     * which are always expected to be provided in linear space.
+     *
+     * The default color space is `R3D_COLORSPACE_SRGB`.
+     *
+     * @param space rl.Color space to use for color inputs (linear or sRGB).
+     */
+    SetColorSpace :: proc(space: ColorSpace) ---
+
+    /**
+     * @brief Get the currently active global rendering layers.
+     *
+     * Returns the bitfield representing the currently active layers in the renderer.
+     * By default, the internal active layers are set to 0, which means that any
+     * non-zero layer assigned to an object will NOT be rendered unless explicitly
+     * activated.
+     *
+     * @return R3D_Layer Bitfield of active layers.
+     */
+    GetActiveLayers :: proc() -> Layer ---
+
+    /**
+     * @brief Set the active global rendering layers.
+     *
+     * Replaces the current set of active layers with the given bitfield.
+     *
+     * @param bitfield Bitfield representing the layers to activate.
+     */
+    SetActiveLayers :: proc(bitfield: Layer) ---
+
+    /**
+     * @brief Enable one or more layers without affecting other active layers.
+     *
+     * This function sets the bits in the global active layers corresponding to
+     * the bits in the provided bitfield. Layers already active remain active.
+     *
+     * @param bitfield Bitfield representing one or more layers to enable.
+     */
+    EnableLayers :: proc(bitfield: Layer) ---
+
+    /**
+     * @brief Disable one or more layers without affecting other active layers.
+     *
+     * This function clears the bits in the global active layers corresponding to
+     * the bits in the provided bitfield. Layers not included in the bitfield
+     * remain unchanged.
+     *
+     * @param bitfield Bitfield representing one or more layers to disable.
+     */
+    DisableLayers :: proc(bitfield: Layer) ---
+}
+
+/**
+ * @brief Bitfield type used to specify rendering layers for 3D objects.
+ *
+ * This type is used by `R3D_Mesh` and `R3D_Sprite` objects to indicate
+ * which rendering layer(s) they belong to. Active layers are controlled
+ * globally via the functions:
+ * 
+ * - void R3D_EnableLayers(R3D_Layer bitfield);
+ * - void R3D_DisableLayers(R3D_Layer bitfield);
+ *
+ * A mesh or sprite will be rendered if at least one of its assigned layers is active.
+ *
+ * For simplicity, 16 layers are defined in this header, but the maximum number
+ * of layers is 32 for an uint32_t.
+ */
+Layer :: enum u32 {
+    LAYER_01 = 1 << 0,
+    LAYER_02 = 1 << 1,
+    LAYER_03 = 1 << 2,
+    LAYER_04 = 1 << 3,
+    LAYER_05 = 1 << 4,
+    LAYER_06 = 1 << 5,
+    LAYER_07 = 1 << 6,
+    LAYER_08 = 1 << 7,
+    LAYER_09 = 1 << 8,
+    LAYER_10 = 1 << 9,
+    LAYER_11 = 1 << 10,
+    LAYER_12 = 1 << 11,
+    LAYER_13 = 1 << 12,
+    LAYER_14 = 1 << 13,
+    LAYER_15 = 1 << 14,
+    LAYER_16 = 1 << 15,
+    LAYER_ALL = 0xFFFFFFFF,
+}

+ 131 - 0
third-party/r3d-odin/r3d/r3d_cubemap.odin

@@ -0,0 +1,131 @@
+/* r3d_cubemap.odin -- R3D Cubemap Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+/**
+ * @brief Supported cubemap source layouts.
+ *
+ * Used when converting an image into a cubemap. AUTO_DETECT tries to guess
+ * the layout based on image dimensions.
+ */
+CubemapLayout :: enum u32 {
+    AUTO_DETECT         = 0, ///< Automatically detect layout type
+    LINE_VERTICAL       = 1, ///< Layout is defined by a vertical line with faces
+    LINE_HORIZONTAL     = 2, ///< Layout is defined by a horizontal line with faces
+    CROSS_THREE_BY_FOUR = 3, ///< Layout is defined by a 3x4 cross with cubemap faces
+    CROSS_FOUR_BY_THREE = 4, ///< Layout is defined by a 4x3 cross with cubemap faces
+    PANORAMA            = 5, ///< Layout is defined by an equirectangular panorama
+}
+
+/**
+ * @brief GPU cubemap texture.
+ *
+ * Holds the OpenGL texture handle and its base resolution (per face).
+ */
+Cubemap :: struct {
+    texture: u32,
+    fbo:     u32,
+    size:    i32,
+}
+
+/**
+ * @brief Parameters for procedural sky generation.
+ *
+ * Curves control gradient falloff (lower = sharper transition at horizon).
+ */
+CubemapSky :: struct {
+    skyTopColor:        rl.Color,   // Sky color at zenith
+    skyHorizonColor:    rl.Color,   // Sky color at horizon
+    skyHorizonCurve:    f32,     // Gradient curve exponent (0.01 - 1.0, typical: 0.15)
+    skyEnergy:          f32,     // Sky brightness multiplier
+    groundBottomColor:  rl.Color,   // Ground color at nadir
+    groundHorizonColor: rl.Color,   // Ground color at horizon
+    groundHorizonCurve: f32,     // Gradient curve exponent (typical: 0.02)
+    groundEnergy:       f32,     // Ground brightness multiplier
+    sunDirection:       rl.Vector3, // Direction from which light comes (can take not normalized)
+    sunColor:           rl.Color,   // Sun disk color
+    sunSize:            f32,     // Sun angular size in radians (real sun: ~0.0087 rad = 0.5°)
+    sunCurve:           f32,     // Sun edge softness exponent (typical: 0.15)
+    sunEnergy:          f32,     // Sun brightness multiplier
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Loads a cubemap from an image file.
+     *
+     * The layout parameter tells how faces are arranged inside the source image.
+     */
+    LoadCubemap :: proc(fileName: cstring, layout: CubemapLayout) -> Cubemap ---
+
+    /**
+     * @brief Builds a cubemap from an existing rl.Image.
+     *
+     * Same behavior as R3D_LoadCubemap(), but without loading from disk.
+     */
+    LoadCubemapFromImage :: proc(image: rl.Image, layout: CubemapLayout) -> Cubemap ---
+
+    /**
+     * @brief Generates a procedural sky cubemap.
+     *
+     * Creates a GPU cubemap with procedural gradient sky and sun rendering.
+     * The cubemap is ready for use as environment map or IBL source.
+     */
+    GenCubemapSky :: proc(size: i32, params: CubemapSky) -> Cubemap ---
+
+    /**
+     * @brief Releases GPU resources associated with a cubemap.
+     */
+    UnloadCubemap :: proc(cubemap: Cubemap) ---
+
+    /**
+     * @brief Updates an existing procedural sky cubemap.
+     *
+     * Re-renders the cubemap with new parameters. Faster than unload + generate
+     * when animating sky conditions (time of day, weather, etc.).
+     */
+    UpdateCubemapSky :: proc(cubemap: ^Cubemap, params: CubemapSky) ---
+}
+
+CUBEMAP_SKY_BASE :: CubemapSky {
+    skyTopColor = {98, 116, 140, 255},
+    skyHorizonColor = {165, 167, 171, 255},
+    skyHorizonCurve = 0.15,
+    skyEnergy = 1.0,
+    groundBottomColor = {51, 43, 34, 255},
+    groundHorizonColor = {165, 167, 171, 255},
+    groundHorizonCurve = 0.02,
+    groundEnergy = 1.0,
+    sunDirection = {-1.0, -1.0, -1.0},
+    sunColor = {255, 255, 255, 255},
+    sunSize = 1.5 * rl.DEG2RAD,
+    sunCurve = 0.15,
+    sunEnergy = 1.0,
+}

+ 98 - 0
third-party/r3d-odin/r3d/r3d_decal.odin

@@ -0,0 +1,98 @@
+/* r3d_decal.odin -- R3D Decal Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+/**
+ * @brief Represents a decal and its properties.
+ *
+ * This structure defines a decal that can be projected onto geometry that has already been rendered.
+ *
+ * @note Decals are drawn using deferred screen space rendering and do not interact with any
+ * forward rendered or non-opaque objects.
+ */
+Decal :: struct {
+    albedo:          AlbedoMap,      ///< Albedo map (if the texture is undefined, implicitly treat `applyColor` as false, with alpha = 1.0)
+    emission:        EmissionMap,    ///< Emission map
+    normal:          NormalMap,      ///< Normal map
+    orm:             OrmMap,         ///< Occlusion-Roughness-Metalness map
+    uvOffset:        rl.Vector2,        ///< UV offset (default: {0.0f, 0.0f})
+    uvScale:         rl.Vector2,        ///< UV scale (default: {1.0f, 1.0f})
+    alphaCutoff:     f32,            ///< Alpha cutoff threshold (default: 0.01f)
+    normalThreshold: f32,            ///< Maximum angle against the surface normal to draw decal. 0.0f disables threshold. (default: 0.0f)
+    fadeWidth:       f32,            ///< The width of fading along the normal threshold (default: 0.0f)
+    applyColor:      bool,           ///< Indicates that the albedo color will not be rendered, only the alpha component of the albedo will be used as a mask. (default: true)
+    shader:          ^SurfaceShader, ///< Custom shader applied to the decal (default: NULL)
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Unload all map textures assigned to a R3D_Decal.
+     *
+     * Frees all underlying textures in a R3D_Decal that are not a default texture.
+     *
+     * @param decal to unload maps from.
+     */
+    UnloadDecalMaps :: proc(decal: Decal) ---
+}
+
+/**
+ * @brief Default decal configuration.
+ *
+ * Contains a R3D_Decal structure with sensible default values for all rendering parameters.
+ */
+DECAL_BASE :: Decal {
+    albedo = {
+        texture = {},
+        color = {255, 255, 255, 255},
+    },
+    emission = {
+        texture = {},
+        color = {255, 255, 255, 255},
+        energy = 0.0,
+    },
+    normal = {
+        texture = {},
+        scale = 1.0,
+    },
+    orm = {
+        texture = {},
+        occlusion = 1.0,
+        roughness = 1.0,
+        metalness = 0.0,
+    },
+    uvOffset = {0.0, 0.0},
+    uvScale = {1.0, 1.0},
+    alphaCutoff = 0.01,
+    normalThreshold = 0,
+    fadeWidth = 0,
+    applyColor = true,
+    shader = nil,
+}

+ 249 - 0
third-party/r3d-odin/r3d/r3d_draw.odin

@@ -0,0 +1,249 @@
+/* r3d_draw.odin -- R3D Draw Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Begins a rendering session using the given camera.
+     *
+     * Rendering output is directed to the default framebuffer.
+     *
+     * @param camera rl.Camera used to render the scene.
+     */
+    Begin :: proc(camera: rl.Camera3D) ---
+
+    /**
+     * @brief Begins a rendering session with a custom render target.
+     *
+     * If the render target is invalid (ID = 0), rendering goes to the screen.
+     *
+     * @param target Render texture to render into.
+     * @param camera rl.Camera used to render the scene.
+     */
+    BeginEx :: proc(target: rl.RenderTexture, camera: rl.Camera3D) ---
+
+    /**
+     * @brief Ends the current rendering session.
+     *
+     * This function is the one that actually performs the full
+     * rendering of the described scene. It carries out culling,
+     * sorting, shadow rendering, scene rendering, and screen /
+     * post-processing effects.
+     */
+    End :: proc() ---
+
+    /**
+     * @brief Begins a clustered draw pass.
+     *
+     * All draw calls submitted in this pass are first tested against the
+     * cluster AABB. If the cluster fails the scene/shadow frustum test,
+     * none of the contained objects are tested or drawn.
+     *
+     * @param aabb Bounding box used as the cluster-level frustum test.
+     */
+    BeginCluster :: proc(aabb: rl.BoundingBox) ---
+
+    /**
+     * @brief Ends the current clustered draw pass.
+     *
+     * Stops submitting draw calls to the active cluster.
+     */
+    EndCluster :: proc() ---
+
+    /**
+     * @brief Queues a mesh draw command with position and uniform scale.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawMesh :: proc(mesh: Mesh, material: Material, position: rl.Vector3, scale: f32) ---
+
+    /**
+     * @brief Queues a mesh draw command with position, rotation and non-uniform scale.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawMeshEx :: proc(mesh: Mesh, material: Material, position: rl.Vector3, rotation: rl.Quaternion, scale: rl.Vector3) ---
+
+    /**
+     * @brief Queues a mesh draw command using a full transform matrix.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawMeshPro :: proc(mesh: Mesh, material: Material, transform: rl.Matrix) ---
+
+    /**
+     * @brief Queues an instanced mesh draw command.
+     *
+     * Draws multiple instances using the provided instance buffer.
+     * Does nothing if the number of instances is <= 0.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawMeshInstanced :: proc(mesh: Mesh, material: Material, instances: InstanceBuffer, count: i32) ---
+
+    /**
+     * @brief Queues an instanced mesh draw command with an additional transform.
+     *
+     * Does nothing if the number of instances is <= 0.
+     * The transform is applied to all instances.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawMeshInstancedEx :: proc(mesh: Mesh, material: Material, instances: InstanceBuffer, count: i32, transform: rl.Matrix) ---
+
+    /**
+     * @brief Queues a model draw command with position and uniform scale.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawModel :: proc(model: Model, position: rl.Vector3, scale: f32) ---
+
+    /**
+     * @brief Queues a model draw command with position, rotation and non-uniform scale.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawModelEx :: proc(model: Model, position: rl.Vector3, rotation: rl.Quaternion, scale: rl.Vector3) ---
+
+    /**
+     * @brief Queues a model draw command using a full transform matrix.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawModelPro :: proc(model: Model, transform: rl.Matrix) ---
+
+    /**
+     * @brief Queues an instanced model draw command.
+     *
+     * Draws multiple instances using the provided instance buffer.
+     * Does nothing if the number of instances is <= 0.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawModelInstanced :: proc(model: Model, instances: InstanceBuffer, count: i32) ---
+
+    /**
+     * @brief Queues an instanced model draw command with an additional transform.
+     *
+     * Does nothing if the number of instances is <= 0.
+     * The transform is applied to all instances.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawModelInstancedEx :: proc(model: Model, instances: InstanceBuffer, count: i32, transform: rl.Matrix) ---
+
+    /**
+     * @brief Queues an animated model draw command.
+     *
+     * Uses the provided animation player to compute the pose.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawAnimatedModel :: proc(model: Model, player: AnimationPlayer, position: rl.Vector3, scale: f32) ---
+
+    /**
+     * @brief Queues an animated model draw command with position, rotation and non-uniform scale.
+     *
+     * Uses the provided animation player to compute the pose.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawAnimatedModelEx :: proc(model: Model, player: AnimationPlayer, position: rl.Vector3, rotation: rl.Quaternion, scale: rl.Vector3) ---
+
+    /**
+     * @brief Queues an animated model draw command using a full transform matrix.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawAnimatedModelPro :: proc(model: Model, player: AnimationPlayer, transform: rl.Matrix) ---
+
+    /**
+     * @brief Queues an instanced animated model draw command.
+     *
+     * Draws multiple animated instances using the provided instance buffer.
+     * Does nothing if the number of instances is <= 0.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawAnimatedModelInstanced :: proc(model: Model, player: AnimationPlayer, instances: InstanceBuffer, count: i32) ---
+
+    /**
+     * @brief Queues an instanced animated model draw command with an additional transform.
+     *
+     * Does nothing if the number of instances is <= 0.
+     * The transform is applied to all instances.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawAnimatedModelInstancedEx :: proc(model: Model, player: AnimationPlayer, instances: InstanceBuffer, count: i32, transform: rl.Matrix) ---
+
+    /**
+     * @brief Queues a decal draw command with position and uniform scale.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawDecal :: proc(decal: Decal, position: rl.Vector3, scale: f32) ---
+
+    /**
+     * @brief Queues a decal draw command with position, rotation and non-uniform scale.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawDecalEx :: proc(decal: Decal, position: rl.Vector3, rotation: rl.Quaternion, scale: rl.Vector3) ---
+
+    /**
+     * @brief Queues a decal draw command using a full transform matrix.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawDecalPro :: proc(decal: Decal, transform: rl.Matrix) ---
+
+    /**
+     * @brief Queues an instanced decal draw command.
+     *
+     * Draws multiple instances using the provided instance buffer.
+     * Does nothing if the number of instances is <= 0.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawDecalInstanced :: proc(decal: Decal, instances: InstanceBuffer, count: i32) ---
+
+    /**
+     * @brief Queues an instanced decal draw command with an additional transform.
+     *
+     * Does nothing if the number of instances is <= 0.
+     * The transform is applied to all instances.
+     *
+     * The command is executed during R3D_End().
+     */
+    DrawDecalInstancedEx :: proc(decal: Decal, instances: InstanceBuffer, count: i32, transform: rl.Matrix) ---
+}
+

+ 335 - 0
third-party/r3d-odin/r3d/r3d_environment.odin

@@ -0,0 +1,335 @@
+/* r3d_environment.odin -- R3D Environment Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+/**
+ * @brief Bloom effect modes.
+ *
+ * Different blending methods for the bloom glow effect.
+ */
+Bloom :: enum u32 {
+    DISABLED = 0, ///< No bloom effect applied
+    MIX      = 1, ///< Linear interpolation blend between scene and bloom
+    ADDITIVE = 2, ///< Additive blending, intensifying bright regions
+    SCREEN   = 3, ///< Screen blending for softer highlight enhancement
+}
+
+/**
+ * @brief Fog effect modes.
+ *
+ * Distance-based fog density distribution methods.
+ */
+Fog :: enum u32 {
+    DISABLED = 0, ///< No fog effect
+    LINEAR   = 1, ///< Linear density increase between start and end distances
+    EXP2     = 2, ///< Exponential squared density (exp2), more realistic
+    EXP      = 3, ///< Simple exponential density increase
+}
+
+/**
+ * @brief Depth of field modes.
+ */
+DoF :: enum u32 {
+    DISABLED = 0, ///< No depth of field effect
+    ENABLED  = 1, ///< Depth of field enabled with focus point and blur
+}
+
+/**
+ * @brief Tone mapping algorithms.
+ *
+ * HDR to LDR color compression methods.
+ */
+Tonemap :: enum u32 {
+    LINEAR   = 0, ///< Direct linear mapping (no compression)
+    REINHARD = 1, ///< Reinhard operator, balanced HDR compression
+    FILMIC   = 2, ///< Film-like response curve
+    ACES     = 3, ///< Academy rl.Color Encoding System (cinematic standard)
+    AGX      = 4, ///< Modern algorithm preserving highlights and shadows
+    COUNT    = 5, ///< Internal: number of tonemap modes
+}
+
+/**
+ * @brief Background and skybox configuration.
+ */
+EnvBackground :: struct {
+    color:    rl.Color,      ///< Background color when there is no skybox
+    energy:   f32,        ///< Energy multiplier applied to background (skybox or color)
+    skyBlur:  f32,        ///< Sky blur factor [0,1], based on mipmaps, very fast
+    sky:      Cubemap,    ///< Skybox asset (used if ID is non-zero)
+    rotation: rl.Quaternion, ///< Skybox rotation (pitch, yaw, roll as quaternion)
+}
+
+/**
+ * @brief Ambient lighting configuration.
+ */
+EnvAmbient :: struct {
+    color:  rl.Color,      ///< Ambient light color when there is no ambient map
+    energy: f32,        ///< Energy multiplier for ambient light (map or color)
+    _map:   AmbientMap, ///< IBL environment map, can be generated from skybox
+}
+
+/**
+ * @brief Screen Space Ambient Occlusion (SSAO) settings.
+ *
+ * Darkens areas where surfaces are close together, such as corners and crevices.
+ */
+EnvSSAO :: struct {
+    sampleCount: i32,  ///< Number of samples to compute SSAO (default: 16)
+    intensity:   f32,  ///< Base occlusion strength multiplier (default: 1.0)
+    power:       f32,  ///< Exponential falloff for sharper darkening (default: 1.5)
+    radius:      f32,  ///< Sampling radius in world space (default: 0.25)
+    bias:        f32,  ///< Depth bias to prevent self-shadowing, good value is ~2% of the radius (default: 0.007)
+    enabled:     bool, ///< Enable/disable SSAO effect (default: false)
+}
+
+/**
+ * @brief Screen Space Indirect Lighting (SSIL) settings.
+ *
+ * Approximates indirect lighting by gathering light from nearby surfaces in screen space.
+ */
+EnvSSIL :: struct {
+    sampleCount:  i32,  ///< Number of samples to compute indirect lighting (default: 4)
+    sliceCount:   i32,  ///< Number of depth slices for accumulation (default: 4)
+    sampleRadius: f32,  ///< Maximum distance to gather light from (default: 5.0)
+    hitThickness: f32,  ///< Thickness threshold for occluders (default: 0.5)
+    aoPower:      f32,  ///< Exponential falloff for visibility factor (too high = more noise) (default: 1.0)
+    energy:       f32,  ///< Multiplier for indirect light intensity (default: 1.0)
+    bounce:       f32,  /**< Bounce feeback factor. (default: 0.5)
+                              *  Simulates light bounces by re-injecting the SSIL from the previous frame into the current direct light.
+                              *  Be careful not to make the factor too high in order to avoid a feedback loop.
+                              */
+    convergence:  f32,  /**< Temporal convergence factor (0 disables it, default 0.5).
+                              *  Smooths sudden light flashes by blending with previous frames.
+                              *  Higher values produce smoother results but may cause ghosting.
+                              *  Tip: The faster the screen changes, the higher the convergence can be acceptable.
+                              *  Requires an additional history buffer (so require more memory). 
+                              *  If multiple SSIL passes are done in the same frame, the history may be inconsistent, 
+                              *  in that case, enable SSIL/convergence for only one pass per frame.
+                              */
+    enabled:      bool, ///< Enable/disable SSIL effect (default: false)
+}
+
+/**
+ * @brief Screen Space Reflections (SSR) settings.
+ *
+ * Real-time reflections calculated in screen space.
+ */
+EnvSSR :: struct {
+    maxRaySteps: i32,  ///< Maximum ray marching steps (default: 32)
+    binarySteps: i32,  ///< Binary search refinement steps (default: 4)
+    stepSize:    f32,  ///< rl.Ray step size (default: 0.125)
+    thickness:   f32,  ///< Depth tolerance for valid hits (default: 0.2)
+    maxDistance: f32,  ///< Maximum ray distance (default: 4.0)
+    edgeFade:    f32,  ///< Screen edge fade start [0,1] (default: 0.25)
+    enabled:     bool, ///< Enable/disable SSR (default: false)
+}
+
+/**
+ * @brief Bloom post-processing settings.
+ *
+ * Glow effect around bright areas in the scene.
+ */
+EnvBloom :: struct {
+    mode:          Bloom, ///< Bloom blending mode (default: R3D_BLOOM_DISABLED)
+    levels:        f32,   ///< Mipmap spread factor [0-1]: higher = wider glow (default: 0.5)
+    intensity:     f32,   ///< Bloom strength multiplier (default: 0.05)
+    threshold:     f32,   ///< Minimum brightness to trigger bloom (default: 0.0)
+    softThreshold: f32,   ///< Softness of brightness cutoff transition (default: 0.5)
+    filterRadius:  f32,   ///< Blur filter radius during upscaling (default: 1.0)
+}
+
+/**
+ * @brief Fog atmospheric effect settings.
+ */
+EnvFog :: struct {
+    mode:      Fog,   ///< Fog distribution mode (default: R3D_FOG_DISABLED)
+    color:     rl.Color, ///< Fog tint color (default: white)
+    start:     f32,   ///< Linear mode: distance where fog begins (default: 1.0)
+    end:       f32,   ///< Linear mode: distance of full fog density (default: 50.0)
+    density:   f32,   ///< Exponential modes: fog thickness factor (default: 0.05)
+    skyAffect: f32,   ///< Fog influence on skybox [0-1] (default: 0.5)
+}
+
+/**
+ * @brief Depth of Field (DoF) camera focus settings.
+ *
+ * Blurs objects outside the focal plane.
+ */
+EnvDoF :: struct {
+    mode:        DoF, ///< Enable/disable state (default: R3D_DOF_DISABLED)
+    focusPoint:  f32, ///< Focus distance in meters from camera (default: 10.0)
+    focusScale:  f32, ///< Depth of field depth: lower = shallower (default: 1.0)
+    maxBlurSize: f32, ///< Maximum blur radius, similar to aperture (default: 20.0)
+}
+
+/**
+ * @brief Tone mapping and exposure settings.
+ *
+ * Converts HDR colors to displayable LDR range.
+ */
+EnvTonemap :: struct {
+    mode:     Tonemap, ///< Tone mapping algorithm (default: R3D_TONEMAP_LINEAR)
+    exposure: f32,     ///< Scene brightness multiplier (default: 1.0)
+    white:    f32,     ///< Reference white point (not used for AGX) (default: 1.0)
+}
+
+/**
+ * @brief rl.Color grading adjustments.
+ *
+ * Final color correction applied after all other effects.
+ */
+EnvColor :: struct {
+    brightness: f32, ///< Overall brightness multiplier (default: 1.0)
+    contrast:   f32, ///< Contrast between dark and bright areas (default: 1.0)
+    saturation: f32, ///< rl.Color intensity (default: 1.0)
+}
+
+/**
+ * @brief Complete environment configuration structure.
+ *
+ * Contains all rendering environment parameters: background, lighting, and post-processing effects.
+ * Initialize with R3D_ENVIRONMENT_BASE for default values.
+ */
+Environment :: struct {
+    background: EnvBackground, ///< Background and skybox settings
+    ambient:    EnvAmbient,    ///< Ambient lighting configuration
+    ssao:       EnvSSAO,       ///< Screen space ambient occlusion
+    ssil:       EnvSSIL,       ///< Screen space indirect lighting
+    ssr:        EnvSSR,        ///< Screen space reflections
+    bloom:      EnvBloom,      ///< Bloom glow effect
+    fog:        EnvFog,        ///< Atmospheric fog
+    dof:        EnvDoF,        ///< Depth of field focus effect
+    tonemap:    EnvTonemap,    ///< HDR tone mapping
+    color:      EnvColor,      ///< rl.Color grading adjustments
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Retrieves a pointer to the current environment configuration.
+     *
+     * Provides direct read/write access to environment settings.
+     * Modifications take effect immediately.
+     *
+     * @return Pointer to the active R3D_Environment structure
+     */
+    GetEnvironment :: proc() -> ^Environment ---
+
+    /**
+     * @brief Replaces the entire environment configuration.
+     *
+     * Copies all settings from the provided structure to the active environment.
+     * Useful for switching between presets or restoring saved states.
+     *
+     * @param env Pointer to the R3D_Environment structure to copy from
+     */
+    SetEnvironment :: proc(env: ^Environment) ---
+}
+
+/**
+ * @brief Default environment configuration.
+ *
+ * Initializes an R3D_Environment structure with sensible default values for all
+ * rendering parameters. Use this as a starting point for custom configurations.
+ */
+ENVIRONMENT_BASE :: Environment {
+    background = {
+        color    = rl.GRAY,
+        energy   = 1.0,
+        skyBlur  = 0.0,
+        sky      = {},
+        rotation = quaternion(x=0.0, y=0.0, z=0.0, w=1.0),
+    },
+    ambient = {
+        color  = rl.BLACK,
+        energy = 1.0,
+        _map   = {},
+    },
+    ssao = {
+        sampleCount = 16,
+        intensity   = 0.5,
+        power       = 1.5,
+        radius      = 0.5,
+        bias        = 0.02,
+        enabled     = false,
+    },
+    ssil = {
+        sampleCount  = 4,
+        sliceCount   = 4,
+        sampleRadius = 2.0,
+        hitThickness = 0.5,
+        aoPower      = 1.0,
+        energy       = 1.0,
+        bounce       = 0.5,
+        convergence  = 0.5,
+        enabled      = false,
+    },
+    ssr = {
+        maxRaySteps = 32,
+        binarySteps = 4,
+        stepSize    = 0.125,
+        thickness   = 0.2,
+        maxDistance = 4.0,
+        edgeFade    = 0.25,
+        enabled     = false,
+    },
+    bloom = {
+        mode          = .DISABLED,
+        levels        = 0.5,
+        intensity     = 0.05,
+        threshold     = 0.0,
+        softThreshold = 0.5,
+        filterRadius  = 1.0,
+    },
+    fog = {
+        mode      = .DISABLED,
+        color     = {255, 255, 255, 255},
+        start     = 1.0,
+        end       = 50.0,
+        density   = 0.05,
+        skyAffect = 0.5,
+    },
+    dof = {
+        mode        = .DISABLED,
+        focusPoint  = 10.0,
+        focusScale  = 1.0,
+        maxBlurSize = 20.0,
+    },
+    tonemap = {
+        mode     = .LINEAR,
+        exposure = 1.0,
+        white    = 1.0,
+    },
+    color = {
+        brightness = 1.0,
+        contrast   = 1.0,
+        saturation = 1.0,
+    },
+}

+ 104 - 0
third-party/r3d-odin/r3d/r3d_importer.odin

@@ -0,0 +1,104 @@
+/* r3d_importer.odin -- R3D Importer Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+Importer :: struct {}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Load an importer from a file.
+     *
+     * Creates an importer instance from the specified file path.
+     * The file is parsed once and can be reused to extract multiple
+     * resources such as models and animations.
+     *
+     * @param filePath Path to the asset file.
+     * @param flags Importer behavior flags.
+     *
+     * @return Pointer to a new importer instance, or NULL on failure.
+     */
+    LoadImporter :: proc(filePath: cstring, flags: ImportFlags) -> ^Importer ---
+
+    /**
+     * @brief Load an importer from a memory buffer.
+     *
+     * Creates an importer instance from in-memory asset data.
+     * This is useful for embedded assets or streamed content.
+     *
+     * @param data Pointer to the asset data.
+     * @param size Size of the data buffer in bytes.
+     * @param hint Optional file format hint (may be NULL).
+     * @param flags Importer behavior flags.
+     *
+     * @return Pointer to a new importer instance, or NULL on failure.
+     */
+    LoadImporterFromMemory :: proc(data: rawptr, size: u32, hint: cstring, flags: ImportFlags) -> ^Importer ---
+
+    /**
+     * @brief Destroy an importer instance.
+     *
+     * Frees all resources associated with the importer.
+     * Any models or animations extracted from it remain valid.
+     *
+     * @param importer Importer instance to destroy.
+     */
+    UnloadImporter :: proc(importer: ^Importer) ---
+}
+
+/**
+ * @typedef R3D_ImportFlags
+ * @brief Flags controlling importer behavior.
+ *
+ * These flags define how the importer processes the source asset.
+ */
+ImportFlag :: enum u32 {
+
+    /**
+     * @brief Keep a CPU-side copy of mesh data.
+     *
+     * When enabled, raw mesh data is preserved in RAM after model import.
+     */
+    MESH_DATA = 0,
+
+    /**
+     * @brief Enable high-quality import processing.
+     *
+     * When enabled, the importer uses a higher-quality post-processing
+     * (e.g. smooth normals, mesh optimization, data validation).
+     * This mode is intended for editor usage and offline processing.
+     *
+     * When disabled, a faster import preset is used, suitable for runtime.
+     */
+    QUALITY = 1,
+
+}
+
+ImportFlags :: bit_set[ImportFlag; u32]

+ 96 - 0
third-party/r3d-odin/r3d/r3d_instance.odin

@@ -0,0 +1,96 @@
+/* r3d_instance.odin -- R3D Instance Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+R3D_INSTANCE_CUSTOM     :: (1<<4)    /*< rl.Vector4     */
+
+/**
+ * @brief GPU buffers storing instance attribute streams.
+ *
+ * buffers: One VBO per attribute (indexed by flag order).
+ * capcity: Maximum number of instances.
+ * flags: Enabled attribute mask.
+ */
+InstanceBuffer :: struct {
+    buffers:  [5]u32,
+    capacity: i32,
+    flags:    i32,
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Create instance buffers on the GPU.
+     * @param capacity Max instances.
+     * @param flags Attribute mask to allocate.
+     * @return Initialized instance buffer.
+     */
+    LoadInstanceBuffer :: proc(capacity: i32, flags: InstanceFlags) -> InstanceBuffer ---
+
+    /**
+     * @brief Destroy all GPU buffers owned by this instance buffer.
+     */
+    UnloadInstanceBuffer :: proc(buffer: InstanceBuffer) ---
+
+    /**
+     * @brief Upload a contiguous range of instance data.
+     * @param flag Attribute being updated (single bit).
+     * @param offset First instance index.
+     * @param count Number of instances.
+     * @param data Source pointer.
+     */
+    UploadInstances :: proc(buffer: InstanceBuffer, flag: InstanceFlags, offset: i32, count: i32, data: rawptr) ---
+
+    /**
+     * @brief Map an attribute buffer for CPU write access.
+     * @param flag Attribute to map (single bit).
+     * @return Writable pointer, or NULL on error.
+     */
+    MapInstances :: proc(buffer: InstanceBuffer, flag: InstanceFlags) -> rawptr ---
+
+    /**
+     * @brief Unmap one or more previously mapped attribute buffers.
+     * @param flags Bitmask of attributes to unmap.
+     */
+    UnmapInstances :: proc(buffer: InstanceBuffer, flags: InstanceFlags) ---
+}
+
+/**
+ * @brief Bitmask defining which instance attributes are present.
+ */
+InstanceFlag :: enum u32 {
+    POSITION = 0,
+    ROTATION = 1,
+    SCALE = 2,
+    COLOR = 3,
+    CUSTOM = 4,
+}
+
+InstanceFlags :: bit_set[InstanceFlag; u32]

+ 376 - 0
third-party/r3d-odin/r3d/r3d_kinematics.odin

@@ -0,0 +1,376 @@
+/* r3d_kinematics.odin -- R3D Kinematics Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+/**
+ * @brief Capsule shape defined by two endpoints and radius
+ */
+Capsule :: struct {
+    start:  rl.Vector3, ///< Start point of capsule axis
+    end:    rl.Vector3, ///< End point of capsule axis
+    radius: f32,     ///< Capsule radius
+}
+
+/**
+ * @brief Penetration information from an overlap test
+ */
+Penetration :: struct {
+    collides: bool,    ///< Whether shapes are overlapping
+    depth:    f32,     ///< Penetration depth
+    normal:   rl.Vector3, ///< Collision normal (direction to resolve penetration)
+    mtv:      rl.Vector3, ///< Minimum Translation Vector (normal * depth)
+}
+
+/**
+ * @brief Collision information from a sweep test
+ */
+SweepCollision :: struct {
+    hit:    bool,    ///< Whether a collision occurred
+    time:   f32,     ///< Time of impact [0-1], fraction along velocity vector
+    point:  rl.Vector3, ///< World space collision point
+    normal: rl.Vector3, ///< Surface normal at collision point
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Check if capsule intersects with box
+     * @param capsule Capsule shape
+     * @param box Bounding box
+     * @return true if collision detected
+     */
+    CheckCollisionCapsuleBox :: proc(capsule: Capsule, box: rl.BoundingBox) -> bool ---
+
+    /**
+     * @brief Check if capsule intersects with sphere
+     * @param capsule Capsule shape
+     * @param center Sphere center
+     * @param radius Sphere radius
+     * @return true if collision detected
+     */
+    CheckCollisionCapsuleSphere :: proc(capsule: Capsule, center: rl.Vector3, radius: f32) -> bool ---
+
+    /**
+     * @brief Check if two capsules intersect
+     * @param a First capsule
+     * @param b Second capsule
+     * @return true if collision detected
+     */
+    CheckCollisionCapsules :: proc(a: Capsule, b: Capsule) -> bool ---
+
+    /**
+     * @brief Check if capsule intersects with mesh
+     * @param capsule Capsule shape
+     * @param mesh Mesh data
+     * @param transform Mesh transform
+     * @return true if collision detected
+     */
+    CheckCollisionCapsuleMesh :: proc(capsule: Capsule, mesh: MeshData, transform: rl.Matrix) -> bool ---
+
+    /**
+     * @brief Check penetration between capsule and box
+     * @param capsule Capsule shape
+     * @param box Bounding box
+     * @return Penetration information.
+     */
+    CheckPenetrationCapsuleBox :: proc(capsule: Capsule, box: rl.BoundingBox) -> Penetration ---
+
+    /**
+     * @brief Check penetration between capsule and sphere
+     * @param capsule Capsule shape
+     * @param center Sphere center
+     * @param radius Sphere radius
+     * @return Penetration information.
+     */
+    CheckPenetrationCapsuleSphere :: proc(capsule: Capsule, center: rl.Vector3, radius: f32) -> Penetration ---
+
+    /**
+     * @brief Check penetration between two capsules
+     * @param a First capsule
+     * @param b Second capsule
+     * @return Penetration information.
+     */
+    CheckPenetrationCapsules :: proc(a: Capsule, b: Capsule) -> Penetration ---
+
+    /**
+     * @brief Calculate slide velocity along surface
+     * @param velocity Original velocity
+     * @param normal Surface normal (must be normalized)
+     * @return Velocity sliding along surface (perpendicular component removed)
+     */
+    SlideVelocity :: proc(velocity: rl.Vector3, normal: rl.Vector3) -> rl.Vector3 ---
+
+    /**
+     * @brief Calculate bounce velocity after collision
+     * @param velocity Incoming velocity
+     * @param normal Surface normal (must be normalized)
+     * @param bounciness Coefficient of restitution (0=no bounce, 1=perfect bounce)
+     * @return Reflected velocity
+     */
+    BounceVelocity :: proc(velocity: rl.Vector3, normal: rl.Vector3, bounciness: f32) -> rl.Vector3 ---
+
+    /**
+     * @brief Slide sphere along box surface, resolving collisions
+     * @param center Sphere center position
+     * @param radius Sphere radius
+     * @param velocity Desired movement vector
+     * @param box Obstacle bounding box
+     * @param outNormal Optional: receives collision normal if collision occurred
+     * @return Actual movement applied (may be reduced/redirected by collision)
+     */
+    SlideSphereBox :: proc(center: rl.Vector3, radius: f32, velocity: rl.Vector3, box: rl.BoundingBox, outNormal: ^rl.Vector3) -> rl.Vector3 ---
+
+    /**
+     * @brief Slide sphere along mesh surface, resolving collisions
+     * @param center Sphere center position
+     * @param radius Sphere radius
+     * @param velocity Desired movement vector
+     * @param mesh Mesh data to collide against
+     * @param transform Mesh world transform
+     * @param outNormal Optional: receives collision normal if collision occurred
+     * @return Actual movement applied (may be reduced/redirected by collision)
+     */
+    SlideSphereMesh :: proc(center: rl.Vector3, radius: f32, velocity: rl.Vector3, mesh: MeshData, transform: rl.Matrix, outNormal: ^rl.Vector3) -> rl.Vector3 ---
+
+    /**
+     * @brief Slide capsule along box surface, resolving collisions
+     * @param capsule Capsule shape
+     * @param velocity Desired movement vector
+     * @param box Obstacle bounding box
+     * @param outNormal Optional: receives collision normal if collision occurred
+     * @return Actual movement applied (may be reduced/redirected by collision)
+     */
+    SlideCapsuleBox :: proc(capsule: Capsule, velocity: rl.Vector3, box: rl.BoundingBox, outNormal: ^rl.Vector3) -> rl.Vector3 ---
+
+    /**
+     * @brief Slide capsule along mesh surface, resolving collisions
+     * @param capsule Capsule shape
+     * @param velocity Desired movement vector
+     * @param mesh Mesh data to collide against
+     * @param transform Mesh world transform
+     * @param outNormal Optional: receives collision normal if collision occurred
+     * @return Actual movement applied (may be reduced/redirected by collision)
+     */
+    SlideCapsuleMesh :: proc(capsule: Capsule, velocity: rl.Vector3, mesh: MeshData, transform: rl.Matrix, outNormal: ^rl.Vector3) -> rl.Vector3 ---
+
+    /**
+     * @brief Push sphere out of box if penetrating
+     * @param center Sphere center (modified in place if penetrating)
+     * @param radius Sphere radius
+     * @param box Obstacle box
+     * @param outPenetration Optional: receives penetration depth
+     * @return true if depenetration occurred
+     */
+    DepenetrateSphereBox :: proc(center: ^rl.Vector3, radius: f32, box: rl.BoundingBox, outPenetration: ^f32) -> bool ---
+
+    /**
+     * @brief Push capsule out of box if penetrating
+     * @param capsule Capsule shape (modified in place if penetrating)
+     * @param box Obstacle box
+     * @param outPenetration Optional: receives penetration depth
+     * @return true if depenetration occurred
+     */
+    DepenetrateCapsuleBox :: proc(capsule: ^Capsule, box: rl.BoundingBox, outPenetration: ^f32) -> bool ---
+
+    /**
+     * @brief Cast a ray against mesh geometry
+     * @param ray rl.Ray to cast
+     * @param mesh Mesh data to test against
+     * @param transform Mesh world transform
+     * @return rl.Ray collision info (hit, distance, point, normal)
+     */
+    RaycastMesh :: proc(ray: rl.Ray, mesh: MeshData, transform: rl.Matrix) -> rl.RayCollision ---
+
+    /**
+     * @brief Cast a ray against a model (tests all meshes)
+     * @param ray rl.Ray to cast
+     * @param model Model to test against (must have valid meshData)
+     * @param transform Model world transform
+     * @return rl.Ray collision info for closest hit (hit=false if no meshData)
+     */
+    RaycastModel :: proc(ray: rl.Ray, model: Model, transform: rl.Matrix) -> rl.RayCollision ---
+
+    /**
+     * @brief Sweep sphere against single point
+     * @param center Sphere center position
+     * @param radius Sphere radius
+     * @param velocity Movement vector (direction and magnitude)
+     * @param point Point to test against
+     * @return Sweep collision info (hit, time, point, normal)
+     */
+    SweepSpherePoint :: proc(center: rl.Vector3, radius: f32, velocity: rl.Vector3, point: rl.Vector3) -> SweepCollision ---
+
+    /**
+     * @brief Sweep sphere against line segment
+     * @param center Sphere center position
+     * @param radius Sphere radius
+     * @param velocity Movement vector (direction and magnitude)
+     * @param a Segment start point
+     * @param b Segment end point
+     * @return Sweep collision info (hit, time, point, normal)
+     */
+    SweepSphereSegment :: proc(center: rl.Vector3, radius: f32, velocity: rl.Vector3, a: rl.Vector3, b: rl.Vector3) -> SweepCollision ---
+
+    /**
+     * @brief Sweep sphere against triangle plane (no edge/vertex clipping)
+     * @param center Sphere center position
+     * @param radius Sphere radius
+     * @param velocity Movement vector (direction and magnitude)
+     * @param a Triangle vertex A
+     * @param b Triangle vertex B
+     * @param c Triangle vertex C
+     * @return Sweep collision info (hit, time, point, normal)
+     */
+    SweepSphereTrianglePlane :: proc(center: rl.Vector3, radius: f32, velocity: rl.Vector3, a: rl.Vector3, b: rl.Vector3, _c: rl.Vector3) -> SweepCollision ---
+
+    /**
+     * @brief Sweep sphere against triangle with edge/vertex handling
+     * @param center Sphere center position
+     * @param radius Sphere radius
+     * @param velocity Movement vector (direction and magnitude)
+     * @param a Triangle vertex A
+     * @param b Triangle vertex B
+     * @param c Triangle vertex C
+     * @return Sweep collision info (hit, time, point, normal)
+     */
+    SweepSphereTriangle :: proc(center: rl.Vector3, radius: f32, velocity: rl.Vector3, a: rl.Vector3, b: rl.Vector3, _c: rl.Vector3) -> SweepCollision ---
+
+    /**
+     * @brief Sweep sphere along velocity vector
+     * @param center Sphere center position
+     * @param radius Sphere radius
+     * @param velocity Movement vector (direction and magnitude)
+     * @param box Obstacle bounding box
+     * @return Sweep collision info (hit, distance, point, normal)
+     */
+    SweepSphereBox :: proc(center: rl.Vector3, radius: f32, velocity: rl.Vector3, box: rl.BoundingBox) -> SweepCollision ---
+
+    /**
+     * @brief Sweep sphere along velocity vector against mesh geometry
+     * @param center Sphere center position
+     * @param radius Sphere radius
+     * @param velocity Movement vector (direction and magnitude)
+     * @param mesh Mesh data to test against
+     * @param transform Mesh world transform
+     * @return Sweep collision info (hit, time, point, normal)
+     */
+    SweepSphereMesh :: proc(center: rl.Vector3, radius: f32, velocity: rl.Vector3, mesh: MeshData, transform: rl.Matrix) -> SweepCollision ---
+
+    /**
+     * @brief Sweep capsule along velocity vector
+     * @param capsule Capsule shape to sweep
+     * @param velocity Movement vector (direction and magnitude)
+     * @param box Obstacle bounding box
+     * @return Sweep collision info (hit, distance, point, normal)
+     */
+    SweepCapsuleBox :: proc(capsule: Capsule, velocity: rl.Vector3, box: rl.BoundingBox) -> SweepCollision ---
+
+    /**
+     * @brief Sweep capsule along velocity vector against mesh geometry
+     * @param capsule Capsule shape to sweep
+     * @param velocity Movement vector (direction and magnitude)
+     * @param mesh Mesh data to test against
+     * @param transform Mesh world transform
+     * @return Sweep collision info (hit, time, point, normal)
+     */
+    SweepCapsuleMesh :: proc(capsule: Capsule, velocity: rl.Vector3, mesh: MeshData, transform: rl.Matrix) -> SweepCollision ---
+
+    /**
+     * @brief Check if sphere is grounded against a box
+     * @param center Sphere center
+     * @param radius Sphere radius
+     * @param checkDistance How far below to check
+     * @param ground Ground box to test against
+     * @param outGround Optional: receives raycast hit info
+     * @return true if grounded within checkDistance
+     */
+    IsSphereGroundedBox :: proc(center: rl.Vector3, radius: f32, checkDistance: f32, ground: rl.BoundingBox, outGround: ^rl.RayCollision) -> bool ---
+
+    /**
+     * @brief Check if sphere is grounded against mesh geometry
+     * @param center Sphere center
+     * @param radius Sphere radius
+     * @param checkDistance How far below to check
+     * @param mesh Mesh data to test against
+     * @param transform Mesh world transform
+     * @param outGround Optional: receives raycast hit info
+     * @return true if grounded within checkDistance
+     */
+    IsSphereGroundedMesh :: proc(center: rl.Vector3, radius: f32, checkDistance: f32, mesh: MeshData, transform: rl.Matrix, outGround: ^rl.RayCollision) -> bool ---
+
+    /**
+     * @brief Check if capsule is grounded against a box
+     * @param capsule Character capsule
+     * @param checkDistance How far below to check (e.g., 0.1)
+     * @param ground Ground box to test against
+     * @param outGround Optional: receives raycast hit info
+     * @return true if grounded within checkDistance
+     */
+    IsCapsuleGroundedBox :: proc(capsule: Capsule, checkDistance: f32, ground: rl.BoundingBox, outGround: ^rl.RayCollision) -> bool ---
+
+    /**
+     * @brief Check if capsule is grounded against mesh geometry
+     * @param capsule Character capsule
+     * @param checkDistance How far below to check
+     * @param mesh Mesh data to test against
+     * @param transform Mesh world transform
+     * @param outGround Optional: receives raycast hit info
+     * @return true if grounded within checkDistance
+     */
+    IsCapsuleGroundedMesh :: proc(capsule: Capsule, checkDistance: f32, mesh: MeshData, transform: rl.Matrix, outGround: ^rl.RayCollision) -> bool ---
+
+    /**
+     * @brief Find closest point on line segment to given point
+     * @param point Query point
+     * @param start Segment start
+     * @param end Segment end
+     * @return Closest point on segment [start, end]
+     */
+    ClosestPointOnSegment :: proc(point: rl.Vector3, start: rl.Vector3, end: rl.Vector3) -> rl.Vector3 ---
+
+    /**
+     * @brief Find closest point on triangle to given point
+     * @param p Query point
+     * @param a Triangle vertex A
+     * @param b Triangle vertex B
+     * @param c Triangle vertex C
+     * @return Closest point on triangle surface
+     */
+    ClosestPointOnTriangle :: proc(p: rl.Vector3, a: rl.Vector3, b: rl.Vector3, _c: rl.Vector3) -> rl.Vector3 ---
+
+    /**
+     * @brief Find closest point on box surface to given point
+     * @param point Query point
+     * @param box Bounding box
+     * @return Closest point on/in box (clamped to box bounds)
+     */
+    ClosestPointOnBox :: proc(point: rl.Vector3, box: rl.BoundingBox) -> rl.Vector3 ---
+}
+

+ 566 - 0
third-party/r3d-odin/r3d/r3d_lighting.odin

@@ -0,0 +1,566 @@
+/* r3d_lighting.odin -- R3D Lighting Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+/**
+ * @brief Types of lights supported by the rendering engine.
+ *
+ * Each light type has different behaviors and use cases.
+ */
+LightType :: enum u32 {
+    DIR        = 0, ///< Directional light, affects the entire scene with parallel rays.
+    SPOT       = 1, ///< Spot light, emits light in a cone shape.
+    OMNI       = 2, ///< Omni light, emits light in all directions from a single point.
+    TYPE_COUNT = 3,
+}
+
+/**
+ * @brief Modes for updating shadow maps.
+ *
+ * Determines how often the shadow maps are refreshed.
+ */
+ShadowUpdateMode :: enum u32 {
+    MANUAL     = 0, ///< Shadow maps update only when explicitly requested.
+    INTERVAL   = 1, ///< Shadow maps update at defined time intervals.
+    CONTINUOUS = 2, ///< Shadow maps update every frame for real-time accuracy.
+}
+
+/**
+ * @brief Unique identifier for an R3D light.
+ *
+ * ID type used to reference a light.
+ * A negative value indicates an invalid light.
+ */
+Light :: i32
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Creates a new light of the specified type.
+     *
+     * This function creates a light of the given type. The light must be destroyed
+     * manually when no longer needed by calling `R3D_DestroyLight`.
+     *
+     * @param type The type of light to create (directional, spot or omni-directional).
+     * @return The ID of the created light.
+     */
+    CreateLight :: proc(type: LightType) -> Light ---
+
+    /**
+     * @brief Destroys the specified light.
+     *
+     * This function deallocates the resources associated with the light and makes
+     * the light ID invalid. It must be called after the light is no longer needed.
+     *
+     * @param id The ID of the light to destroy.
+     */
+    DestroyLight :: proc(id: Light) ---
+
+    /**
+     * @brief Checks if a light exists.
+     *
+     * This function checks if the specified light ID is valid and if the light exists.
+     *
+     * @param id The ID of the light to check.
+     * @return True if the light exists, false otherwise.
+     */
+    IsLightExist :: proc(id: Light) -> bool ---
+
+    /**
+     * @brief Gets the type of a light.
+     *
+     * This function returns the type of the specified light (directional, spot or omni-directional).
+     *
+     * @param id The ID of the light.
+     * @return The type of the light.
+     */
+    GetLightType :: proc(id: Light) -> LightType ---
+
+    /**
+     * @brief Checks if a light is active.
+     *
+     * This function checks whether the specified light is currently active (enabled or disabled).
+     *
+     * @param id The ID of the light to check.
+     * @return True if the light is active, false otherwise.
+     */
+    IsLightActive :: proc(id: Light) -> bool ---
+
+    /**
+     * @brief Toggles the state of a light (active or inactive).
+     *
+     * This function toggles the state of the specified light, turning it on if it is off,
+     * or off if it is on.
+     *
+     * @param id The ID of the light to toggle.
+     */
+    ToggleLight :: proc(id: Light) ---
+
+    /**
+     * @brief Sets the active state of a light.
+     *
+     * This function allows manually turning a light on or off by specifying its active state.
+     *
+     * @param id The ID of the light to set the active state for.
+     * @param active True to activate the light, false to deactivate it.
+     */
+    SetLightActive :: proc(id: Light, active: bool) ---
+
+    /**
+     * @brief Gets the color of a light.
+     *
+     * This function retrieves the color of the specified light as a `rl.Color` structure.
+     *
+     * @param id The ID of the light.
+     * @return The color of the light as a `rl.Color` structure.
+     */
+    GetLightColor :: proc(id: Light) -> rl.Color ---
+
+    /**
+     * @brief Gets the color of a light as a `rl.Vector3`.
+     *
+     * This function retrieves the color of the specified light as a `rl.Vector3`, where each
+     * component (x, y, z) represents the RGB values of the light.
+     *
+     * @param id The ID of the light.
+     * @return The color of the light as a `rl.Vector3`.
+     */
+    GetLightColorV :: proc(id: Light) -> rl.Vector3 ---
+
+    /**
+     * @brief Sets the color of a light.
+     *
+     * This function sets the color of the specified light using a `rl.Color` structure.
+     *
+     * @param id The ID of the light.
+     * @param color The new color to set for the light.
+     */
+    SetLightColor :: proc(id: Light, color: rl.Color) ---
+
+    /**
+     * @brief Sets the color of a light using a `rl.Vector3`.
+     *
+     * This function sets the color of the specified light using a `rl.Vector3`, where each
+     * component (x, y, z) represents the RGB values of the light.
+     *
+     * @param id The ID of the light.
+     * @param color The new color to set for the light as a `rl.Vector3`.
+     */
+    SetLightColorV :: proc(id: Light, color: rl.Vector3) ---
+
+    /**
+     * @brief Gets the position of a light.
+     *
+     * This function retrieves the position of the specified light.
+     * Only applicable to spot lights or omni-lights.
+     *
+     * @param id The ID of the light.
+     * @return The position of the light as a `rl.Vector3`.
+     */
+    GetLightPosition :: proc(id: Light) -> rl.Vector3 ---
+
+    /**
+     * @brief Sets the position of a light.
+     *
+     * This function sets the position of the specified light.
+     * Only applicable to spot lights or omni-lights.
+     *
+     * @note Has no effect for directional lights.
+     *       If called on a directional light,
+     *       a warning will be logged.
+     *
+     * @param id The ID of the light.
+     * @param position The new position to set for the light.
+     */
+    SetLightPosition :: proc(id: Light, position: rl.Vector3) ---
+
+    /**
+     * @brief Gets the direction of a light.
+     *
+     * This function retrieves the direction of the specified light.
+     * Only applicable to directional lights or spot lights.
+     *
+     * @param id The ID of the light.
+     * @return The direction of the light as a `rl.Vector3`.
+     */
+    GetLightDirection :: proc(id: Light) -> rl.Vector3 ---
+
+    /**
+     * @brief Sets the direction of a light.
+     *
+     * This function sets the direction of the specified light.
+     * Only applicable to directional lights or spot lights.
+     *
+     * @note Has no effect for omni-directional lights.
+     *       If called on an omni-directional light,
+     *       a warning will be logged.
+     *
+     * @param id The ID of the light.
+     * @param direction The new direction to set for the light.
+     *                  The vector is automatically normalized.
+     */
+    SetLightDirection :: proc(id: Light, direction: rl.Vector3) ---
+
+    /**
+     * @brief Sets the position and direction of a light to look at a target point.
+     *
+     * This function sets both the position and the direction of the specified light,
+     * causing it to "look at" a given target point.
+     *
+     * @note - For directional lights, only the direction is updated (position is ignored).
+     *       - For omni-directional lights, only the position is updated (direction is not calculated).
+     *       - For spot lights, both position and direction are set accordingly.
+     *       - This function does **not** emit any warning or log message.
+     *
+     * @param id The ID of the light.
+     * @param position The position to set for the light.
+     * @param target The point the light should look at.
+     */
+    LightLookAt :: proc(id: Light, position: rl.Vector3, target: rl.Vector3) ---
+
+    /**
+     * @brief Gets the energy level of a light.
+     *
+     * This function retrieves the energy level (intensity) of the specified light.
+     * Energy typically affects the brightness of the light.
+     *
+     * @param id The ID of the light.
+     * @return The energy level of the light.
+     */
+    GetLightEnergy :: proc(id: Light) -> f32 ---
+
+    /**
+     * @brief Sets the energy level of a light.
+     *
+     * This function sets the energy (intensity) of the specified light.
+     * A higher energy value will result in a brighter light.
+     *
+     * @param id The ID of the light.
+     * @param energy The new energy value to set for the light.
+     */
+    SetLightEnergy :: proc(id: Light, energy: f32) ---
+
+    /**
+     * @brief Gets the specular intensity of a light.
+     *
+     * This function retrieves the current specular intensity of the specified light.
+     * Specular intensity affects how shiny surfaces appear when reflecting the light.
+     *
+     * @param id The ID of the light.
+     * @return The current specular intensity of the light.
+     */
+    GetLightSpecular :: proc(id: Light) -> f32 ---
+
+    /**
+     * @brief Sets the specular intensity of a light.
+     *
+     * This function sets the specular intensity of the specified light.
+     * Higher specular values result in stronger and sharper highlights on reflective surfaces.
+     *
+     * @param id The ID of the light.
+     * @param specular The new specular intensity value to set for the light.
+     */
+    SetLightSpecular :: proc(id: Light, specular: f32) ---
+
+    /**
+     * @brief Gets the range of a light.
+     *
+     * This function retrieves the range of the specified light, which determines how far the light can affect.
+     * Only applicable to spot lights or omni-lights.
+     *
+     * @param id The ID of the light.
+     * @return The range of the light.
+     */
+    GetLightRange :: proc(id: Light) -> f32 ---
+
+    /**
+     * @brief Sets the range parameter of a light.
+     *
+     * For spot and omni lights, this defines the maximum illumination distance.
+     * For directional lights, this defines the shadow rendering radius around the camera.
+     *
+     * @param id The ID of the light.
+     * @param range The range value to apply.
+     */
+    SetLightRange :: proc(id: Light, range: f32) ---
+
+    /**
+     * @brief Gets the attenuation factor of a light.
+     *
+     * This function retrieves the attenuation factor of the specified light.
+     * Attenuation controls how the intensity of a light decreases with distance.
+     * Only applicable to spot lights or omni-lights.
+     *
+     * @param id The ID of the light.
+     * @return The attenuation factor of the light.
+     */
+    GetLightAttenuation :: proc(id: Light) -> f32 ---
+
+    /**
+     * @brief Sets the attenuation factor of a light.
+     *
+     * This function sets the attenuation factor of the specified light.
+     * A higher attenuation value causes the light to lose intensity more quickly as the distance increases.
+     * For a realistic effect, an attenuation factor of 2.0f is typically used.
+     * Only applicable to spot lights or omni-lights.
+     *
+     * @param id The ID of the light.
+     * @param attenuation The new attenuation factor to set for the light.
+     */
+    SetLightAttenuation :: proc(id: Light, attenuation: f32) ---
+
+    /**
+     * @brief Gets the inner cutoff angle of a spotlight.
+     *
+     * This function retrieves the inner cutoff angle of a spotlight.
+     * The inner cutoff defines the cone of light where the light is at full intensity.
+     *
+     * @param id The ID of the light.
+     * @return The inner cutoff angle in degrees of the spotlight.
+     */
+    GetLightInnerCutOff :: proc(id: Light) -> f32 ---
+
+    /**
+     * @brief Sets the inner cutoff angle of a spotlight.
+     *
+     * This function sets the inner cutoff angle of a spotlight.
+     * The inner cutoff angle defines the cone where the light is at full intensity.
+     * Anything outside this cone starts to fade.
+     *
+     * @param id The ID of the light.
+     * @param degrees The new inner cutoff angle in degrees.
+     */
+    SetLightInnerCutOff :: proc(id: Light, degrees: f32) ---
+
+    /**
+     * @brief Gets the outer cutoff angle of a spotlight.
+     *
+     * This function retrieves the outer cutoff angle of a spotlight.
+     * The outer cutoff defines the outer boundary of the light's cone, where the light starts to fade.
+     *
+     * @param id The ID of the light.
+     * @return The outer cutoff angle in degrees of the spotlight.
+     */
+    GetLightOuterCutOff :: proc(id: Light) -> f32 ---
+
+    /**
+     * @brief Sets the outer cutoff angle of a spotlight.
+     *
+     * This function sets the outer cutoff angle of a spotlight.
+     * The outer cutoff defines the boundary of the light's cone where the light intensity starts to gradually decrease.
+     *
+     * @param id The ID of the light.
+     * @param degrees The new outer cutoff angle in degrees.
+     */
+    SetLightOuterCutOff :: proc(id: Light, degrees: f32) ---
+
+    /**
+     * @brief Enables shadow rendering for a light.
+     *
+     * Turns on shadow rendering for the light. The engine will allocate a shadow
+     * map if needed, or reuse one previously allocated for another light.
+     *
+     * Shadow map resolutions are fixed: 2048x2048 for spot and point lights,
+     * and 4096x4096 for directional lights.
+     *
+     * @param id The ID of the light.
+     *
+     * @note Creating too many shadow-casting lights can exhaust GPU memory and
+     * potentially crash the graphics driver. Disabling shadows on one light and
+     * enabling them on another is free, since existing shadow maps are reused.
+     */
+    EnableShadow :: proc(id: Light) ---
+
+    /**
+     * @brief Disables shadow rendering for a light.
+     *
+     * Turns off shadow rendering for the light. The associated shadow map is
+     * kept in memory and may later be reused by another light.
+     *
+     * @param id The ID of the light.
+     */
+    DisableShadow :: proc(id: Light) ---
+
+    /**
+     * @brief Checks if shadow casting is enabled for a light.
+     *
+     * This function checks if shadow casting is currently enabled for the specified light.
+     *
+     * @param id The ID of the light.
+     * @return True if shadow casting is enabled, false otherwise.
+     */
+    IsShadowEnabled :: proc(id: Light) -> bool ---
+
+    /**
+     * @brief Gets the shadow map update mode of a light.
+     *
+     * This function retrieves the current mode for updating the shadow map of a light. The mode can be:
+     * - Interval: Updates the shadow map at a fixed interval.
+     * - Continuous: Updates the shadow map continuously.
+     * - Manual: Updates the shadow map manually (via explicit function calls).
+     *
+     * @param id The ID of the light.
+     * @return The shadow map update mode.
+     */
+    GetShadowUpdateMode :: proc(id: Light) -> ShadowUpdateMode ---
+
+    /**
+     * @brief Sets the shadow map update mode of a light.
+     *
+     * This function sets the mode for updating the shadow map of the specified light.
+     * The update mode controls when and how often the shadow map is refreshed.
+     *
+     * @param id The ID of the light.
+     * @param mode The update mode to set for the shadow map (Interval, Continuous, or Manual).
+     */
+    SetShadowUpdateMode :: proc(id: Light, mode: ShadowUpdateMode) ---
+
+    /**
+     * @brief Gets the frequency of shadow map updates for the interval update mode.
+     *
+     * This function retrieves the frequency (in milliseconds) at which the shadow map should be updated when
+     * the interval update mode is enabled. This function is only relevant if the shadow map update mode is set
+     * to "Interval".
+     *
+     * @param id The ID of the light.
+     * @return The frequency in milliseconds at which the shadow map is updated.
+     */
+    GetShadowUpdateFrequency :: proc(id: Light) -> i32 ---
+
+    /**
+     * @brief Sets the frequency of shadow map updates for the interval update mode.
+     *
+     * This function sets the frequency (in milliseconds) at which the shadow map should be updated when
+     * the interval update mode is enabled. This function is only relevant if the shadow map update mode is set
+     * to "Interval".
+     *
+     * @param id The ID of the light.
+     * @param msec The frequency in milliseconds at which to update the shadow map.
+     */
+    SetShadowUpdateFrequency :: proc(id: Light, msec: i32) ---
+
+    /**
+     * @brief Forces an immediate update of the shadow map during the next rendering pass.
+     *
+     * This function forces the shadow map of the specified light to be updated during the next call to `R3D_End`.
+     * This is primarily used for the manual update mode, but may also work for the interval mode.
+     *
+     * @param id The ID of the light.
+     */
+    UpdateShadowMap :: proc(id: Light) ---
+
+    /**
+     * @brief Retrieves the softness radius used to simulate penumbra in shadows.
+     *
+     * The softness is expressed as a sampling radius in texels within the shadow map.
+     *
+     * @param id The ID of the light.
+     * @return The softness radius in texels currently set for the shadow.
+     */
+    GetShadowSoftness :: proc(id: Light) -> f32 ---
+
+    /**
+     * @brief Sets the softness radius used to simulate penumbra in shadows.
+     *
+     * This function adjusts the softness of the shadow edges for the specified light.
+     * The softness value corresponds to a number of texels in the shadow map, independent
+     * of its resolution. Larger values increase the blur radius, resulting in softer,
+     * more diffuse shadows, while smaller values yield sharper shadows.
+     *
+     * @param id The ID of the light.
+     * @param softness The softness radius in texels to apply (must be >= 0).
+     *
+     * @note The softness must be set only after shadows have been enabled for the light,
+     *       since the shadow map resolution must be known before the softness can be applied.
+     */
+    SetShadowSoftness :: proc(id: Light, softness: f32) ---
+
+    /**
+     * @brief Gets the shadow depth bias value.
+     */
+    GetShadowDepthBias :: proc(id: Light) -> f32 ---
+
+    /**
+     * @brief Sets the shadow depth bias value.
+     *
+     * A higher bias helps reduce "shadow acne" artifacts
+     * (shadows flickering or appearing misaligned on surfaces).
+     * Be careful: too large values may cause shadows to look detached
+     * or floating away from objects.
+     */
+    SetShadowDepthBias :: proc(id: Light, value: f32) ---
+
+    /**
+     * @brief Gets the shadow slope bias value.
+     */
+    GetShadowSlopeBias :: proc(id: Light) -> f32 ---
+
+    /**
+     * @brief Sets the shadow slope bias value.
+     *
+     * This bias mainly compensates artifacts on surfaces angled
+     * relative to the light. It helps prevent shadows from
+     * incorrectly appearing or disappearing along object edges.
+     */
+    SetShadowSlopeBias :: proc(id: Light, value: f32) ---
+
+    /**
+     * @brief Returns the bounding box encompassing the light's area of influence.
+     *
+     * This function computes the axis-aligned bounding box (AABB) that encloses the
+     * volume affected by the specified light, based on its type:
+     *
+     * - For spotlights, the bounding box encloses the light cone.
+     * - For omni-directional lights, it encloses a sphere representing the light's range.
+     * - For directional lights, it returns an infinite bounding box to represent global influence.
+     *
+     * This bounding box is primarily useful for spatial partitioning, culling, or visual debugging.
+     *
+     * @param light The light for which to compute the bounding box.
+     *
+     * @return A rl.BoundingBox struct that encloses the light's influence volume.
+     */
+    GetLightBoundingBox :: proc(light: Light) -> rl.BoundingBox ---
+
+    /**
+     * @brief Draws the area of influence of the light in 3D space.
+     *
+     * This function visualizes the area affected by a light in 3D space.
+     * It draws the light's influence, such as the cone for spotlights or the volume for omni-lights.
+     * This function is only relevant for spotlights and omni-lights.
+     *
+     * @note This function should be called while using the default 3D rendering mode of raylib,
+     *       not with R3D's rendering mode. It uses raylib's 3D drawing functions to render the light's shape.
+     *
+     * @param id The ID of the light.
+     */
+    DrawLightShape :: proc(id: Light) ---
+}
+

+ 471 - 0
third-party/r3d-odin/r3d/r3d_material.odin

@@ -0,0 +1,471 @@
+/* r3d_material.odin -- R3D Material Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+/**
+ * @brief Transparency modes.
+ *
+ * This enumeration defines how a material handles transparency during rendering.
+ * It controls whether transparency is disabled, rendered using a depth pre-pass,
+ * or rendered with standard alpha blending.
+ */
+TransparencyMode :: enum u32 {
+    DISABLED = 0, ///< No transparency, supports alpha cutoff.
+    PREPASS  = 1, ///< Supports transparency with shadows. Writes shadows for alpha > 0.1 and depth for alpha > 0.99.
+    ALPHA    = 2, ///< Standard transparency without shadows or depth writes.
+}
+
+/**
+ * @brief Billboard modes.
+ *
+ * This enumeration defines how a 3D object aligns itself relative to the camera.
+ * It provides options to disable billboarding or to enable specific modes of alignment.
+ */
+BillboardMode :: enum u32 {
+    DISABLED = 0, ///< Billboarding is disabled; the object retains its original orientation.
+    FRONT    = 1, ///< Full billboarding; the object fully faces the camera, rotating on all axes.
+    Y_AXIS   = 2, /**< Y-axis constrained billboarding; the object rotates only around the Y-axis,
+                                         keeping its "up" orientation fixed. This is suitable for upright objects like characters or signs. */
+}
+
+/**
+ * @brief Blend modes.
+ *
+ * Defines common blending modes used in 3D rendering to combine source and destination colors.
+ * @note The blend mode is applied only if you are in forward rendering mode or auto-detect mode.
+ */
+BlendMode :: enum u32 {
+    MIX                 = 0, ///< Default mode: the result will be opaque or alpha blended depending on the transparency mode.
+    ADDITIVE            = 1, ///< Additive blending: source color is added to the destination, making bright effects.
+    MULTIPLY            = 2, ///< Multiply blending: source color is multiplied with the destination, darkening the image.
+    PREMULTIPLIED_ALPHA = 3, ///< Premultiplied alpha blending: source color is blended with the destination assuming the source color is already multiplied by its alpha.
+}
+
+/**
+ * @brief Comparison modes.
+ *
+ * Defines how fragments are tested against the depth/stencil buffer during rendering.
+ * @note The depth/stencil comparison mode affects both forward and deferred rendering passes.
+ */
+CompareMode :: enum u32 {
+    LESS     = 0, ///< Passes if 'value' <  'buffer' (default)
+    LEQUAL   = 1, ///< Passes if 'value' <= 'buffer'
+    EQUAL    = 2, ///< Passes if 'value' == 'buffer'
+    GREATER  = 3, ///< Passes if 'value' >  'buffer'
+    GEQUAL   = 4, ///< Passes if 'value' >= 'buffer'
+    NOTEQUAL = 5, ///< Passes if 'value' != 'buffer'
+    ALWAYS   = 6, ///< Always passes
+    NEVER    = 7, ///< Never passes
+}
+
+/**
+ * @brief Stencil buffer operations.
+ *
+ * Defines how the stencil buffer value is modified based on test results.
+ */
+StencilOp :: enum u32 {
+    KEEP    = 0, ///< Keep the current stencil value
+    ZERO    = 1, ///< Set stencil value to 0
+    REPLACE = 2, ///< Replace with reference value
+    INCR    = 3, ///< Increment stencil value (clamped)
+    DECR    = 4, ///< Decrement stencil value (clamped)
+}
+
+/**
+ * @brief Face culling modes.
+ *
+ * Specifies which faces of a geometry are discarded during rendering based on their winding order.
+ */
+CullMode :: enum u32 {
+    NONE  = 0, ///< No culling; all faces are rendered.
+    BACK  = 1, ///< Cull back-facing polygons (faces with clockwise winding order).
+    FRONT = 2, ///< Cull front-facing polygons (faces with counter-clockwise winding order).
+}
+
+/**
+ * @brief Albedo (base color) map.
+ *
+ * Provides the base color texture and a color multiplier.
+ */
+AlbedoMap :: struct {
+    texture: rl.Texture2D, ///< Base color texture (default: WHITE)
+    color:   rl.Color,     ///< rl.Color multiplier (default: WHITE)
+}
+
+/**
+ * @brief Emission map.
+ *
+ * Provides emission texture, color, and energy multiplier.
+ */
+EmissionMap :: struct {
+    texture: rl.Texture2D, ///< Emission texture (default: WHITE)
+    color:   rl.Color,     ///< Emission color (default: WHITE)
+    energy:  f32,       ///< Emission strength (default: 0.0f)
+}
+
+/**
+ * @brief Normal map.
+ *
+ * Provides normal map texture and scale factor.
+ */
+NormalMap :: struct {
+    texture: rl.Texture2D, ///< Normal map texture (default: Front Facing)
+    scale:   f32,       ///< Normal scale (default: 1.0f)
+}
+
+/**
+ * @brief Combined Occlusion-Roughness-Metalness (ORM) map.
+ *
+ * Provides texture and individual multipliers for occlusion, roughness, and metalness.
+ */
+OrmMap :: struct {
+    texture:   rl.Texture2D, ///< ORM texture (default: WHITE)
+    occlusion: f32,       ///< Occlusion multiplier (default: 1.0f)
+    roughness: f32,       ///< Roughness multiplier (default: 1.0f)
+    metalness: f32,       ///< Metalness multiplier (default: 0.0f)
+}
+
+/**
+ * @brief Depth buffer state configuration.
+ *
+ * Controls how fragments interact with the depth buffer during rendering..
+ *
+ * @note This structure does not directly control depth buffer writes for technical reasons.
+ *       To render objects without writing to the depth buffer, use alpha blending mode instead.
+ */
+DepthState :: struct {
+    mode:         CompareMode, ///< Comparison function for depth test (default: LESS)
+    offsetFactor: f32,         ///< Scales the maximum depth slope for polygon offset (default: 0.0f)
+    offsetUnits:  f32,         ///< Constant depth offset value (default: 0.0f)
+    rangeNear:    f32,         ///< Near clipping plane for depth range mapping (default: 0.0f)
+    rangeFar:     f32,         ///< Far clipping plane for depth range mapping (default: 1.0f)
+}
+
+/**
+ * @brief Stencil buffer state configuration.
+ *
+ * Controls how fragments interact with the stencil buffer during rendering.
+ * The stencil buffer can be used for effects like x-ray vision, outlines,
+ * portals, and masking.
+ */
+StencilState :: struct {
+    mode:    CompareMode, ///< Comparison function for stencil test (default: ALWAYS)
+    ref:     u8,          ///< Reference value (0-255) for comparison and replace operations (default: 0x00)
+    mask:    u8,          ///< Bit mask applied to both reference and stencil values during comparison (default: 0xFF)
+    opFail:  StencilOp,   ///< Operation when stencil test fails (default: KEEP)
+    opZFail: StencilOp,   ///< Operation when stencil test passes but depth test fails (default: KEEP)
+    opPass:  StencilOp,   ///< Operation when both stencil and depth tests pass (default: REPLACE)
+}
+
+/**
+ * @brief Material definition.
+ *
+ * Combines multiple texture maps and rendering parameters for shading.
+ */
+Material :: struct {
+    albedo:           AlbedoMap,        ///< Albedo map
+    emission:         EmissionMap,      ///< Emission map
+    normal:           NormalMap,        ///< Normal map
+    orm:              OrmMap,           ///< Occlusion-Roughness-Metalness map
+    uvOffset:         rl.Vector2,          ///< UV offset (default: {0.0f, 0.0f})
+    uvScale:          rl.Vector2,          ///< UV scale (default: {1.0f, 1.0f})
+    alphaCutoff:      f32,              ///< Alpha cutoff threshold (default: 0.01f)
+    depth:            DepthState,       ///< Depth test configuration (default: standard)
+    stencil:          StencilState,     ///< Stencil test configuration (default: disabled)
+    transparencyMode: TransparencyMode, ///< Transparency mode (default: DISABLED)
+    billboardMode:    BillboardMode,    ///< Billboard mode (default: DISABLED)
+    blendMode:        BlendMode,        ///< Blend mode (default: MIX)
+    cullMode:         CullMode,         ///< Face culling mode (default: BACK)
+    unlit:            bool,             ///< If true, material does not participate in lighting (default: false)
+    shader:           ^SurfaceShader,   ///< Custom shader applied to the material (default: NULL)
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Get the default material configuration.
+     *
+     * Returns `R3D_MATERIAL_BASE` by default,
+     * or the material defined via `R3D_SetDefaultMaterial()`.
+     *
+     * @return Default material structure with standard properties.
+     */
+    GetDefaultMaterial :: proc() -> Material ---
+
+    /**
+     * @brief Set the default material configuration.
+     *
+     * Allows you to override the default material.
+     * The default material will be used as the basis for loading 3D models.
+     *
+     * @param material Default material to define.
+     */
+    SetDefaultMaterial :: proc(material: Material) ---
+
+    /**
+     * @brief Load materials from a file.
+     *
+     * Parses a 3D model file and loads its associated materials.
+     *
+     * @param filePath Path to the 3D model file.
+     * @param materialCount Pointer to an integer to store the number of loaded materials.
+     * @return Pointer to an array of loaded R3D_Material, or NULL on failure.
+     */
+    LoadMaterials :: proc(filePath: cstring, materialCount: ^i32) -> ^Material ---
+
+    /**
+     * @brief Load materials from memory.
+     *
+     * Loads materials directly from a memory buffer containing 3D model data.
+     *
+     * @param data Pointer to the memory buffer containing the model data.
+     * @param size Size of the data buffer in bytes.
+     * @param hint Hint on the model format (can be NULL).
+     * @param materialCount Pointer to an integer to store the number of loaded materials.
+     * @return Pointer to an array of loaded R3D_Material, or NULL on failure.
+     */
+    LoadMaterialsFromMemory :: proc(data: rawptr, size: u32, hint: cstring, materialCount: ^i32) -> ^Material ---
+
+    /**
+     * @brief Load materials from an importer.
+     *
+     * Loads materials that were previously imported via an R3D_Importer instance.
+     *
+     * @param importer Pointer to a valid R3D_Importer.
+     * @param materialCount Pointer to an integer to store the number of loaded materials.
+     * @return Pointer to an array of loaded R3D_Material, or NULL on failure.
+     */
+    LoadMaterialsFromImporter :: proc(importer: ^Importer, materialCount: ^i32) -> ^Material ---
+
+    /**
+     * @brief Unload a material and its associated textures.
+     *
+     * Frees all memory associated with a material, including its textures.
+     * This function will unload all textures that are not default textures.
+     *
+     * @warning Only call this function if you are certain that the textures
+     * are not shared with other materials or objects, as this will permanently
+     * free the texture data.
+     *
+     * @param material Pointer to the material structure to be unloaded.
+     */
+    UnloadMaterial :: proc(material: Material) ---
+
+    /**
+     * @brief Load an albedo (base color) map from file.
+     *
+     * Loads an image, uploads it as an sRGB texture (if enabled),
+     * and applies the provided tint color.
+     *
+     * @param fileName Path to the texture file.
+     * @param color Multiplicative tint applied in the shader.
+     * @return Albedo map structure. Returns an empty map on failure.
+     */
+    LoadAlbedoMap :: proc(fileName: cstring, color: rl.Color) -> AlbedoMap ---
+
+    /**
+     * @brief Load an albedo (base color) map from memory.
+     *
+     * Same behavior as R3D_LoadAlbedoMap(), but reads from memory instead of disk.
+     *
+     * @param fileType rl.Image format hint (e.g. "png", "jpg").
+     * @param fileData Pointer to image data.
+     * @param dataSize Size of image data in bytes.
+     * @param color Multiplicative tint applied in the shader.
+     * @return Albedo map structure. Returns an empty map on failure.
+     */
+    LoadAlbedoMapFromMemory :: proc(fileType: cstring, fileData: rawptr, dataSize: i32, color: rl.Color) -> AlbedoMap ---
+
+    /**
+     * @brief Unload an albedo map texture.
+     *
+     * Frees the underlying texture unless it is a default texture.
+     *
+     * @param map Albedo map to unload.
+     */
+    UnloadAlbedoMap :: proc(_map: AlbedoMap) ---
+
+    /**
+     * @brief Load an emission map from file.
+     *
+     * Loads an emissive texture (sRGB if enabled) and sets color + energy.
+     *
+     * @param fileName Path to the texture file.
+     * @param color Emission color.
+     * @param energy Emission intensity multiplier.
+     * @return Emission map. Returns an empty map on failure.
+     */
+    LoadEmissionMap :: proc(fileName: cstring, color: rl.Color, energy: f32) -> EmissionMap ---
+
+    /**
+     * @brief Load an emission map from memory.
+     *
+     * Same behavior as R3D_LoadEmissionMap(), but reads from memory.
+     *
+     * @param fileType rl.Image format hint.
+     * @param fileData Pointer to image data.
+     * @param dataSize Size of image data in bytes.
+     * @param color Emission color.
+     * @param energy Emission intensity multiplier.
+     * @return Emission map. Returns an empty map on failure.
+     */
+    LoadEmissionMapFromMemory :: proc(fileType: cstring, fileData: rawptr, dataSize: i32, color: rl.Color, energy: f32) -> EmissionMap ---
+
+    /**
+     * @brief Unload an emission map texture.
+     *
+     * Frees the texture unless it is a default texture.
+     *
+     * @param map Emission map to unload.
+     */
+    UnloadEmissionMap :: proc(_map: EmissionMap) ---
+
+    /**
+     * @brief Load a normal map from file.
+     *
+     * Uploads the texture in linear space and stores the normal scale factor.
+     *
+     * @param fileName Path to the texture file.
+     * @param scale Normal intensity multiplier.
+     * @return Normal map. Returns an empty map on failure.
+     */
+    LoadNormalMap :: proc(fileName: cstring, scale: f32) -> NormalMap ---
+
+    /**
+     * @brief Load a normal map from memory.
+     *
+     * Same behavior as R3D_LoadNormalMap(), but reads from memory.
+     *
+     * @param fileType rl.Image format hint.
+     * @param fileData Pointer to image data.
+     * @param dataSize Size of image data in bytes.
+     * @param scale Normal intensity multiplier.
+     * @return Normal map. Returns an empty map on failure.
+     */
+    LoadNormalMapFromMemory :: proc(fileType: cstring, fileData: rawptr, dataSize: i32, scale: f32) -> NormalMap ---
+
+    /**
+     * @brief Unload a normal map texture.
+     *
+     * Frees the texture unless it is a default texture.
+     *
+     * @param map Normal map to unload.
+     */
+    UnloadNormalMap :: proc(_map: NormalMap) ---
+
+    /**
+     * @brief Load a combined ORM (Occlusion-Roughness-Metalness) map from file.
+     *
+     * Uploads the texture in linear space and applies the provided multipliers.
+     *
+     * @param fileName Path to the ORM texture.
+     * @param occlusion Occlusion multiplier.
+     * @param roughness Roughness multiplier.
+     * @param metalness Metalness multiplier.
+     * @return ORM map. Returns an empty map on failure.
+     */
+    LoadOrmMap :: proc(fileName: cstring, occlusion: f32, roughness: f32, metalness: f32) -> OrmMap ---
+
+    /**
+     * @brief Load a combined ORM (Occlusion-Roughness-Metalness) map from memory.
+     *
+     * Same behavior as R3D_LoadOrmMap(), but reads from memory.
+     *
+     * @param fileType rl.Image format hint.
+     * @param fileData Pointer to image data.
+     * @param dataSize Size of image data in bytes.
+     * @param occlusion Occlusion multiplier.
+     * @param roughness Roughness multiplier.
+     * @param metalness Metalness multiplier.
+     * @return ORM map. Returns an empty map on failure.
+     */
+    LoadOrmMapFromMemory :: proc(fileType: cstring, fileData: rawptr, dataSize: i32, occlusion: f32, roughness: f32, metalness: f32) -> OrmMap ---
+
+    /**
+     * @brief Unload an ORM map texture.
+     *
+     * Frees the texture unless it is a default texture.
+     *
+     * @param map ORM map to unload.
+     */
+    UnloadOrmMap :: proc(_map: OrmMap) ---
+}
+
+/**
+ * @brief Default material configuration.
+ *
+ * Initializes an R3D_Material structure with sensible default values for all
+ * rendering parameters. Use this as a starting point for custom configurations.
+ */
+MATERIAL_BASE :: Material {
+    albedo = {
+        texture = {},
+        color   = {255, 255, 255, 255},
+    },
+    emission = {
+        texture = {},
+        color   = {255, 255, 255, 255},
+        energy  = 0.0,
+    },
+    normal = {
+        texture = {},
+        scale   = 1.0,
+    },
+    orm = {
+        texture   = {},
+        occlusion = 1.0,
+        roughness = 1.0,
+        metalness = 0.0,
+    },
+    uvOffset = {0.0, 0.0},
+    uvScale  = {1.0, 1.0},
+    alphaCutoff = 0.01,
+    depth = {
+        mode         = .LESS,
+        offsetFactor = 0.0,
+        offsetUnits  = 0.0,
+        rangeNear    = 0.0,
+        rangeFar     = 1.0,
+    },
+    stencil = {
+        mode     = .ALWAYS,
+        ref      = 0x00,
+        mask     = 0xFF,
+        opFail   = .KEEP,
+        opZFail  = .KEEP,
+        opPass   = .REPLACE,
+    },
+    transparencyMode = .DISABLED,
+    billboardMode    = .DISABLED,
+    blendMode        = .MIX,
+    cullMode         = .BACK,
+    unlit  = false,
+    shader = nil,
+}

+ 283 - 0
third-party/r3d-odin/r3d/r3d_mesh.odin

@@ -0,0 +1,283 @@
+/* r3d_mesh.odin -- R3D Mesh Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+/**
+ * @brief Hint on how a mesh will be used.
+ */
+MeshUsage :: enum u32 {
+    STATIC_MESH   = 0, ///< Will never be updated.
+    DYNAMIC_MESH  = 1, ///< Will be updated occasionally.
+    STREAMED_MESH = 2, ///< Will be update on each frame.
+}
+
+/**
+ * @brief Defines the geometric primitive type.
+ */
+PrimitiveType :: enum u32 {
+    POINTS         = 0, ///< Each vertex represents a single point.
+    LINES          = 1, ///< Each pair of vertices forms an independent line segment.
+    LINE_STRIP     = 2, ///< Connected series of line segments sharing vertices.
+    LINE_LOOP      = 3, ///< Closed loop of connected line segments.
+    TRIANGLES      = 4, ///< Each set of three vertices forms an independent triangle.
+    TRIANGLE_STRIP = 5, ///< Connected strip of triangles sharing vertices.
+    TRIANGLE_FAN   = 6, ///< Fan of triangles sharing the first vertex.
+}
+
+/**
+ * @brief Shadow casting modes for objects.
+ *
+ * Controls how an object interacts with the shadow mapping system.
+ * These modes determine whether the object contributes to shadows,
+ * and if so, whether it is also rendered in the main pass.
+ */
+ShadowCastMode :: enum u32 {
+    ON_AUTO           = 0, ///< The object casts shadows; the faces used are determined by the material's culling mode.
+    ON_DOUBLE_SIDED   = 1, ///< The object casts shadows with both front and back faces, ignoring face culling.
+    ON_FRONT_SIDE     = 2, ///< The object casts shadows with only front faces, culling back faces.
+    ON_BACK_SIDE      = 3, ///< The object casts shadows with only back faces, culling front faces.
+    ONLY_AUTO         = 4, ///< The object only casts shadows; the faces used are determined by the material's culling mode.
+    ONLY_DOUBLE_SIDED = 5, ///< The object only casts shadows with both front and back faces, ignoring face culling.
+    ONLY_FRONT_SIDE   = 6, ///< The object only casts shadows with only front faces, culling back faces.
+    ONLY_BACK_SIDE    = 7, ///< The object only casts shadows with only back faces, culling front faces.
+    DISABLED          = 8, ///< The object does not cast shadows at all.
+}
+
+/**
+ * @brief Represents a 3D mesh.
+ *
+ * Stores vertex and index data, shadow casting settings, bounding box, and layer information.
+ * Can represent a static or skinned mesh.
+ */
+Mesh :: struct {
+    vao, vbo, ebo:                     u32,            ///< OpenGL objects handles.
+    vertexCount, indexCount:           i32,            ///< Number of vertices and indices currently in use.
+    allocVertexCount, allocIndexCount: i32,            ///< Number of vertices and indices allocated in GPU buffers.
+    shadowCastMode:                    ShadowCastMode, ///< Shadow casting mode for the mesh.
+    primitiveType:                     PrimitiveType,  ///< Type of primitive that constitutes the vertices.
+    usage:                             MeshUsage,      ///< Hint about the usage of the mesh, retained in case of update if there is a reallocation.
+    layerMask:                         Layer,          ///< Bitfield indicating the rendering layer(s) of this mesh.
+    aabb:                              rl.BoundingBox,    ///< Axis-Aligned Bounding Box in local space.
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Creates a 3D mesh from CPU-side mesh data.
+     * @param type Primitive type used to interpret vertex data.
+     * @param data R3D_MeshData containing vertices and indices (cannot be NULL).
+     * @param aabb Optional pointer to a bounding box. If NULL, it will be computed automatically.
+     * @param usage Hint on how the mesh will be used.
+     * @return Created R3D_Mesh.
+     * @note The function copies all vertex and index data into GPU buffers.
+     */
+    LoadMesh :: proc(type: PrimitiveType, data: MeshData, aabb: ^rl.BoundingBox, usage: MeshUsage) -> Mesh ---
+
+    /**
+     * @brief Destroys a 3D mesh and frees its resources.
+     * @param mesh R3D_Mesh to destroy.
+     */
+    UnloadMesh :: proc(mesh: Mesh) ---
+
+    /**
+     * @brief Check if a mesh is valid for rendering.
+     *
+     * Returns true if the mesh has a valid VAO and VBO.
+     *
+     * @param mesh The mesh to check.
+     * @return true if valid, false otherwise.
+     */
+    IsMeshValid :: proc(mesh: Mesh) -> bool ---
+
+    /**
+     * @brief Generate a quad mesh with orientation.
+     * @param width Width along local X axis.
+     * @param length Length along local Z axis.
+     * @param resX Subdivisions along width.
+     * @param resZ Subdivisions along length.
+     * @param frontDir Direction vector for the quad's front face.
+     * @return Mesh ready for rendering.
+     * @see R3D_GenMeshDataQuad
+     */
+    GenMeshQuad :: proc(width: f32, length: f32, resX: i32, resZ: i32, frontDir: rl.Vector3) -> Mesh ---
+
+    /**
+     * @brief Generate a plane mesh.
+     * @param width Width along X axis.
+     * @param length Length along Z axis.
+     * @param resX Subdivisions along X axis.
+     * @param resZ Subdivisions along Z axis.
+     * @return Mesh ready for rendering.
+     * @see R3D_GenMeshDataPlane
+     */
+    GenMeshPlane :: proc(width: f32, length: f32, resX: i32, resZ: i32) -> Mesh ---
+
+    /**
+     * @brief Generate a polygon mesh.
+     * @param sides Number of sides (min 3).
+     * @param radius Radius of the polygon.
+     * @param frontDir Direction vector for the polygon's front face.
+     * @return Mesh ready for rendering.
+     * @see R3D_GenMeshDataPoly
+     */
+    GenMeshPoly :: proc(sides: i32, radius: f32, frontDir: rl.Vector3) -> Mesh ---
+
+    /**
+     * @brief Generate a cube mesh.
+     * @param width Width along X axis.
+     * @param height Height along Y axis.
+     * @param length Length along Z axis.
+     * @return Mesh ready for rendering.
+     * @see R3D_GenMeshDataCube
+     */
+    GenMeshCube :: proc(width: f32, height: f32, length: f32) -> Mesh ---
+
+    /**
+     * @brief Generate a subdivided cube mesh.
+     * @param width Width along X axis.
+     * @param height Height along Y axis.
+     * @param length Length along Z axis.
+     * @param resX Subdivisions along X axis.
+     * @param resY Subdivisions along Y axis.
+     * @param resZ Subdivisions along Z axis.
+     * @return Mesh ready for rendering.
+     * @see R3D_GenMeshDataCubeEx
+     */
+    GenMeshCubeEx :: proc(width: f32, height: f32, length: f32, resX: i32, resY: i32, resZ: i32) -> Mesh ---
+
+    /**
+     * @brief Generate a slope mesh.
+     * @param width Width along X axis.
+     * @param height Height along Y axis.
+     * @param length Length along Z axis.
+     * @param slopeNormal Direction of the slope.
+     * @return Mesh ready for rendering.
+     * @see R3D_GenMeshDataSlope
+     */
+    GenMeshSlope :: proc(width: f32, height: f32, length: f32, slopeNormal: rl.Vector3) -> Mesh ---
+
+    /**
+     * @brief Generate a sphere mesh.
+     * @param radius Sphere radius.
+     * @param rings Number of latitude divisions.
+     * @param slices Number of longitude divisions.
+     * @return Mesh ready for rendering.
+     * @see R3D_GenMeshDataSphere
+     */
+    GenMeshSphere :: proc(radius: f32, rings: i32, slices: i32) -> Mesh ---
+
+    /**
+     * @brief Generate a hemisphere mesh.
+     * @param radius Hemisphere radius.
+     * @param rings Number of latitude divisions.
+     * @param slices Number of longitude divisions.
+     * @return Mesh ready for rendering.
+     * @see R3D_GenMeshDataHemiSphere
+     */
+    GenMeshHemiSphere :: proc(radius: f32, rings: i32, slices: i32) -> Mesh ---
+
+    /**
+     * @brief Generate a cylinder mesh.
+     * @param bottomRadius Bottom radius.
+     * @param topRadius Top radius.
+     * @param height Height along Y axis.
+     * @param slices Radial subdivisions.
+     * @return Mesh ready for rendering.
+     * @see R3D_GenMeshDataCylinder
+     */
+    GenMeshCylinder :: proc(bottomRadius: f32, topRadius: f32, height: f32, slices: i32) -> Mesh ---
+
+    /**
+     * @brief Generate a capsule mesh.
+     * @param radius Capsule radius.
+     * @param height Height along Y axis.
+     * @param rings Number of latitude divisions.
+     * @param slices Number of longitude divisions.
+     * @return Mesh ready for rendering.
+     * @see R3D_GenMeshDataCapsule
+     */
+    GenMeshCapsule :: proc(radius: f32, height: f32, rings: i32, slices: i32) -> Mesh ---
+
+    /**
+     * @brief Generate a torus mesh.
+     * @param radius Major radius (center to tube).
+     * @param size Minor radius (tube thickness).
+     * @param radSeg Segments around major radius.
+     * @param sides Sides around tube cross-section.
+     * @return Mesh ready for rendering.
+     * @see R3D_GenMeshDataTorus
+     */
+    GenMeshTorus :: proc(radius: f32, size: f32, radSeg: i32, sides: i32) -> Mesh ---
+
+    /**
+     * @brief Generate a trefoil knot mesh.
+     * @param radius Major radius.
+     * @param size Minor radius.
+     * @param radSeg Segments around major radius.
+     * @param sides Sides around tube cross-section.
+     * @return Mesh ready for rendering.
+     * @see R3D_GenMeshDataKnot
+     */
+    GenMeshKnot :: proc(radius: f32, size: f32, radSeg: i32, sides: i32) -> Mesh ---
+
+    /**
+     * @brief Generate a heightmap terrain mesh.
+     * @param heightmap Heightmap image.
+     * @param size 3D dimensions of terrain.
+     * @return Mesh ready for rendering.
+     * @see R3D_GenMeshDataHeightmap
+     */
+    GenMeshHeightmap :: proc(heightmap: rl.Image, size: rl.Vector3) -> Mesh ---
+
+    /**
+     * @brief Generate a cubicmap voxel mesh.
+     * @param cubicmap Cubicmap image.
+     * @param cubeSize Size of each cube.
+     * @return Mesh ready for rendering.
+     * @see R3D_GenMeshDataCubicmap
+     */
+    GenMeshCubicmap :: proc(cubicmap: rl.Image, cubeSize: rl.Vector3) -> Mesh ---
+
+    /**
+     * @brief Upload a mesh data on the GPU.
+     *
+     * This function uploads a mesh's vertex and optional index data to the GPU.
+     *
+     * If `aabb` is provided, it will be used as the mesh's bounding box; if null,
+     * the bounding box is automatically recalculated from the vertex data.
+     *
+     * @param mesh Pointer to the mesh structure to update.
+     * @param data Mesh data (vertices and indices) to upload.
+     * @param aabb Optional bounding box; if null, it is recalculated automatically.
+     * @return Returns true if the update is successful, false otherwise.
+     */
+    UpdateMesh :: proc(mesh: ^Mesh, data: MeshData, aabb: ^rl.BoundingBox) -> bool ---
+}
+

+ 392 - 0
third-party/r3d-odin/r3d/r3d_mesh_data.odin

@@ -0,0 +1,392 @@
+/* r3d_mesh_data.odin -- R3D Mesh Data Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+/**
+ * @brief Represents a vertex and all its attributes for a mesh.
+ */
+Vertex :: struct {
+    position: rl.Vector3, /**< The 3D position of the vertex in object space. */
+    texcoord: rl.Vector2, /**< The 2D texture coordinates (UV) for mapping textures. */
+    normal:   rl.Vector3, /**< The normal vector used for lighting calculations. */
+    color:    rl.Color,   /**< Vertex color, in RGBA32. */
+    tangent:  rl.Vector4, /**< The tangent vector, used in normal mapping (often with a handedness in w). */
+    boneIds:  [4]i32,  /**< Indices of up to 4 bones that influence this vertex (for skinning). */
+    weights:  [4]f32,  /**< Corresponding bone weights (should sum to 1.0). Defines the influence of each bone. */
+}
+
+/**
+ * @brief Represents a mesh stored in CPU memory.
+ *
+ * R3D_MeshData is the CPU-side container of a mesh. It stores vertex and index data,
+ * and provides utility functions to generate, transform, and process geometry before
+ * uploading it to the GPU as an R3D_Mesh.
+ *
+ * Think of it as a toolbox for procedural or dynamic mesh generation on the CPU.
+ */
+MeshData :: struct {
+    vertices:    [^]Vertex, ///< Pointer to vertex data in CPU memory.
+    indices:     [^]u32,    ///< Pointer to index data in CPU memory.
+    vertexCount: i32,       ///< Number of vertices.
+    indexCount:  i32,       ///< Number of indices.
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Creates an empty mesh data container.
+     *
+     * Allocates memory for vertex and index buffers. All allocated buffers
+     * are zero-initialized.
+     *
+     * @param vertexCount Number of vertices to allocate. Must be non-zero.
+     * @param indexCount Number of indices to allocate. May be zero.
+     *                   If zero, no index buffer is allocated.
+     *
+     * @return A new R3D_MeshData instance with allocated memory.
+     */
+    CreateMeshData :: proc(vertexCount: i32, indexCount: i32) -> MeshData ---
+
+    /**
+     * @brief Releases memory used by a mesh data container.
+     * @param meshData R3D_MeshData to destroy.
+     */
+    UnloadMeshData :: proc(meshData: MeshData) ---
+
+    /**
+     * @brief Check if mesh data is valid.
+     *
+     * Returns true if the mesh data contains at least one vertex buffer
+     * with a positive number of vertices.
+     *
+     * @param meshData Mesh data to check.
+     * @return true if valid, false otherwise.
+     */
+    IsMeshDataValid :: proc(meshData: MeshData) -> bool ---
+
+    /**
+     * @brief Generate a quad mesh with specified dimensions, resolution, and orientation.
+     *
+     * Creates a flat rectangular quad mesh with customizable facing direction.
+     * The mesh can be subdivided for higher resolution or displacement mapping.
+     * The quad is centered at the origin and oriented according to the frontDir parameter,
+     * which defines both the face direction and the surface normal.
+     *
+     * @param width Width of the quad along its local X axis.
+     * @param length Length of the quad along its local Z axis.
+     * @param resX Number of subdivisions along the width.
+     * @param resZ Number of subdivisions along the length.
+     * @param frontDir Direction vector defining the quad's front face and normal.
+     *                 This vector will be normalized internally.
+     *
+     * @return Generated quad mesh structure with proper normals, tangents, and UVs.
+     */
+    GenMeshDataQuad :: proc(width: f32, length: f32, resX: i32, resZ: i32, frontDir: rl.Vector3) -> MeshData ---
+
+    /**
+     * @brief Generate a plane mesh with specified dimensions and resolution.
+     *
+     * Creates a flat plane mesh in the XZ plane, centered at the origin.
+     * The mesh can be subdivided for higher resolution or displacement mapping.
+     *
+     * @param width Width of the plane along the X axis.
+     * @param length Length of the plane along the Z axis.
+     * @param resX Number of subdivisions along the X axis.
+     * @param resZ Number of subdivisions along the Z axis.
+     *
+     * @return Generated plane mesh structure.
+     */
+    GenMeshDataPlane :: proc(width: f32, length: f32, resX: i32, resZ: i32) -> MeshData ---
+
+    /**
+     * @brief Generate a polygon mesh with specified number of sides.
+     *
+     * Creates a regular polygon mesh centered at the origin in the XY plane.
+     * The polygon is generated with vertices evenly distributed around a circle.
+     *
+     * @param sides Number of sides for the polygon (minimum 3).
+     * @param radius Radius of the circumscribed circle.
+     * @param frontDir Direction vector defining the polygon's front face and normal.
+     *                 This vector will be normalized internally.
+     *
+     * @return Generated polygon mesh structure.
+     */
+    GenMeshDataPoly :: proc(sides: i32, radius: f32, frontDir: rl.Vector3) -> MeshData ---
+
+    /**
+     * @brief Generate a cube mesh with specified dimensions.
+     *
+     * Creates a cube mesh centered at the origin with the specified width, height, and length.
+     * Each face consists of two triangles with proper normals and texture coordinates.
+     *
+     * @param width Width of the cube along the X axis.
+     * @param height Height of the cube along the Y axis.
+     * @param length Length of the cube along the Z axis.
+     *
+     * @return Generated cube mesh structure.
+     */
+    GenMeshDataCube :: proc(width: f32, height: f32, length: f32) -> MeshData ---
+
+    /**
+     * @brief Generate a subdivided cube mesh with specified dimensions.
+     *
+     * Extension of R3D_GenMeshDataCube() allowing per-axis subdivision.
+     * Each face can be tessellated along the X, Y, and Z axes according
+     * to the provided resolutions.
+     *
+     * @param width Width of the cube along the X axis.
+     * @param height Height of the cube along the Y axis.
+     * @param length Length of the cube along the Z axis.
+     * @param resX Number of subdivisions along the X axis.
+     * @param resY Number of subdivisions along the Y axis.
+     * @param resZ Number of subdivisions along the Z axis.
+     *
+     * @return Generated cube mesh structure.
+     */
+    GenMeshDataCubeEx :: proc(width: f32, height: f32, length: f32, resX: i32, resY: i32, resZ: i32) -> MeshData ---
+
+    /**
+     * @brief Generate a slope mesh by cutting a cube with a plane.
+     *
+     * Creates a slope mesh by slicing a cube with a plane that passes through the origin.
+     * The plane is defined by its normal vector, and the portion of the cube on the side
+     * opposite to the normal direction is kept. This allows creating ramps, wedges, and
+     * angled surfaces with arbitrary orientations.
+     *
+     * @param width Width of the base cube along the X axis.
+     * @param height Height of the base cube along the Y axis.
+     * @param length Length of the base cube along the Z axis.
+     * @param slopeNormal Normal vector of the cutting plane. The mesh keeps the portion
+     *                    of the cube in the direction opposite to this normal.
+     *                    Example: {-1, 0, 0} creates a ramp rising towards +X.
+     *                             {0, 1, 0} creates a wedge with the slope facing up.
+     *                             {-1.0, 1.0, 0} creates a 45° diagonal slope.
+     *
+     * @return Generated slope mesh structure.
+     *
+     * @note The normal vector will be automatically normalized internally.
+     * @note The cutting plane always passes through the center of the cube (origin).
+     */
+    GenMeshDataSlope :: proc(width: f32, height: f32, length: f32, slopeNormal: rl.Vector3) -> MeshData ---
+
+    /**
+     * @brief Generate a sphere mesh with specified parameters.
+     *
+     * Creates a UV sphere mesh centered at the origin using latitude-longitude subdivision.
+     * Higher ring and slice counts produce smoother spheres but with more vertices.
+     *
+     * @param radius Radius of the sphere.
+     * @param rings Number of horizontal rings (latitude divisions).
+     * @param slices Number of vertical slices (longitude divisions).
+     *
+     * @return Generated sphere mesh structure.
+     */
+    GenMeshDataSphere :: proc(radius: f32, rings: i32, slices: i32) -> MeshData ---
+
+    /**
+     * @brief Generate a hemisphere mesh with specified parameters.
+     *
+     * Creates a half-sphere mesh (dome) centered at the origin, extending upward in the Y axis.
+     * Uses the same UV sphere generation technique as R3D_GenMeshSphere but only the upper half.
+     *
+     * @param radius Radius of the hemisphere.
+     * @param rings Number of horizontal rings (latitude divisions).
+     * @param slices Number of vertical slices (longitude divisions).
+     *
+     * @return Generated hemisphere mesh structure.
+     */
+    GenMeshDataHemiSphere :: proc(radius: f32, rings: i32, slices: i32) -> MeshData ---
+
+    /**
+     * @brief Generate a cylinder mesh with specified parameters.
+     *
+     * Creates a mesh centered at the origin, extending along the Y axis.
+     * The mesh includes top and bottom caps and smooth side surfaces.
+     * A cone is produced when bottomRadius and topRadius differ.
+     *
+     * @param bottomRadius Radius of the bottom cap.
+     * @param topRadius Radius of the top cap.
+     * @param height Height of the shape along the Y axis.
+     * @param slices Number of radial subdivisions around the shape.
+     *
+     * @return Generated mesh structure.
+     */
+    GenMeshDataCylinder :: proc(bottomRadius: f32, topRadius: f32, height: f32, slices: i32) -> MeshData ---
+
+    /**
+     * @brief Generate a capsule mesh with specified parameters.
+     *
+     * Creates a capsule mesh centered at the origin, extending along the Y axis.
+     * The capsule consists of a cylindrical body with hemispherical caps on both ends.
+     * The total height of the capsule is height + 2 * radius.
+     *
+     * @param radius Radius of the capsule (both cylindrical body and hemispherical caps).
+     * @param height Height of the cylindrical portion along the Y axis.
+     * @param rings Total number of latitudinal subdivisions for both hemispheres combined.
+     * @param slices Number of radial subdivisions around the shape.
+     *
+     * @return Generated mesh structure.
+     */
+    GenMeshDataCapsule :: proc(radius: f32, height: f32, rings: i32, slices: i32) -> MeshData ---
+
+    /**
+     * @brief Generate a torus mesh with specified parameters.
+     *
+     * Creates a torus (donut shape) mesh centered at the origin in the XZ plane.
+     * The torus is defined by a major radius (distance from center to tube center)
+     * and a minor radius (tube thickness).
+     *
+     * @param radius Major radius of the torus (distance from center to tube center).
+     * @param size Minor radius of the torus (tube thickness/radius).
+     * @param radSeg Number of segments around the major radius.
+     * @param sides Number of sides around the tube cross-section.
+     *
+     * @return Generated torus mesh structure.
+     */
+    GenMeshDataTorus :: proc(radius: f32, size: f32, radSeg: i32, sides: i32) -> MeshData ---
+
+    /**
+     * @brief Generate a trefoil knot mesh with specified parameters.
+     *
+     * Creates a trefoil knot mesh, which is a mathematical knot shape.
+     * Similar to a torus but with a twisted, knotted topology.
+     *
+     * @param radius Major radius of the knot.
+     * @param size Minor radius (tube thickness) of the knot.
+     * @param radSeg Number of segments around the major radius.
+     * @param sides Number of sides around the tube cross-section.
+     *
+     * @return Generated trefoil knot mesh structure.
+     */
+    GenMeshDataKnot :: proc(radius: f32, size: f32, radSeg: i32, sides: i32) -> MeshData ---
+
+    /**
+     * @brief Generate a terrain mesh from a heightmap image.
+     *
+     * Creates a terrain mesh by interpreting the brightness values of a heightmap image
+     * as height values. The resulting mesh represents a 3D terrain surface.
+     *
+     * @param heightmap rl.Image containing height data (grayscale values represent elevation).
+     * @param size 3D vector defining the terrain dimensions (width, max height, depth).
+     *
+     * @return Generated heightmap terrain mesh structure.
+     */
+    GenMeshDataHeightmap :: proc(heightmap: rl.Image, size: rl.Vector3) -> MeshData ---
+
+    /**
+     * @brief Generate a voxel-style mesh from a cubicmap image.
+     *
+     * Creates a mesh composed of cubes based on a cubicmap image, where each pixel
+     * represents the presence or absence of a cube at that position. Useful for
+     * creating voxel-based or block-based geometry.
+     *
+     * @param cubicmap rl.Image where pixel values determine cube placement.
+     * @param cubeSize 3D vector defining the size of each individual cube.
+     *
+     * @return Generated cubicmap mesh structure.
+     */
+    GenMeshDataCubicmap :: proc(cubicmap: rl.Image, cubeSize: rl.Vector3) -> MeshData ---
+
+    /**
+     * @brief Creates a deep copy of an existing mesh data container.
+     * @param meshData Source mesh data to duplicate.
+     * @return A new R3D_MeshData containing a copy of the source data.
+     */
+    DuplicateMeshData :: proc(meshData: MeshData) -> MeshData ---
+
+    /**
+     * @brief Merges two mesh data containers into a single one.
+     * @param a First mesh data.
+     * @param b Second mesh data.
+     * @return A new R3D_MeshData containing the merged geometry.
+     */
+    MergeMeshData :: proc(a: MeshData, b: MeshData) -> MeshData ---
+
+    /**
+     * @brief Translates all vertices by a given offset.
+     * @param meshData Mesh data to modify.
+     * @param translation Offset to apply to all vertex positions.
+     */
+    TranslateMeshData :: proc(meshData: ^MeshData, translation: rl.Vector3) ---
+
+    /**
+     * @brief Rotates all vertices using a quaternion.
+     * @param meshData Mesh data to modify.
+     * @param rotation rl.Quaternion representing the rotation.
+     */
+    RotateMeshData :: proc(meshData: ^MeshData, rotation: rl.Quaternion) ---
+
+    /**
+     * @brief Scales all vertices by given factors.
+     * @param meshData Mesh data to modify.
+     * @param scale Scaling factors for each axis.
+     */
+    ScaleMeshData :: proc(meshData: ^MeshData, scale: rl.Vector3) ---
+
+    /**
+     * @brief Generates planar UV coordinates.
+     * @param meshData Mesh data to modify.
+     * @param uvScale Scaling factors for UV coordinates.
+     * @param axis Axis along which to project the planar mapping.
+     */
+    GenMeshDataUVsPlanar :: proc(meshData: ^MeshData, uvScale: rl.Vector2, axis: rl.Vector3) ---
+
+    /**
+     * @brief Generates spherical UV coordinates.
+     * @param meshData Mesh data to modify.
+     */
+    GenMeshDataUVsSpherical :: proc(meshData: ^MeshData) ---
+
+    /**
+     * @brief Generates cylindrical UV coordinates.
+     * @param meshData Mesh data to modify.
+     */
+    GenMeshDataUVsCylindrical :: proc(meshData: ^MeshData) ---
+
+    /**
+     * @brief Computes vertex normals from triangle geometry.
+     * @param meshData Mesh data to modify.
+     */
+    GenMeshDataNormals :: proc(meshData: ^MeshData) ---
+
+    /**
+     * @brief Computes vertex tangents based on existing normals and UVs.
+     * @param meshData Mesh data to modify.
+     */
+    GenMeshDataTangents :: proc(meshData: ^MeshData) ---
+
+    /**
+     * @brief Calculates the axis-aligned bounding box of the mesh.
+     * @param meshData Mesh data to analyze.
+     * @return The computed bounding box.
+     */
+    CalculateMeshDataBoundingBox :: proc(meshData: MeshData) -> rl.BoundingBox ---
+}
+

+ 134 - 0
third-party/r3d-odin/r3d/r3d_model.odin

@@ -0,0 +1,134 @@
+/* r3d_model.odin -- R3D Model Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+/**
+ * @brief Represents a complete 3D model with meshes and materials.
+ *
+ * Contains multiple meshes and their associated materials, along with animation and bounding information.
+ */
+Model :: struct {
+    meshes:        [^]Mesh,     ///< Array of meshes composing the model.
+    meshData:      [^]MeshData, ///< Array of meshes data in RAM (optional, can be NULL).
+    materials:     [^]Material, ///< Array of materials used by the model.
+    meshMaterials: [^]i32,      ///< Array of material indices, one per mesh.
+    meshCount:     i32,         ///< Number of meshes.
+    materialCount: i32,         ///< Number of materials.
+    aabb:          rl.BoundingBox, ///< Axis-Aligned Bounding Box encompassing the whole model.
+    skeleton:      Skeleton,    ///< Skeleton hierarchy and bind pose used for skinning (NULL if non-skinned).
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Load a 3D model from a file.
+     *
+     * Loads a 3D model from the specified file path. Supports various 3D file formats
+     * and automatically parses meshes, materials, and texture references.
+     *
+     * @param filePath Path to the 3D model file to load.
+     *
+     * @return Loaded model structure containing meshes and materials.
+     */
+    LoadModel :: proc(filePath: cstring) -> Model ---
+
+    /**
+     * @brief Load a 3D model from a file with import flags.
+     *
+     * Extended version of R3D_LoadModel() allowing control over the import
+     * process through additional flags.
+     *
+     * @param filePath Path to the 3D model file to load.
+     * @param flags Importer behavior flags.
+     *
+     * @return Loaded model structure containing meshes and materials.
+     */
+    LoadModelEx :: proc(filePath: cstring, flags: ImportFlags) -> Model ---
+
+    /**
+     * @brief Load a 3D model from memory buffer.
+     *
+     * Loads a 3D model from a memory buffer containing the file data.
+     * Useful for loading models from embedded resources or network streams.
+     *
+     * @param data Pointer to the memory buffer containing the model data.
+     * @param size Size of the data buffer in bytes.
+     * @param hint Hint on the model format (can be NULL).
+     *
+     * @return Loaded model structure containing meshes and materials.
+     *
+     * @note External dependencies (e.g., textures or linked resources) are not supported.
+     *       The model data must be fully self-contained. Use embedded formats like .glb to ensure compatibility.
+     */
+    LoadModelFromMemory :: proc(data: rawptr, size: u32, hint: cstring) -> Model ---
+
+    /**
+     * @brief Load a 3D model from a memory buffer with import flags.
+     *
+     * Extended version of R3D_LoadModelFromMemory() allowing control over
+     * the import process through additional flags.
+     *
+     * @param data Pointer to the memory buffer containing the model data.
+     * @param size Size of the data buffer in bytes.
+     * @param hint Hint on the model format (can be NULL).
+     * @param flags Importer behavior flags.
+     *
+     * @return Loaded model structure containing meshes and materials.
+     *
+     * @note External dependencies (e.g., textures or linked resources) are not supported.
+     *       The model data must be fully self-contained.
+     */
+    LoadModelFromMemoryEx :: proc(data: rawptr, size: u32, hint: cstring, flags: ImportFlags) -> Model ---
+
+    /**
+     * @brief Load a 3D model from an existing importer.
+     *
+     * Creates a model from a previously loaded importer instance.
+     * This avoids re-importing the source file.
+     *
+     * @param importer Importer instance to extract the model from.
+     *
+     * @return Loaded model structure containing meshes and materials.
+     */
+    LoadModelFromImporter :: proc(importer: ^Importer) -> Model ---
+
+    /**
+     * @brief Unload a model and optionally its materials.
+     *
+     * Frees all memory associated with a model, including its meshes.
+     * Materials can be optionally unloaded as well.
+     *
+     * @param model The model to be unloaded.
+     * @param unloadMaterials If true, also unloads all materials associated with the model.
+     * Set to false if textures are still being used elsewhere to avoid freeing shared resources.
+     */
+    UnloadModel :: proc(model: Model, unloadMaterials: bool) ---
+}
+

+ 31 - 0
third-party/r3d-odin/r3d/r3d_platform.odin

@@ -0,0 +1,31 @@
+/* r3d_platform.odin -- Platform definitions.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+

+ 214 - 0
third-party/r3d-odin/r3d/r3d_probe.odin

@@ -0,0 +1,214 @@
+/* r3d_probe.odin -- R3D Probe Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+/**
+ * @brief Modes for updating probes.
+ *
+ * Controls how often probe captures are refreshed.
+ */
+ProbeUpdateMode :: enum u32 {
+    ONCE   = 0, ///< Updated only when its state or content changes
+    ALWAYS = 1, ///< Updated during every frames
+}
+
+/**
+ * @brief Unique identifier for an R3D probe.
+ *
+ * Negative values indicate an invalid probe.
+ */
+Probe :: i32
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Creates a new probe of the specified type.
+     *
+     * The returned probe must be destroyed using ::R3D_DestroyProbe
+     * when it is no longer needed.
+     *
+     * @param flags IBL components that the probe must support.
+     * @return A valid probe ID, or a negative value on failure.
+     */
+    CreateProbe :: proc(flags: ProbeFlags) -> Probe ---
+
+    /**
+     * @brief Destroys a probe and frees its resources.
+     *
+     * @param id Probe ID to destroy.
+     */
+    DestroyProbe :: proc(id: Probe) ---
+
+    /**
+     * @brief Returns whether a probe exists.
+     *
+     * @param id Probe ID.
+     * @return true if the probe is valid and allocated, otherwise false.
+     */
+    IsProbeExist :: proc(id: Probe) -> bool ---
+
+    /**
+     * @brief Returns the probe flags.
+     *
+     * @param id Probe ID.
+     * @return The flags assigned to the probe.
+     */
+    GetProbeFlags :: proc(id: Probe) -> ProbeFlags ---
+
+    /**
+     * @brief Returns whether a probe is currently active.
+     *
+     * Inactive probes do not contribute to lighting.
+     *
+     * @param id Probe ID.
+     * @return true if active, otherwise false.
+     */
+    IsProbeActive :: proc(id: Probe) -> bool ---
+
+    /**
+     * @brief Enables or disables a probe.
+     *
+     * @param id Probe ID.
+     * @param active Set to true to enable the probe.
+     */
+    SetProbeActive :: proc(id: Probe, active: bool) ---
+
+    /**
+     * @brief Gets the probe update mode.
+     *
+     * - R3D_PROBE_UPDATE_ONCE:
+     *     Captured once, then reused unless its state changes.
+     *
+     * - R3D_PROBE_UPDATE_ALWAYS:
+     *     Recaptured every frame.
+     *
+     * Use "ONCE" for static scenes, "ALWAYS" for highly dynamic scenes.
+     */
+    GetProbeUpdateMode :: proc(id: Probe) -> ProbeUpdateMode ---
+
+    /**
+     * @brief Sets the probe update mode.
+     *
+     * Controls when the probe capture is refreshed.
+     *
+     * @param id Probe ID.
+     * @param mode Update mode to apply.
+     */
+    SetProbeUpdateMode :: proc(id: Probe, mode: ProbeUpdateMode) ---
+
+    /**
+     * @brief Returns whether the probe is considered indoors.
+     *
+     * Indoor probes do not sample skybox or environment maps.
+     * Instead they rely only on ambient and background colors.
+     *
+     * Use this for rooms, caves, tunnels, etc...
+     * where outside lighting should not bleed inside.
+     */
+    GetProbeInterior :: proc(id: Probe) -> bool ---
+
+    /**
+     * @brief Enables or disables indoor mode for the probe.
+     */
+    SetProbeInterior :: proc(id: Probe, active: bool) ---
+
+    /**
+     * @brief Returns whether shadows are captured by this probe.
+     *
+     * When enabled, shadowing is baked into the captured lighting.
+     * This improves realism, but increases capture cost.
+     */
+    GetProbeShadows :: proc(id: Probe) -> bool ---
+
+    /**
+     * @brief Enables or disables shadow rendering during probe capture.
+     */
+    SetProbeShadows :: proc(id: Probe, active: bool) ---
+
+    /**
+     * @brief Gets the world position of the probe.
+     */
+    GetProbePosition :: proc(id: Probe) -> rl.Vector3 ---
+
+    /**
+     * @brief Sets the world position of the probe.
+     */
+    SetProbePosition :: proc(id: Probe, position: rl.Vector3) ---
+
+    /**
+     * @brief Gets the effective range of the probe.
+     *
+     * The range defines the radius (in world units) within which this probe
+     * contributes to lighting. Objects outside this sphere receive no influence.
+     */
+    GetProbeRange :: proc(id: Probe) -> f32 ---
+
+    /**
+     * @brief Sets the effective range of the probe.
+     *
+     * @param range Radius in world units. Must be > 0.
+     */
+    SetProbeRange :: proc(id: Probe, range: f32) ---
+
+    /**
+     * @brief Gets the falloff factor applied to probe contributions.
+     *
+     * Falloff controls how lighting fades as distance increases.
+     *
+     * Internally this uses a power curve:
+     *     attenuation = 1.0 - pow(dist / probe.range, probe.falloff)
+     *
+     * Effects:
+     *   - falloff = 1 -> linear fade
+     *   - falloff > 1 -> light stays strong near the probe, drops faster at the edge
+     *   - falloff < 1 -> softer fade across the whole range
+     */
+    GetProbeFalloff :: proc(id: Probe) -> f32 ---
+
+    /**
+     * @brief Sets the falloff factor used for distance attenuation.
+     *
+     * Larger values make the probe feel more localized.
+     */
+    SetProbeFalloff :: proc(id: Probe, falloff: f32) ---
+}
+
+/**
+ * @brief Bit-flags controlling what components are generated.
+ *
+ * - R3D_PROBE_ILLUMINATION -> generate diffuse irradiance
+ * - R3D_PROBE_REFLECTION   -> generate specular prefiltered map
+ */
+ProbeFlag :: enum u32 {
+    ILLUMINATION = 0,
+    REFLECTION   = 1,
+}
+
+ProbeFlags :: bit_set[ProbeFlag; u32]

+ 126 - 0
third-party/r3d-odin/r3d/r3d_screen_shader.odin

@@ -0,0 +1,126 @@
+/* r3d_screen_shader.odin -- R3D Screen Shader Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+ScreenShader :: struct {}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Loads a screen shader from a file.
+     *
+     * The shader must define a single entry point:
+     * `void fragment()`. Any other entry point, such as `vertex()`,
+     * or any varyings will be ignored.
+     *
+     * @param filePath Path to the shader source file.
+     * @return Pointer to the loaded screen shader, or NULL on failure.
+     */
+    LoadScreenShader :: proc(filePath: cstring) -> ^ScreenShader ---
+
+    /**
+     * @brief Loads a screen shader from a source code string in memory.
+     *
+     * The shader must define a single entry point:
+     * `void fragment()`. Any other entry point, such as `vertex()`,
+     * or any varyings will be ignored.
+     *
+     * @param code Null-terminated shader source code.
+     * @return Pointer to the loaded screen shader, or NULL on failure.
+     */
+    LoadScreenShaderFromMemory :: proc(code: cstring) -> ^ScreenShader ---
+
+    /**
+     * @brief Unloads and destroys a screen shader.
+     *
+     * @param shader Screen shader to unload.
+     */
+    UnloadScreenShader :: proc(shader: ^ScreenShader) ---
+
+    /**
+     * @brief Sets a uniform value for the current frame.
+     *
+     * Once a uniform is set, it remains valid for the all frames.
+     * If an uniform is set multiple times during the same frame,
+     * the last value defined before R3D_End() is used.
+     *
+     * Supported types:
+     * bool, int, float,
+     * ivec2, ivec3, ivec4,
+     * vec2, vec3, vec4,
+     * mat2, mat3, mat4
+     *
+     * @warning Boolean values are read as 4 bytes.
+     *
+     * @param shader Target screen shader.
+     *               May be NULL. In that case, the call is ignored
+     *               and a warning is logged.
+     * @param name   Name of the uniform. Must not be NULL.
+     * @param value  Pointer to the uniform value. Must not be NULL.
+     */
+    SetScreenShaderUniform :: proc(shader: ^ScreenShader, name: cstring, value: rawptr) ---
+
+    /**
+     * @brief Sets a texture sampler for the current frame.
+     *
+     * Once a sampler is set, it remains valid for all frames.
+     * If a sampler is set multiple times during the same frame,
+     * the last value defined before R3D_End() is used.
+     *
+     * Supported samplers:
+     * sampler1D, sampler2D, sampler3D, samplerCube
+     *
+     * @param shader  Target screen shader.
+     *                May be NULL. In that case, the call is ignored
+     *                and a warning is logged.
+     * @param name    Name of the sampler uniform. Must not be NULL.
+     * @param texture rl.Texture to bind to the sampler.
+     */
+    SetScreenShaderSampler :: proc(shader: ^ScreenShader, name: cstring, texture: rl.Texture) ---
+
+    /**
+     * @brief Sets the list of screen shaders to execute at the end of the frame.
+     *
+     * The maximum number of shaders is defined by `R3D_MAX_SCREEN_SHADERS`.
+     * If the provided count exceeds this limit, a warning is emitted and only
+     * the first `R3D_MAX_SCREEN_SHADERS` shaders are used.
+     *
+     * Shader pointers are copied internally, so the original array can be modified or freed after the call.
+     * NULL entries are allowed safely within the list.
+     *
+     * Calling this function resets all internal screen shaders before copying the new list.
+     * To disable all screen shaders, call this function with `shaders = NULL` and/or `count = 0`.
+     *
+     * @param shaders Array of pointers to R3D_ScreenShader objects.
+     * @param count Number of shaders in the array.
+     */
+    SetScreenShaderChain :: proc(shaders: ^^ScreenShader, count: i32) ---
+}
+

+ 130 - 0
third-party/r3d-odin/r3d/r3d_skeleton.odin

@@ -0,0 +1,130 @@
+/* r3d_skeleton.odin -- R3D Skeleton Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+/**
+ * @brief Stores bone information for skeletal animation.
+ *
+ * Contains the bone name and the index of its parent bone.
+ */
+BoneInfo :: struct {
+    name:   [32]i8, ///< Bone name (max 31 characters + null terminator).
+    parent: i32,    ///< Index of the parent bone (-1 if root).
+}
+
+/**
+ * @brief Represents a skeletal hierarchy used for skinning.
+ *
+ * Defines the bone structure, reference poses, and inverse bind matrices
+ * required for skeletal animation. The skeleton provides both local and
+ * global bind poses used during skinning and animation playback.
+ */
+Skeleton :: struct {
+    bones:       [^]BoneInfo, ///< Array of bone descriptors defining the hierarchy and names.
+    boneCount:   i32,         ///< Total number of bones in the skeleton.
+    localBind:   [^]rl.Matrix,   ///< Bind pose matrices relative to parent
+    modelBind:   [^]rl.Matrix,   ///< Bind pose matrices in model/global space
+    invBind:     [^]rl.Matrix,   ///< Inverse bind matrices (model space) for skinning
+    rootBind:    [16]f32,     ///< Root correction if local bind is not identity
+    skinTexture: u32,         ///< rl.Texture ID that contains the bind pose for GPU skinning. This is a 1D rl.Texture RGBA16F 4*boneCount.
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Loads a skeleton hierarchy from a 3D model file.
+     *
+     * Skeletons are automatically loaded when importing a model,
+     * but can be loaded manually for advanced use cases.
+     *
+     * @param filePath Path to the model file containing the skeleton data.
+     * @return Return the loaded R3D_Skeleton.
+     */
+    LoadSkeleton :: proc(filePath: cstring) -> Skeleton ---
+
+    /**
+     * @brief Loads a skeleton hierarchy from memory data.
+     *
+     * Allows manual loading of skeletons directly from a memory buffer.
+     * Typically used for advanced or custom asset loading workflows.
+     *
+     * @param data Pointer to the memory buffer containing skeleton data.
+     * @param size Size of the memory buffer in bytes.
+     * @param hint Optional format hint (can be NULL).
+     * @return Return the loaded R3D_Skeleton.
+     */
+    LoadSkeletonFromMemory :: proc(data: rawptr, size: u32, hint: cstring) -> Skeleton ---
+
+    /**
+     * @brief Loads a skeleton hierarchy from an existing importer.
+     *
+     * Extracts the skeleton data from a previously loaded importer instance.
+     *
+     * @param importer Importer instance to extract the skeleton from.
+     * @return Return the loaded R3D_Skeleton.
+     */
+    LoadSkeletonFromImporter :: proc(importer: ^Importer) -> Skeleton ---
+
+    /**
+     * @brief Frees the memory allocated for a skeleton.
+     *
+     * @param skeleton R3D_Skeleton to destroy.
+     */
+    UnloadSkeleton :: proc(skeleton: Skeleton) ---
+
+    /**
+     * @brief Check if a skeleton is valid.
+     *
+     * Returns true if atleast the texBindPose is greater than zero.
+     *
+     * @param skeleton The skeleton to check.
+     * @return true if valid, false otherwise.
+     */
+    IsSkeletonValid :: proc(skeleton: Skeleton) -> bool ---
+
+    /**
+     * @brief Returns the index of the bone with the given name.
+     *
+     * @param skeleton Skeleton to search in.
+     * @param boneName Name of the bone to find.
+     * @return Index of the bone, or a negative value if not found.
+     */
+    GetSkeletonBoneIndex :: proc(skeleton: Skeleton, boneName: cstring) -> i32 ---
+
+    /**
+     * @brief Returns a pointer to the bone with the given name.
+     *
+     * @param skeleton Skeleton to search in.
+     * @param boneName Name of the bone to find.
+     * @return Pointer to the bone, or NULL if not found.
+     */
+    GetSkeletonBone :: proc(skeleton: Skeleton, boneName: cstring) -> ^BoneInfo ---
+}
+

+ 106 - 0
third-party/r3d-odin/r3d/r3d_surface_shader.odin

@@ -0,0 +1,106 @@
+/* r3d_surface_shader.odin -- R3D Surface Shader Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+SurfaceShader :: struct {}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Loads a surface shader from a file.
+     *
+     * The shader must define at least one entry point: `void vertex()` or `void fragment()`.
+     * It can define either one or both.
+     *
+     * @param filePath Path to the shader source file.
+     * @return Pointer to the loaded surface shader, or NULL on failure.
+     */
+    LoadSurfaceShader :: proc(filePath: cstring) -> ^SurfaceShader ---
+
+    /**
+     * @brief Loads a surface shader from a source code string in memory.
+     *
+     * The shader must define at least one entry point: `void vertex()` or `void fragment()`.
+     * It can define either one or both.
+     *
+     * @param code Null-terminated shader source code.
+     * @return Pointer to the loaded surface shader, or NULL on failure.
+     */
+    LoadSurfaceShaderFromMemory :: proc(code: cstring) -> ^SurfaceShader ---
+
+    /**
+     * @brief Unloads and destroys a surface shader.
+     *
+     * @param shader Surface shader to unload.
+     */
+    UnloadSurfaceShader :: proc(shader: ^SurfaceShader) ---
+
+    /**
+     * @brief Sets a uniform value for the current frame.
+     *
+     * Once a uniform is set, it remains valid for the all frames.
+     * If an uniform is set multiple times during the same frame,
+     * the last value defined before R3D_End() is used.
+     *
+     * Supported types:
+     * bool, int, float,
+     * ivec2, ivec3, ivec4,
+     * vec2, vec3, vec4,
+     * mat2, mat3, mat4
+     *
+     * @warning Boolean values are read as 4 bytes.
+     *
+     * @param shader Target surface shader.
+     *               May be NULL. In that case, the call is ignored
+     *               and a warning is logged.
+     * @param name   Name of the uniform. Must not be NULL.
+     * @param value  Pointer to the uniform value. Must not be NULL.
+     */
+    SetSurfaceShaderUniform :: proc(shader: ^SurfaceShader, name: cstring, value: rawptr) ---
+
+    /**
+     * @brief Sets a texture sampler for the current frame.
+     *
+     * Once a sampler is set, it remains valid for all frames.
+     * If a sampler is set multiple times during the same frame,
+     * the last value defined before R3D_End() is used.
+     *
+     * Supported samplers:
+     * sampler1D, sampler2D, sampler3D, samplerCube
+     *
+     * @param shader  Target surface shader.
+     *                May be NULL. In that case, the call is ignored
+     *                and a warning is logged.
+     * @param name    Name of the sampler uniform. Must not be NULL.
+     * @param texture rl.Texture to bind to the sampler.
+     */
+    SetSurfaceShaderSampler :: proc(shader: ^SurfaceShader, name: cstring, texture: rl.Texture) ---
+}
+

+ 126 - 0
third-party/r3d-odin/r3d/r3d_utils.odin

@@ -0,0 +1,126 @@
+/* r3d_utils.odin -- R3D Utility Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Retrieves a default white texture.
+     *
+     * This texture is fully white (1,1,1,1), useful for default material properties.
+     *
+     * @return A white texture.
+     */
+    GetWhiteTexture :: proc() -> rl.Texture2D ---
+
+    /**
+     * @brief Retrieves a default black texture.
+     *
+     * This texture is fully black (0,0,0,1), useful for masking or default values.
+     *
+     * @return A black texture.
+     */
+    GetBlackTexture :: proc() -> rl.Texture2D ---
+
+    /**
+     * @brief Retrieves a default normal map texture.
+     *
+     * This texture represents a neutral normal map (0.5, 0.5, 1.0), which applies no normal variation.
+     *
+     * @return A neutral normal texture.
+     */
+    GetNormalTexture :: proc() -> rl.Texture2D ---
+
+    /**
+     * @brief Retrieves the buffer containing the scene's normal data.
+     *
+     * This texture stores octahedral-compressed normals using two 16-bit per-channel RG components.
+     *
+     * @note You can find the decoding functions in the embedded shaders, such as 'screen/lighting.fs.glsl'.
+     *
+     * @return The normal buffer texture.
+     */
+    GetBufferNormal :: proc() -> rl.Texture2D ---
+
+    /**
+     * @brief Retrieves the final depth buffer.
+     *
+     * This texture is an R16 texture containing a linear depth value
+     * normalized between the near and far clipping planes.
+     * It does not include a stencil buffer.
+     *
+     * The texture is intended for post-processing effects outside of R3D
+     * that require access to linear depth information.
+     *
+     * @return The final depth buffer texture (R16, linear depth).
+     */
+    GetBufferDepth :: proc() -> rl.Texture2D ---
+
+    /**
+     * @brief Retrieves the view matrix.
+     *
+     * This matrix represents the camera's transformation from world space to view space.
+     * It is updated at the last call to 'R3D_Begin'.
+     *
+     * @return The current view matrix.
+     */
+    GetMatrixView :: proc() -> rl.Matrix ---
+
+    /**
+     * @brief Retrieves the inverse view matrix.
+     *
+     * This matrix transforms coordinates from view space back to world space.
+     * It is updated at the last call to 'R3D_Begin'.
+     *
+     * @return The current inverse view matrix.
+     */
+    GetMatrixInvView :: proc() -> rl.Matrix ---
+
+    /**
+     * @brief Retrieves the projection matrix.
+     *
+     * This matrix defines the transformation from view space to clip space.
+     * It is updated at the last call to 'R3D_Begin'.
+     *
+     * @return The current projection matrix.
+     */
+    GetMatrixProjection :: proc() -> rl.Matrix ---
+
+    /**
+     * @brief Retrieves the inverse projection matrix.
+     *
+     * This matrix transforms coordinates from clip space back to view space.
+     * It is updated at the last call to 'R3D_Begin'.
+     *
+     * @return The current inverse projection matrix.
+     */
+    GetMatrixInvProjection :: proc() -> rl.Matrix ---
+}
+

+ 80 - 0
third-party/r3d-odin/r3d/r3d_visibility.odin

@@ -0,0 +1,80 @@
+/* r3d_visibility.odin -- R3D Visibility Module.
+ *
+ * Copyright (c) 2025-2026 Le Juez Victor
+ *
+ * This software is provided 'as-is', without any express or implied warranty.
+ * For conditions of distribution and use, see accompanying LICENSE file.
+ */
+package r3d
+
+import rl "vendor:raylib"
+
+when ODIN_OS == .Windows {
+    foreign import lib {
+        "windows/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Linux {
+    foreign import lib {
+        "linux/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+} else when ODIN_OS == .Darwin {
+    foreign import lib {
+        "darwin/libr3d.a",
+        "system:raylib",
+        "system:assimp",
+    }
+}
+
+@(default_calling_convention="c", link_prefix="R3D_")
+foreign lib {
+    /**
+     * @brief Checks if a point is inside the view frustum.
+     *
+     * Tests whether a 3D point lies within the camera's frustum by checking against all six planes.
+     * You can call this only after `R3D_Begin`, which updates the internal frustum state.
+     *
+     * @param position The 3D point to test.
+     * @return `true` if inside the frustum, `false` otherwise.
+     */
+    IsPointVisible :: proc(position: rl.Vector3) -> bool ---
+
+    /**
+     * @brief Checks if a sphere is inside the view frustum.
+     *
+     * Tests whether a sphere intersects the camera's frustum using plane-sphere tests.
+     * You can call this only after `R3D_Begin`, which updates the internal frustum state.
+     *
+     * @param position The center of the sphere.
+     * @param radius The sphere's radius (must be positive).
+     * @return `true` if at least partially inside the frustum, `false` otherwise.
+     */
+    IsSphereVisible :: proc(position: rl.Vector3, radius: f32) -> bool ---
+
+    /**
+     * @brief Checks if an AABB is inside the view frustum.
+     *
+     * Determines whether an axis-aligned bounding box intersects the frustum.
+     * You can call this only after `R3D_Begin`, which updates the internal frustum state.
+     *
+     * @param aabb The bounding box to test.
+     * @return `true` if at least partially inside the frustum, `false` otherwise.
+     */
+    IsBoundingBoxVisible :: proc(aabb: rl.BoundingBox) -> bool ---
+
+    /**
+     * @brief Checks if an OBB is inside the view frustum.
+     *
+     * Tests an oriented bounding box (transformed AABB) for frustum intersection.
+     * You can call this only after `R3D_Begin`, which updates the internal frustum state.
+     *
+     * @param aabb Local-space bounding box.
+     * @param transform World-space transform matrix.
+     * @return `true` if the transformed box intersects the frustum, `false` otherwise.
+     */
+    IsOrientedBoxVisible :: proc(aabb: rl.BoundingBox, transform: rl.Matrix) -> bool ---
+}
+

BIN
third-party/r3d-odin/r3d/windows/libr3d.a