欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

骨骼动画

最编程 2024-08-04 10:06:05
...

欢迎关注公众号:sumsmile, 图形学、移动开发~~

本文内容、代码、插图主要参考learnopengl -Skeletal-Animation ,原文代码有bug,不能正常运行,修复后的代码见文末链接

骨骼动画效果

前置知识

  1. opengl基础知识、Mesh概念
  2. 3D模型加载 learnopengl-cn.github.io/03 Model Loading/01 Assimp/

如果不熟悉以上知识,理解会比较跳跃

什么是骨骼动画

3D模型动画中,很重要的一种是"骨骼动画",在模型里定义一系列有"层级关系"的骨骼,通过骨骼的运动,驱动模型各模块作出不同的运动。

图1. 骨骼动画(Source: Redway 3D)

如上图人体模型,大腿相对小腿处于较高的层级,大腿运动小腿跟着动。

骨骼动画原理

感性的认识骨骼动画

假如图1中是个机器人的骨骼。机器人的动作比较僵硬,抬起小臂,只有小臂运动,不会影响其他部位。

像人类、动物,一个部位通常会受到多个骨骼的影响,比如肩膀的肌肉受到大臂、背、脖子等部位的影响。在实际工程中,单个点受到的影响来源是有上限的,比如下面的demo里我们设置成4,另外,每个影响源的权重也不一样。

图2-1

图2-2

图2-3

插值

骨骼动画只定义了离散的关键帧,中间的平滑需要做插值。

关键帧

插值后

常用的线性插值算法:
x = a * (1 - t) + b * t
a 和 b分别表示一个点的起始、终止坐标,随着时间t的变化,x表示t时刻的坐标值。

线性插值

OK,以上就是骨骼动画需要了解的最基础的知识。

代码实现

工程结构

demo工程使用CLion开发。主要考虑CLion跨平台,往其他平台迁移较方便。CLion的操作界面和主流的IDE差不多,很容易上手。

实现比较粗暴,调用的逻辑、渲染的loop直接在main.cpp里实现了,没有做过多的封装。

工程目录

文件明细

  1. main.cpp 处理渲染的调度逻辑
  2. render目录包含 "文件读写"、"骨骼数据"、"动画数据"、"mesh、model封装"、"shader封装"
  3. shader目录放置渲染中用到的着色器(如果连着色器都不知道,建议先从opengl开始学吧)
  4. third-part 第三方工具库,用到了5个三方库
  • assimp 比较简单的3D模型加载工具
  • glad opengl兼容工具,兼容各平台opengl环境的差异,简化gl环境配置
  • glfw 窗口管理类,opengl与硬件的中间层,和EGL、SDL类似
  • glm 常见的数学库,处理向量、矩阵等操作
  • stb image 图片加载工具,用来加载纹理资源

所有的三方库,我在demo工程里均以源码的方式集成联编,方便调试和学习。这些都是图形领域经典的开源库。

看下cmake文件,大概了解整个工程是如何组织的,注释写的很详细了。

# 设置cmake 最小版本为3.20,这个无所谓,只要设置的版本能跑的起来就行,一般不会报错
cmake_minimum_required(VERSION 3.20)
project(summerGL)
# 设置c++ 标准为c++ 14
set(CMAKE_CXX_STANDARD 14)
# 链接本地库,用到了系统库里的zip
LINK_DIRECTORIES(/usr/local/lib)
# 编译子工程 glfw
add_subdirectory(third-part/glfw-3.3.6)
# 链接glfw库
link_libraries(glfw)

# 编译子工程assimp
add_subdirectory(third-part/assimp-5.0.1)
# 链接assimp库
link_libraries(assimp)

#编译子工程glm
add_subdirectory(third-part/glm-0.9.9.8)
#链接glm库
link_libraries(glm)

# 添加要编译的源文件
set(SRCS src/glad.c )
# 编译主工程
add_executable(${PROJECT_NAME} ${SRCS} main.cpp )

# 配置root_directory,利用cmake的机制,动态生成项目根目录的读取类
configure_file(configuration/root_directory.h.in configuration/root_directory.h)
# 设置 头文件include目录
include_directories(${CMAKE_BINARY_DIR}/configuration)

# 设置第三方库的头文件导入目录
set(THIRD_PARTY_INC
        render/include
        third-part/glfw-3.3.6/include/
        third-part/glad/include/
        third-part/assimp-5.0.1/include
        third-part/glm-0.9.9.8
        third-part/stb-master)

message(STATUS ${THIRD_PARTY_INC})

# 导入第三方头文件
target_include_directories(${PROJECT_NAME} PUBLIC ${THIRD_PARTY_INC})

下面进入重点了,同学们打起精神了!!

模型加载

先简单扫一眼代码的实现,后面会详细讲为什么是这样

    // load models
    // Model 封装了3D模型文件的加载及数据
    Model ourModel(FileSystem::getPath("resources/objects/vampire/dancing_vampire.dae"));
    // Animation 封装了3D模型里动画定义的数据,还包括骨骼的权重
    Animation danceAnimation(FileSystem::getPath("resources/objects/vampire/dancing_vampire.dae"),&ourModel);
    // 动画执行器,主要工作是做插值,计算权重等
    Animator animator(&danceAnimation);

用到了3个类

  1. Model 封装了3D模型文件的加载及数据
  2. Animation 封装了3D模型里动画定义的数据,还包括骨骼的权重
  3. 动画执行器,主要工作是做插值,计算权重等

在渲染线程的while循环里,持续更新骨骼位置,驱动3D模型每个顶点、每个三角面片更新坐标,之后再不断的重新绘制3D模型,就形成了连续的动画.

相关代码:

while (!glfwWindowShouldClose(window))
{
    ...
    // 1. 更新骨骼、顶点坐标
    float currentFrame = glfwGetTime();
    deltaTime = currentFrame - lastFrame;
    lastFrame = currentFrame;
    animator.UpdateAnimation(deltaTime);
    ...
    
    
    // 2. 更新shader 变量,骨骼的权重设置到shader里,通过shader来控制点的偏移
    auto transforms = animator.GetFinalBoneMatrices();
        for (int i = 0; i < transforms.size(); ++i)
    ourShader.setMat4("finalBonesMatrices[" + std::to_string(i) + "]", transforms[i]);
    
    // 3. 绘制模型
    ourModel.Draw(ourShader);
}

demo工程里,还有键盘、鼠标输入的处理,用来控制相机视角的变化,比如旋转、缩放等。

可能比较抽象,建议把代码下载下来,自己跑一跑,对着讲解去理解代码的逻辑。

模型和动画数据解析

demo工程里使用dae格式的3D模型,实际上3D模型有几百种类型,最常见的大概十来种,dae格式的特点是能包含一个完整场景(多个3D模型、光照、相机、动画等),即一个dae文件可以渲染出一幅完整的画面。 dae 格式简介

DAE files can store data for an entire scene (Source: 3DVia via YouTube)

通过assimp加载出来的数据,包含aiScene(场景),如果你有游戏开发的基础,就很熟悉这套术语了。

3D动画数据结构

aiScene包含一个根节点 RootNode,和一组动画数据Animations[]

RootNode是一个aiNode类型,包含name、4*4矩阵变换(Transformation),一组子节点,简单解释下子节点的概念,如下图所示:

node的概念

aiScene里还包含一个Mesh数组,一个Mesh是一个最小渲染模块,包含了成像的所有元素,包括顶点、骨骼及每个骨骼影响其他顶点的权重、纹理素材重等。

骨骼对顶点的影响,通过顶点的vertexID来映射查找

骨骼权重

最后看下shader 片元着色器,对texture_diffuse纹理采样,没有特殊的处理

#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture_diffuse1;
void main()
{
    FragColor = texture(texture_diffuse1, TexCoords);
}

逻辑都在顶点着色器里,核心逻辑是找到影响当前点的所有骨骼,按照权重计算骨骼影响点的总和。注意,当前设置的影响骨骼上限是4,当然越多越真实但是会影响性能额。

其他的切线向量、法向量其实都没用上,可以不用关心,如果要加上光照,法向量norm就要参与计算了。

#version 330 core

layout(location = 0) in vec3 pos;
layout(location = 1) in vec3 norm;
layout(location = 2) in vec2 tex;
layout(location = 3) in vec3 tangent;
layout(location = 4) in vec3 bitangent;
layout(location = 5) in ivec4 boneIds;
layout(location = 6) in vec4 weights;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

const int MAX_BONES = 100;
const int MAX_BONE_INFLUENCE = 4;
uniform mat4 finalBonesMatrices[MAX_BONES];

out vec2 TexCoords;

void main()
{
    vec4 totalPosition = vec4(0.0f);
    for(int i = 0 ; i < MAX_BONE_INFLUENCE ; i++)
    {
        if(boneIds[i] == -1)
        continue;
        if(boneIds[i] >=MAX_BONES)
        {
            totalPosition = vec4(pos,1.0f);
            break;
        }
        vec4 localPosition = finalBonesMatrices[boneIds[i]] * vec4(pos,1.0f);
        totalPosition += localPosition * weights[i];
        vec3 localNormal = mat3(finalBonesMatrices[boneIds[i]]) * norm;
    }

    mat4 viewModel = view * model;
    gl_Position =  projection * viewModel * totalPosition;
    TexCoords = tex;
}

整个工程文件太大,超过了github的上限了,只上传了主要文件 github.com/summer-go/g…

完整工程及素材文件放在云盘: 链接: pan.baidu.com/s/1DJ4B4vNj… 密码: i2d7

欢迎关注公众号:sumsmile, 图形学、移动开发~~