首页
/ three.js MD2CharacterComplex 详解:构建可换肤、可换武器、支持行为状态机与动画融合的 MD2 角色管理器

three.js MD2CharacterComplex 详解:构建可换肤、可换武器、支持行为状态机与动画融合的 MD2 角色管理器

2026-09-07 11:54:02作者:伍希望

MD2CharacterComplex 是 three.js addons 中面向 MD2(Quake II 时代经典模型格式)动画角色的一体化管理组件,位于 examples/jsm/misc/MD2CharacterComplex.js。相比同目录下的 MD2Character,它提供了更大、更完整的 API:不仅能加载“身体 + 武器”多部件模型与多套皮肤,还内建了由输入驱动的运动学模型(加速/减速/转向)、攻击/跳跃/蹲伏等行为到动画的状态映射,以及基于过渡帧的动画混合。读完本文,你将掌握该组件的配置结构、全部公开属性与方法,并能照搬仓库自带的控制台示例实现“方向键操控 + 换肤 + 换武器 + 阴影 + 克隆”的完整角色场景。

webgl_loader_md2_control 示例截图

上图:使用 MD2CharacterComplex 生成的一排不同皮肤 Ogre 角色(示例 webgl_loader_md2_control.html 运行效果)。

概览:它解决什么问题

MD2 是一种早期 3D 游戏模型格式:几何体本身不含骨骼,动画由一系列“顶点变形目标(morph targets)”表达。MD2CharacterComplex 正是围绕这一特性设计的角色管理器,它把以下职责全部封装起来:

  • 通过 MD2Loader 加载 .md2 几何体,并把“身体网格(body)”与“武器网格(weapon)”组合到一个 rootObject3D)节点下;
  • 为网格自动解析 morph 帧并生成动画片段;
  • 支持多套身体皮肤与武器皮肤、多个武器模型,提供 setSkin() / setWeapon() 切换 API;
  • 内置一组行为/控制标志(前进、后退、左转、右转、蹲伏、跳跃、攻击),自动在待机/移动/攻击/跳跃等动画之间切换;
  • 内置运动学模型updateMovementModel),按加速度、最大速度、转向角速度真实地驱动角色位移;
  • 过渡帧计数实现新旧动画的权重混合(crossfade)。

与 API 较简单的 MD2Character 相比,后者基于 AnimationMixer 管理动画,而 MD2CharacterComplex 基于 MorphBlendMesh,额外拥有行走/下蹲两档速度、行为状态机与完整移动模型,更适合做游戏类主角。

导入方式

MD2CharacterComplex 是 addon 组件,不属于 three.js 核心,必须显式导入(参见仓库手册 docs 目录下的安装/手册说明):

import { MD2CharacterComplex } from 'three/addons/misc/MD2CharacterComplex.js';

若在本地示例页面中以 ES module 方式运行(仓库示例使用 importmap,把 threethree/addons/ 分别映射到构建产物与 examples/jsm/),则写法如下:

import { MD2CharacterComplex } from 'three/addons/misc/MD2CharacterComplex.js';

其源码依赖链为:examples/jsm/misc/MD2CharacterComplex.jsMD2Loader(负责解析 .md2 与 morph targets)+ MorphBlendMesh(负责逐帧/加权混合播放 morph 动画)。

架构与底层原理

root 场景树

构造时组件会创建一个空的 this.root = new Object3D()(见 源码),此后身体网格、武器网格都被挂载到 root 之下。使用者只需把 character.root 加入自己的 Scene,所有网格便随之一同渲染、移动、显隐。

MorphBlendMesh 与动画自动生成

每个部件网格都是 MorphBlendMesh 实例。加载几何体后,内部 _createPart() 会调用:

mesh.autoCreateAnimations( this.animationFPS );

源码 _createPartMorphBlendMesh.autoCreateAnimations()MorphBlendMesh.js)会按正则 /([a-z]+)_?(\d+)/i 扫描 morphTargetDictionary 中的名字,例如 stand_0stand_1run_0… 会归并为 standrun 等命名帧区间,并为每个区间创建一个动画,FPS 使用 animationFPS(默认 6)。这正是 MD2 的“逐帧关键帧动画”能在 three.js 中还原的原因。

MorphBlendMesh 的动画支持 weight 权重(setAnimationWeight)、播放方向(正向/反向)与时间同步(setAnimationTime / getAnimationTime),这些是 MD2CharacterComplex 实现平滑动画切换与“身体-武器动画同步”的底层能力。

双材质设计

_createPart() 为每个部件创建两个 MeshLambertMaterial 并挂在网格上:

  • materialTexture:白色 map 皮肤贴图,正常渲染使用;
  • materialWireframe:纯色 0xffaa00wireframe: true,用于调试线框。

两者都随网格保存为 mesh.materialTexture / mesh.materialWireframe,供 setWireframe() 切换。

构造函数

new MD2CharacterComplex()

无参构造。构造器一次性完成默认属性的赋值并创建 root、空皮肤/武器数组,以及若干内部状态变量(speedbodyOrientationwalkSpeed = maxSpeedcrouchSpeed = maxSpeed * 0.5activeAnimationoldAnimation 等)。所有默认值见下表。

属性一览

属性 类型 默认值 说明
.scale number 1 网格统一缩放。注意加载时 root.position.y 会结合缩放与包围盒计算,保证角色“脚踩地”。
.animationFPS number 6 生成动画片段时使用的 FPS(MD2 动画通常较低帧率)。
.transitionFrames number 15 动画切换时的过渡帧数,控制融合时长(帧计数递减)。
.maxSpeed number 275 行走最大前向速度(每帧位移单位)。
.maxReverseSpeed number -275 最大反向(后退)速度,更新运动模型时被重写为 -maxSpeed
.frontAcceleration number 600 前进加速度。
.backAcceleration number 600 后退/倒车加速度。
.frontDeceleration number 600 松开前进键后的减速系数。
.angularSpeed number 2.5 左右转向的角速度(弧度/秒)。
.root Object3D new Object3D() 角色场景根节点,需手动加入 Scene
.meshBody Mesh null 身体网格,加载完成后可用。
.meshWeapon Mesh null 当前激活的武器网格。
.controls Object null 输入控制标志对象(布尔字段见下文)。置 null 时角色不做移动。
.skinsBody Array<Texture> [] 身体皮肤贴图数组。
.skinsWeapon Array<Texture> [] 武器皮肤贴图数组。
.weapons Array<Mesh> [] 已加载的武器网格数组(默认全部不可见,由 setWeapon 控制显隐)。
.currentSkin Texture undefined 当前皮肤(由 setSkin 设置)。
(内部)speed number 0 当前瞬时速度。
(内部)bodyOrientation number 0 当前身体朝向角。
(内部)walkSpeed / crouchSpeed number 见源码 行走/下蹲两档速度,可由配置覆盖。

以上默认值均可对照 构造函数源码 核实。其中供 updateBehaviors 使用的动画查找表存放在 this.animations,由 loadParts(config) 从配置中拷入。

controls 字段约定

updateBehaviors()updateMovementModel() 读取 this.controls 上的布尔标志(仓库示例 webgl_loader_md2_control.html 中定义):

const controls = {
    moveForward: false,
    moveBackward: false,
    moveLeft: false,
    moveRight: false,
    // crouch: false,
    // jump: false,
    // attack: false,
};

即:moveForward/moveBackward/moveLeft/moveRight(四方向)、crouch(蹲伏)、jump(跳跃)、attack(攻击)。你的键盘/手柄事件处理器只需把这些字段置 true/false,行为状态机与移动模型会自动响应。

loadParts(config):配置驱动的模型加载

loadParts(config) 是组件的主入口,config 是一个普通对象,字段含义如下(结构来自示例 webgl_loader_md2_control.html):

配置字段 类型 说明
baseUrl string 模型与皮肤所在的基础 URL(末尾带 /)。
body string 身体 .md2 文件名,最终加载 baseUrl + body
skins string[] 身体皮肤文件名数组,从 baseUrl + 'skins/' 加载,一个文件一套贴图。
weapons Array 武器描述数组,每项为 [ 模型文件名, 皮肤文件名 ],同样拼接在 skins/ 之下。
animations Object 行为键 → MD2 动画名映射,键包括 move / idle / jump / attack / crouchMove / crouchIdle / crouchAttack
walkSpeed number 行走档最大速度,覆盖默认 275
crouchSpeed number 蹲伏档最大速度。

Ogre 角色配置实例如下:

const configOgro = {

    baseUrl: 'models/md2/ogro/',

    body: 'ogro.md2',
    skins: [ 'grok.jpg', 'ogrobase.png', 'arboshak.png', 'ctf_r.png', 'ctf_b.png', 'darkam.png',
             'freedom.png', 'gib.png', 'gordogh.png', 'igdosh.png', 'khorne.png', 'nabogro.png',
             'sharokh.png' ],
    weapons: [ [ 'weapon.md2', 'weapon.jpg' ] ],
    animations: {
        move: 'run',
        idle: 'stand',
        jump: 'jump',
        attack: 'attack',
        crouchMove: 'cwalk',
        crouchIdle: 'cstand'
    },

    walkSpeed: 350,
    crouchSpeed: 175

};

上述资源真实存在于仓库 examples/models/md2/ogro/(另一个更完整的持枪角色资源见 examples/models/md2/ratamahatta/,含 12 种武器模型与对应贴图)。

加载过程(源码 loadParts):

  1. 记录动画映射、两档速度,并计算加载计数 loadCounter = config.weapons.length * 2 + config.skins.length + 1(身体几何 1 + 每个武器几何与其贴图 + 全部身体贴图)。
  2. loadTextures()TextureLoader 异步加载身体/武器贴图:设置 mapping = UVMappingcolorSpace = SRGBColorSpace,每成功一个调用一次 checkLoadingComplete()
  3. MD2Loader 加载身体 .md2:以 Box3 计算包围盒最小 Y,然后令 root.position.y = - scale * boundingBox.min.y,让角色底部贴地;随后 _createPart 生成带默认皮肤的身体网格并挂到 root
  4. 循环 config.weapons,用闭包 generateCallback(index, name) 为每个武器生成独立回调,加载武器几何、创建网格、初始 visible = false,存入 this.weapons[index]
  5. loadCounter 归零,调用 this.onLoadComplete()。因此务必在 onLoadComplete 中做后续初始化(加进场景、换肤、换武器等)。

从源码结构看,所有纹理的 URL 都被强制拼在 baseUrl + 'skins/' 目录下,模型文件则直接拼 baseUrl,布置资源目录时应遵守这一约定。

方法详解

场景装配

  • .enableShadows( enable : boolean ):遍历内部 meshes 数组(身体 + 全部武器),把每个网格的 castShadowreceiveShadow 一并设为传入值。使用前记得开启 renderer.shadowMap(示例见下节)。
  • .setVisible( enable : boolean ):遍历 meshes,把全部网格的 visible 设为传入值,可整体隐藏/显示角色。
  • .setWireframe( wireframeEnabled : boolean ):为身体/武器网格在 materialWireframematerialTexture 之间切换材质,实现调试用线框或恢复贴图。

换肤与换武器

  • .setSkin( index : number ):将 skinsBody[index] 赋给身体材质 map,并更新 currentSkin。注意只有当材质不在线框模式(wireframe === false)时才生效。
  • .setWeapon( index : number ):先把所有武器 visible = false,再将 weapons[index] 显示出来并令其成为 meshWeapon;若当前有正在播放的动画(activeAnimation),还会让新武器立即 playAnimation 该片段,并调用 setAnimationTime 把武器对齐到身体当前帧——实现“换枪后动作无缝衔接”。

动画播放

  • .setAnimation( animationName : string ):把指定动画设为活动片段。源码会先做去重(同名或空名直接 return),随后让身体(若存在)把新动画权重置 0、开始播放,并记录 oldAnimation,最后令 blendCounter = transitionFrames 启动过渡;武器网格若存在会同步切换。见 源码 setAnimation
  • .setPlaybackRate( rate : number ):对身体与武器同时生效,实现为 duration = baseDuration / rate(rate 大于 1 加速,小于 1 减速)。因为 MD2 动画基于固定帧数,调整的是 MorphBlendMesh 的时间换算。
  • .updateAnimations( delta : number ):必须在动画循环中调用。核心逻辑是过渡帧插值:只要 blendCounter > 0,就计算 mix = (transitionFrames - blendCounter) / transitionFrames 并递减计数;随后对 meshBodymeshWeapon 分别执行 update(delta) 并把活动动画权重设为 mix、旧动画权重设为 1 - mix。于是两个动画在 transitionFrames 帧内平滑交叉淡化。

行为状态机与运动模型

  • .updateBehaviors():读取 controlsanimations 表,按下表决策“移动动画 / 待机动画”:

    • 蹲伏:crouchMove / crouchIdle,站立:move / idle
    • jump 为真:两者都指向 jump
    • attack 为真:蹲伏时指向 crouchAttack,站立时指向 attack
    • 只要任意方向键按下就切换为移动动画;当 |speed| < 0.2 * maxSpeed 且无方向输入时回到待机动画;
    • 依据 moveForward / moveBackward 调用 MorphBlendMesh 的 setAnimationDirectionForward/Backward,让动画正放/倒放(MD2 中后退常复用走路帧倒放)。

    以上映射可对照 源码 updateBehaviors

  • .updateMovementModel( delta : number ):按物理式模型更新速度与朝向(源码):

    1. 蹲伏时 maxSpeed = crouchSpeed,否则 maxSpeed = walkSpeed,并令 maxReverseSpeed = -maxSpeed
    2. 前进/后退时用 MathUtils.clamp[maxReverseSpeed, maxSpeed] 区间内累积 speed ± delta * acceleration
    3. 左/右转时累加 bodyOrientation ± delta * angularSpeed,并同时给一点前向速度(“转弯时不停下”);
    4. 无纵向输入时按 exponentialEaseOut(speed / maxSpeed) 平滑衰减速度直至 0,衰减系数取前后减速/加速度;
    5. 计算前向位移 forwardDelta = speed * delta,执行
      root.position.x += Math.sin( bodyOrientation ) * forwardDelta;
      root.position.z += Math.cos( bodyOrientation ) * forwardDelta;
      root.rotation.y = bodyOrientation;
      
      实现角色朝 bodyOrientation 方向前进且身体转向一致。
  • .update( delta : number ):便捷总入口(源码中存在但文档 API 列表未单列)。若 controls 非空先调用 updateMovementModel(delta),然后调用 updateBehaviors()updateAnimations(delta)。动画循环中通常只需调用它。

资源复用

  • .shareParts( original : MD2CharacterComplex ):从另一个已加载完成的 MD2CharacterComplex 复用其动画映射、两档速度、身体/武器皮肤数组,并基于 original.meshBody.geometry 与各武器几何重建自身的身体与武器网格(即几何体只加载一次,多角色共享,纹理通过不同 skinsBody[index] 区分外观)。这是“用一只基类角色克隆一排不同皮肤角色”的核心手段,实现见 源码 shareParts

实战:克隆多个可换肤角色

仓库自带的 examples/webgl_loader_md2_control.htmlMD2CharacterComplex 的完整演示,其流程可直接作为集成模板:

  1. 准备 controls 对象并监听键盘:方向键/WASD 对应四方向布尔位;示例中把 crouch / jump / attack 注释掉了,若需要可自行放开(并确保 config.animations 提供了对应片段)。
  2. 构造多个角色实例(一行内每个皮肤一个):
    const characters = [];
    const nSkins = configOgro.skins.length; // 13
    for ( let i = 0; i < nSkins; i ++ ) {
        const character = new MD2CharacterComplex();
        character.scale = 3;
        character.controls = controls;
        characters.push( character );
    }
    
  3. 只加载一次基类,在其 onLoadComplete 中克隆并初始化:
    baseCharacter.onLoadComplete = function () {
        let k = 0;
        for ( let i = 0; i < nSkins; i ++ ) {
            const cloneCharacter = characters[ k ];
            cloneCharacter.shareParts( baseCharacter );   // 共享几何与皮肤
            cloneCharacter.enableShadows( true );          // 投射/接收阴影
            cloneCharacter.setWeapon( 0 );                 // 拿起 0 号武器
            cloneCharacter.setSkin( i );                   // 第 i 套皮肤
            cloneCharacter.root.position.x = ( i - nSkins / 2 ) * 150;
            scene.add( cloneCharacter.root );
            k ++;
        }
    };
    baseCharacter.loadParts( configOgro );
    
  4. 让角色共享同一个 controls,在渲染循环中调用 update
    for ( let i = 0; i < characters.length; i ++ ) {
        characters[ i ].update( delta );
    }
    

示例还把一个 Gyroscope 挂到中间角色的 root 上(用 Gyroscope 隔离旋转,保证相机在角色转向时保持水平),并用 SunLight 与接受阴影的草地地面呈现效果。运行该示例需要的灯光/阴影侧配置为:开启 renderer.shadowMap、给主光源 castShadow = true、地面 receiveShadow = true,与 enableShadows(true) 配合即可看到角色实时阴影。

使用要点与注意事项

  • 加载计数与 onLoadCompleteloadParts 是异步的,loadCounter 依赖“几何与纹理逐个完成”递减。请把依赖加载结果的操作(换肤、换武器、加入场景、克隆)放进 onLoadComplete,否则可能拿到 meshBody === null
  • 几何复用优先 shareParts:要显示多个不同皮肤的角色时,先 loadParts 一个基类,再对每个实例 shareParts(baseCharacter),避免为每个角色重复加载同一份 .md2 与皮肤纹理。
  • 动画命名决定一切autoCreateAnimations 依赖 morph target 名字里的字母+数字模式分组,config.animations 的 value 必须与解析出的动画名一致(如 runstand)。若切不出动画,先检查 .md2 内帧命名与 animations 映射。
  • 面向与位移一致:位移采用 sin/cos(bodyOrientation)root.rotation.y = bodyOrientation,因此移动模型始终“朝哪转往哪走”。若要自行控制角色位置,修改 bodyOrientationspeed 比直接改坐标更符合其设计。
  • 性能与精度:MD2 动画帧率低(默认 6 FPS)会带来明显的逐帧感,这是格式特性而非 bug;若要更顺滑可提高 animationFPS(受限于模型本身帧数)或用帧间插值方案。贴图均按 SRGBColorSpace 处理,保证颜色在物理光照下正确。

小结

MD2CharacterComplex 把“MD2 模型加载、morph 动画生成、多皮肤/多武器、行为状态机、运动学移动、动画融合、多角色资源复用”全部收敛到一套简单的属性 + 方法 API 中。本文所述 API 与默认值均以仓库源码 examples/jsm/misc/MD2CharacterComplex.js 及可运行示例 examples/webgl_loader_md2_control.html 为准;需要更轻量的单角色方案可参考 MD2Character 文档,而模型解析细节可在 MD2Loader 文档MD2Loader 源码 中继续深入。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390