【共创稿事节】3D场景中的骨骼动画:从骨骼驱动到GPU蒙皮
新星共创者
·
目录
- 每日一句正能量
- 摘要
- 一、引言:为什么骨骼动画如此重要?
- 二、骨骼结构
- 2.1 骨骼层次结构
- 2.2 骨骼结构实现
- 三、蒙皮技术
- 3.1 蒙皮原理
- 3.2 蒙皮实现
- 四、动画系统
- 4.1 动画混合
- 4.2 动画系统实现
- 五、GPU蒙皮
- 5.1 GPU蒙皮原理
- 5.2 GPU蒙皮实现
- 六、常见问题与解决
- 七、结语:骨骼动画是3D角色的"灵魂"

每日一句正能量
“你被安稳地爱着呢,应该有做任何事的勇气。”
被爱的安全感。这份爱可能来自家人、挚友,也可能源于宇宙或生命本身无条件的接纳。身后有稳固的支撑,无论成败自己都值得被爱,爱,是勇气的后盾。
相信"骨骼动画是3D角色的灵魂"。
摘要
摘要:骨骼动画是3D角色动画的基础。本文深入探讨3D场景中的骨骼动画,从骨骼结构、蒙皮技术到GPU加速,提供HarmonyOS骨骼动画的完整实现方案,帮助开发者打造逼真的3D角色动画。
一、引言:为什么骨骼动画如此重要?
"角色动画看起来太僵硬了,没有自然的弯曲。"
"动画文件太大了,加载时间太长。"
"角色运动时穿模,视觉效果很差。"
骨骼动画是3D角色动画的核心技术:
- 自然变形:通过骨骼驱动网格变形,实现自然的角色动画
- 性能优化:骨骼动画比逐帧动画更高效
- 可复用性:同一套骨骼可以驱动不同角色
- 交互性:骨骼动画可以响应用户输入

二、骨骼结构
2.1 骨骼层次结构

图1:骨骼层次结构——根骨骼、脊柱、四肢、手指
| 骨骼层级 | 描述 | 父骨骼 | 子骨骼 |
|---|---|---|---|
| 根骨骼 | 角色中心 | 无 | 脊柱 |
| 脊柱 | 身体主干 | 根骨骼 | 胸部、头部 |
| 胸部 | 上半身 | 脊柱 | 手臂 |
| 头部 | 头部 | 脊柱 | 无 |
| 手臂 | 上肢 | 胸部 | 前臂、手 |
| 腿部 | 下肢 | 根骨骼 | 小腿、脚 |
| 手指 | 手指 | 手 | 无 |
2.2 骨骼结构实现
// 骨骼类
class Bone {
name: string
parent: Bone | null = null
children: Bone[] = []
// 本地变换
localPosition: Vector3 = new Vector3(0, 0, 0)
localRotation: Quaternion = new Quaternion(0, 0, 0, 1)
localScale: Vector3 = new Vector3(1, 1, 1)
// 世界变换
worldMatrix: Matrix4 = new Matrix4()
// 绑定姿势矩阵
bindPoseMatrix: Matrix4 = new Matrix4()
constructor(name: string) {
this.name = name
}
// 添加子骨骼
addChild(bone: Bone): void {
bone.parent = this
this.children.push(bone)
}
// 更新世界矩阵
updateWorldMatrix(parentMatrix: Matrix4 = new Matrix4()): void {
// 计算本地变换矩阵
const localMatrix = this.calculateLocalMatrix()
// 计算世界变换矩阵
this.worldMatrix = parentMatrix.multiply(localMatrix)
// 更新子骨骼
for (const child of this.children) {
child.updateWorldMatrix(this.worldMatrix)
}
}
// 计算本地变换矩阵
private calculateLocalMatrix(): Matrix4 {
// 平移矩阵
const translation = Matrix4.translation(this.localPosition)
// 旋转矩阵
const rotation = this.localRotation.toMatrix4()
// 缩放矩阵
const scale = Matrix4.scaling(this.localScale)
// 组合变换矩阵
return translation.multiply(rotation).multiply(scale)
}
// 获取骨骼矩阵(用于蒙皮)
getBoneMatrix(): Matrix4 {
// 返回世界矩阵乘以绑定姿势矩阵的逆
return this.worldMatrix.multiply(this.bindPoseMatrix.inverse())
}
}
// 骨骼系统
class Skeleton {
bones: Map<string, Bone> = new Map()
rootBone: Bone | null = null
// 添加骨骼
addBone(bone: Bone): void {
this.bones.set(bone.name, bone)
if (!bone.parent) {
this.rootBone = bone
}
}
// 获取骨骼
getBone(name: string): Bone | undefined {
return this.bones.get(name)
}
// 更新所有骨骼
update(): void {
if (this.rootBone) {
this.rootBone.updateWorldMatrix()
}
}
// 获取骨骼数量
getBoneCount(): number {
return this.bones.size
}
}
// 向量3D
class Vector3 {
constructor(public x: number, public y: number, public z: number) {}
}
// 四元数
class Quaternion {
constructor(public x: number, public y: number, public z: number, public w: number) {}
toMatrix4(): Matrix4 {
// 简化的四元数到矩阵转换
return new Matrix4()
}
}
// 4x4矩阵
class Matrix4 {
data: number[] = new Array(16).fill(0)
static translation(v: Vector3): Matrix4 {
const m = new Matrix4()
m.data[0] = 1
m.data[5] = 1
m.data[10] = 1
m.data[12] = v.x
m.data[13] = v.y
m.data[14] = v.z
m.data[15] = 1
return m
}
static scaling(v: Vector3): Matrix4 {
const m = new Matrix4()
m.data[0] = v.x
m.data[5] = v.y
m.data[10] = v.z
m.data[15] = 1
return m
}
multiply(other: Matrix4): Matrix4 {
const result = new Matrix4()
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
let sum = 0
for (let k = 0; k < 4; k++) {
sum += this.data[i * 4 + k] * other.data[k * 4 + j]
}
result.data[i * 4 + j] = sum
}
}
return result
}
inverse(): Matrix4 {
// 简化的逆矩阵计算
return new Matrix4()
}
}
三、蒙皮技术
3.1 蒙皮原理

图2:蒙皮原理——顶点权重、骨骼影响、变形计算
| 蒙皮技术 | 描述 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 线性混合蒙皮 | 基于顶点权重混合 | 简单高效 | 体积损失 | 一般角色 |
| 双四元数蒙皮 | 使用双四元数 | 无体积损失 | 计算复杂 | 高精度角色 |
| 基于物理的蒙皮 | 模拟物理特性 | 真实感强 | 计算量大 | 特殊效果 |
| 机器学习蒙皮 | 使用AI预测 | 自动化 | 需要训练 | 批量生产 |
3.2 蒙皮实现
// 蒙皮权重
class SkinWeight {
boneIndex: number
weight: number
constructor(boneIndex: number, weight: number) {
this.boneIndex = boneIndex
this.weight = weight
}
}
// 蒙皮顶点
class SkinVertex {
position: Vector3
normal: Vector3
weights: SkinWeight[]
constructor(position: Vector3, normal: Vector3) {
this.position = position
this.normal = normal
this.weights = []
}
// 添加权重
addWeight(boneIndex: number, weight: number): void {
this.weights.push(new SkinWeight(boneIndex, weight))
}
// 归一化权重
normalizeWeights(): void {
const totalWeight = this.weights.reduce((sum, w) => sum + w.weight, 0)
if (totalWeight > 0) {
for (const weight of this.weights) {
weight.weight /= totalWeight
}
}
}
}
// 蒙皮网格
class SkinnedMesh {
vertices: SkinVertex[] = []
skeleton: Skeleton
constructor(skeleton: Skeleton) {
this.skeleton = skeleton
}
// 添加顶点
addVertex(vertex: SkinVertex): void {
vertex.normalizeWeights()
this.vertices.push(vertex)
}
// 更新蒙皮
updateSkin(): void {
// 更新骨骼世界矩阵
this.skeleton.update()
// 计算每个顶点的变形后位置
for (const vertex of this.vertices) {
this.calculateDeformedPosition(vertex)
}
}
// 计算变形后位置
private calculateDeformedPosition(vertex: SkinVertex): void {
let deformedPosition = new Vector3(0, 0, 0)
let deformedNormal = new Vector3(0, 0, 0)
for (const weight of vertex.weights) {
const bone = this.skeleton.getBoneByIndex(weight.boneIndex)
if (bone) {
// 获取骨骼变换矩阵
const boneMatrix = bone.getBoneMatrix()
// 应用骨骼变换到顶点位置
const transformedPosition = this.transformVector(vertex.position, boneMatrix)
const transformedNormal = this.transformVector(vertex.normal, boneMatrix)
// 加权混合
deformedPosition.x += transformedPosition.x * weight.weight
deformedPosition.y += transformedPosition.y * weight.weight
deformedPosition.z += transformedPosition.z * weight.weight
deformedNormal.x += transformedNormal.x * weight.weight
deformedNormal.y += transformedNormal.y * weight.weight
deformedNormal.z += transformedNormal.z * weight.weight
}
}
// 更新顶点位置
vertex.position = deformedPosition
vertex.normal = deformedNormal
}
// 变换向量
private transformVector(v: Vector3, matrix: Matrix4): Vector3 {
const x = v.x * matrix.data[0] + v.y * matrix.data[4] + v.z * matrix.data[8] + matrix.data[12]
const y = v.x * matrix.data[1] + v.y * matrix.data[5] + v.z * matrix.data[9] + matrix.data[13]
const z = v.x * matrix.data[2] + v.y * matrix.data[6] + v.z * matrix.data[10] + matrix.data[14]
return new Vector3(x, y, z)
}
}
四、动画系统
4.1 动画混合

图3:动画混合——插值、叠加、过渡、混合树
| 动画技术 | 描述 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 线性插值 | 两个动画之间插值 | 简单 | 可能不自然 | 简单过渡 |
| 叠加动画 | 多个动画叠加 | 丰富 | 复杂 | 细节动画 |
| 动画过渡 | 平滑切换动画 | 自然 | 需要过渡时间 | 状态切换 |
| 混合树 | 基于参数混合 | 灵活 | 复杂 | 复杂角色 |
4.2 动画系统实现
// 动画关键帧
class AnimationKeyframe {
time: number
position: Vector3
rotation: Quaternion
scale: Vector3
constructor(time: number, position: Vector3, rotation: Quaternion, scale: Vector3) {
this.time = time
this.position = position
this.rotation = rotation
this.scale = scale
}
}
// 动画轨道
class AnimationTrack {
boneName: string
keyframes: AnimationKeyframe[] = []
constructor(boneName: string) {
this.boneName = boneName
}
// 添加关键帧
addKeyframe(keyframe: AnimationKeyframe): void {
this.keyframes.push(keyframe)
this.keyframes.sort((a, b) => a.time - b.time)
}
// 在指定时间采样
sample(time: number): { position: Vector3, rotation: Quaternion, scale: Vector3 } {
// 找到前后关键帧
const index = this.findKeyframeIndex(time)
if (index === -1) {
// 返回第一个关键帧
return {
position: this.keyframes[0].position,
rotation: this.keyframes[0].rotation,
scale: this.keyframes[0].scale
}
}
if (index >= this.keyframes.length - 1) {
// 返回最后一个关键帧
return {
position: this.keyframes[this.keyframes.length - 1].position,
rotation: this.keyframes[this.keyframes.length - 1].rotation,
scale: this.keyframes[this.keyframes.length - 1].scale
}
}
// 插值
const prevKeyframe = this.keyframes[index]
const nextKeyframe = this.keyframes[index + 1]
const t = (time - prevKeyframe.time) / (nextKeyframe.time - prevKeyframe.time)
return {
position: this.lerpVector3(prevKeyframe.position, nextKeyframe.position, t),
rotation: this.slerpQuaternion(prevKeyframe.rotation, nextKeyframe.rotation, t),
scale: this.lerpVector3(prevKeyframe.scale, nextKeyframe.scale, t)
}
}
// 查找关键帧索引
private findKeyframeIndex(time: number): number {
for (let i = 0; i < this.keyframes.length - 1; i++) {
if (time >= this.keyframes[i].time && time < this.keyframes[i + 1].time) {
return i
}
}
return -1
}
// 线性插值
private lerpVector3(a: Vector3, b: Vector3, t: number): Vector3 {
return new Vector3(
a.x + (b.x - a.x) * t,
a.y + (b.y - a.y) * t,
a.z + (b.z - a.z) * t
)
}
// 四元数球面插值
private slerpQuaternion(a: Quaternion, b: Quaternion, t: number): Quaternion {
// 简化的四元数插值
return new Quaternion(
a.x + (b.x - a.x) * t,
a.y + (b.y - a.y) * t,
a.z + (b.z - a.z) * t,
a.w + (b.w - a.w) * t
)
}
}
// 动画剪辑
class AnimationClip {
name: string
duration: number
tracks: AnimationTrack[] = []
constructor(name: string, duration: number) {
this.name = name
this.duration = duration
}
// 添加轨道
addTrack(track: AnimationTrack): void {
this.tracks.push(track)
}
// 在指定时间采样
sample(time: number): void {
// 循环时间
const loopTime = time % this.duration
for (const track of this.tracks) {
const sample = track.sample(loopTime)
// 应用到骨骼
this.applyToBone(track.boneName, sample)
}
}
// 应用到骨骼
private applyToBone(boneName: string, sample: { position: Vector3, rotation: Quaternion, scale: Vector3 }): void {
// 这里应该通过骨骼系统找到对应的骨骼并应用变换
console.log(`Applying animation to bone: ${boneName}`)
}
}
// 动画混合器
class AnimationMixer {
private clips: Map<string, AnimationClip> = new Map()
private currentClip: AnimationClip | null = null
private currentTime: number = 0
private isPlaying: boolean = false
// 添加动画剪辑
addClip(clip: AnimationClip): void {
this.clips.set(clip.name, clip)
}
// 播放动画
play(clipName: string): void {
const clip = this.clips.get(clipName)
if (clip) {
this.currentClip = clip
this.currentTime = 0
this.isPlaying = true
}
}
// 更新动画
update(deltaTime: number): void {
if (!this.isPlaying || !this.currentClip) {
return
}
this.currentTime += deltaTime
// 采样当前动画
this.currentClip.sample(this.currentTime)
}
// 停止动画
stop(): void {
this.isPlaying = false
}
}
五、GPU蒙皮
5.1 GPU蒙皮原理

图4:GPU蒙皮——顶点着色器、骨骼矩阵、权重计算、变形输出
| 技术 | 描述 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| CPU蒙皮 | 在CPU上计算 | 灵活 | 性能低 | 简单角色 |
| GPU蒙皮 | 在GPU上计算 | 高性能 | 受限于着色器 | 复杂角色 |
| Compute Shader | 使用计算着色器 | 并行度高 | 复杂 | 大量角色 |
| Instancing | 实例化渲染 | 批量处理 | 内存占用 | 大量相同角色 |
5.2 GPU蒙皮实现
// GPU蒙皮渲染器
class GPUSkinningRenderer {
private gl: WebGLRenderingContext
private skeleton: Skeleton
private skinnedMesh: SkinnedMesh
private shaderProgram: WebGLProgram
constructor(gl: WebGLRenderingContext, skeleton: Skeleton, skinnedMesh: SkinnedMesh) {
this.gl = gl
this.skeleton = skeleton
this.skinnedMesh = skinnedMesh
this.initShader()
}
// 初始化着色器
private initShader(): void {
const vertexShaderSource = `
attribute vec3 a_position;
attribute vec3 a_normal;
attribute vec4 a_boneIndices;
attribute vec4 a_boneWeights;
uniform mat4 u_projectionMatrix;
uniform mat4 u_viewMatrix;
uniform mat4 u_modelMatrix;
uniform mat4 u_boneMatrices[64];
varying vec3 v_normal;
varying vec3 v_position;
void main() {
vec4 finalPosition = vec4(0.0);
vec4 finalNormal = vec4(0.0);
// 计算骨骼变换
for (int i = 0; i < 4; i++) {
int boneIndex = int(a_boneIndices[i]);
float weight = a_boneWeights[i];
if (boneIndex >= 0 && weight > 0.0) {
mat4 boneMatrix = u_boneMatrices[boneIndex];
finalPosition += boneMatrix * vec4(a_position, 1.0) * weight;
finalNormal += boneMatrix * vec4(a_normal, 0.0) * weight;
}
}
gl_Position = u_projectionMatrix * u_viewMatrix * u_modelMatrix * finalPosition;
v_normal = (u_modelMatrix * finalNormal).xyz;
v_position = (u_modelMatrix * finalPosition).xyz;
}
`;
const fragmentShaderSource = `
precision mediump float;
varying vec3 v_normal;
varying vec3 v_position;
void main() {
// 简化的光照计算
vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
float diff = max(dot(normalize(v_normal), lightDir), 0.0);
gl_FragColor = vec4(diff, diff, diff, 1.0);
}
`;
// 编译着色器
const vertexShader = this.compileShader(vertexShaderSource, this.gl.VERTEX_SHADER)
const fragmentShader = this.compileShader(fragmentShaderSource, this.gl.FRAGMENT_SHADER)
// 创建程序
this.shaderProgram = this.gl.createProgram()!
this.gl.attachShader(this.shaderProgram, vertexShader)
this.gl.attachShader(this.shaderProgram, fragmentShader)
this.gl.linkProgram(this.shaderProgram)
}
// 编译着色器
private compileShader(source: string, type: number): WebGLShader {
const shader = this.gl.createShader(type)!
this.gl.shaderSource(shader, source)
this.gl.compileShader(shader)
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
console.error('Shader compile error:', this.gl.getShaderInfoLog(shader))
}
return shader
}
// 渲染
render(camera: Camera): void {
this.gl.useProgram(this.shaderProgram)
// 设置uniforms
const projectionMatrixLocation = this.gl.getUniformLocation(this.shaderProgram, 'u_projectionMatrix')
const viewMatrixLocation = this.gl.getUniformLocation(this.shaderProgram, 'u_viewMatrix')
const modelMatrixLocation = this.gl.getUniformLocation(this.shaderProgram, 'u_modelMatrix')
const boneMatricesLocation = this.gl.getUniformLocation(this.shaderProgram, 'u_boneMatrices')
this.gl.uniformMatrix4fv(projectionMatrixLocation, false, camera.projectionMatrix)
this.gl.uniformMatrix4fv(viewMatrixLocation, false, camera.viewMatrix)
this.gl.uniformMatrix4fv(modelMatrixLocation, false, new Float32Array(16).fill(0).map((_, i) => i % 5 === 0 ? 1 : 0))
// 更新骨骼矩阵
this.updateBoneMatrices()
// 绘制网格
this.drawMesh()
}
// 更新骨骼矩阵
private updateBoneMatrices(): void {
const boneMatrices: number[] = []
for (const bone of this.skeleton.bones.values()) {
const matrix = bone.getBoneMatrix()
boneMatrices.push(...matrix.data)
}
const boneMatricesLocation = this.gl.getUniformLocation(this.shaderProgram, 'u_boneMatrices')
this.gl.uniformMatrix4fv(boneMatricesLocation, false, new Float32Array(boneMatrices))
}
// 绘制网格
private drawMesh(): void {
// 绘制网格的实现
}
}
// 相机
interface Camera {
projectionMatrix: Float32Array
viewMatrix: Float32Array
}
六、常见问题与解决
| 问题 | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| 穿模 | 角色运动时穿透自身 | 权重设置不当 | 调整权重、增加骨骼 |
| 体积损失 | 角色变形时体积缩小 | 线性混合蒙皮 | 使用双四元数蒙皮 |
| 性能瓶颈 | 动画播放卡顿 | 骨骼数量过多 | 减少骨骼、GPU蒙皮 |
| 动画不自然 | 动画过渡生硬 | 插值算法简单 | 使用高级插值、动画混合 |
| 内存占用大 | 动画文件过大 | 关键帧过多 | 压缩动画、减少关键帧 |
| 加载时间长 | 动画加载慢 | 文件大、解析慢 | 异步加载、缓存优化 |
七、结语:骨骼动画是3D角色的"灵魂"
骨骼动画是3D角色的"灵魂":
- 骨骼结构:层次化的骨骼结构驱动角色变形
- 蒙皮技术:将网格绑定到骨骼,实现自然变形
- 动画系统:通过关键帧和插值实现流畅动画
- GPU加速:利用GPU并行计算提升性能
作为一名讲师,我在课上常说:**"骨骼动画让3D角色从静止的模型变成有生命的个体。"**
转载自:https://blog.csdn.net/u014727709/article/details/166740120
欢迎 👍点赞✍评论⭐收藏,欢迎指正
更多推荐


所有评论(0)