目录

  • 每日一句正能量
  • 摘要
  • 一、引言:为什么骨骼动画如此重要?
  • 二、骨骼结构
  • 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
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐