Animation blending

- transition smoothly between idle and run animations
- use quaternions (versors) instead of yaw/pitch/roll
- hide mouse and enable free-look mode by default on startup
- rotate model 180 degrees in Blender so it faces away from the camera by default
- add some makefile commands
This commit is contained in:
var
2026-05-03 14:38:57 -05:00
parent 0f38e9b4a2
commit d1530525ca
12 changed files with 161 additions and 96 deletions

View File

@@ -14,7 +14,7 @@ OBJDIRS := $(patsubst $(SRC)%, $(OBJ)%, $(shell find $(SRC) -type d))
CLEANDIRS := $(addsuffix /.clean, $(OBJDIRS)); CLEANDIRS := $(addsuffix /.clean, $(OBJDIRS));
# Explicit targets # Explicit targets
.PHONY: all release clean .PHONY: all release clean rebuild run
.DEFAULT_GOAL := all .DEFAULT_GOAL := all
# Build executable # Build executable
@@ -39,3 +39,11 @@ clean: $(CLEANDIRS)
rm -f $(EXE) $(REL) rm -f $(EXE) $(REL)
%.clean: %.clean:
rm -f $**.o rm -f $**.o
rebuild:
make clean
make all
run:
make all
./$(EXE)

Binary file not shown.

View File

@@ -10,11 +10,8 @@ void Camera_Init(Camera* c)
c->width[1] = 5.8f; c->width[1] = 5.8f;
c->width[2] = 0.6f; c->width[2] = 0.6f;
glm_vec3_zero(c->velocity); glm_vec3_zero(c->velocity);
glm_vec3_zero(c->look); glm_quat_identity(c->rotation);
glm_vec3_zero(c->up); Camera_UpdateVectors(c);
glm_vec3_zero(c->front);
glm_vec3_zero(c->right);
glm_vec3_zero(c->rotation);
} }
void Camera_GetViewMatrix(Camera* c, mat4 m) void Camera_GetViewMatrix(Camera* c, mat4 m)
@@ -24,30 +21,10 @@ void Camera_GetViewMatrix(Camera* c, mat4 m)
void Camera_UpdateVectors(Camera* c) void Camera_UpdateVectors(Camera* c)
{ {
if (c->rotation[0] > 180.0f) c->rotation[0] -= 360.0f; glm_quat_rotatev(c->rotation, GLM_FORWARD, c->front);
else if (c->rotation[0] <= -180.0f) c->rotation[0] += 360.0f; glm_quat_rotatev(c->rotation, GLM_YUP, c->up);
glm_quat_rotatev(c->rotation, GLM_XUP, c->right);
c->rotation[1] = glm_clamp(c->rotation[1], -89.0f, 89.0f);
c->rotation[2] = glm_clamp(c->rotation[2], -30.0f, 30.0f);
// calculate the front facing unit vector by yaw and pitch
c->front[0] = cos(glm_rad(c->rotation[0])) * cos(glm_rad(c->rotation[1]));
c->front[1] = sin(glm_rad(c->rotation[1]));
c->front[2] = sin(glm_rad(c->rotation[0])) * cos(glm_rad(c->rotation[1]));
// the look vector is the position + front // the look vector is the position + front
glm_vec3_add(c->position, c->front, c->look); glm_vec3_add(c->position, c->front, c->look);
// reset the up vector
c->up[0] = 0.0f;
c->up[1] = 1.0f;
c->up[2] = 0.0f;
// cross product points to the right, perpendicular to both front and up
glm_vec3_cross(c->front, c->up, c->right);
// pitch up or down, around the axis of the cross product
glm_vec3_rotate(c->up, glm_rad(c->rotation[1]), c->right);
// roll around the axis of the front vector
glm_vec3_rotate(c->up, glm_rad(c->rotation[2]), c->front);
// recalculate right vector after rolling
glm_vec3_cross(c->front, c->up, c->right);
} }

View File

@@ -11,7 +11,7 @@ typedef struct
vec3 up; vec3 up;
vec3 front; vec3 front;
vec3 right; vec3 right;
vec3 rotation; versor rotation;
} Camera; } Camera;
void Camera_Init(Camera* c); void Camera_Init(Camera* c);

View File

@@ -1,5 +1,6 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdio.h> #include <stdio.h>
#include <math.h>
#include "SDL2/SDL_timer.h" #include "SDL2/SDL_timer.h"
#include "game.h" #include "game.h"
#include "render.h" #include "render.h"
@@ -13,6 +14,7 @@ GameState *Game_New()
if (!Render_Init(gs)) return NULL; if (!Render_Init(gs)) return NULL;
gs->running = true; gs->running = true;
gs->input.freeLook = true;
return gs; return gs;
} }
@@ -38,7 +40,6 @@ void Game_Update(GameState *gs)
Camera_UpdateVectors(&gs->camera); Camera_UpdateVectors(&gs->camera);
const float speed = 0.3f;
vec3 move, front, right; vec3 move, front, right;
glm_vec3_zero(move); glm_vec3_zero(move);
front[0] = gs->camera.front[0]; front[0] = gs->camera.front[0];
@@ -50,6 +51,7 @@ void Game_Update(GameState *gs)
right[2] = gs->camera.right[2]; right[2] = gs->camera.right[2];
glm_vec3_normalize(right); glm_vec3_normalize(right);
// get player's desired movement direction
if (gs->input.w) glm_vec3_add(move, front, move); if (gs->input.w) glm_vec3_add(move, front, move);
else if (gs->input.s) glm_vec3_sub(move, front, move); else if (gs->input.s) glm_vec3_sub(move, front, move);
if (gs->input.a) glm_vec3_sub(move, right, move); if (gs->input.a) glm_vec3_sub(move, right, move);
@@ -57,15 +59,70 @@ void Game_Update(GameState *gs)
if (gs->input.q) move[1] = 1.0f; if (gs->input.q) move[1] = 1.0f;
else if (gs->input.e) move[1] = -1.0f; else if (gs->input.e) move[1] = -1.0f;
glm_vec3_scale_as(move, speed, move); gs->charIsMoving = gs->input.w || gs->input.s || gs->input.a || gs->input.d;
// blend idle and run animations
if (gs->charIsMoving)
{
gs->animBlend += delta / 500.0f;
if (gs->animBlend > 1.0f) gs->animBlend = 1.0f;
}
else
{
gs->animBlend -= delta / 500.0f;
if (gs->animBlend < 0.0f) gs->animBlend = 0.0f;
}
// apply acceleration
ShapeInstance *target = &(gs->testModel.instance); ShapeInstance *target = &(gs->testModel.instance);
float accel = 0.005f * delta;
glm_vec3_scale_as(move, accel, move);
glm_vec3_add(target->velocity, move, target->velocity);
glm_vec3_clamp(target->velocity, -1.0f, 1.0f);
// apply velocity, make camera follow
vec3 displacement; vec3 displacement;
glm_vec3_scale(gs->camera.front, -30.0f, displacement); glm_vec3_scale(gs->camera.front, -30.0f, displacement);
displacement[1] += 10.0f; displacement[1] += 10.0f;
glm_vec3_add(target->position, move, target->position); glm_vec3_add(target->position, target->velocity, target->position);
glm_vec3_add(target->position, displacement, gs->camera.position); glm_vec3_add(target->position, displacement, gs->camera.position);
// decelerate
glm_vec3_scale(target->velocity, 1.0f / ((0.01f * delta) + 1.0f), target->velocity);
if (gs->charIsMoving)
{
// gradually rotate model to face toward direction of velocity
float alpha = delta > 200.0f ? 1.0f : delta / 200.0f;
versor velQuat;
glm_quat_for(target->velocity, GLM_YUP, velQuat);
glm_quat_slerp(target->rotation, velQuat, alpha, target->rotation);
}
if (!gs->input.freeLook)
{
// stay within a certain angle relative to the camera yaw
const float pi = 3.14159265358979323846f;
const float turnThreshold = pi / 3.0f;
versor camRot, inverse, difference;
glm_quat_from_vecs(GLM_FORWARD, front, camRot);
glm_quat_inv(target->rotation, inverse);
glm_quat_mul(camRot, inverse, difference);
vec3 axis;
glm_quat_axis(difference, axis);
float angle = glm_quat_angle(difference);
if (angle > pi) angle -= pi + pi;
if (fabsf(angle) > turnThreshold)
{
angle += angle < 0.0f ? turnThreshold : -turnThreshold;
glm_quatv(difference, angle, axis);
glm_quat_mul(target->rotation, difference, target->rotation);
}
}
Camera_UpdateVectors(&gs->camera); Camera_UpdateVectors(&gs->camera);
} }

View File

@@ -22,6 +22,8 @@ typedef struct
typedef struct typedef struct
{ {
bool running; bool running;
bool charIsMoving;
float animBlend;
uint64_t previousTicks; uint64_t previousTicks;
InputState input; InputState input;
SDL_Window *window; SDL_Window *window;

View File

@@ -48,10 +48,26 @@ static void HandleKeyUp(GameState *gs, SDL_KeyCode sym)
static void HandleMouseMotion(SDL_MouseMotionEvent e, GameState* gs) static void HandleMouseMotion(SDL_MouseMotionEvent e, GameState* gs)
{ {
const float sensitivity = 0.4f; const float sensitivity = 0.01f;
Camera *camera = &gs->camera; Camera *camera = &gs->camera;
camera->rotation[0] += e.xrel * sensitivity; vec3 front;
camera->rotation[1] -= e.yrel * sensitivity; glm_vec3_copy(camera->front, front);
// extract the pitch angle and clamp it so the camera can't go upside down
float pitch = glm_deg(glm_rad(90) - glm_vec3_angle(GLM_YUP, front) - (e.yrel * sensitivity));
pitch = glm_rad(glm_clamp(pitch, -89.0f, 89.0f));
// apply the new pitch using the existing "right" vector
front[1] = 0.0f;
glm_vec3_normalize(front);
glm_vec3_rotate(front, pitch, camera->right);
// apply yaw (invalidates the "right" vector, that's why pitch is applied first)
glm_vec3_rotate(front, -e.xrel * sensitivity, GLM_YUP);
// update camera
glm_quat_for(front, GLM_YUP, camera->rotation);
Camera_UpdateVectors(camera);
} }
static void HandleMouseDown(SDL_MouseButtonEvent e, GameState* gs) static void HandleMouseDown(SDL_MouseButtonEvent e, GameState* gs)

View File

@@ -87,6 +87,7 @@ Model Model_LoadGltf(char *path)
printf("Loading glTF model file...\n"); printf("Loading glTF model file...\n");
Model model = {0}; Model model = {0};
model.instance.scale = 4.0f; model.instance.scale = 4.0f;
glm_quat_identity(model.instance.rotation);
cgltf_options options = {0}; cgltf_options options = {0};
cgltf_data *data = NULL; cgltf_data *data = NULL;
cgltf_result result = cgltf_parse_file(&options, path, &data); cgltf_result result = cgltf_parse_file(&options, path, &data);
@@ -217,6 +218,22 @@ Model Model_LoadGltf(char *path)
printf("skin: %s, joints: %d\n", skin->name, skin->joints_count); printf("skin: %s, joints: %d\n", skin->name, skin->joints_count);
model.jointMatrices = calloc(skin->joints_count, sizeof(mat4)); model.jointMatrices = calloc(skin->joints_count, sizeof(mat4));
for (int i = 0; i < data->animations_count; i++)
{
cgltf_animation *anim = data->animations + i;
if (0 == strncmp(anim->name, "run", 3))
{
printf("run animation: %s\n", anim->name);
model.run = anim;
}
else if (0 == strncmp(anim->name, "idle", 4))
{
printf("idle animation: %s\n", anim->name);
model.idle = anim;
}
}
for (int i = 0; i < skin->joints_count; i++) for (int i = 0; i < skin->joints_count; i++)
{ {
cgltf_node *joint = skin->joints[i]; cgltf_node *joint = skin->joints[i];

View File

@@ -9,6 +9,8 @@ typedef struct
void *data; void *data;
void *skin; void *skin;
void *head; void *head;
void *idle;
void *run;
mat4 *jointMatrices; mat4 *jointMatrices;
GLuint *vbos; GLuint *vbos;
GLuint vao; GLuint vao;

View File

@@ -1,6 +1,7 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <math.h>
#include "SDL2/SDL.h" #include "SDL2/SDL.h"
#include "GL/glew.h" #include "GL/glew.h"
#include "SDL2/SDL_opengl.h" #include "SDL2/SDL_opengl.h"
@@ -220,6 +221,7 @@ bool Render_Init(GameState *gs)
gs->window = SDL_CreateWindow("Sandbox", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1920, 1080, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);; gs->window = SDL_CreateWindow("Sandbox", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1920, 1080, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);;
gs->glContext = SDL_GL_CreateContext(gs->window); gs->glContext = SDL_GL_CreateContext(gs->window);
SDL_SetRelativeMouseMode(SDL_TRUE);
printf("Created OpenGL window.\n"); printf("Created OpenGL window.\n");
if (SDL_GL_SetSwapInterval(1) == 0) printf("Enabled VSync.\n"); if (SDL_GL_SetSwapInterval(1) == 0) printf("Enabled VSync.\n");
@@ -318,10 +320,7 @@ static void Transform(ShapeInstance *instance, mat4 *matrix)
glm_mat4_identity(tempMat); glm_mat4_identity(tempMat);
glm_translate(tempMat, instance->position); glm_translate(tempMat, instance->position);
glm_quat_rotate(tempMat, instance->rotation, tempMat);
glm_rotate_x(tempMat, instance->rotation[0], tempMat);
glm_rotate_y(tempMat, instance->rotation[1], tempMat);
glm_rotate_z(tempMat, instance->rotation[2], tempMat);
vec3 scale; vec3 scale;
scale[0] = instance->scale; scale[0] = instance->scale;
@@ -332,43 +331,8 @@ static void Transform(ShapeInstance *instance, mat4 *matrix)
memcpy(matrix, tempMat, sizeof(mat4)); memcpy(matrix, tempMat, sizeof(mat4));
} }
static void DrawModel(GameState *gs) static void ApplyAnim(cgltf_animation *anim, float t, float blend)
{ {
Model *model = &gs->testModel;
ShapeInstance instance = model->instance;
cgltf_data *data = model->data;
cgltf_skin *skin = model->skin;
cgltf_animation *anim = data->animations;
mat4 modelMatrix;
glm_mat4_identity(modelMatrix);
if (!gs->input.freeLook)
{
// rotate the whole model to stay within this angle relative to the camera yaw
const float turnThreshold = 1.047197f; // 60 degrees
const float deg180 = 3.14159f;
const float deg360 = 6.28318f;
// convert camera yaw (rotation around Y axis) to radians and add 90 degrees so it aligns with the gltf model
float camYawRadsGltf = (gs->camera.rotation[0] / -57.295828f) + 1.570795f;
if (camYawRadsGltf > deg180) camYawRadsGltf = camYawRadsGltf - deg360;
float *modelYaw = model->instance.rotation + 1;
float diff = camYawRadsGltf - *modelYaw;
if (diff > deg180) diff -= deg360;
else if (diff < -deg180) diff += deg360;
if (turnThreshold < diff)
*modelYaw = camYawRadsGltf - turnThreshold;
else if (diff < -turnThreshold)
*modelYaw = camYawRadsGltf + turnThreshold;
}
Transform(&model->instance, &modelMatrix);
uint64_t ticks = SDL_GetTicks64();
float t = ticks / 1000.0f;
while (t > 1.0f) t -= 1.0f;
// apply local transformations for each bone for the current animation frame
for (int i = 0; i < anim->channels_count; i++) for (int i = 0; i < anim->channels_count; i++)
{ {
cgltf_animation_channel* channel = &anim->channels[i]; cgltf_animation_channel* channel = &anim->channels[i];
@@ -406,7 +370,9 @@ static void DrawModel(GameState *gs)
cgltf_accessor_read_float(sampler->output, k + 1, p1, 3); cgltf_accessor_read_float(sampler->output, k + 1, p1, 3);
glm_vec3_lerp(p0, p1, alpha, result); glm_vec3_lerp(p0, p1, alpha, result);
// glm_vec3_copy is not used here due to potential struct alignment issues // glm_vec3_copy is not used here due to potential struct alignment issues
for (int x = 0; x < 3; x++) node->translation[x] = result[x]; memcpy(p0, node->translation, sizeof(vec3));
glm_vec3_lerp(p0, result, blend, result);
memcpy(node->translation, result, sizeof(vec3));
node->has_translation = true; node->has_translation = true;
} }
break; break;
@@ -417,7 +383,10 @@ static void DrawModel(GameState *gs)
cgltf_accessor_read_float(sampler->output, k + 1, q1, 4); cgltf_accessor_read_float(sampler->output, k + 1, q1, 4);
// slerp (not lerp) for rotation // slerp (not lerp) for rotation
glm_quat_slerp(q0, q1, alpha, result); glm_quat_slerp(q0, q1, alpha, result);
for (int x = 0; x < 4; x++) node->rotation[x] = result[x]; memcpy(q0, node->rotation, sizeof(versor));
glm_quat_slerp(q0, result, blend, result);
memcpy(node->rotation, result, sizeof(versor));
node->has_rotation = true; node->has_rotation = true;
} }
break; break;
@@ -427,7 +396,9 @@ static void DrawModel(GameState *gs)
cgltf_accessor_read_float(sampler->output, k, s0, 3); cgltf_accessor_read_float(sampler->output, k, s0, 3);
cgltf_accessor_read_float(sampler->output, k + 1, s1, 3); cgltf_accessor_read_float(sampler->output, k + 1, s1, 3);
glm_vec3_lerp(s0, s1, alpha, result); glm_vec3_lerp(s0, s1, alpha, result);
for (int x = 0; x < 3; x++) node->scale[x] = result[x]; memcpy(s0, node->scale, sizeof(vec3));
glm_vec3_lerp(s0, result, blend, result);
memcpy(node->scale, result, sizeof(vec3));
node->has_scale = true; node->has_scale = true;
} }
break; break;
@@ -435,25 +406,40 @@ static void DrawModel(GameState *gs)
break; break;
} }
} }
}
static void DrawModel(GameState *gs)
{
Model *model = &gs->testModel;
ShapeInstance *instance = &model->instance;
cgltf_data *data = model->data;
cgltf_skin *skin = model->skin;
mat4 modelMatrix;
glm_mat4_identity(modelMatrix);
Transform(instance, &modelMatrix);
uint64_t ticks = SDL_GetTicks64();
float t = ticks / 1000.0f;
while (t > 1.0f) t -= 1.0f;
// apply local transformations for each bone for the current animation frame
ApplyAnim(model->idle, t, 1.0f);
ApplyAnim(model->run, t, gs->animBlend);
if (!gs->input.freeLook) if (!gs->input.freeLook)
{ {
// make head look where camera is looking // make head look where camera is looking
cgltf_node *head = model->head; cgltf_node *head = model->head;
vec3 a, b, yAxis; vec3 modelFront, camFront;
glm_vec3_zero(a); glm_quat_rotatev(instance->rotation, GLM_FORWARD, modelFront);
glm_vec3_zero(b); glm_vec3_copy(gs->camera.front, camFront);
glm_vec3_zero(yAxis); modelFront[1] = 0.0f;
yAxis[1] = 1.0f; camFront[1] = 0.0f;
a[2] = 1.0f;
glm_vec3_rotate(a, model->instance.rotation[1], yAxis);
b[0] = gs->camera.front[0];
b[2] = gs->camera.front[2];
versor desiredRotation, temp; versor desiredRotation, temp;
glm_quat_from_vecs(a, b, desiredRotation); glm_quat_from_vecs(modelFront, camFront, desiredRotation);
for (int i = 0; i < 4; i++) temp[i] = head->rotation[i]; memcpy(temp, head->rotation, sizeof(versor));
glm_quat_mul(temp, desiredRotation, temp); glm_quat_mul(temp, desiredRotation, temp);
for (int i = 0; i < 4; i++) head->rotation[i] = temp[i]; memcpy(head->rotation, temp, sizeof(versor));
} }
// recalculate the joint matrices // recalculate the joint matrices

View File

@@ -32,7 +32,7 @@ Shape *Shape_New(GLfloat *vertices, int numVertices, GLushort *indices, int numI
instance->scale = 2.0f; instance->scale = 2.0f;
instance->collisionRadius = 1.0f; instance->collisionRadius = 1.0f;
glm_vec3_zero(instance->velocity); glm_vec3_zero(instance->velocity);
glm_vec3_zero(instance->rotation); glm_quat_identity(instance->rotation);
float *textureId = shape->instanceData + (i * 17); float *textureId = shape->instanceData + (i * 17);
*textureId = 6; *textureId = 6;
mat4 *matrix = (void*)(textureId + 1); mat4 *matrix = (void*)(textureId + 1);
@@ -105,7 +105,7 @@ Shape *Shape_MakePyramid(int numInstances)
instance->scale = 1.0f; instance->scale = 1.0f;
instance->collisionRadius = 1.0f; instance->collisionRadius = 1.0f;
glm_vec3_zero(instance->velocity); glm_vec3_zero(instance->velocity);
glm_vec3_zero(instance->rotation); glm_quat_identity(instance->rotation);
float *textureId = shape->instanceData + (i * 17); float *textureId = shape->instanceData + (i * 17);
*textureId = i; *textureId = i;
mat4 *matrix = (void*)(textureId + 1); mat4 *matrix = (void*)(textureId + 1);
@@ -154,7 +154,7 @@ Shape *Shape_MakePlane()
instance->scale = 1.0f; instance->scale = 1.0f;
instance->collisionRadius = 1.0f; instance->collisionRadius = 1.0f;
glm_vec3_zero(instance->velocity); glm_vec3_zero(instance->velocity);
glm_vec3_zero(instance->rotation); glm_quat_identity(instance->rotation);
float *textureId = shape->instanceData + (0 * 17); float *textureId = shape->instanceData + (0 * 17);
*textureId = 5; *textureId = 5;
mat4 *matrix = (void*)(textureId + 1); mat4 *matrix = (void*)(textureId + 1);

View File

@@ -11,10 +11,10 @@ enum
typedef struct typedef struct
{ {
vec3 position; vec3 position;
vec3 rotation;
vec3 velocity; vec3 velocity;
float scale; float scale;
float collisionRadius; float collisionRadius;
versor rotation;
} ShapeInstance; } ShapeInstance;
typedef struct typedef struct