Watch
2
0
Fork
You've already forked raylib-cs
0

chore: clean recommit

This commit is contained in:
tiger tiger tiger 2026-07-07 23:47:38 +02:00
commit 60ad2e7fb1
122 changed files with 23950 additions and 323 deletions

View file

@ -0,0 +1,390 @@
/*******************************************************************************************
*
* raylib [models] example - animation blend custom
*
* Example complexity rating: [] 4/4
*
* Example originally created with raylib 5.5, last time updated with raylib 6.0
*
* Example contributed by dmitrii-brand (@dmitrii-brand) and reviewed by Ramon Santamaria (@raysan5)
*
* DETAILS: Example demonstrates per-bone animation blending, allowing smooth transitions
* between two animations by interpolating bone transforms. This is useful for:
* - Blending movement animations (walk/run) with action animations (jump/attack)
* - Creating smooth animation transitions
* - Layering animations (e.g., upper body attack while lower body walks)
*
* WARNING: GPU skinning must be enabled in raylib with a compilation flag,
* if not enabled, CPU skinning will be used instead
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2026 dmitrii-brand (@dmitrii-brand)
*
********************************************************************************************/
using System;
using System.Numerics;
using static Raylib_cs.Raylib;
using static Raylib_cs.Raymath;
namespace Examples.Models;
public partial class AnimationBlendCustom : IExample
{
private const int screenWidth = 800;
private const int screenHeight = 450;
#if BROWSER
private const int GlslVersion = 100; // WebGL1 needs GLSL ES 100
#else
private const int GlslVersion = 330;
#endif
public string Name => "Models / Animation Blend Custom";
public string Title => "raylib [models] example - animation blend custom";
private Camera3D camera;
private Model model;
private Vector3 position;
private Shader skinningShader;
private unsafe ModelAnimation* anims;
private int animCount;
private int animIndex0;
private int animIndex1;
private int animCurrentFrame0;
private int animCurrentFrame1;
private bool upperBodyBlend;
public unsafe void Init()
{
// Define the camera to look into our 3d world
camera = new();
camera.Position = new Vector3(4.0f, 4.0f, 4.0f); // Camera position
camera.Target = new Vector3(0.0f, 1.0f, 0.0f); // Camera looking at point
camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target)
camera.FovY = 45.0f; // Camera field-of-view Y
camera.Projection = CameraProjection.Perspective; // Camera projection type
// Load gltf model
model = LoadModel("resources/models/gltf/greenman.glb");
position = new Vector3(0.0f, 0.0f, 0.0f); // Set model position
// Load skinning shader
// WARNING: GPU skinning must be enabled in raylib with a compilation flag,
// if not enabled, CPU skinning will be used instead
skinningShader = LoadShader(
$"resources/shaders/glsl{GlslVersion}/skinning.vs",
$"resources/shaders/glsl{GlslVersion}/skinning.fs"
);
model.Materials[1].Shader = skinningShader;
// Load gltf model animations
animCount = 0;
anims = LoadModelAnimations("resources/models/gltf/greenman.glb", ref animCount);
// Use specific animation indices: 2-walk/move, 3-attack
animIndex0 = 2; // Walk/Move animation (index 2)
animIndex1 = 3; // Attack animation (index 3)
animCurrentFrame0 = 0;
animCurrentFrame1 = 0;
// Validate indices
if (animIndex0 >= animCount)
{
animIndex0 = 0;
}
if (animIndex1 >= animCount)
{
animIndex1 = (animCount > 1) ? 1 : 0;
}
upperBodyBlend = true; // Toggle: true = upper/lower body blending, false = uniform blending (50/50)
}
public unsafe void Update()
{
// Update
//----------------------------------------------------------------------------------
UpdateCamera(ref camera, CameraMode.Orbital);
// Toggle upper/lower body blending mode (SPACE key)
if (IsKeyPressed(KeyboardKey.Space))
{
upperBodyBlend = !upperBodyBlend;
}
// Update animation frames
var anim0 = anims[animIndex0];
var anim1 = anims[animIndex1];
animCurrentFrame0 = (animCurrentFrame0 + 1) % anim0.KeyFrameCount;
animCurrentFrame1 = (animCurrentFrame1 + 1) % anim1.KeyFrameCount;
// Blend the two animations
// When upperBodyBlend is ON: upper body = attack (1.0), lower body = walk (0.0)
// When upperBodyBlend is OFF: uniform blend at 0.5 (50% walk, 50% attack)
var blendFactor = upperBodyBlend ? 1.0f : 0.5f;
UpdateModelAnimationBones(anim0, animCurrentFrame0, anim1, animCurrentFrame1, blendFactor, upperBodyBlend);
// raylib provided animation blending function
//UpdateModelAnimationEx(model, anim0, (float)animCurrentFrame0,
// anim1, (float)animCurrentFrame1, blendFactor);
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(Color.RayWhite);
BeginMode3D(camera);
DrawModel(model, position, 1.0f, Color.White);
DrawGrid(10, 1.0f);
EndMode3D();
// Draw UI
DrawText($"ANIM 0: {anim0.NameToString()}", 10, 10, 20, Color.Gray);
DrawText($"ANIM 1: {anim1.NameToString()}", 10, 40, 20, Color.Gray);
DrawText($"[SPACE] Toggle blending mode: {(upperBodyBlend ? "Upper/Lower Body Blending" : "Uniform Blending")}",
10, GetScreenHeight() - 30, 20, Color.DarkGray);
EndDrawing();
//----------------------------------------------------------------------------------
}
public unsafe void Unload()
{
UnloadModelAnimations(anims, animCount); // Unload model animation
UnloadModel(model); // Unload model and meshes/material
UnloadShader(skinningShader); // Unload GPU skinning shader
}
// Check if a bone is part of upper body (for selective blending)
private static bool IsUpperBodyBone(string boneName)
{
// Common upper body bone names (adjust based on your model)
if (boneName is "spine" or "spine1" or "spine2" or
"chest" or "upperChest" or
"neck" or "head" or
"shoulder" or "shoulder_L" or "shoulder_R" or
"upperArm" or "upperArm_L" or "upperArm_R" or
"lowerArm" or "lowerArm_L" or "lowerArm_R" or
"hand" or "hand_L" or "hand_R" or
"clavicle" or "clavicle_L" or "clavicle_R")
{
return true;
}
// Check if bone name contains upper body keywords
if (boneName.Contains("spine") || boneName.Contains("chest") ||
boneName.Contains("neck") || boneName.Contains("head") ||
boneName.Contains("shoulder") || boneName.Contains("arm") ||
boneName.Contains("hand") || boneName.Contains("clavicle"))
{
return true;
}
return false;
}
// Blend two animations per-bone with selective upper/lower body blending
private unsafe void UpdateModelAnimationBones(ModelAnimation anim0, int frame0,
ModelAnimation anim1, int frame1, float blend, bool upperBodyBlend)
{
// Validate inputs
if ((anim0.BoneCount != 0) && (anim0.KeyframePoses != null) &&
(anim1.BoneCount != 0) && (anim1.KeyframePoses != null) &&
(model.Skeleton.BoneCount != 0) && (model.Skeleton.BindPose != null))
{
// Clamp blend factor to [0, 1]
blend = MathF.Min(1.0f, MathF.Max(0.0f, blend));
// Ensure frame indices are valid
if (frame0 >= anim0.KeyFrameCount)
{
frame0 = anim0.KeyFrameCount - 1;
}
if (frame1 >= anim1.KeyFrameCount)
{
frame1 = anim1.KeyFrameCount - 1;
}
if (frame0 < 0)
{
frame0 = 0;
}
if (frame1 < 0)
{
frame1 = 0;
}
// Get bone count (use minimum of all to be safe)
var boneCount = model.Skeleton.BoneCount;
if (anim0.BoneCount < boneCount)
{
boneCount = anim0.BoneCount;
}
if (anim1.BoneCount < boneCount)
{
boneCount = anim1.BoneCount;
}
// Blend each bone
for (var boneIndex = 0; boneIndex < boneCount; boneIndex++)
{
// Determine blend factor for this bone
var boneBlendFactor = blend;
// If upper body blending is enabled, use different blend factors for upper vs lower body
if (upperBodyBlend)
{
var boneName = model.Skeleton.Bones[boneIndex].NameToString();
var isUpperBody = IsUpperBodyBone(boneName);
// Upper body: use anim1 (attack), Lower body: use anim0 (walk)
// blend = 0.0 means full anim0 (walk), 1.0 means full anim1 (attack)
if (isUpperBody)
{
boneBlendFactor = blend; // Upper body: blend towards anim1 (attack)
}
else
{
boneBlendFactor = 1.0f - blend; // Lower body: blend towards anim0 (walk) - invert the blend
}
}
// Get transforms from both animations
var bindTransform = model.Skeleton.BindPose[boneIndex];
var animTransform0 = anim0.KeyframePoses[frame0][boneIndex];
var animTransform1 = anim1.KeyframePoses[frame1][boneIndex];
// Blend the transforms
Transform blended = new();
blended.Translation = Vector3Lerp(animTransform0.Translation, animTransform1.Translation, boneBlendFactor);
blended.Rotation = QuaternionSlerp(animTransform0.Rotation, animTransform1.Rotation, boneBlendFactor);
blended.Scale = Vector3Lerp(animTransform0.Scale, animTransform1.Scale, boneBlendFactor);
// Convert bind pose to matrix
var bindMatrix = MatrixMultiply(MatrixMultiply(
MatrixScale(bindTransform.Scale.X, bindTransform.Scale.Y, bindTransform.Scale.Z),
QuaternionToMatrix(bindTransform.Rotation)),
MatrixTranslate(bindTransform.Translation.X, bindTransform.Translation.Y, bindTransform.Translation.Z));
// Convert blended transform to matrix
var blendedMatrix = MatrixMultiply(MatrixMultiply(
MatrixScale(blended.Scale.X, blended.Scale.Y, blended.Scale.Z),
QuaternionToMatrix(blended.Rotation)),
MatrixTranslate(blended.Translation.X, blended.Translation.Y, blended.Translation.Z));
// Calculate final bone matrix (similar to UpdateModelAnimationBones)
model.BoneMatrices[boneIndex] = MatrixMultiply(MatrixInvert(bindMatrix), blendedMatrix);
}
// CPU skinning, updates CPU buffers and uploads them to GPU (if available)
// NOTE: Fallback in case GPU skinning is not supported or enabled
for (var m = 0; m < model.MeshCount; m++)
{
var mesh = model.Meshes[m];
Vector3 animVertex;
Vector3 animNormal;
var vertexValuesCount = mesh.VertexCount * 3;
var boneCounter = 0;
var bufferUpdateRequired = false; // Flag to check when anim vertex information is updated
// Skip if missing bone data or missing anim buffers initialization
if ((mesh.BoneWeights == null) || (mesh.BoneIndices == null) ||
(mesh.AnimVertices == null) || (mesh.AnimNormals == null))
{
continue;
}
for (var vCounter = 0; vCounter < vertexValuesCount; vCounter += 3)
{
mesh.AnimVertices[vCounter] = 0;
mesh.AnimVertices[vCounter + 1] = 0;
mesh.AnimVertices[vCounter + 2] = 0;
if (mesh.AnimNormals != null)
{
mesh.AnimNormals[vCounter] = 0;
mesh.AnimNormals[vCounter + 1] = 0;
mesh.AnimNormals[vCounter + 2] = 0;
}
// Iterates over 4 bones per vertex
for (var j = 0; j < 4; j++, boneCounter++)
{
var boneWeight = mesh.BoneWeights[boneCounter];
var boneIndex = mesh.BoneIndices[boneCounter];
// Early stop when no transformation will be applied
if (boneWeight == 0.0f)
{
continue;
}
animVertex = new Vector3(mesh.Vertices[vCounter], mesh.Vertices[vCounter + 1], mesh.Vertices[vCounter + 2]);
animVertex = Vector3Transform(animVertex, model.BoneMatrices[boneIndex]);
mesh.AnimVertices[vCounter] += animVertex.X * boneWeight;
mesh.AnimVertices[vCounter + 1] += animVertex.Y * boneWeight;
mesh.AnimVertices[vCounter + 2] += animVertex.Z * boneWeight;
bufferUpdateRequired = true;
// Normals processing
// NOTE: We use meshes.baseNormals (default normal) to calculate meshes.normals (animated normals)
if ((mesh.Normals != null) && (mesh.AnimNormals != null))
{
animNormal = new Vector3(mesh.Normals[vCounter], mesh.Normals[vCounter + 1], mesh.Normals[vCounter + 2]);
animNormal = Vector3Transform(animNormal, MatrixTranspose(MatrixInvert(model.BoneMatrices[boneIndex])));
mesh.AnimNormals[vCounter] += animNormal.X * boneWeight;
mesh.AnimNormals[vCounter + 1] += animNormal.Y * boneWeight;
mesh.AnimNormals[vCounter + 2] += animNormal.Z * boneWeight;
}
}
}
if (bufferUpdateRequired)
{
// Update GPU vertex buffers with updated data (position + normals)
Rlgl.UpdateVertexBuffer(mesh.VboId[(int)ShaderLocationIndex.VertexPosition], mesh.AnimVertices, mesh.VertexCount * 3 * sizeof(float), 0);
if (mesh.Normals != null)
{
Rlgl.UpdateVertexBuffer(mesh.VboId[(int)ShaderLocationIndex.VertexNormal], mesh.AnimNormals, mesh.VertexCount * 3 * sizeof(float), 0);
}
}
}
}
}
public static int Main()
{
// Initialization
//--------------------------------------------------------------------------------------
InitWindow(screenWidth, screenHeight, "raylib [models] example - animation blend custom");
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
//--------------------------------------------------------------------------------------
var game = new AnimationBlendCustom();
game.Init();
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
game.Update();
}
game.Unload();
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}
}

View file

@ -0,0 +1,474 @@
/*******************************************************************************************
*
* raylib [models] example - animation blending
*
* Example complexity rating: [] 4/4
*
* Example originally created with raylib 5.5, last time updated with raylib 6.0
*
* Example contributed by Kirandeep (@Kirandeep-Singh-Khehra) and reviewed by Ramon Santamaria (@raysan5)
*
* WARNING: GPU skinning must be enabled in raylib with a compilation flag,
* if not enabled, CPU skinning will be used instead
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2024-2026 Kirandeep (@Kirandeep-Singh-Khehra) and Ramon Santamaria (@raysan5)
*
********************************************************************************************/
// NOTE: The upstream example uses raygui for its UI controls (dropdowns, sliders, progress bars).
// raygui is not part of raylib-cs, so the required controls are reimplemented here with
// basic raylib drawing primitives, preserving the original behaviour.
using System.Numerics;
using static Raylib_cs.Raylib;
namespace Examples.Models;
public partial class AnimationBlending : IExample
{
private const int screenWidth = 800;
private const int screenHeight = 450;
#if BROWSER
private const int GlslVersion = 100; // WebGL1 needs GLSL ES 100
#else
private const int GlslVersion = 330;
#endif
public string Name => "Models / Animation Blending";
public string Title => "raylib [models] example - animation blending";
private Camera3D camera;
private Model model;
private Vector3 position;
private Shader skinningShader;
private unsafe ModelAnimation* anims;
private int animCount;
private int currentAnimPlaying;
private int nextAnimToPlay;
private bool animTransition;
private int animIndex0;
private float animCurrentFrame0;
private float animFrameSpeed0;
private int animIndex1;
private float animCurrentFrame1;
private float animFrameSpeed1;
private float animBlendFactor;
private float animBlendTime;
private float animBlendTimeCounter;
private bool animPause;
private string[] animNames;
private bool dropdownEditMode0;
private bool dropdownEditMode1;
private float animFrameProgress0;
private float animFrameProgress1;
private float animBlendProgress;
public unsafe void Init()
{
// Define the camera to look into our 3d world
camera = new();
camera.Position = new Vector3(6.0f, 6.0f, 6.0f); // Camera position
camera.Target = new Vector3(0.0f, 2.0f, 0.0f); // Camera looking at point
camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target)
camera.FovY = 45.0f; // Camera field-of-view Y
camera.Projection = CameraProjection.Perspective; // Camera projection type
// Load model
model = LoadModel("resources/models/gltf/robot.glb"); // Load character model
position = new Vector3(0.0f, 0.0f, 0.0f); // Set model world position
// Load skinning shader
// NOTE: It must be a valid shader, following raylib attribs/uniform conventions for GPU skinning
skinningShader = LoadShader(
$"resources/shaders/glsl{GlslVersion}/skinning.vs",
$"resources/shaders/glsl{GlslVersion}/skinning.fs"
);
// Skinning shader could be required to be assigned to all materials shaders, just to make
// sure required uniforms are being updated for the mesh using that material (and shader)
for (var i = 0; i < model.MaterialCount; i++)
{
model.Materials[i].Shader = skinningShader;
}
// Load model animations
animCount = 0;
anims = LoadModelAnimations("resources/models/gltf/robot.glb", ref animCount);
// Animation playing variables
// NOTE: Two animations are played with a smooth transition between them
currentAnimPlaying = 0; // Current animation playing (0 o 1)
nextAnimToPlay = 1; // Next animation to play (to transition)
animTransition = false; // Flag to register anim transition state
animIndex0 = 10; // Current animation playing (walking)
animCurrentFrame0 = 0.0f; // Current animation frame (supporting interpolated frames)
animFrameSpeed0 = 0.5f; // Current animation play speed
animIndex1 = 6; // Next animation to play (running)
animCurrentFrame1 = 0.0f; // Next animation frame (supporting interpolated frames)
animFrameSpeed1 = 0.5f; // Next animation play speed
animBlendFactor = 0.0f; // Blend factor from anim0[frame0] --> anim1[frame1], [0.0f..1.0f]
animBlendTime = 2.0f; // Time to blend from one playing animation to another (in seconds)
animBlendTimeCounter = 0.0f; // Time counter (delta time)
animPause = false; // Pause animation
// UI required variables
animNames = new string[animCount];
for (var i = 0; i < animCount; i++)
{
animNames[i] = anims[i].NameToString();
}
dropdownEditMode0 = false;
dropdownEditMode1 = false;
animFrameProgress0 = 0.0f;
animFrameProgress1 = 0.0f;
animBlendProgress = 0.0f;
}
public unsafe void Update()
{
// Update
//----------------------------------------------------------------------------------
UpdateCamera(ref camera, CameraMode.Orbital);
if (IsKeyPressed(KeyboardKey.P))
{
animPause = !animPause;
}
if (!animPause)
{
// Start transition from anim0[] to anim1[]
if (IsKeyPressed(KeyboardKey.Space) && !animTransition)
{
if (currentAnimPlaying == 0)
{
// Transition anim0 --> anim1
nextAnimToPlay = 1;
animCurrentFrame1 = 0.0f;
}
else
{
// Transition anim1 --> anim0
nextAnimToPlay = 0;
animCurrentFrame0 = 0.0f;
}
// Set animation transition
animTransition = true;
animBlendTimeCounter = 0.0f;
animBlendFactor = 0.0f;
}
if (animTransition)
{
// Playing anim0 and anim1 at the same time
animCurrentFrame0 += animFrameSpeed0;
if (animCurrentFrame0 >= anims[animIndex0].KeyFrameCount)
{
animCurrentFrame0 = 0.0f;
}
animCurrentFrame1 += animFrameSpeed1;
if (animCurrentFrame1 >= anims[animIndex1].KeyFrameCount)
{
animCurrentFrame1 = 0.0f;
}
// Increment blend factor over time to transition from anim0 --> anim1 over time
// NOTE: Time blending could be other than linear, using some easing
animBlendFactor = animBlendTimeCounter / animBlendTime;
animBlendTimeCounter += GetFrameTime();
animBlendProgress = animBlendFactor;
// Update model with animations blending
if (nextAnimToPlay == 1)
{
// Blend anim0 --> anim1
UpdateModelAnimationEx(model, anims[animIndex0], animCurrentFrame0,
anims[animIndex1], animCurrentFrame1, animBlendFactor);
}
else
{
// Blend anim1 --> anim0
UpdateModelAnimationEx(model, anims[animIndex1], animCurrentFrame1,
anims[animIndex0], animCurrentFrame0, animBlendFactor);
}
// Check if transition completed
if (animBlendFactor > 1.0f)
{
// Reset frame states
if (currentAnimPlaying == 0)
{
animCurrentFrame0 = 0.0f;
}
else if (currentAnimPlaying == 1)
{
animCurrentFrame1 = 0.0f;
}
currentAnimPlaying = nextAnimToPlay; // Update current animation playing
animBlendFactor = 0.0f; // Reset blend factor
animTransition = false; // Exit transition mode
animBlendTimeCounter = 0.0f;
}
}
else
{
// Play only one anim, the current one
if (currentAnimPlaying == 0)
{
// Playing anim0 at defined speed
animCurrentFrame0 += animFrameSpeed0;
if (animCurrentFrame0 >= anims[animIndex0].KeyFrameCount)
{
animCurrentFrame0 = 0.0f;
}
UpdateModelAnimation(model, anims[animIndex0], animCurrentFrame0);
}
else if (currentAnimPlaying == 1)
{
// Playing anim1 at defined speed
animCurrentFrame1 += animFrameSpeed1;
if (animCurrentFrame1 >= anims[animIndex1].KeyFrameCount)
{
animCurrentFrame1 = 0.0f;
}
UpdateModelAnimation(model, anims[animIndex1], animCurrentFrame1);
}
}
}
// Update progress bars values with current frame for each animation
animFrameProgress0 = animCurrentFrame0;
animFrameProgress1 = animCurrentFrame1;
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(Color.RayWhite);
BeginMode3D(camera);
DrawModel(model, position, 1.0f, Color.White); // Draw animated model
DrawGrid(10, 1.0f);
EndMode3D();
if (animTransition)
{
DrawText("ANIM TRANSITION BLENDING!", 170, 50, 30, Color.Blue);
}
// Draw UI elements
//---------------------------------------------------------------------------------------------
GuiSlider(new Rectangle(10, 38, 160, 12), null, $"x{animFrameSpeed0:0.0}", ref animFrameSpeed0, 0.1f, 2.0f);
GuiSlider(new Rectangle(GetScreenWidth() - 170.0f, 38, 160, 12), $"{animFrameSpeed1:0.0}x", null, ref animFrameSpeed1, 0.1f, 2.0f);
// Blending process progress bar
GuiProgressBar(new Rectangle(180, 14, 440, 16), null, null, animBlendProgress, 0.0f, 1.0f);
// Centered "PRESS SPACE" label
const string spaceLabel = "PRESS SPACE to START BLENDING";
var spaceLabelWidth = MeasureText(spaceLabel, 20);
DrawText(spaceLabel, (GetScreenWidth() - spaceLabelWidth) / 2, (int)(GetScreenHeight() - 100.0f + 10), 20, Color.DarkGray);
// Draw playing timeline with keyframes for anim0[]
GuiProgressBar(new Rectangle(60, GetScreenHeight() - 60.0f, GetScreenWidth() - 180.0f, 20), "ANIM 0",
$"FRAME: {animFrameProgress0:0.00} / {anims[animIndex0].KeyFrameCount}",
animFrameProgress0, 0.0f, anims[animIndex0].KeyFrameCount);
for (var i = 0; i < anims[animIndex0].KeyFrameCount; i++)
{
DrawRectangle(60 + (int)(((float)(GetScreenWidth() - 180) / anims[animIndex0].KeyFrameCount) * i),
GetScreenHeight() - 60, 1, 20, Color.Blue);
}
// Draw playing timeline with keyframes for anim1[]
GuiProgressBar(new Rectangle(60, GetScreenHeight() - 30.0f, GetScreenWidth() - 180.0f, 20), "ANIM 1",
$"FRAME: {animFrameProgress1:0.00} / {anims[animIndex1].KeyFrameCount}",
animFrameProgress1, 0.0f, anims[animIndex1].KeyFrameCount);
for (var i = 0; i < anims[animIndex1].KeyFrameCount; i++)
{
DrawRectangle(60 + (int)(((float)(GetScreenWidth() - 180) / anims[animIndex1].KeyFrameCount) * i),
GetScreenHeight() - 30, 1, 20, Color.Blue);
}
// Draw animation selectors for blending transition (drawn last so open lists render on top)
// NOTE: Transition does not start until requested
if (GuiDropdownBox(new Rectangle(10, 10, 160, 24), animNames, ref animIndex0, dropdownEditMode0))
{
dropdownEditMode0 = !dropdownEditMode0;
}
if (GuiDropdownBox(new Rectangle(GetScreenWidth() - 170.0f, 10, 160, 24), animNames, ref animIndex1, dropdownEditMode1))
{
dropdownEditMode1 = !dropdownEditMode1;
}
//---------------------------------------------------------------------------------------------
EndDrawing();
//----------------------------------------------------------------------------------
}
public unsafe void Unload()
{
UnloadModelAnimations(anims, animCount); // Unload model animation
UnloadModel(model); // Unload model and meshes/material
UnloadShader(skinningShader); // Unload GPU skinning shader
}
// Minimal immediate-mode slider (raygui replacement)
private static bool GuiSlider(Rectangle bounds, string textLeft, string textRight, ref float value, float minValue, float maxValue)
{
var mouse = GetMousePosition();
var dragging = false;
if (CheckCollisionPointRec(mouse, bounds) && IsMouseButtonDown(MouseButton.Left))
{
value = minValue + ((mouse.X - bounds.X) / bounds.Width) * (maxValue - minValue);
if (value < minValue)
{
value = minValue;
}
if (value > maxValue)
{
value = maxValue;
}
dragging = true;
}
DrawRectangleRec(bounds, Color.LightGray);
var pct = (value - minValue) / (maxValue - minValue);
DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), Color.SkyBlue);
DrawRectangleLinesEx(bounds, 1, Color.Gray);
if (!string.IsNullOrEmpty(textLeft))
{
DrawText(textLeft, (int)(bounds.X - MeasureText(textLeft, 10) - 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
}
if (!string.IsNullOrEmpty(textRight))
{
DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
}
return dragging;
}
// Minimal immediate-mode progress bar (raygui replacement)
private static void GuiProgressBar(Rectangle bounds, string textLeft, string textRight, float value, float minValue, float maxValue)
{
DrawRectangleRec(bounds, Color.LightGray);
var pct = maxValue > minValue ? (value - minValue) / (maxValue - minValue) : 0.0f;
if (pct < 0)
{
pct = 0;
}
if (pct > 1)
{
pct = 1;
}
DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), Color.SkyBlue);
DrawRectangleLinesEx(bounds, 1, Color.Gray);
if (!string.IsNullOrEmpty(textLeft))
{
DrawText(textLeft, (int)(bounds.X - MeasureText(textLeft, 10) - 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
}
if (!string.IsNullOrEmpty(textRight))
{
DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
}
}
// Minimal immediate-mode dropdown box (raygui replacement)
private static bool GuiDropdownBox(Rectangle bounds, string[] items, ref int active, bool editMode)
{
var result = false;
var mouse = GetMousePosition();
// Draw main box
DrawRectangleRec(bounds, Color.LightGray);
DrawRectangleLinesEx(bounds, 1, Color.Gray);
if (active >= 0 && active < items.Length)
{
DrawText(items[active], (int)bounds.X + 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
}
DrawText(editMode ? "^" : "v", (int)(bounds.X + bounds.Width - 12), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
// Draw items when open
if (editMode)
{
for (var i = 0; i < items.Length; i++)
{
Rectangle item = new(bounds.X, bounds.Y + bounds.Height * (i + 1), bounds.Width, bounds.Height);
var hover = CheckCollisionPointRec(mouse, item);
DrawRectangleRec(item, hover ? Color.SkyBlue : Color.LightGray);
DrawRectangleLinesEx(item, 1, Color.Gray);
DrawText(items[i], (int)item.X + 5, (int)(item.Y + item.Height / 2 - 5), 10, Color.DarkGray);
}
}
if (IsMouseButtonPressed(MouseButton.Left))
{
if (editMode)
{
for (var i = 0; i < items.Length; i++)
{
Rectangle item = new(bounds.X, bounds.Y + bounds.Height * (i + 1), bounds.Width, bounds.Height);
if (CheckCollisionPointRec(mouse, item))
{
active = i;
break;
}
}
result = true; // any click closes the dropdown
}
else if (CheckCollisionPointRec(mouse, bounds))
{
result = true; // open the dropdown
}
}
return result;
}
public static int Main()
{
// Initialization
//--------------------------------------------------------------------------------------
InitWindow(screenWidth, screenHeight, "raylib [models] example - animation blending");
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
//--------------------------------------------------------------------------------------
var game = new AnimationBlending();
game.Init();
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
game.Update();
}
game.Unload();
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}
}

View file

@ -0,0 +1,160 @@
/*******************************************************************************************
*
* raylib [models] example - animation gpu skinning
*
* Example complexity rating: [] 3/4
*
* Example originally created with raylib 4.5, last time updated with raylib 4.5
*
* Example contributed by Daniel Holden (@orangeduck) and reviewed by Ramon Santamaria (@raysan5)
*
* WARNING: GPU skinning must be enabled in raylib with a compilation flag,
* if not enabled, CPU skinning will be used instead
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2024-2025 Daniel Holden (@orangeduck)
*
********************************************************************************************/
using System.Numerics;
using static Raylib_cs.Raylib;
namespace Examples.Models;
public partial class AnimationGpuSkinning : IExample
{
private const int screenWidth = 800;
private const int screenHeight = 450;
#if BROWSER
private const int GlslVersion = 100; // WebGL1 needs GLSL ES 100
#else
private const int GlslVersion = 330;
#endif
public string Name => "Models / Animation GPU Skinning";
public string Title => "raylib [models] example - animation gpu skinning";
private Camera3D camera;
private Model model;
private Vector3 position;
private Shader skinningShader;
private unsafe ModelAnimation* anims;
private int animCount;
private int animIndex;
private int animCurrentFrame;
public unsafe void Init()
{
// Define the camera to look into our 3d world
camera = new();
camera.Position = new Vector3(5.0f, 5.0f, 5.0f); // Camera position
camera.Target = new Vector3(0.0f, 1.0f, 0.0f); // Camera looking at point
camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target)
camera.FovY = 45.0f; // Camera field-of-view Y
camera.Projection = CameraProjection.Perspective; // Camera projection type
// Load gltf model
model = LoadModel("resources/models/gltf/greenman.glb"); // Load character model
position = new Vector3(0.0f, 0.0f, 0.0f); // Set model position
// Load skinning shader
// NOTE: It must be a valid shader, following raylib attribs/uniform conventions for GPU skinning
skinningShader = LoadShader(
$"resources/shaders/glsl{GlslVersion}/skinning.vs",
$"resources/shaders/glsl{GlslVersion}/skinning.fs"
);
// Skinning shader could be required to be assigned to all materials shaders, just to make
// sure required uniforms are being updated for the mesh using that material (and shader)
model.Materials[1].Shader = skinningShader; // Just assigning to materials[1] for this model
// Load gltf model animations
animCount = 0;
anims = LoadModelAnimations("resources/models/gltf/greenman.glb", ref animCount);
// Animation playing variables
animIndex = 0; // Current animation playing
animCurrentFrame = 0; // Current animation frame
}
public unsafe void Update()
{
// Update
//----------------------------------------------------------------------------------
UpdateCamera(ref camera, CameraMode.Orbital);
// Select current animation
if (IsKeyPressed(KeyboardKey.Right))
{
animIndex = (animIndex + 1) % animCount;
}
else if (IsKeyPressed(KeyboardKey.Left))
{
animIndex = (animIndex + animCount - 1) % animCount;
}
// Update model animation
animCurrentFrame = (animCurrentFrame + 1) % anims[animIndex].KeyFrameCount;
UpdateModelAnimation(model, anims[animIndex], animCurrentFrame);
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(Color.RayWhite);
BeginMode3D(camera);
DrawModel(model, position, 1.0f, Color.White);
DrawGrid(10, 1.0f);
EndMode3D();
DrawText($"Current animation: {anims[animIndex].NameToString()}", 10, 40, 20, Color.Maroon);
DrawText("Use the LEFT/RIGHT keys to switch animation", 10, 10, 20, Color.Gray);
EndDrawing();
//----------------------------------------------------------------------------------
}
public unsafe void Unload()
{
UnloadModelAnimations(anims, animCount); // Unload model animation
UnloadModel(model); // Unload model and meshes/material
UnloadShader(skinningShader); // Unload GPU skinning shader
}
public static int Main()
{
// Initialization
//--------------------------------------------------------------------------------------
InitWindow(screenWidth, screenHeight, "raylib [models] example - animation gpu skinning");
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
//--------------------------------------------------------------------------------------
var game = new AnimationGpuSkinning();
game.Init();
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
game.Update();
}
game.Unload();
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}
}

View file

@ -0,0 +1,292 @@
/*******************************************************************************************
*
* raylib [models] example - animation timing
*
* Example complexity rating: [] 3/4
*
* Example originally created with raylib 6.0, last time updated with raylib 6.0
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2026 Ramon Santamaria (@raysan5)
*
********************************************************************************************/
// NOTE: The upstream example uses raygui for its UI controls (dropdown, slider, progress bar).
// raygui is not part of raylib-cs, so the required controls are reimplemented here with
// basic raylib drawing primitives, preserving the original behaviour.
using System.Numerics;
using static Raylib_cs.Raylib;
namespace Examples.Models;
public partial class AnimationTiming : IExample
{
private const int screenWidth = 800;
private const int screenHeight = 450;
public string Name => "Models / Animation Timing";
public string Title => "raylib [models] example - animation timing";
private Camera3D camera;
private Model model;
private Vector3 position;
private unsafe ModelAnimation* anims;
private int animCount;
private int animIndex;
private float animCurrentFrame;
private float animFrameSpeed;
private bool animPause;
private string[] animNames;
private bool dropdownEditMode;
private float animFrameProgress;
public unsafe void Init()
{
// Define the camera to look into our 3d world
camera = new();
camera.Position = new Vector3(6.0f, 6.0f, 6.0f); // Camera position
camera.Target = new Vector3(0.0f, 2.0f, 0.0f); // Camera looking at point
camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target)
camera.FovY = 45.0f; // Camera field-of-view Y
camera.Projection = CameraProjection.Perspective; // Camera projection type
// Load model
model = LoadModel("resources/models/gltf/robot.glb");
position = new Vector3(0.0f, 0.0f, 0.0f); // Set model world position
// Load model animations
animCount = 0;
anims = LoadModelAnimations("resources/models/gltf/robot.glb", ref animCount);
// Animation playing variables
animIndex = 10; // Current animation playing
animCurrentFrame = 0.0f; // Current animation frame (supporting interpolated frames)
animFrameSpeed = 0.5f; // Animation play speed
animPause = false; // Pause animation
// UI required variables
animNames = new string[animCount];
for (var i = 0; i < animCount; i++)
{
animNames[i] = anims[i].NameToString();
}
dropdownEditMode = false;
animFrameProgress = 0.0f;
}
public unsafe void Update()
{
// Update
//----------------------------------------------------------------------------------
UpdateCamera(ref camera, CameraMode.Orbital);
if (IsKeyPressed(KeyboardKey.P))
{
animPause = !animPause;
}
if (!animPause && (animIndex < animCount))
{
// Update model animation
animCurrentFrame += animFrameSpeed;
if (animCurrentFrame >= anims[animIndex].KeyFrameCount)
{
animCurrentFrame = 0.0f;
}
UpdateModelAnimation(model, anims[animIndex], animCurrentFrame);
}
// NOTE: Animation and playing speed selected through UI
// Update progressbar value with current frame
animFrameProgress = animCurrentFrame;
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(Color.RayWhite);
BeginMode3D(camera);
DrawModel(model, position, 1.0f, Color.White);
DrawGrid(10, 1.0f);
EndMode3D();
// Draw UI, select anim and playing speed
GuiSlider(new Rectangle(260, 10, 500, 24), "FRAME SPEED: ", $"x{animFrameSpeed:0.0}", ref animFrameSpeed, 0.1f, 2.0f);
// Draw playing timeline with keyframes
DrawText($"CURRENT FRAME: {animFrameProgress:0.00} / {anims[animIndex].KeyFrameCount}",
10, (int)(GetScreenHeight() - 64.0f), 10, Color.DarkGray);
GuiProgressBar(new Rectangle(10, GetScreenHeight() - 40.0f, GetScreenWidth() - 20.0f, 24), null, null,
animFrameProgress, 0.0f, anims[animIndex].KeyFrameCount);
for (var i = 0; i < anims[animIndex].KeyFrameCount; i++)
{
DrawRectangle(10 + (int)(((float)(GetScreenWidth() - 20) / anims[animIndex].KeyFrameCount) * i),
GetScreenHeight() - 40, 1, 24, Color.Blue);
}
// NOTE: Dropdown drawn last so its open item list renders on top
if (GuiDropdownBox(new Rectangle(10, 10, 140, 24), animNames, ref animIndex, dropdownEditMode))
{
dropdownEditMode = !dropdownEditMode;
}
EndDrawing();
//----------------------------------------------------------------------------------
}
public unsafe void Unload()
{
UnloadModelAnimations(anims, animCount); // Unload model animation
UnloadModel(model); // Unload model and meshes/material
}
// Minimal immediate-mode slider (raygui replacement)
private static bool GuiSlider(Rectangle bounds, string textLeft, string textRight, ref float value, float minValue, float maxValue)
{
var mouse = GetMousePosition();
var dragging = false;
if (CheckCollisionPointRec(mouse, bounds) && IsMouseButtonDown(MouseButton.Left))
{
value = minValue + ((mouse.X - bounds.X) / bounds.Width) * (maxValue - minValue);
if (value < minValue)
{
value = minValue;
}
if (value > maxValue)
{
value = maxValue;
}
dragging = true;
}
DrawRectangleRec(bounds, Color.LightGray);
var pct = (value - minValue) / (maxValue - minValue);
DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), Color.SkyBlue);
DrawRectangleLinesEx(bounds, 1, Color.Gray);
if (!string.IsNullOrEmpty(textLeft))
{
DrawText(textLeft, (int)(bounds.X - MeasureText(textLeft, 10) - 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
}
if (!string.IsNullOrEmpty(textRight))
{
DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
}
return dragging;
}
// Minimal immediate-mode progress bar (raygui replacement)
private static void GuiProgressBar(Rectangle bounds, string textLeft, string textRight, float value, float minValue, float maxValue)
{
DrawRectangleRec(bounds, Color.LightGray);
var pct = maxValue > minValue ? (value - minValue) / (maxValue - minValue) : 0.0f;
if (pct < 0)
{
pct = 0;
}
if (pct > 1)
{
pct = 1;
}
DrawRectangleRec(new Rectangle(bounds.X, bounds.Y, bounds.Width * pct, bounds.Height), Color.SkyBlue);
DrawRectangleLinesEx(bounds, 1, Color.Gray);
if (!string.IsNullOrEmpty(textLeft))
{
DrawText(textLeft, (int)(bounds.X - MeasureText(textLeft, 10) - 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
}
if (!string.IsNullOrEmpty(textRight))
{
DrawText(textRight, (int)(bounds.X + bounds.Width + 5), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
}
}
// Minimal immediate-mode dropdown box (raygui replacement)
private static bool GuiDropdownBox(Rectangle bounds, string[] items, ref int active, bool editMode)
{
var result = false;
var mouse = GetMousePosition();
// Draw main box
DrawRectangleRec(bounds, Color.LightGray);
DrawRectangleLinesEx(bounds, 1, Color.Gray);
if (active >= 0 && active < items.Length)
{
DrawText(items[active], (int)bounds.X + 5, (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
}
DrawText(editMode ? "^" : "v", (int)(bounds.X + bounds.Width - 12), (int)(bounds.Y + bounds.Height / 2 - 5), 10, Color.DarkGray);
// Draw items when open
if (editMode)
{
for (var i = 0; i < items.Length; i++)
{
Rectangle item = new(bounds.X, bounds.Y + bounds.Height * (i + 1), bounds.Width, bounds.Height);
var hover = CheckCollisionPointRec(mouse, item);
DrawRectangleRec(item, hover ? Color.SkyBlue : Color.LightGray);
DrawRectangleLinesEx(item, 1, Color.Gray);
DrawText(items[i], (int)item.X + 5, (int)(item.Y + item.Height / 2 - 5), 10, Color.DarkGray);
}
}
if (IsMouseButtonPressed(MouseButton.Left))
{
if (editMode)
{
for (var i = 0; i < items.Length; i++)
{
Rectangle item = new(bounds.X, bounds.Y + bounds.Height * (i + 1), bounds.Width, bounds.Height);
if (CheckCollisionPointRec(mouse, item))
{
active = i;
break;
}
}
result = true; // any click closes the dropdown
}
else if (CheckCollisionPointRec(mouse, bounds))
{
result = true; // open the dropdown
}
}
return result;
}
public static int Main()
{
// Initialization
//--------------------------------------------------------------------------------------
InitWindow(screenWidth, screenHeight, "raylib [models] example - animation timing");
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
//--------------------------------------------------------------------------------------
var game = new AnimationTiming();
game.Init();
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
game.Update();
}
game.Unload();
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}
}

View file

@ -0,0 +1,202 @@
/*******************************************************************************************
*
* raylib [models] example - basic voxel
*
* Example complexity rating: [] 2/4
*
* Example originally created with raylib 5.5, last time updated with raylib 5.5
*
* Example contributed by Tim Little (@timlittle) and reviewed by Ramon Santamaria (@raysan5)
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2025 Tim Little (@timlittle)
*
********************************************************************************************/
using System.Numerics;
using static Raylib_cs.Raylib;
namespace Examples.Models;
public partial class BasicVoxel : IExample
{
private const int screenWidth = 800;
private const int screenHeight = 450;
private const int WorldSize = 8; // Size of our voxel world (8x8x8 cubes)
public string Name => "Models / Basic Voxel";
public string Title => "raylib [models] example - basic voxel";
public bool CursorDisabled => true;
private Camera3D camera;
private Model cubeModel;
private bool[,,] voxels;
public unsafe void Init()
{
// Define the camera to look into our 3d world (first person)
camera = new();
camera.Position = new Vector3(-2.0f, 0.0f, -2.0f); // Camera position at ground level
camera.Target = new Vector3(0.0f, 0.0f, 0.0f); // Camera looking at point
camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector
camera.FovY = 45.0f; // Camera field-of-view Y
camera.Projection = CameraProjection.Perspective; // Camera projection type
// Create a cube model
var cubeMesh = GenMeshCube(1.0f, 1.0f, 1.0f); // Create a unit cube mesh
cubeModel = LoadModelFromMesh(cubeMesh); // Convert mesh to a model
cubeModel.Materials[0].Maps[(int)MaterialMapIndex.Diffuse].Color = Color.Beige;
// Initialize voxel world - fill with voxels
voxels = new bool[WorldSize, WorldSize, WorldSize];
for (var x = 0; x < WorldSize; x++)
{
for (var y = 0; y < WorldSize; y++)
{
for (var z = 0; z < WorldSize; z++)
{
voxels[x, y, z] = true;
}
}
}
}
public void Update()
{
// Update
//----------------------------------------------------------------------------------
UpdateCamera(ref camera, CameraMode.FirstPerson);
// Handle voxel removal with mouse click
// This method is quite inefficient. Ray marching through the voxel grid using DDA would be faster, but more complex.
if (IsMouseButtonPressed(MouseButton.Left))
{
// Cast a ray from the screen center (where crosshair would be)
Vector2 screenCenter = new(GetScreenWidth() / 2.0f, GetScreenHeight() / 2.0f);
var ray = GetScreenToWorldRay(screenCenter, camera);
// Check ray collision with all voxels
var closestDistance = 99999.0f;
Vector3 closestVoxelPosition = new(-1, -1, -1);
var voxelFound = false;
for (var x = 0; x < WorldSize; x++)
{
for (var y = 0; y < WorldSize; y++)
{
for (var z = 0; z < WorldSize; z++)
{
if (!voxels[x, y, z])
{
continue; // Skip empty voxels
}
// Build a bounding box for this voxel
Vector3 position = new(x, y, z);
BoundingBox box = new(
new Vector3(position.X - 0.5f, position.Y - 0.5f, position.Z - 0.5f),
new Vector3(position.X + 0.5f, position.Y + 0.5f, position.Z + 0.5f)
);
// Check ray-box collision
var collision = GetRayCollisionBox(ray, box);
if (collision.Hit && (collision.Distance < closestDistance))
{
closestDistance = collision.Distance;
closestVoxelPosition = new Vector3(x, y, z);
voxelFound = true;
}
}
}
}
// Remove the closest voxel if one was hit
if (voxelFound)
{
voxels[(int)closestVoxelPosition.X,
(int)closestVoxelPosition.Y,
(int)closestVoxelPosition.Z] = false;
}
}
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(Color.RayWhite);
BeginMode3D(camera);
DrawGrid(10, 1.0f);
// Draw all voxels
for (var x = 0; x < WorldSize; x++)
{
for (var y = 0; y < WorldSize; y++)
{
for (var z = 0; z < WorldSize; z++)
{
if (!voxels[x, y, z])
{
continue;
}
Vector3 position = new(x, y, z);
DrawModel(cubeModel, position, 1.0f, Color.Beige);
DrawCubeWires(position, 1.0f, 1.0f, 1.0f, Color.Black);
}
}
}
EndMode3D();
// Draw reference point for raycasting to delete blocks
DrawCircle(GetScreenWidth() / 2, GetScreenHeight() / 2, 4, Color.Red);
DrawText("Left-click a voxel to remove it!", 10, 10, 20, Color.DarkGray);
DrawText("WASD to move, mouse to look around", 10, 35, 10, Color.Gray);
EndDrawing();
//----------------------------------------------------------------------------------
}
public void Unload()
{
UnloadModel(cubeModel);
}
public static int Main()
{
// Initialization
//--------------------------------------------------------------------------------------
InitWindow(screenWidth, screenHeight, "raylib [models] example - basic voxel");
DisableCursor(); // Lock mouse to window center
SetTargetFPS(60);
//--------------------------------------------------------------------------------------
var game = new BasicVoxel();
game.Init();
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
game.Update();
}
game.Unload();
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow();
//--------------------------------------------------------------------------------------
return 0;
}
}

View file

@ -0,0 +1,246 @@
/*******************************************************************************************
*
* raylib [models] example - bone socket
*
* Example complexity rating: [] 4/4
*
* Example originally created with raylib 4.5, last time updated with raylib 4.5
*
* Example contributed by iP (@ipzaur) and reviewed by Ramon Santamaria (@raysan5)
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2024-2025 iP (@ipzaur)
*
********************************************************************************************/
using System.Numerics;
using static Raylib_cs.Raylib;
using static Raylib_cs.Raymath;
namespace Examples.Models;
public partial class BoneSocket : IExample
{
private const int screenWidth = 800;
private const int screenHeight = 450;
private const int BoneSockets = 3;
private const int BoneSocketHat = 0;
private const int BoneSocketHandR = 1;
private const int BoneSocketHandL = 2;
public string Name => "Models / Bone Socket";
public string Title => "raylib [models] example - bone socket";
public bool CursorDisabled => true;
private Camera3D camera;
private Model characterModel;
private Model[] equipModel;
private bool[] showEquip;
private int animsCount;
private int animIndex;
private int animCurrentFrame;
private unsafe ModelAnimation* modelAnimations;
private int[] boneSocketIndex;
private Vector3 position;
private int angle;
public unsafe void Init()
{
// Define the camera to look into our 3d world
camera = new();
camera.Position = new Vector3(5.0f, 5.0f, 5.0f); // Camera position
camera.Target = new Vector3(0.0f, 2.0f, 0.0f); // Camera looking at point
camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target)
camera.FovY = 45.0f; // Camera field-of-view Y
camera.Projection = CameraProjection.Perspective; // Camera projection type
// Load gltf model
characterModel = LoadModel("resources/models/gltf/greenman.glb"); // Load character model
equipModel = new Model[BoneSockets]
{
LoadModel("resources/models/gltf/greenman_hat.glb"), // Index for the hat model is the same as BONE_SOCKET_HAT
LoadModel("resources/models/gltf/greenman_sword.glb"), // Index for the sword model is the same as BONE_SOCKET_HAND_R
LoadModel("resources/models/gltf/greenman_shield.glb") // Index for the shield model is the same as BONE_SOCKET_HAND_L
};
showEquip = new bool[3] { true, true, true }; // Toggle on/off equip
// Load gltf model animations
animsCount = 0;
animIndex = 0;
animCurrentFrame = 0;
modelAnimations = LoadModelAnimations("resources/models/gltf/greenman.glb", ref animsCount);
// Indices of bones for sockets
boneSocketIndex = new int[BoneSockets] { -1, -1, -1 };
// Search bones for sockets
for (var i = 0; i < characterModel.Skeleton.BoneCount; i++)
{
var boneName = characterModel.Skeleton.Bones[i].NameToString();
if (boneName == "socket_hat")
{
boneSocketIndex[BoneSocketHat] = i;
continue;
}
if (boneName == "socket_hand_R")
{
boneSocketIndex[BoneSocketHandR] = i;
continue;
}
if (boneName == "socket_hand_L")
{
boneSocketIndex[BoneSocketHandL] = i;
continue;
}
}
position = new Vector3(0.0f, 0.0f, 0.0f); // Set model position
angle = 0; // Set angle for rotate character
}
public unsafe void Update()
{
// Update
//----------------------------------------------------------------------------------
UpdateCamera(ref camera, CameraMode.ThirdPerson);
// Rotate character
if (IsKeyDown(KeyboardKey.F))
{
angle = (angle + 1) % 360;
}
else if (IsKeyDown(KeyboardKey.H))
{
angle = (360 + angle - 1) % 360;
}
// Select current animation
if (IsKeyPressed(KeyboardKey.T))
{
animIndex = (animIndex + 1) % animsCount;
}
else if (IsKeyPressed(KeyboardKey.G))
{
animIndex = (animIndex + animsCount - 1) % animsCount;
}
// Toggle shown of equip
if (IsKeyPressed(KeyboardKey.One))
{
showEquip[BoneSocketHat] = !showEquip[BoneSocketHat];
}
if (IsKeyPressed(KeyboardKey.Two))
{
showEquip[BoneSocketHandR] = !showEquip[BoneSocketHandR];
}
if (IsKeyPressed(KeyboardKey.Three))
{
showEquip[BoneSocketHandL] = !showEquip[BoneSocketHandL];
}
// Update model animation
var anim = modelAnimations[animIndex];
animCurrentFrame = (animCurrentFrame + 1) % anim.KeyFrameCount;
UpdateModelAnimation(characterModel, anim, animCurrentFrame);
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(Color.RayWhite);
BeginMode3D(camera);
// Draw character
var characterRotate = QuaternionFromAxisAngle(new Vector3(0.0f, 1.0f, 0.0f), angle * DEG2RAD);
characterModel.Transform = MatrixMultiply(QuaternionToMatrix(characterRotate), MatrixTranslate(position.X, position.Y, position.Z));
UpdateModelAnimation(characterModel, anim, animCurrentFrame);
DrawMesh(characterModel.Meshes[0], characterModel.Materials[1], characterModel.Transform);
// Draw equipments (hat, sword, shield)
for (var i = 0; i < BoneSockets; i++)
{
if (!showEquip[i])
{
continue;
}
var transform = &anim.KeyframePoses[animCurrentFrame][boneSocketIndex[i]];
var inRotation = characterModel.Skeleton.BindPose[boneSocketIndex[i]].Rotation;
var outRotation = transform->Rotation;
// Calculate socket rotation (angle between bone in initial pose and same bone in current animation frame)
var rotate = QuaternionMultiply(outRotation, QuaternionInvert(inRotation));
var matrixTransform = QuaternionToMatrix(rotate);
// Translate socket to its position in the current animation
matrixTransform = MatrixMultiply(matrixTransform, MatrixTranslate(transform->Translation.X, transform->Translation.Y, transform->Translation.Z));
// Transform the socket using the transform of the character (angle and translate)
matrixTransform = MatrixMultiply(matrixTransform, characterModel.Transform);
// Draw mesh at socket position with socket angle rotation
DrawMesh(equipModel[i].Meshes[0], equipModel[i].Materials[1], matrixTransform);
}
DrawGrid(10, 1.0f);
EndMode3D();
DrawText("Use the T/G to switch animation", 10, 10, 20, Color.Gray);
DrawText("Use the F/H to rotate character left/right", 10, 35, 20, Color.Gray);
DrawText("Use the 1,2,3 to toggle shown of hat, sword and shield", 10, 60, 20, Color.Gray);
EndDrawing();
//----------------------------------------------------------------------------------
}
public unsafe void Unload()
{
UnloadModelAnimations(modelAnimations, animsCount);
UnloadModel(characterModel); // Unload character model and meshes/material
// Unload equipment model and meshes/material
for (var i = 0; i < BoneSockets; i++)
{
UnloadModel(equipModel[i]);
}
}
public static int Main()
{
// Initialization
//--------------------------------------------------------------------------------------
InitWindow(screenWidth, screenHeight, "raylib [models] example - bone socket");
DisableCursor(); // Limit cursor to relative movement inside the window
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
//--------------------------------------------------------------------------------------
var game = new BoneSocket();
game.Init();
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
game.Update();
}
game.Unload();
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}
}

592
Examples/Models/Decals.cs Normal file
View file

@ -0,0 +1,592 @@
/*******************************************************************************************
*
* raylib [models] example - decals
*
* Example complexity rating: [] 4/4
*
* Example originally created with raylib 6.0, last time updated with raylib 6.0
*
* Example contributed by JP Mortiboys (@themushroompirates) and reviewed by Ramon Santamaria (@raysan5)
* Based on previous work by @mrdoob
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2025 JP Mortiboys (@themushroompirates) and Ramon Santamaria (@raysan5)
*
********************************************************************************************/
using System;
using System.Collections.Generic;
using System.Numerics;
using static Raylib_cs.Raylib;
using static Raylib_cs.Raymath;
namespace Examples.Models;
public partial class Decals : IExample
{
private const int screenWidth = 800;
private const int screenHeight = 450;
private const int MaxDecals = 256;
public string Name => "Models / Decals";
public string Title => "raylib [models] example - decals";
public ConfigFlags ConfigFlags => ConfigFlags.Msaa4xHint;
private Camera3D camera;
private Model model;
private Texture2D modelTexture;
private BoundingBox modelBBox;
private float decalSize;
private float decalOffset;
private Model placementCube;
private Material decalMaterial;
private Texture2D decalTexture;
private bool showModel;
private readonly List<Model> decalModels = new();
public unsafe void Init()
{
// Define the camera to look into our 3d world
camera = new();
camera.Position = new Vector3(5.0f, 5.0f, 5.0f); // Camera position
camera.Target = new Vector3(0.0f, 1.0f, 0.0f); // Camera looking at point
camera.Up = new Vector3(0.0f, 1.6f, 0.0f); // Camera up vector (rotation towards target)
camera.FovY = 45.0f; // Camera field-of-view Y
camera.Projection = CameraProjection.Perspective; // Camera projection type
// Load character model
model = LoadModel("resources/models/obj/character.obj");
// Apply character skin
modelTexture = LoadTexture("resources/models/obj/character_diffuse.png");
SetTextureFilter(modelTexture, TextureFilter.Bilinear);
model.Materials[0].Maps[(int)MaterialMapIndex.Diffuse].Texture = modelTexture;
modelBBox = GetMeshBoundingBox(model.Meshes[0]); // Get mesh bounding box
camera.Target = Vector3Lerp(modelBBox.Min, modelBBox.Max, 0.5f);
camera.Position = modelBBox.Max * 1.0f;
camera.Position.X *= 0.1f;
var modelSize = MathF.Min(
MathF.Min(MathF.Abs(modelBBox.Max.X - modelBBox.Min.X), MathF.Abs(modelBBox.Max.Y - modelBBox.Min.Y)),
MathF.Abs(modelBBox.Max.Z - modelBBox.Min.Z));
camera.Position = new Vector3(0.0f, modelBBox.Max.Y * 1.2f, modelSize * 3.0f);
decalSize = modelSize * 0.25f;
decalOffset = 0.01f;
placementCube = LoadModelFromMesh(GenMeshCube(decalSize, decalSize, decalSize));
placementCube.Materials[0].Maps[0].Color = Color.Lime;
decalMaterial = LoadMaterialDefault();
decalMaterial.Maps[0].Color = Color.Yellow;
var decalImage = LoadImage("resources/raylib_logo.png");
ImageResizeNN(ref decalImage, decalImage.Width / 4, decalImage.Height / 4);
decalTexture = LoadTextureFromImage(decalImage);
UnloadImage(decalImage);
SetTextureFilter(decalTexture, TextureFilter.Bilinear);
decalMaterial.Maps[(int)MaterialMapIndex.Diffuse].Texture = decalTexture;
decalMaterial.Maps[(int)MaterialMapIndex.Diffuse].Color = Color.RayWhite;
showModel = true;
decalModels.Clear();
}
public unsafe void Update()
{
// Update
//----------------------------------------------------------------------------------
if (IsMouseButtonDown(MouseButton.Right))
{
UpdateCamera(ref camera, CameraMode.ThirdPerson);
}
// Display information about closest hit
RayCollision collision = new();
collision.Distance = float.MaxValue;
collision.Hit = false;
// Get mouse ray
var ray = GetScreenToWorldRay(GetMousePosition(), camera);
// Check ray collision against bounding box first, before trying the full ray-mesh test
var boxHitInfo = GetRayCollisionBox(ray, modelBBox);
RayCollision meshHitInfo = new();
if (boxHitInfo.Hit && (decalModels.Count < MaxDecals))
{
// Check ray collision against model meshes
for (var m = 0; m < model.MeshCount; m++)
{
// NOTE: We consider the model.transform for the collision check but
// it can be checked against any transform Matrix, used when checking against same
// model drawn multiple times with multiple transforms
meshHitInfo = GetRayCollisionMesh(ray, model.Meshes[m], model.Transform);
if (meshHitInfo.Hit)
{
// Save the closest hit mesh
if (!collision.Hit || (collision.Distance > meshHitInfo.Distance))
{
collision = meshHitInfo;
}
}
}
if (meshHitInfo.Hit)
{
collision = meshHitInfo;
}
}
// Add decal to mesh on hit point
if (collision.Hit && IsMouseButtonPressed(MouseButton.Left) && (decalModels.Count < MaxDecals))
{
// Create the transformation to project the decal
var origin = collision.Point + (collision.Normal * 1.0f);
var splat = MatrixLookAt(collision.Point, origin, new Vector3(0.0f, 1.0f, 0.0f));
// Spin the placement around a bit
splat = MatrixMultiply(splat, MatrixRotateZ(DEG2RAD * GetRandomValue(-180, 180)));
var decalMesh = GenMeshDecal(model, splat, decalSize, decalOffset);
if (decalMesh.VertexCount > 0)
{
var decalModel = LoadModelFromMesh(decalMesh);
decalModel.Materials[0].Maps[0] = decalMaterial.Maps[0];
decalModels.Add(decalModel);
}
}
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(Color.RayWhite);
BeginMode3D(camera);
// Draw the model at the origin and default scale
if (showModel)
{
DrawModel(model, new Vector3(0.0f, 0.0f, 0.0f), 1.0f, Color.White);
}
// Draw the decal models
for (var i = 0; i < decalModels.Count; i++)
{
DrawModel(decalModels[i], Vector3.Zero, 1.0f, Color.White);
}
// If we hit the mesh, draw the box for the decal
if (collision.Hit)
{
var origin = collision.Point + (collision.Normal * 1.0f);
var splat = MatrixLookAt(collision.Point, origin, new Vector3(0, 1, 0));
placementCube.Transform = MatrixInvert(splat);
DrawModel(placementCube, Vector3.Zero, 1.0f, Fade(Color.White, 0.5f));
}
DrawGrid(10, 10.0f);
EndMode3D();
float yPos = 10;
var x0 = GetScreenWidth() - 300.0f;
var x1 = x0 + 100;
var x2 = x1 + 100;
DrawText("Vertices", (int)x1, (int)yPos, 10, Color.Lime);
DrawText("Triangles", (int)x2, (int)yPos, 10, Color.Lime);
yPos += 15;
var vertexCount = 0;
var triangleCount = 0;
for (var i = 0; i < model.MeshCount; i++)
{
vertexCount += model.Meshes[i].VertexCount;
triangleCount += model.Meshes[i].TriangleCount;
}
DrawText("Main model", (int)x0, (int)yPos, 10, Color.Lime);
DrawText($"{vertexCount}", (int)x1, (int)yPos, 10, Color.Lime);
DrawText($"{triangleCount}", (int)x2, (int)yPos, 10, Color.Lime);
yPos += 15;
for (var i = 0; i < decalModels.Count; i++)
{
if (i == 20)
{
DrawText("...", (int)x0, (int)yPos, 10, Color.Lime);
yPos += 15;
}
if (i < 20)
{
DrawText($"Decal #{i + 1}", (int)x0, (int)yPos, 10, Color.Lime);
DrawText($"{decalModels[i].Meshes[0].VertexCount}", (int)x1, (int)yPos, 10, Color.Lime);
DrawText($"{decalModels[i].Meshes[0].TriangleCount}", (int)x2, (int)yPos, 10, Color.Lime);
yPos += 15;
}
vertexCount += decalModels[i].Meshes[0].VertexCount;
triangleCount += decalModels[i].Meshes[0].TriangleCount;
}
DrawText("TOTAL", (int)x0, (int)yPos, 10, Color.Lime);
DrawText($"{vertexCount}", (int)x1, (int)yPos, 10, Color.Lime);
DrawText($"{triangleCount}", (int)x2, (int)yPos, 10, Color.Lime);
yPos += 15;
DrawText("Hold RMB to move camera", 10, 430, 10, Color.Gray);
DrawText("(c) Character model and texture from kenney.nl", screenWidth - 260, screenHeight - 20, 10, Color.Gray);
// UI elements
if (GuiButton(new Rectangle(10, screenHeight - 1000.0f, 100, 60), showModel ? "Hide Model" : "Show Model"))
{
showModel = !showModel;
}
if (GuiButton(new Rectangle(10 + 110, screenHeight - 100.0f, 100, 60), "Clear Decals"))
{
// Clear decals, unload all decal models
for (var i = 0; i < decalModels.Count; i++)
{
UnloadModel(decalModels[i]);
}
decalModels.Clear();
}
DrawFPS(10, 10);
EndDrawing();
//----------------------------------------------------------------------------------
}
public void Unload()
{
UnloadModel(model);
UnloadTexture(modelTexture);
// Unload decal models
for (var i = 0; i < decalModels.Count; i++)
{
UnloadModel(decalModels[i]);
}
decalModels.Clear();
UnloadTexture(decalTexture);
UnloadModel(placementCube);
}
// Clip segment
private static Vector3 ClipSegment(Vector3 v0, Vector3 v1, Vector3 p, float s)
{
var d0 = Vector3.Dot(v0, p) - s;
var d1 = Vector3.Dot(v1, p) - s;
var s0 = d0 / (d0 - d1);
return Vector3.Lerp(v0, v1, s0);
}
// Generate mesh decals for provided model
private static unsafe Mesh GenMeshDecal(Model target, Matrix4x4 projection, float decalSize, float decalOffset)
{
// We're going to use these to build up our decal meshes
var meshBuilders = new List<Vector3>[2] { new(), new() };
// We're going to need the inverse matrix
var invProj = MatrixInvert(projection);
// We'll be flip-flopping between the two mesh builders
// Reading from one and writing to the other, then swapping
var mbIndex = 0;
// First pass, just get any triangle inside the bounding box (for each mesh of the model)
for (var meshIndex = 0; meshIndex < target.MeshCount; meshIndex++)
{
var mesh = target.Meshes[meshIndex];
for (var tri = 0; tri < mesh.TriangleCount; tri++)
{
var vertices = new Vector3[3];
// The way we calculate the vertices of the mesh triangle
// depend on whether the mesh vertices are indexed or not
if (mesh.Indices == null)
{
for (var v = 0; v < 3; v++)
{
vertices[v] = new Vector3(
mesh.Vertices[3 * 3 * tri + 3 * v + 0],
mesh.Vertices[3 * 3 * tri + 3 * v + 1],
mesh.Vertices[3 * 3 * tri + 3 * v + 2]
);
}
}
else
{
for (var v = 0; v < 3; v++)
{
vertices[v] = new Vector3(
mesh.Vertices[3 * mesh.Indices[3 * tri + 0] + v],
mesh.Vertices[3 * mesh.Indices[3 * tri + 1] + v],
mesh.Vertices[3 * mesh.Indices[3 * tri + 2] + v]
);
}
}
// Transform all 3 vertices of the triangle
// and check if they are inside our decal box
var insideCount = 0;
for (var i = 0; i < 3; i++)
{
// To projection space
var v = Vector3Transform(vertices[i], projection);
if ((MathF.Abs(v.X) < decalSize) || (MathF.Abs(v.Y) <= decalSize) || (MathF.Abs(v.Z) <= decalSize))
{
insideCount++;
}
// We need to keep the transformed vertex
vertices[i] = v;
}
// If any of them are inside, we add the triangle - we'll clip it later
if (insideCount > 0)
{
meshBuilders[mbIndex].Add(vertices[0]);
meshBuilders[mbIndex].Add(vertices[1]);
meshBuilders[mbIndex].Add(vertices[2]);
}
}
}
// Clipping time! We need to clip against all 6 directions
Vector3[] planes =
{
new(1, 0, 0),
new(-1, 0, 0),
new(0, 1, 0),
new(0, -1, 0),
new(0, 0, 1),
new(0, 0, -1)
};
for (var face = 0; face < 6; face++)
{
// Swap current model builder (so we read from the one we just wrote to)
mbIndex = 1 - mbIndex;
var inMesh = meshBuilders[1 - mbIndex];
var outMesh = meshBuilders[mbIndex];
// Reset write builder
outMesh.Clear();
var s = 0.5f * decalSize;
for (var i = 0; i < inMesh.Count; i += 3)
{
Vector3 nV1, nV2, nV3, nV4;
var d1 = Vector3.Dot(inMesh[i + 0], planes[face]) - s;
var d2 = Vector3.Dot(inMesh[i + 1], planes[face]) - s;
var d3 = Vector3.Dot(inMesh[i + 2], planes[face]) - s;
var v1Out = d1 > 0;
var v2Out = d2 > 0;
var v3Out = d3 > 0;
// Calculate, how many vertices of the face lie outside of the clipping plane
var total = (v1Out ? 1 : 0) + (v2Out ? 1 : 0) + (v3Out ? 1 : 0);
switch (total)
{
case 0:
// The entire face lies inside of the plane, no clipping needed
outMesh.Add(inMesh[i]);
outMesh.Add(inMesh[i + 1]);
outMesh.Add(inMesh[i + 2]);
break;
case 1:
// One vertex lies outside of the plane, perform clipping
if (v2Out)
{
nV1 = inMesh[i];
nV2 = inMesh[i + 2];
nV3 = ClipSegment(inMesh[i + 1], nV1, planes[face], s);
nV4 = ClipSegment(inMesh[i + 1], nV2, planes[face], s);
outMesh.Add(nV3); outMesh.Add(nV2); outMesh.Add(nV1);
outMesh.Add(nV2); outMesh.Add(nV3); outMesh.Add(nV4);
}
else
{
if (v1Out)
{
nV1 = inMesh[i + 1];
nV2 = inMesh[i + 2];
nV3 = ClipSegment(inMesh[i], nV1, planes[face], s);
nV4 = ClipSegment(inMesh[i], nV2, planes[face], s);
}
else // v3Out
{
nV1 = inMesh[i];
nV2 = inMesh[i + 1];
nV3 = ClipSegment(inMesh[i + 2], nV1, planes[face], s);
nV4 = ClipSegment(inMesh[i + 2], nV2, planes[face], s);
}
outMesh.Add(nV1); outMesh.Add(nV2); outMesh.Add(nV3);
outMesh.Add(nV4); outMesh.Add(nV3); outMesh.Add(nV2);
}
break;
case 2:
// Two vertices lies outside of the plane, perform clipping
if (!v1Out)
{
nV1 = inMesh[i];
nV2 = ClipSegment(nV1, inMesh[i + 1], planes[face], s);
nV3 = ClipSegment(nV1, inMesh[i + 2], planes[face], s);
outMesh.Add(nV1); outMesh.Add(nV2); outMesh.Add(nV3);
}
if (!v2Out)
{
nV1 = inMesh[i + 1];
nV2 = ClipSegment(nV1, inMesh[i + 2], planes[face], s);
nV3 = ClipSegment(nV1, inMesh[i], planes[face], s);
outMesh.Add(nV1); outMesh.Add(nV2); outMesh.Add(nV3);
}
if (!v3Out)
{
nV1 = inMesh[i + 2];
nV2 = ClipSegment(nV1, inMesh[i], planes[face], s);
nV3 = ClipSegment(nV1, inMesh[i + 1], planes[face], s);
outMesh.Add(nV1); outMesh.Add(nV2); outMesh.Add(nV3);
}
break;
default: // The entire face lies outside of the plane, so let's discard the corresponding vertices
break;
}
}
}
// Now we just need to re-transform the vertices
var theMesh = meshBuilders[mbIndex];
// Allocate room for UVs
if (theMesh.Count > 0)
{
var uvs = new Vector2[theMesh.Count];
for (var i = 0; i < theMesh.Count; i++)
{
var vert = theMesh[i];
// Calculate the UVs based on the projected coords
// They are clipped to (-decalSize .. decalSize) and we want them (0..1)
uvs[i] = new Vector2(vert.X / decalSize + 0.5f, vert.Y / decalSize + 0.5f);
// Tiny nudge in the normal direction so it renders properly over the mesh
vert.Z -= decalOffset;
// From projection space to world space
theMesh[i] = Vector3Transform(vert, invProj);
}
// Decal model data ready, create the mesh and return it
return BuildMesh(theMesh, uvs);
}
// Return a blank mesh as there's nothing to add
return new Mesh();
}
// Build a Mesh from builder data
private static unsafe Mesh BuildMesh(List<Vector3> builderVertices, Vector2[] uvs)
{
Mesh outMesh = new(builderVertices.Count, builderVertices.Count / 3);
outMesh.AllocVertices();
outMesh.AllocTexCoords();
var vertices = outMesh.VerticesAs<Vector3>();
var texcoords = outMesh.TexCoordsAs<Vector2>();
for (var i = 0; i < builderVertices.Count; i++)
{
vertices[i] = builderVertices[i];
texcoords[i] = uvs[i];
}
UploadMesh(ref outMesh, false);
return outMesh;
}
// Button UI element
private static bool GuiButton(Rectangle rec, string label)
{
var bgColor = Color.Gray;
var pressed = false;
if (CheckCollisionPointRec(GetMousePosition(), rec))
{
bgColor = Color.LightGray;
if (IsMouseButtonPressed(MouseButton.Left))
{
pressed = true;
}
}
DrawRectangleRec(rec, bgColor);
DrawRectangleLinesEx(rec, 2.0f, Color.DarkGray);
var fontSize = 10;
var textWidth = MeasureText(label, fontSize);
DrawText(label, (int)(rec.X + rec.Width * 0.5f - textWidth * 0.5f), (int)(rec.Y + rec.Height * 0.5f - fontSize * 0.5f), fontSize, Color.DarkGray);
return pressed;
}
public static int Main()
{
// Initialization
//--------------------------------------------------------------------------------------
SetConfigFlags(ConfigFlags.Msaa4xHint);
InitWindow(screenWidth, screenHeight, "raylib [models] example - decals");
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
//--------------------------------------------------------------------------------------
var game = new Decals();
game.Init();
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
game.Update();
}
game.Unload();
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}
}

View file

@ -0,0 +1,148 @@
/*******************************************************************************************
*
* raylib [models] example - directional billboard
*
* Example complexity rating: [] 2/4
*
* Example originally created with raylib 6.0, last time updated with raylib 6.0
*
* Example contributed by Robin (@RobinsAviary) and reviewed by Ramon Santamaria (@raysan5)
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2025 Robin (@RobinsAviary)
* Killbot art by patvanmackelberg https://opengameart.org/content/killbot-8-directional under CC0
*
********************************************************************************************/
using System;
using System.Numerics;
using static Raylib_cs.Raylib;
using static Raylib_cs.Raymath;
namespace Examples.Models;
public partial class DirectionalBillboard : IExample
{
private const int screenWidth = 800;
private const int screenHeight = 450;
public string Name => "Models / Directional Billboard";
public string Title => "raylib [models] example - directional billboard";
private Camera3D camera;
private Texture2D skillbot;
private float animTimer;
private uint anim;
public void Init()
{
// Set up the camera
camera = new();
camera.Position = new Vector3(2.0f, 1.0f, 2.0f); // Starting position
camera.Target = new Vector3(0.0f, 0.5f, 0.0f); // Target position
camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Up vector
camera.FovY = 45.0f; // FOV
camera.Projection = CameraProjection.Perspective; // Projection type (Standard 3D perspective)
// Load billboard texture
skillbot = LoadTexture("resources/skillbot.png");
// Timer to update animation
animTimer = 0.0f;
// Animation frame
anim = 0;
}
public void Update()
{
// Update
//----------------------------------------------------------------------------------
UpdateCamera(ref camera, CameraMode.Orbital);
// Update timer with delta time
animTimer += GetFrameTime();
// Update frame index after a certain amount of time (half a second)
if (animTimer > 0.5f)
{
animTimer = 0.0f;
anim += 1;
}
// Reset frame index to zero on overflow
if (anim >= 4)
{
anim = 0;
}
// Find the current direction frame based on the camera position to the billboard object
var dir = (float)Math.Floor(((Vector2Angle(new Vector2(2.0f, 0.0f), new Vector2(camera.Position.X, camera.Position.Z)) / MathF.PI) * 4.0f) + 0.25f);
// Correct frame index if angle is negative
if (dir < 0.0f)
{
dir = 8.0f - Math.Abs((int)dir);
}
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(Color.RayWhite);
BeginMode3D(camera);
DrawGrid(10, 1.0f);
// Draw billboard pointing straight up to the sky, rotated relative to the camera and offset from the bottom
DrawBillboardPro(camera, skillbot, new Rectangle(0.0f + (anim * 24.0f), 0.0f + (dir * 24.0f), 24.0f, 24.0f),
Vector3.Zero, new Vector3(0.0f, 1.0f, 0.0f), Vector2.One, new Vector2(0.5f, 0.0f), 0, Color.White);
EndMode3D();
// Render various variables for reference
DrawText($"animation: {anim}", 10, 10, 20, Color.DarkGray);
DrawText($"direction frame: {dir:0}", 10, 40, 20, Color.DarkGray);
EndDrawing();
//----------------------------------------------------------------------------------
}
public void Unload()
{
// Unload billboard texture
UnloadTexture(skillbot);
}
public static int Main()
{
// Initialization
//--------------------------------------------------------------------------------------
InitWindow(screenWidth, screenHeight, "raylib [models] example - directional billboard");
SetTargetFPS(60);
//--------------------------------------------------------------------------------------
var game = new DirectionalBillboard();
game.Init();
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
game.Update();
}
game.Unload();
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}
}

View file

@ -0,0 +1,173 @@
/*******************************************************************************************
*
* raylib [models] example - loading m3d
*
* Example complexity rating: [] 2/4
*
* Example originally created with raylib 4.5, last time updated with raylib 4.5
*
* Example contributed by bzt (@bztsrc) and reviewed by Ramon Santamaria (@raysan5)
*
* NOTES:
* - Model3D (M3D) fileformat specs: https://gitlab.com/bztsrc/model3d
* - Bender M3D exported: https://gitlab.com/bztsrc/model3d/-/tree/master/blender
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2022-2025 bzt (@bztsrc)
*
********************************************************************************************/
using System.Numerics;
using static Raylib_cs.Raylib;
namespace Examples.Models;
public partial class LoadingM3d : IExample
{
private const int screenWidth = 800;
private const int screenHeight = 450;
public string Name => "Models / Loading M3D";
public string Title => "raylib [models] example - loading m3d";
private Camera3D camera;
private Model model;
private Vector3 position;
private unsafe ModelAnimation* anims;
private int animCount;
private int animIndex;
private float animCurrentFrame;
public unsafe void Init()
{
// Define the camera to look into our 3d world
camera = new();
camera.Position = new Vector3(1.5f, 1.5f, 1.5f); // Camera position
camera.Target = new Vector3(0.0f, 0.4f, 0.0f); // Camera looking at point
camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target)
camera.FovY = 45.0f; // Camera field-of-view Y
camera.Projection = CameraProjection.Perspective; // Camera projection type
// Load model
model = LoadModel("resources/models/m3d/cesium_man.m3d"); // Load the animated model mesh and basic data
position = new Vector3(0.0f, 0.0f, 0.0f); // Set model position
// Load animation data
animCount = 0;
anims = LoadModelAnimations("resources/models/m3d/cesium_man.m3d", ref animCount);
// Animation playing variables
animIndex = 0; // Current animation playing
animCurrentFrame = 0.0f; // Current animation frame (supporting interpolated frames)
}
public unsafe void Update()
{
// Update
//----------------------------------------------------------------------------------
UpdateCamera(ref camera, CameraMode.Orbital);
// Select current animation
if (IsKeyPressed(KeyboardKey.Right))
{
animIndex = (animIndex + 1) % animCount;
}
else if (IsKeyPressed(KeyboardKey.Left))
{
animIndex = (animIndex + animCount - 1) % animCount;
}
// Update model animation
animCurrentFrame += 1.0f;
if (animCurrentFrame >= anims[animIndex].KeyFrameCount)
{
animCurrentFrame = 0.0f;
}
UpdateModelAnimation(model, anims[animIndex], animCurrentFrame);
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(Color.RayWhite);
BeginMode3D(camera);
// Draw 3d model with texture
if (!IsKeyDown(KeyboardKey.Space))
{
DrawModel(model, position, 1.0f, Color.White);
}
else
{
// Draw the animated skeleton
DrawModelSkeleton(model.Skeleton, anims[animIndex].KeyframePoses[(int)animCurrentFrame], 1.0f, Color.Red);
}
DrawGrid(10, 1.0f);
EndMode3D();
DrawText($"Current animation: {anims[animIndex].NameToString()}", 10, 10, 20, Color.LightGray);
DrawText("Press SPACE to draw skeleton", 10, 40, 20, Color.Maroon);
DrawText("(c) CesiumMan model by KhronosGroup", GetScreenWidth() - 210, GetScreenHeight() - 20, 10, Color.Gray);
EndDrawing();
//----------------------------------------------------------------------------------
}
public unsafe void Unload()
{
UnloadModelAnimations(anims, animCount); // Unload model animations data
UnloadModel(model); // Unload model
}
// Draw model skeleton
private static unsafe void DrawModelSkeleton(ModelSkeleton skeleton, Transform* pose, float scale, Color color)
{
// Loop to (boneCount - 1) because the last one is a special "no bone" bone,
// needed to workaround buggy models without a -1, a cube is always drawn at the origin
for (var i = 0; i < skeleton.BoneCount - 1; i++)
{
// Display the frame-pose skeleton
DrawCube(pose[i].Translation, scale * 0.05f, scale * 0.05f, scale * 0.05f, color);
if (skeleton.Bones[i].Parent >= 0)
{
DrawLine3D(pose[i].Translation, pose[skeleton.Bones[i].Parent].Translation, color);
}
}
}
public static int Main()
{
// Initialization
//--------------------------------------------------------------------------------------
InitWindow(screenWidth, screenHeight, "raylib [models] example - loading m3d");
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
//--------------------------------------------------------------------------------------
var game = new LoadingM3d();
game.Init();
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
game.Update();
}
game.Unload();
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}
}

View file

@ -0,0 +1,246 @@
/*******************************************************************************************
*
* raylib [models] example - loading vox
*
* Example complexity rating: [] 1/4
*
* Example originally created with raylib 4.0, last time updated with raylib 4.0
*
* Example contributed by Johann Nadalutti (@procfxgen) and reviewed by Ramon Santamaria (@raysan5)
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2021-2025 Johann Nadalutti (@procfxgen) and Ramon Santamaria (@raysan5)
*
********************************************************************************************/
using System.Numerics;
using static Raylib_cs.Raylib;
using static Raylib_cs.Raymath;
using Examples.Shared;
namespace Examples.Models;
public partial class LoadingVox : IExample
{
private const int screenWidth = 800;
private const int screenHeight = 450;
private const int MaxVoxFiles = 4;
private const int MaxLights = 4;
#if BROWSER
private const int GlslVersion = 100; // WebGL1 needs GLSL ES 100
#else
private const int GlslVersion = 330;
#endif
public string Name => "Models / Loading VOX";
public string Title => "raylib [models] example - loading vox";
private static readonly string[] VoxFileNames =
{
"resources/models/vox/chr_knight.vox",
"resources/models/vox/chr_sword.vox",
"resources/models/vox/monu9.vox",
"resources/models/vox/fez.vox"
};
private Camera3D camera;
private Model[] models;
private int currentModel;
private Vector3 modelpos;
private Vector3 camerarot;
private Shader shader;
private Light[] lights;
public unsafe void Init()
{
// Define the camera to look into our 3d world
camera = new();
camera.Position = new Vector3(10.0f, 10.0f, 10.0f); // Camera position
camera.Target = new Vector3(0.0f, 0.0f, 0.0f); // Camera looking at point
camera.Up = new Vector3(0.0f, 1.0f, 0.0f); // Camera up vector (rotation towards target)
camera.FovY = 45.0f; // Camera field-of-view Y
camera.Projection = CameraProjection.Perspective; // Camera projection type
// Load MagicaVoxel files
models = new Model[MaxVoxFiles];
for (var i = 0; i < MaxVoxFiles; i++)
{
// Load VOX file and measure time
var t0 = GetTime() * 1000.0;
models[i] = LoadModel(VoxFileNames[i]);
var t1 = GetTime() * 1000.0;
TraceLog(TraceLogLevel.Info, $"[{VoxFileNames[i]}] Model file loaded in {t1 - t0:0.000} ms");
// Compute model translation matrix to center model on draw position (0, 0 , 0)
var bb = GetModelBoundingBox(models[i]);
Vector3 center = new();
center.X = bb.Min.X + ((bb.Max.X - bb.Min.X) / 2);
center.Z = bb.Min.Z + ((bb.Max.Z - bb.Min.Z) / 2);
var matTranslate = MatrixTranslate(-center.X, 0, -center.Z);
models[i].Transform = matTranslate;
}
currentModel = 0;
modelpos = new Vector3(0, 0, 0);
camerarot = new Vector3(0, 0, 0);
// Load voxel shader
shader = LoadShader(
$"resources/shaders/glsl{GlslVersion}/voxel_lighting.vs",
$"resources/shaders/glsl{GlslVersion}/voxel_lighting.fs"
);
// Get some required shader locations
shader.Locs[(int)ShaderLocationIndex.VectorView] = GetShaderLocation(shader, "viewPos");
// NOTE: "matModel" location name is automatically assigned on shader loading,
// no need to get the location again if using that uniform name
//shader.Locs[(int)ShaderLocationIndex.MatrixModel] = GetShaderLocation(shader, "matModel");
// Ambient light level (some basic lighting)
var ambientLoc = GetShaderLocation(shader, "ambient");
Raylib.SetShaderValue(shader, ambientLoc, new[] { 0.1f, 0.1f, 0.1f, 1.0f }, ShaderUniformDataType.Vec4);
// Assign out lighting shader to model
for (var i = 0; i < MaxVoxFiles; i++)
{
for (var j = 0; j < models[i].MaterialCount; j++)
{
models[i].Materials[j].Shader = shader;
}
}
// Create lights
lights = new Light[MaxLights];
lights[0] = Rlights.CreateLight(0, LightType.Point, new Vector3(-20, 20, -20), Vector3.Zero, Color.Gray, shader);
lights[1] = Rlights.CreateLight(1, LightType.Point, new Vector3(20, -20, 20), Vector3.Zero, Color.Gray, shader);
lights[2] = Rlights.CreateLight(2, LightType.Point, new Vector3(-20, 20, 20), Vector3.Zero, Color.Gray, shader);
lights[3] = Rlights.CreateLight(3, LightType.Point, new Vector3(20, -20, -20), Vector3.Zero, Color.Gray, shader);
}
public unsafe void Update()
{
// Update
//----------------------------------------------------------------------------------
if (IsMouseButtonDown(MouseButton.Middle))
{
var mouseDelta = GetMouseDelta();
camerarot.X = mouseDelta.X * 0.05f;
camerarot.Y = mouseDelta.Y * 0.05f;
}
else
{
camerarot.X = 0;
camerarot.Y = 0;
}
// Update camere movement, custom controls
UpdateCameraPro(ref camera,
new Vector3(
(IsKeyDown(KeyboardKey.W) || IsKeyDown(KeyboardKey.Up) ? 0.1f : 0.0f) - (IsKeyDown(KeyboardKey.S) || IsKeyDown(KeyboardKey.Down) ? 0.1f : 0.0f), // Move forward-backward
(IsKeyDown(KeyboardKey.D) || IsKeyDown(KeyboardKey.Right) ? 0.1f : 0.0f) - (IsKeyDown(KeyboardKey.A) || IsKeyDown(KeyboardKey.Left) ? 0.1f : 0.0f), // Move right-left
0.0f), // Move up-down
camerarot, // Camera rotation
GetMouseWheelMove() * -2.0f); // Move to target (zoom)
// Cycle between models on mouse click
if (IsMouseButtonPressed(MouseButton.Left))
{
currentModel = (currentModel + 1) % MaxVoxFiles;
}
// Update the shader with the camera view vector (points towards { 0.0f, 0.0f, 0.0f })
Raylib.SetShaderValue(shader, shader.Locs[(int)ShaderLocationIndex.VectorView], camera.Position, ShaderUniformDataType.Vec3);
// Update light values (actually, only enable/disable them)
for (var i = 0; i < MaxLights; i++)
{
Rlights.UpdateLightValues(shader, lights[i]);
}
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(Color.RayWhite);
// Draw 3D model
BeginMode3D(camera);
DrawModel(models[currentModel], modelpos, 1.0f, Color.White);
DrawGrid(10, 1.0f);
// Draw spheres to show where the lights are
for (var i = 0; i < MaxLights; i++)
{
if (lights[i].Enabled)
{
DrawSphereEx(lights[i].Position, 0.2f, 8, 8, lights[i].Color);
}
else
{
DrawSphereWires(lights[i].Position, 0.2f, 8, 8, ColorAlpha(lights[i].Color, 0.3f));
}
}
EndMode3D();
// Display info
DrawRectangle(10, 40, 340, 70, Fade(Color.SkyBlue, 0.5f));
DrawRectangleLines(10, 40, 340, 70, Fade(Color.DarkBlue, 0.5f));
DrawText("- MOUSE LEFT BUTTON: CYCLE VOX MODELS", 20, 50, 10, Color.Blue);
DrawText("- MOUSE MIDDLE BUTTON: ZOOM OR ROTATE CAMERA", 20, 70, 10, Color.Blue);
DrawText("- UP-DOWN-LEFT-RIGHT KEYS: MOVE CAMERA", 20, 90, 10, Color.Blue);
DrawText($"VOX model file: {GetFileName(VoxFileNames[currentModel])}", 10, 10, 20, Color.Gray);
EndDrawing();
//----------------------------------------------------------------------------------
}
public void Unload()
{
// Unload models data (GPU VRAM)
for (var i = 0; i < MaxVoxFiles; i++)
{
UnloadModel(models[i]);
}
UnloadShader(shader);
}
public static int Main()
{
// Initialization
//--------------------------------------------------------------------------------------
InitWindow(screenWidth, screenHeight, "raylib [models] example - loading vox");
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
//--------------------------------------------------------------------------------------
var game = new LoadingVox();
game.Init();
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
game.Update();
}
game.Unload();
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}
}

View file

@ -30,8 +30,6 @@ public partial class MeshPicking : IExample
public string Title => "raylib [models] example - mesh picking";
public bool CursorDisabled => true;
private Camera3D camera;
private Ray ray;
private Model tower;

View file

@ -79,8 +79,7 @@ public partial class ModelLoading : IExample
UpdateCamera(ref camera, CameraMode.Orbital);
#if BROWSER
// NOTE: Drag & drop file loading (IsFileDropped) is not available in the browser
// host, so it is skipped here. The default model stays loaded.
// NOTE: drag-and-drop model loading is not supported in the browser host; default loaded model is kept.
#else
// Load new models/textures on drag&drop
if (IsFileDropped())

View file

@ -0,0 +1,235 @@
/*******************************************************************************************
*
* raylib [models] example - point rendering
*
* Example complexity rating: [] 3/4
*
* Example originally created with raylib 5.0, last time updated with raylib 5.0
*
* Example contributed by Reese Gallagher (@satchelfrost) and reviewed by Ramon Santamaria (@raysan5)
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2024-2025 Reese Gallagher (@satchelfrost)
*
********************************************************************************************/
using System;
using System.Numerics;
using static Raylib_cs.Raylib;
namespace Examples.Models;
[ExcludeFromBrowser("rlEnablePointMode needs glPolygonMode, which OpenGL ES/WebGL lacks (renders as triangles)")]
public partial class PointRendering : IExample
{
private const int screenWidth = 800;
private const int screenHeight = 450;
private const int MaxPoints = 10000000; // 10 million
private const int MinPoints = 1000; // 1 thousand
public string Name => "Models / Point Rendering";
public string Title => "raylib [models] example - point rendering";
private static readonly Random Rand = new();
private Camera3D camera;
private Vector3 position;
private bool useDrawModelPoints;
private bool numPointsChanged;
private int numPoints;
private Mesh mesh;
private Model model;
public void Init()
{
camera = new()
{
Position = new Vector3(3.0f, 3.0f, 3.0f),
Target = new Vector3(0.0f, 0.0f, 0.0f),
Up = new Vector3(0.0f, 1.0f, 0.0f),
FovY = 45.0f,
Projection = CameraProjection.Perspective
};
position = new Vector3(0.0f, 0.0f, 0.0f);
useDrawModelPoints = true;
numPointsChanged = false;
numPoints = 1000;
mesh = GenMeshPoints(numPoints);
model = LoadModelFromMesh(mesh);
}
public unsafe void Update()
{
// Update
//----------------------------------------------------------------------------------
UpdateCamera(ref camera, CameraMode.Orbital);
if (IsKeyPressed(KeyboardKey.Space))
{
useDrawModelPoints = !useDrawModelPoints;
}
if (IsKeyPressed(KeyboardKey.Up))
{
numPoints = (numPoints * 10 > MaxPoints) ? MaxPoints : numPoints * 10;
numPointsChanged = true;
}
if (IsKeyPressed(KeyboardKey.Down))
{
numPoints = (numPoints / 10 < MinPoints) ? MinPoints : numPoints / 10;
numPointsChanged = true;
}
// Upload a different point cloud size
if (numPointsChanged)
{
UnloadModel(model);
mesh = GenMeshPoints(numPoints);
model = LoadModelFromMesh(mesh);
numPointsChanged = false;
}
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(Color.Black);
BeginMode3D(camera);
// The new method only uploads the points once to the GPU
if (useDrawModelPoints)
{
DrawModelPoints(model, position, 1.0f, Color.White);
}
else
{
// The old method must continually draw the "points" (lines)
for (var i = 0; i < numPoints; i++)
{
Vector3 pos = new(
mesh.Vertices[i * 3 + 0],
mesh.Vertices[i * 3 + 1],
mesh.Vertices[i * 3 + 2]
);
Color color = new(
mesh.Colors[i * 4 + 0],
mesh.Colors[i * 4 + 1],
mesh.Colors[i * 4 + 2],
mesh.Colors[i * 4 + 3]
);
DrawPoint3D(pos, color);
}
}
// Draw a unit sphere for reference
DrawSphereWires(position, 1.0f, 10, 10, Color.Yellow);
EndMode3D();
// Draw UI text
DrawText($"Point Count: {numPoints}", 10, screenHeight - 50, 40, Color.White);
DrawText("UP - Increase points", 10, 40, 20, Color.White);
DrawText("DOWN - Decrease points", 10, 70, 20, Color.White);
DrawText("SPACE - Drawing function", 10, 100, 20, Color.White);
if (useDrawModelPoints)
{
DrawText("Using: DrawModelPoints()", 10, 130, 20, Color.Green);
}
else
{
DrawText("Using: DrawPoint3D()", 10, 130, 20, Color.Red);
}
DrawFPS(10, 10);
EndDrawing();
//----------------------------------------------------------------------------------
}
public void Unload()
{
UnloadModel(model);
}
// Generate a spherical point cloud
private static unsafe Mesh GenMeshPoints(int numPoints)
{
Mesh mesh = new(numPoints, 1);
mesh.AllocVertices();
mesh.AllocColors();
// REF: https://en.wikipedia.org/wiki/Spherical_coordinate_system
for (var i = 0; i < numPoints; i++)
{
var theta = MathF.PI * (float)Rand.NextDouble();
var phi = 2.0f * MathF.PI * (float)Rand.NextDouble();
var r = 10.0f * (float)Rand.NextDouble();
mesh.Vertices[i * 3 + 0] = r * MathF.Sin(theta) * MathF.Cos(phi);
mesh.Vertices[i * 3 + 1] = r * MathF.Sin(theta) * MathF.Sin(phi);
mesh.Vertices[i * 3 + 2] = r * MathF.Cos(theta);
var color = ColorFromHSV(r * 360.0f, 1.0f, 1.0f);
mesh.Colors[i * 4 + 0] = color.R;
mesh.Colors[i * 4 + 1] = color.G;
mesh.Colors[i * 4 + 2] = color.B;
mesh.Colors[i * 4 + 3] = color.A;
}
// Upload mesh data from CPU (RAM) to GPU (VRAM) memory
UploadMesh(ref mesh, false);
return mesh;
}
// Draw a model points
// WARNING: OpenGL ES 2.0 does not support point mode drawing
private static void DrawModelPoints(Model model, Vector3 position, float scale, Color tint)
{
Rlgl.EnablePointMode();
Rlgl.DisableBackfaceCulling();
DrawModel(model, position, scale, tint);
Rlgl.EnableBackfaceCulling();
Rlgl.DisablePointMode();
}
public static int Main()
{
// Initialization
//--------------------------------------------------------------------------------------
InitWindow(screenWidth, screenHeight, "raylib [models] example - point rendering");
SetTargetFPS(60);
//--------------------------------------------------------------------------------------
var game = new PointRendering();
game.Init();
// Main game loop
while (!WindowShouldClose())
{
game.Update();
}
game.Unload();
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow();
//--------------------------------------------------------------------------------------
return 0;
}
}

View file

@ -0,0 +1,122 @@
/*******************************************************************************************
*
* raylib [models] example - rotating cube
*
* Example complexity rating: [] 1/4
*
* Example originally created with raylib 6.0, last time updated with raylib 6.0
*
* Example contributed by Jopestpe (@jopestpe)
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2025 Jopestpe (@jopestpe)
*
********************************************************************************************/
using System.Numerics;
using static Raylib_cs.Raylib;
namespace Examples.Models;
public partial class RotatingCube : IExample
{
private const int screenWidth = 800;
private const int screenHeight = 450;
public string Name => "Models / Rotating Cube";
public string Title => "raylib [models] example - rotating cube";
private Camera3D camera;
private Model model;
private Texture2D texture;
private float rotation;
public unsafe void Init()
{
// Define the camera to look into our 3d world
camera = new();
camera.Position = new Vector3(0.0f, 3.0f, 3.0f);
camera.Target = new Vector3(0.0f, 0.0f, 0.0f);
camera.Up = new Vector3(0.0f, 1.0f, 0.0f);
camera.FovY = 45.0f;
camera.Projection = CameraProjection.Perspective;
// Load image to create texture for the cube
model = LoadModelFromMesh(GenMeshCube(1.0f, 1.0f, 1.0f));
var img = LoadImage("resources/cubicmap_atlas.png");
var crop = ImageFromImage(img, new Rectangle(0, img.Height / 2.0f, img.Width / 2.0f, img.Height / 2.0f));
texture = LoadTextureFromImage(crop);
UnloadImage(img);
UnloadImage(crop);
model.Materials[0].Maps[(int)MaterialMapIndex.Diffuse].Texture = texture;
rotation = 0.0f;
}
public void Update()
{
// Update
//----------------------------------------------------------------------------------
rotation += 1.0f;
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(Color.RayWhite);
BeginMode3D(camera);
// Draw model defining: position, size, rotation-axis, rotation (degrees), size, and tint-color
DrawModelEx(model, new Vector3(0.0f, 0.0f, 0.0f), new Vector3(0.5f, 1.0f, 0.0f),
rotation, new Vector3(1.0f, 1.0f, 1.0f), Color.White);
DrawGrid(10, 1.0f);
EndMode3D();
DrawFPS(10, 10);
EndDrawing();
//----------------------------------------------------------------------------------
}
public void Unload()
{
UnloadTexture(texture); // Unload texture
UnloadModel(model); // Unload model
}
public static int Main()
{
// Initialization
//--------------------------------------------------------------------------------------
InitWindow(screenWidth, screenHeight, "raylib [models] example - rotating cube");
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
//--------------------------------------------------------------------------------------
var game = new RotatingCube();
game.Init();
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
game.Update();
}
game.Unload();
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}
}

View file

@ -19,13 +19,18 @@ using static Raylib_cs.Raylib;
namespace Examples.Models;
[ExcludeFromBrowser("cubemap generation is too memory-heavy on web (upstream note)")]
public partial class SkyboxDemo : IExample
{
private const int screenWidth = 800;
private const int screenHeight = 450;
// GLSL version used for shaders (330 desktop, 100 web/GLES)
#if BROWSER
public const int GlslVersion = 100;
#else
public const int GlslVersion = 330;
#endif
public string Name => "Models / Skybox Demo";

View file

@ -0,0 +1,163 @@
/*******************************************************************************************
*
* raylib [models] example - tesseract view
*
* NOTE: This example only works on platforms that support drag & drop (Windows, Linux, OSX, Html5?)
*
* Example complexity rating: [] 2/4
*
* Example originally created with raylib 6.0, last time updated with raylib 6.0
*
* Example contributed by Timothy van der Valk (@arceryz) and reviewed by Ramon Santamaria (@raysan5)
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2024-2025 Timothy van der Valk (@arceryz) and Ramon Santamaria (@raysan5)
*
********************************************************************************************/
using System;
using System.Numerics;
using static Raylib_cs.Raylib;
using static Raylib_cs.Raymath;
namespace Examples.Models;
public partial class TesseractView : IExample
{
private const int screenWidth = 800;
private const int screenHeight = 450;
public string Name => "Models / Tesseract View";
public string Title => "raylib [models] example - tesseract view";
// Define the camera to look into our 3d world
private Camera3D camera;
// Find the coordinates by setting XYZW to +-1
private Vector4[] tesseract;
private float rotation;
private Vector3[] transformed;
private float[] wValues;
public void Init()
{
// Define the camera to look into our 3d world
camera = new Camera3D();
camera.Position = new Vector3(4.0f, 4.0f, 4.0f); // Camera position
camera.Target = new Vector3(0.0f, 0.0f, 0.0f); // Camera looking at point
camera.Up = new Vector3(0.0f, 0.0f, 1.0f); // Camera up vector (rotation towards target)
camera.FovY = 50.0f; // Camera field-of-view Y
camera.Projection = CameraProjection.Perspective; // Camera mode type
// Find the coordinates by setting XYZW to +-1
tesseract = new Vector4[16]
{
new( 1, 1, 1, 1 ), new( 1, 1, 1, -1 ),
new( 1, 1, -1, 1 ), new( 1, 1, -1, -1 ),
new( 1, -1, 1, 1 ), new( 1, -1, 1, -1 ),
new( 1, -1, -1, 1 ), new( 1, -1, -1, -1 ),
new(-1, 1, 1, 1 ), new(-1, 1, 1, -1 ),
new(-1, 1, -1, 1 ), new(-1, 1, -1, -1 ),
new(-1, -1, 1, 1 ), new(-1, -1, 1, -1 ),
new(-1, -1, -1, 1 ), new(-1, -1, -1, -1 ),
};
rotation = 0.0f;
transformed = new Vector3[16];
wValues = new float[16];
}
public void Update()
{
// Update
//----------------------------------------------------------------------------------
rotation = DEG2RAD * 45.0f * (float)GetTime();
for (int i = 0; i < 16; i++)
{
Vector4 p = tesseract[i];
// Rotate the XW part of the vector
Vector2 rotXW = Vector2Rotate(new Vector2(p.X, p.W), rotation);
p.X = rotXW.X;
p.W = rotXW.Y;
// Projection from XYZW to XYZ from perspective point (0, 0, 0, 3)
// NOTE: Trace a ray from (0, 0, 0, 3) > p and continue until W = 0
float c = 3.0f / (3.0f - p.W);
p.X = c * p.X;
p.Y = c * p.Y;
p.Z = c * p.Z;
// Split XYZ coordinate and W values later for drawing
transformed[i] = new Vector3(p.X, p.Y, p.Z);
wValues[i] = p.W;
}
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(Color.RayWhite);
BeginMode3D(camera);
for (int i = 0; i < 16; i++)
{
// Draw spheres to indicate the W value
DrawSphere(transformed[i], MathF.Abs(wValues[i] * 0.1f), Color.Red);
for (int j = 0; j < 16; j++)
{
// Two lines are connected if they differ by 1 coordinate
// This way we dont have to keep an edge list
Vector4 v1 = tesseract[i];
Vector4 v2 = tesseract[j];
int diff = (v1.X == v2.X ? 1 : 0) + (v1.Y == v2.Y ? 1 : 0) + (v1.Z == v2.Z ? 1 : 0) + (v1.W == v2.W ? 1 : 0);
// Draw only differing by 1 coordinate and the lower index only (duplicate lines)
if (diff == 3 && i < j) DrawLine3D(transformed[i], transformed[j], Color.Maroon);
}
}
EndMode3D();
EndDrawing();
//----------------------------------------------------------------------------------
}
public void Unload()
{
}
public static int Main()
{
// Initialization
//--------------------------------------------------------------------------------------
InitWindow(screenWidth, screenHeight, "raylib [models] example - tesseract view");
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
//--------------------------------------------------------------------------------------
var game = new TesseractView();
game.Init();
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
game.Update();
}
game.Unload();
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}
}