animation blend custom
There is no browser build of this example yet.
Is it the same as C?
- Desktop:verified The Zig version renders a final frame that is byte-identical to the C example.
- Browser:not checked yet The browser comparison does not cover this example yet.
//! raylib [models] example - animation blend custom
//!
//! Zig-API translation of examples/models/models_animation_blend_custom.c (raylib 6.2).
//! Example contributed by dmitrii-brand (@dmitrii-brand) 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.
//! Copyright (c) 2026 dmitrii-brand (@dmitrii-brand)
const std = @import("std");
const rl = @import("raylib");
const rm = rl.math;
const glsl_version = 330; // Desktop GLSL version
pub fn main() !void {
// Initialization
const screen_width = 800;
const screen_height = 450;
rl.initWindow(screen_width, screen_height, "raylib [models] example - animation blend custom");
defer rl.closeWindow(); // Close window and OpenGL context
// Define the camera to look into our 3d world
var camera: rl.Camera = .{
.position = .{ .x = 4.0, .y = 4.0, .z = 4.0 }, // Camera position
.target = .{ .x = 0.0, .y = 1.0, .z = 0.0 }, // Camera looking at point
.up = .{ .x = 0.0, .y = 1.0, .z = 0.0 }, // Camera up vector (rotation towards target)
.fovy = 45.0, // Camera field-of-view Y
.projection = .perspective, // Camera projection type
};
// Load gltf model
var model = try rl.loadModel("resources/models/gltf/greenman.glb");
defer rl.unloadModel(model); // Unload model and meshes/material
const position: rl.Vector3 = .{ .x = 0.0, .y = 0.0, .z = 0.0 }; // 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
const skinning_shader = try rl.loadShader(
rl.textFormat("resources/shaders/glsl{d}/skinning.vs", .{glsl_version}),
rl.textFormat("resources/shaders/glsl{d}/skinning.fs", .{glsl_version}),
);
defer rl.unloadShader(skinning_shader); // Unload GPU skinning shader
model.materials.?[1].shader = skinning_shader;
// Load gltf model animations
const anims = try rl.loadModelAnimations("resources/models/gltf/greenman.glb");
defer rl.unloadModelAnimations(anims); // Unload model animation
// Use specific animation indices: 2-walk/move, 3-attack
// Validate indices
const anim_index0: usize = if (2 < anims.len) 2 else 0; // Walk/Move animation (index 2)
const anim_index1: usize = if (3 < anims.len) 3 else if (anims.len > 1) 1 else 0; // Attack animation (index 3)
var anim_current_frame0: i32 = 0;
var anim_current_frame1: i32 = 0;
var upper_body_blend = true; // Toggle: true = upper/lower body blending, false = uniform blending (50/50)
rl.setTargetFPS(60); // Set our game to run at 60 frames-per-second
// Main game loop
while (!rl.windowShouldClose()) { // Detect window close button or ESC key
// Update
rl.updateCamera(&camera, .orbital);
// Toggle upper/lower body blending mode (SPACE key)
if (rl.isKeyPressed(.space)) upper_body_blend = !upper_body_blend;
// Update animation frames
const anim0 = anims[anim_index0];
const anim1 = anims[anim_index1];
anim_current_frame0 = @rem(anim_current_frame0 + 1, anim0.keyframeCount);
anim_current_frame1 = @rem(anim_current_frame1 + 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)
const blend_factor: f32 = if (upper_body_blend) 1.0 else 0.5;
updateModelAnimationBoneCustom(&model, anim0, anim_current_frame0, anim1, anim_current_frame1, blend_factor, upper_body_blend);
// raylib provided animation blending function
//rl.updateModelAnimationEx(model, anim0, @floatFromInt(anim_current_frame0),
// anim1, @floatFromInt(anim_current_frame1), blend_factor);
// Draw
rl.beginDrawing();
defer rl.endDrawing();
rl.clearBackground(.ray_white);
{
rl.beginMode3D(camera);
defer rl.endMode3D();
rl.drawModel(model, position, 1.0, .white);
rl.drawGrid(10, 1.0);
}
// Draw UI
rl.drawText(rl.textFormat("ANIM 0: {s}", .{anim0.nameSlice()}), 10, 10, 20, .gray);
rl.drawText(rl.textFormat("ANIM 1: {s}", .{anim1.nameSlice()}), 10, 40, 20, .gray);
rl.drawText(
rl.textFormat("[SPACE] Toggle blending mode: {s}", .{if (upper_body_blend) "Upper/Lower Body Blending" else "Uniform Blending"}),
10,
rl.getScreenHeight() - 30,
20,
.dark_gray,
);
}
}
/// Check if a bone is part of upper body (for selective blending)
fn isUpperBodyBone(bone_name: []const u8) bool {
// Common upper body bone names (adjust based on your model)
const names = [_][]const u8{
"spine", "spine1", "spine2",
"chest", "upperChest", "neck",
"head", "shoulder", "shoulder_L",
"shoulder_R", "upperArm", "upperArm_L",
"upperArm_R", "lowerArm", "lowerArm_L",
"lowerArm_R", "hand", "hand_L",
"hand_R", "clavicle", "clavicle_L",
"clavicle_R",
};
for (names) |name| {
if (std.mem.eql(u8, bone_name, name)) return true;
}
// Check if bone name contains upper body keywords
const keywords = [_][]const u8{ "spine", "chest", "neck", "head", "shoulder", "arm", "hand", "clavicle" };
for (keywords) |keyword| {
if (std.mem.indexOf(u8, bone_name, keyword) != null) return true;
}
return false;
}
/// Blend two animations per-bone with selective upper/lower body blending
fn updateModelAnimationBoneCustom(model: *rl.Model, anim0: rl.ModelAnimation, frame0_in: i32, anim1: rl.ModelAnimation, frame1_in: i32, blend_in: f32, upper_body_blend: bool) void {
// Validate inputs
const poses0 = anim0.keyframePoses orelse return;
const poses1 = anim1.keyframePoses orelse return;
const bind_pose = model.skeleton.bindPose orelse return;
if (anim0.boneCount == 0 or anim1.boneCount == 0 or model.skeleton.boneCount == 0) return;
// Clamp blend factor to [0, 1]
const blend = @min(1.0, @max(0.0, blend_in));
// Ensure frame indices are valid
const frame0: usize = @intCast(std.math.clamp(frame0_in, 0, anim0.keyframeCount - 1));
const frame1: usize = @intCast(std.math.clamp(frame1_in, 0, anim1.keyframeCount - 1));
// Get bone count (use minimum of all to be safe)
const bone_count = @min(model.skeleton.boneCount, anim0.boneCount, anim1.boneCount);
const bones = model.skeleton.bones.?;
const bone_matrices = model.boneMatrices.?;
// Blend each bone
for (0..bone_count) |bone_index| {
// Determine blend factor for this bone
var bone_blend_factor = blend;
// If upper body blending is enabled, use different blend factors for upper vs lower body
if (upper_body_blend) {
const bone_name = bones[bone_index].nameSlice();
// Upper body: use anim1 (attack), Lower body: use anim0 (walk)
// blend = 0.0 means full anim0 (walk), 1.0 means full anim1 (attack)
bone_blend_factor = if (isUpperBodyBone(bone_name))
blend // Upper body: blend towards anim1 (attack)
else
1.0 - blend; // Lower body: blend towards anim0 (walk) - invert the blend
}
// Get transforms from both animations
const bind_transform = bind_pose[bone_index];
const anim_transform0 = poses0[frame0].?[bone_index];
const anim_transform1 = poses1[frame1].?[bone_index];
// Blend the transforms
const blended: rl.Transform = .{
.translation = rm.vector3Lerp(anim_transform0.translation, anim_transform1.translation, bone_blend_factor),
.rotation = rm.quaternionSlerp(anim_transform0.rotation, anim_transform1.rotation, bone_blend_factor),
.scale = rm.vector3Lerp(anim_transform0.scale, anim_transform1.scale, bone_blend_factor),
};
// Convert bind pose and blended transform to matrices
const bind_matrix = transformToMatrix(bind_transform);
const blended_matrix = transformToMatrix(blended);
// Calculate final bone matrix (similar to UpdateModelAnimationBones)
bone_matrices[bone_index] = rm.matrixMultiply(rm.matrixInvert(bind_matrix), blended_matrix);
}
// CPU skinning, updates CPU buffers and uploads them to GPU (if available)
// NOTE: Fallback in case GPU skinning is not supported or enabled
for (model.meshSlice()) |mesh| {
// Skip if missing bone data or missing anim buffers initialization
const bone_weights = mesh.boneWeights orelse continue;
const bone_indices = mesh.boneIndices orelse continue;
const anim_vertices_ptr = mesh.animVertices orelse continue;
const anim_normals_ptr = mesh.animNormals orelse continue;
const vertex_values_count: usize = @intCast(mesh.vertexCount * 3);
const vertices = mesh.vertices.?[0..vertex_values_count];
const normals: ?[]const f32 = if (mesh.normals) |n| n[0..vertex_values_count] else null;
const anim_vertices = anim_vertices_ptr[0..vertex_values_count];
const anim_normals = anim_normals_ptr[0..vertex_values_count];
var bone_counter: usize = 0;
var buffer_update_required = false; // Flag to check when anim vertex information is updated
var v_counter: usize = 0;
while (v_counter < vertex_values_count) : (v_counter += 3) {
const anim_vertex = anim_vertices[v_counter..][0..3];
const anim_normal = anim_normals[v_counter..][0..3];
anim_vertex.* = .{ 0, 0, 0 };
anim_normal.* = .{ 0, 0, 0 };
// Iterates over 4 bones per vertex
for (0..4) |_| {
defer bone_counter += 1;
const bone_weight = bone_weights[bone_counter];
const bone_matrix = bone_matrices[bone_indices[bone_counter]];
// Early stop when no transformation will be applied
if (bone_weight == 0.0) continue;
const v = rm.vector3Transform(.{ .x = vertices[v_counter], .y = vertices[v_counter + 1], .z = vertices[v_counter + 2] }, bone_matrix);
anim_vertex[0] += v.x * bone_weight;
anim_vertex[1] += v.y * bone_weight;
anim_vertex[2] += v.z * bone_weight;
buffer_update_required = true;
// Normals processing
// NOTE: We use meshes.baseNormals (default normal) to calculate meshes.normals (animated normals)
if (normals) |base_normals| {
const n = rm.vector3Transform(
.{ .x = base_normals[v_counter], .y = base_normals[v_counter + 1], .z = base_normals[v_counter + 2] },
rm.matrixTranspose(rm.matrixInvert(bone_matrix)),
);
anim_normal[0] += n.x * bone_weight;
anim_normal[1] += n.y * bone_weight;
anim_normal[2] += n.z * bone_weight;
}
}
}
if (buffer_update_required) {
// Update GPU vertex buffers with updated data (position + normals)
const vbo_id = mesh.vboId.?;
rl.gl.updateVertexBuffer(vbo_id[@intCast(@backingInt(rl.ShaderLocationIndex.vertex_position))], std.mem.sliceAsBytes(anim_vertices), 0);
if (normals != null) rl.gl.updateVertexBuffer(vbo_id[@intCast(@backingInt(rl.ShaderLocationIndex.vertex_normal))], std.mem.sliceAsBytes(anim_normals), 0);
}
}
}
/// Transform to matrix: scale, then rotate, then translate
fn transformToMatrix(t: rl.Transform) rl.Matrix {
return rm.matrixMultiply(
rm.matrixMultiply(rm.matrixScale(t.scale.x, t.scale.y, t.scale.z), rm.quaternionToMatrix(t.rotation)),
rm.matrixTranslate(t.translation.x, t.translation.y, t.translation.z),
);
}/*******************************************************************************************
*
* 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)
*
********************************************************************************************/
#include "raylib.h"
#include "raymath.h"
#include "rlgl.h" // Requried for: rlUpdateVertexBuffer() (CPU-skinning)
#include <string.h> // Required for: memcpy()
#include <stdlib.h> // Required for: NULL
#if defined(PLATFORM_DESKTOP)
#define GLSL_VERSION 330
#else // PLATFORM_ANDROID, PLATFORM_WEB
#define GLSL_VERSION 100
#endif
//------------------------------------------------------------------------------------
// Module Functions Declaration
//------------------------------------------------------------------------------------
static bool IsUpperBodyBone(const char *boneName);
static void UpdateModelAnimationBoneCustom(Model *model, ModelAnimation *anim1, int frame1,
ModelAnimation *anim2, int frame2, float blend, bool upperBodyBlend);
//------------------------------------------------------------------------------------
// Program main entry point
//------------------------------------------------------------------------------------
int main(void)
{
// Initialization
//--------------------------------------------------------------------------------------
const int screenWidth = 800;
const int screenHeight = 450;
InitWindow(screenWidth, screenHeight, "raylib [models] example - animation blend custom");
// Define the camera to look into our 3d world
Camera camera = { 0 };
camera.position = (Vector3){ 4.0f, 4.0f, 4.0f }; // Camera position
camera.target = (Vector3){ 0.0f, 1.0f, 0.0f }; // Camera looking at point
camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; // Camera up vector (rotation towards target)
camera.fovy = 45.0f; // Camera field-of-view Y
camera.projection = CAMERA_PERSPECTIVE; // Camera projection type
// Load gltf model
Model model = LoadModel("resources/models/gltf/greenman.glb");
Vector3 position = { 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
Shader skinningShader = LoadShader(TextFormat("resources/shaders/glsl%i/skinning.vs", GLSL_VERSION),
TextFormat("resources/shaders/glsl%i/skinning.fs", GLSL_VERSION));
model.materials[1].shader = skinningShader;
// Load gltf model animations
int animCount = 0;
ModelAnimation *anims = LoadModelAnimations("resources/models/gltf/greenman.glb", &animCount);
// Use specific animation indices: 2-walk/move, 3-attack
int animIndex0 = 2; // Walk/Move animation (index 2)
int animIndex1 = 3; // Attack animation (index 3)
int animCurrentFrame0 = 0;
int animCurrentFrame1 = 0;
// Validate indices
if (animIndex0 >= animCount) animIndex0 = 0;
if (animIndex1 >= animCount) animIndex1 = (animCount > 1) ? 1 : 0;
bool upperBodyBlend = true; // Toggle: true = upper/lower body blending, false = uniform blending (50/50)
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
//--------------------------------------------------------------------------------------
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
// Update
//----------------------------------------------------------------------------------
UpdateCamera(&camera, CAMERA_ORBITAL);
// Toggle upper/lower body blending mode (SPACE key)
if (IsKeyPressed(KEY_SPACE)) upperBodyBlend = !upperBodyBlend;
// Update animation frames
ModelAnimation anim0 = anims[animIndex0];
ModelAnimation 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)
float blendFactor = (upperBodyBlend? 1.0f : 0.5f);
UpdateModelAnimationBoneCustom(&model, &anim0, animCurrentFrame0,
&anim1, animCurrentFrame1, blendFactor, upperBodyBlend);
// raylib provided animation blending function
//UpdateModelAnimationEx(model, anim0, (float)animCurrentFrame0,
// anim1, (float)animCurrentFrame1, blendFactor);
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(RAYWHITE);
BeginMode3D(camera);
DrawModel(model, position, 1.0f, WHITE);
DrawGrid(10, 1.0f);
EndMode3D();
// Draw UI
DrawText(TextFormat("ANIM 0: %s", anim0.name), 10, 10, 20, GRAY);
DrawText(TextFormat("ANIM 1: %s", anim1.name), 10, 40, 20, GRAY);
DrawText(TextFormat("[SPACE] Toggle blending mode: %s",
upperBodyBlend? "Upper/Lower Body Blending" : "Uniform Blending"),
10, GetScreenHeight() - 30, 20, DARKGRAY);
EndDrawing();
//----------------------------------------------------------------------------------
}
// De-Initialization
//--------------------------------------------------------------------------------------
UnloadModelAnimations(anims, animCount); // Unload model animation
UnloadModel(model); // Unload model and meshes/material
UnloadShader(skinningShader); // Unload GPU skinning shader
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}
//----------------------------------------------------------------------------------
// Module Functions Definition
//----------------------------------------------------------------------------------
// Check if a bone is part of upper body (for selective blending)
static bool IsUpperBodyBone(const char *boneName)
{
// Common upper body bone names (adjust based on your model)
if (TextIsEqual(boneName, "spine") || TextIsEqual(boneName, "spine1") || TextIsEqual(boneName, "spine2") ||
TextIsEqual(boneName, "chest") || TextIsEqual(boneName, "upperChest") ||
TextIsEqual(boneName, "neck") || TextIsEqual(boneName, "head") ||
TextIsEqual(boneName, "shoulder") || TextIsEqual(boneName, "shoulder_L") || TextIsEqual(boneName, "shoulder_R") ||
TextIsEqual(boneName, "upperArm") || TextIsEqual(boneName, "upperArm_L") || TextIsEqual(boneName, "upperArm_R") ||
TextIsEqual(boneName, "lowerArm") || TextIsEqual(boneName, "lowerArm_L") || TextIsEqual(boneName, "lowerArm_R") ||
TextIsEqual(boneName, "hand") || TextIsEqual(boneName, "hand_L") || TextIsEqual(boneName, "hand_R") ||
TextIsEqual(boneName, "clavicle") || TextIsEqual(boneName, "clavicle_L") || TextIsEqual(boneName, "clavicle_R"))
{
return true;
}
// Check if bone name contains upper body keywords
if (strstr(boneName, "spine") != NULL || strstr(boneName, "chest") != NULL ||
strstr(boneName, "neck") != NULL || strstr(boneName, "head") != NULL ||
strstr(boneName, "shoulder") != NULL || strstr(boneName, "arm") != NULL ||
strstr(boneName, "hand") != NULL || strstr(boneName, "clavicle") != NULL)
{
return true;
}
return false;
}
// Blend two animations per-bone with selective upper/lower body blending
static void UpdateModelAnimationBoneCustom(Model *model, 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 = fminf(1.0f, fmaxf(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)
unsigned int boneCount = model->skeleton.boneCount;
if (anim0->boneCount < boneCount) boneCount = anim0->boneCount;
if (anim1->boneCount < boneCount) boneCount = anim1->boneCount;
// Blend each bone
for (unsigned int boneIndex = 0; boneIndex < boneCount; boneIndex++)
{
// Determine blend factor for this bone
float boneBlendFactor = blend;
// If upper body blending is enabled, use different blend factors for upper vs lower body
if (upperBodyBlend)
{
const char *boneName = model->skeleton.bones[boneIndex].name;
bool 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
Transform *bindTransform = &model->skeleton.bindPose[boneIndex];
Transform *animTransform0 = &anim0->keyframePoses[frame0][boneIndex];
Transform *animTransform1 = &anim1->keyframePoses[frame1][boneIndex];
// Blend the transforms
Transform blended = { 0 };
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
Matrix 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
Matrix 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 (int m = 0; m < model->meshCount; m++)
{
Mesh mesh = model->meshes[m];
Vector3 animVertex = { 0 };
Vector3 animNormal = { 0 };
const int vertexValuesCount = mesh.vertexCount*3;
int boneIndex = 0;
int boneCounter = 0;
float boneWeight = 0.0f;
bool 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 (int 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 (int j = 0; j < 4; j++, boneCounter++)
{
boneWeight = mesh.boneWeights[boneCounter];
boneIndex = mesh.boneIndices[boneCounter];
// Early stop when no transformation will be applied
if (boneWeight == 0.0f) continue;
animVertex = (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 = (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)
rlUpdateVertexBuffer(mesh.vboId[SHADER_LOC_VERTEX_POSITION], mesh.animVertices, mesh.vertexCount*3*sizeof(float), 0);
if (mesh.normals != NULL) rlUpdateVertexBuffer(mesh.vboId[SHADER_LOC_VERTEX_NORMAL], mesh.animNormals, mesh.vertexCount*3*sizeof(float), 0);
}
}
}
}
From raylib 6.2. The original on GitHub.