从零实战)
最近在做一个角色展示类的 Demo 时遇到一个很常见的需求让角色站在舞台中央用各种姿势展示外观和动作效果。这个功能在游戏里叫 Pose Show在数字人项目里叫角色预览或试衣间在电商 3D 展示里叫商品互动展示。标题虽然写得比较“燃”但背后的技术其实并不复杂核心就是模型、动画、相机、场景四件事。这篇文章会把从零搭建一个 3D 角色姿势展示系统的完整流程拆开讲包含可复制的 C# 脚本、场景搭建步骤、常见报错排查清单以及我在实际开发中总结的工程建议。适合刚接触 Unity 的开发者也适合想快速落地一个角色展示页面的朋友。1. 什么是 Pose Show它解决什么问题Pose Show 从字面理解是“姿势展示”。在游戏开发和虚拟数字人领域它通常指这样一个功能让一个 3D 角色站在舞台或展示台上通过切换不同的待机、胜利、技能、欢呼、坐下等姿势向玩家或用户展示角色的外观、服饰、武器和动作特征。这个功能最常见的应用场景有三个游戏商城角色预览玩家在购买皮肤或角色前需要旋转视角、切换动作确认这个角色值不值得买。抽卡结果展示抽到新角色后播放一段“获得角色”的 Pose Show强化获得感和仪式感。捏脸/换装系统试穿用户在编辑角色外观后需要实时看到不同姿势下的整体效果。很多刚接触 Unity 的开发者会把 Pose Show 理解成“让角色播放动画”但实际落地时会发现它比单纯播放动画要复杂一些。你需要处理模型导入、骨骼映射、动画状态切换、相机控制、场景灯光、UI 交互等多层内容。任何一个环节没配置好角色都可能出现姿势穿插、动画不播放、画面发黑等问题。从更高的角度理解Pose Show 是在解决“如何让用户快速、直观、低门槛地感知一个 3D 角色的表现力”这个问题。它把抽象的模型数据变成用户可以交互的视觉体验这也是为什么几乎所有包含角色买卖、收集、展示玩法的项目都需要这个功能。标题中的“突破天空和次元”更像是一个产品体验目标当角色从一个静态模型变成一个有姿势、有光影、有镜头交互的动态展示时它就已经“走出次元壁”成为用户可以感知的“角色”了。下面我们开始动手实现。2. 环境准备与版本说明本文以 Unity 为例讲解因为 Unity 在角色模型导入、Animation、Animator、UI 交互、移动端适配上都有现成的完整链路最适合快速搭建 Pose Show 系统。我的开发环境参考如下你的版本可以略有不同配置思路是一致的工具说明Unity以 2021.3 LTS 为例2022.3 LTS 同样适用开发语言C#模型工具Blender / 3ds Max / Mixamo可选模型格式FBX 或 GLB推荐 FBX动画来源模型自带动画、Mixamo 下载动画、自录动画操作系统Windows / macOS 均可IDEVisual Studio 2022 或 Rider也可用 VS Code需要说明的是版本不是硬性要求。Unity 2019 之后的版本在 Animator 和 Animation Clip 的使用方式上基本一致本文的核心思路可以直接复用。如果你的项目是 Unity 6 或更新版本也只需要注意个别 API 的命名空间变化。除了 Unity 本身你还需要准备一个带骨骼的人物模型。如果没有现成模型可以使用 Unity Asset Store 中的免费角色模型。使用 Mixamo 网站下载角色和动画Mixamo 支持自动绑定骨骼导出 FBX 后导入 Unity 非常方便。使用自己项目里的角色模型。动画素材方面Pose Show 需要的是“单姿势动画”或“短循环动作”例如待机、持剑站立、胜利姿势、触发技能姿势等。每个姿势最好单独一个 Animation Clip方便代码切换。3. 核心概念与实现原理在写代码之前先把涉及的几个核心概念弄清楚。这部分理解了后面排错会轻松很多。3.1 骨骼、T-Pose 与 Avatar3D 角色模型之所以能动是因为模型内部有骨骼层级结构网格顶点绑定到骨骼上骨骼旋转时网格会跟着变形。T-Pose 是模型建模时的标准姿势角色双臂水平展开像字母 T。A-Pose 双臂下垂约 45 度像字母 A。这两种姿势都是为了让骨骼绑定和蒙皮更方便。在 Unity 中导入带骨骼的 FBX 模型后需要告诉 Unity 这个模型的骨骼结构这就是 Avatar。Unity 支持两种 Avatar 类型Humanoid人形骨骼Unity 会自动映射骨骼支持动画重定向。Pose Show 用这种最方便因为你可以把同一个动画套在不同角色上。Generic非人形或自定义骨骼Unity 不会做标准映射适合四足动物、怪物或机械体。Pose Show 的目标是展示角色所以推荐使用 Humanoid。如果模型骨骼映射有问题会出现模型扭曲、动画错位、手指变形等现象。3.2 Animation Clip 与 AnimatorAnimation Clip 是一段独立的动画资源记录了骨骼在某个时间段内的位姿变化。Pose Show 中的每个姿势本质上就是一个 Animation Clip。Animator 是 Unity 中控制动画播放的组件它通过 Animator Controller 来管理动画状态。Animator Controller 里有多个 StateState 之间可以设置 Transition代码通过 Animator.Play、Animator.CrossFade 等接口切换状态。Pose Show 的实现思路通常有两种把所有姿势动画放在同一个 Animator Controller 中用参数控制切换。代码里直接拿到 Animation Clip 数组用 Animator.Play 按名称或索引播放。对于姿势展示这种场景我更推荐第二种思路的变体把 Clip 做成一个可配置数组代码按当前索引播放对应姿势。这样新增姿势时不需要改动画状态机只需要往数组里加一个 Clip。3.3 相机控制与交互Pose Show 的核心体验之一是“用户能自由观察角色”所以相机不能固定不动。常见的交互方式有鼠标拖拽旋转按住鼠标左键或中键拖动相机围绕角色旋转。滚轮缩放调整观察距离。自动旋转角色静止时相机缓慢环绕增加展示感。相机控制本质上是一个“围绕目标点旋转”的数学问题。我们可以维护两个角度值水平角 yaw 和垂直角 pitch然后根据目标点位置、距离、角度计算出相机坐标。计算方法Vector3 direction Quaternion.Euler(pitch, yaw, 0) * Vector3.back; camera.transform.position target.position direction * distance; camera.transform.LookAt(target);这个公式是后续 CameraOrbit 脚本的核心。4. 完整实战搭建一个 Pose Show 角色展示系统下面进入核心部分。我们搭建一个包含角色展示台、姿势切换、相机环绕、天空盒和次元门特效的练手项目。为了便于理解先把整体流程拆成几个步骤创建 Unity 项目与目录结构。导入角色模型和姿势动画。配置 Animator 和 Animation Clip。编写 PoseShowController 脚本。编写 CameraOrbit 相机脚本。搭建场景地面、灯光、天空盒、次元门粒子。添加 UI 按钮并绑定事件。运行验证。4.1 创建项目与目录结构打开 Unity Hub新建一个 3D 项目项目名称可以叫PoseShowDemo。项目创建完成后先在 Assets 目录下建好分类文件夹Assets/ ├── Scenes/ // 存放场景 ├── Models/ // 存放角色模型 ├── Animations/ // 存放姿势动画 ├── Scripts/ // 存放 C# 脚本 ├── Materials/ // 存放材质 ├── Prefabs/ // 存放预制体 └── UI/ // 存放 UI 资源建立清晰目录是工程习惯避免项目变大后资源找不到。特别是动画资源多的时候命名和分类直接影响协作效率。4.2 导入角色模型与姿势动画把自己准备好的 FBX 模型拖入Assets/Models目录。导入后在 Project 窗口选中模型在 Inspector 中检查以下配置Model 选项卡Scale Factor 通常为 1如果角色太大或太小先调整这个值不要直接在场景里放大模型。Rig 选项卡Animation Type 选择 Humanoid然后点击 Apply。Animation 选项卡如果 FBX 自带动画会列在 Animations 列表中把不需要的动画取消勾选减少包体体积。如果是单独的姿势动画文件比如从 Mixamo 下载的 FBX同样导入项目。Mixamo 下载时建议选择Without Skin这样只导入动画不会重复导入模型网格。导入后在 Project 窗口里找到每个动画文件在 Inspector 中确认Loop Time 是否勾选单姿势展示如果只是固定摆姿势可以不循环如果是呼吸、待机动作建议勾选。Loop Pose 是否勾选如果动画首尾帧有小跳变勾选后可以优化。4.3 创建 Animator 并配置动画我们用一个 Animator Controller 管理所有姿势动画。在Assets/Animations目录下右键 - Create - Animator Controller命名为PoseShowController。双击打开 Animator 窗口把准备好的姿势动画 Clip 拖入 Animator 窗口。此时每个 Clip 会自动生成一个 State但默认状态是第一个拖入的动画。由于我们是代码直接按下标播放所以 Animator 窗口里不需要设置复杂过渡保持每个 State 独立即可。然后把 Animator Controller 赋值给角色的 Animator 组件。选中场景中的角色模型确保 Inspector 中有 Animator 组件Controller 设置为PoseShowControllerAvatar 会自动绑定模型自带的 Avatar。这里说明一下为什么不在 Animator 窗口里做 Any State 过渡Pose Show 的姿势是离散展示代码直接 Play 指定 Clip 就能满足需求。如果后续要做“两个姿势之间平滑过渡”可以在代码中用CrossFadeInFixedTime同样不需要把状态机画得很复杂。4.4 编写 PoseShowController 脚本在Assets/Scripts目录下新建 C# 脚本命名为PoseShowController.cs。这个脚本的核心职责是持有 Animator 引用。持有姿势动画 Clip 数组。提供切换上一个、下一个、指定姿势的方法。支持自动轮播。代码如下using UnityEngine; /// summary /// Pose Show 姿势展示控制器 /// 负责切换角色展示姿势 /// /summary public class PoseShowController : MonoBehaviour { [Header(动画组件)] [SerializeField] private Animator animator; [Header(姿势动画列表)] [SerializeField] private AnimationClip[] poseClips; [Header(自动轮播设置)] [SerializeField] private bool autoPlay false; [SerializeField] private float switchInterval 3f; private int currentIndex 0; private float timer 0f; private void Start() { if (animator null) animator GetComponentAnimator(); if (poseClips null || poseClips.Length 0) { Debug.LogWarning(PoseShowController: poseClips 为空请配置姿势动画列表); return; } PlayPose(0); } private void Update() { if (!autoPlay) return; if (poseClips null || poseClips.Length 0) return; timer Time.deltaTime; if (timer switchInterval) { timer 0f; NextPose(); } } /// summary /// 播放下一个姿势 /// /summary public void NextPose() { if (poseClips null || poseClips.Length 0) return; int nextIndex (currentIndex 1) % poseClips.Length; PlayPose(nextIndex); } /// summary /// 播放上一个姿势 /// /summary public void PreviousPose() { if (poseClips null || poseClips.Length 0) return; int prevIndex (currentIndex - 1 poseClips.Length) % poseClips.Length; PlayPose(prevIndex); } /// summary /// 按索引播放指定姿势 /// /summary public void PlayPose(int index) { if (poseClips null || poseClips.Length 0) { Debug.LogWarning(PoseShowController: 姿势动画列表为空); return; } if (index 0 || index poseClips.Length) { Debug.LogWarning($PoseShowController: 索引 {index} 超出范围); return; } currentIndex index; animator.CrossFadeInFixedTime(poseClips[index].name, 0.15f); Debug.Log($当前展示姿势: {poseClips[index].name}); } /// summary /// 随机播放一个姿势 /// /summary public void PlayRandomPose() { if (poseClips null || poseClips.Length 0) return; int randomIndex Random.Range(0, poseClips.Length); PlayPose(randomIndex); } }代码说明CrossFadeInFixedTime的作用是让姿势切换有一个短暂的过渡而不是瞬间跳变。0.15f 表示过渡耗时 0.15 秒观感更自然。使用取模运算%实现循环切换这样当索引到达末尾时会自动回到第一个。autoPlay字段方便做展览模式角色自动轮播姿势。所有方法都是 public后续可以直接绑定给 UI 按钮。4.5 编写 CameraOrbit 相机环绕脚本接下来实现相机围绕角色旋转的功能。在Assets/Scripts目录下新建 C# 脚本命名为CameraOrbit.cs。这个脚本挂在 Main Camera 上通过鼠标拖拽控制视角通过滚轮控制距离。using UnityEngine; /// summary /// 相机围绕目标点旋转与缩放 /// /summary public class CameraOrbit : MonoBehaviour { [Header(目标点)] public Transform target; [Header(角度与距离)] public float distance 3f; public float yaw 0f; public float pitch 15f; [Header(操作灵敏度)] public float rotateSpeed 5f; public float scrollSpeed 1f; [Header(角度和距离限制)] public float minPitch -30f; public float maxPitch 60f; public float minDistance 1f; public float maxDistance 8f; [Header(自动旋转)] public bool autoRotate true; public float autoRotateSpeed 10f; private void Start() { if (target null) { GameObject obj GameObject.FindWithTag(Player); if (obj ! null) target obj.transform; } } private void LateUpdate() { if (target null) return; HandleMouseInput(); HandleScrollInput(); if (autoRotate) { yaw autoRotateSpeed * Time.deltaTime; } ApplyCameraPosition(); } private void HandleMouseInput() { if (Input.GetMouseButton(0)) { float deltaX Input.GetAxis(Mouse X) * rotateSpeed; float deltaY Input.GetAxis(Mouse Y) * rotateSpeed; yaw deltaX; pitch - deltaY; pitch Mathf.Clamp(pitch, minPitch, maxPitch); } } private void HandleScrollInput() { float scroll Input.GetAxis(Mouse ScrollWheel); distance - scroll * scrollSpeed; distance Mathf.Clamp(distance, minDistance, maxDistance); } private void ApplyCameraPosition() { Quaternion rotation Quaternion.Euler(pitch, yaw, 0f); Vector3 direction rotation * Vector3.back; Vector3 position target.position direction * distance; transform.position position; transform.LookAt(target.position); } }代码说明yaw是水平旋转角度pitch是垂直旋转角度。垂直方向限制在 -30 到 60 度防止视角钻到地面以下或绕过头顶。LateUpdate里更新相机位置这样可以在所有 Update 逻辑执行完后设置相机减少抖动。自动旋转开启后角色展示台会缓慢转动适合放在大屏或展厅模式。如果场景里没有目标对象脚本会自动查找 Tag 为Player的对象需要的情况下可以给角色设置 Player Tag。有一点需要注意新版本 Unity 如果项目启用了 Input System 包默认情况下Input.GetMouseButton会报错。你可以在 Player Settings 里把 Active Input Handling 设置为Both或者改用新 Input System 的接口。建议先用 Both方便新老代码共存。4.6 搭建“突破天空和次元”展示场景Pose Show 的视觉表现一部分来自角色本身另一部分来自场景氛围。我们可以通过天空盒、灯光、地面和粒子特效营造出“角色从次元裂缝中走出来”的感觉。4.6.1 地面与展示台新建一个空物体命名为Stage作为展示台父节点。在 Stage 下创建一个 Plane用作地面。将 Plane 的 Scale 调整为 (2, 1, 2)避免地面过大影响观感。在 Stage 下创建一个 Cylinder用作展示台底座。调整 Cylinder 的 Scale 为 (1.2, 0.1, 1.2)放在 Plane 上方一点比如 Y 0.1。给展示台底座一个带HDRP/Lit或URP/Lit的材质颜色选偏冷的蓝色或紫色配合后面的粒子特效会比较有“次元”感。4.6.2 灯光与天空盒场景中默认有平行光 Directional Light调整它的角度和强度让角色面部清晰、阴影自然。建议打开软阴影能让展示效果更柔和。设置天空盒菜单栏打开Window - Rendering - Lighting - Environment。Scene 选项卡下找到 Skybox Material。选择Skybox/Procedural按需调节 Atmosphere Thickness、Exposure 等参数。如果你使用了 URP 或 HDRP 项目天空盒设置入口可能略有不同但都能在 Lighting 环境中找到。4.6.3 添加“次元门”粒子特效次元门可以用 Particle System 实现不需要额外下载特效资源。创建步骤在场景中右键 - Effects - Particle System命名为PortalParticle。将 PortalParticle 放在展示台后方比如 (0, 1.2, -2)。让其初始旋转为 X 90 度这样粒子会螺旋向上旋转。设置粒子材质为偏紫色的发光材质颜色可以在 Material 里调整。调整 Start Speed、Start Size、Start Lifetime 等参数让粒子呈现旋转扩散效果。粒子参数不需要严格照抄根据你的场景尺寸微调即可。核心思路是PortalParticle 作为一个“背景动态光效”强化跨界面的氛围感。4.6.4 角色摆放将角色模型从 Project 窗口拖入 Stage 下调整位置使角色站在展示台中央比如 (0, 0, 0)再在展示台底座上微调 Y 轴高度。给角色添加 Animator 组件Controller 选择PoseShowController然后把姿势动画 Clip 数组在 Inspector 中拖入PoseShowController脚本的poseClips列表。同时把角色的 Tag 设置为Player这样相机脚本会自动找到目标点。4.7 添加 UI 按钮并绑定事件为了让用户能手动切换姿势我们添加两个 UI 按钮上一个、下一个。创建 UI在 Hierarchy 中右键 - UI - CanvasUnity 会自动创建 Canvas 和 EventSystem。在 Canvas 下创建两个 Button分别命名为PrevButton和NextButton。调整按钮文本为“上一个姿势”和“下一个姿势”。绑定按钮事件选中PrevButton在 Inspector 中找到 Button 组件的 On Click() 列表。点击加号把场景中挂有PoseShowController脚本的角色对象拖入。函数下拉框选择PoseShowController - PreviousPose()。同样方式给NextButton绑定NextPose()。配置完成后点击 Play可以看到角色展示第一个姿势。点击“下一个姿势”角色切换到下一个动作。按住鼠标左键拖拽相机围绕角色旋转。滚动滚轮视角拉近拉远。如果开启了自动轮播角色会每隔几秒自动切换姿势。5. 常见问题与排查思路在实现 Pose Show 的过程中最常遇到的问题集中在模型导入、动画播放和相机交互三个方向。下面整理一份排查清单。问题现象常见原因解决思路模型是粉红色材质丢失或未正确导入检查模型材质、Shader 是否与渲染管线匹配URP 项目需要使用 URP Shader角色播放动画后扭曲Avatar 骨骼映射不完整检查 Rig 类型确保 Humanoid 骨骼完全映射必要时使用 Auto Map动画播放没反应Animator 未正确赋值 / Clip 未配置检查 Animator 组件、Controller、poseClips 数组是否为空姿势切换瞬间跳变动画 Clip 首尾帧不一致勾选 Loop Pose或增加 CrossFade 过渡时间相机无法旋转没有按住鼠标左键 / 脚本未挂载确认 CameraOrbit 脚本挂在相机上且 target 已赋值相机穿模到角色内部minDistance 设置过小调大 minDistance或给相机添加碰撞检测UI 按钮点击无效Canvas 没有 EventSystem / UI 被遮挡检查场景中是否有 EventSystem检查 Canvas 层级打包后动画丢失动画 Clip 未加入 Build Settings确认动画资源在 Assets 中被引用而不是运行时动态加载重点说一下最隐蔽的一个坑模型显示粉红色。这个问题在新手项目中非常常见原因通常是项目使用 URP 或 HDRP 渲染管线但模型自带的材质 Shader 是旧版内置管线 Shader。解决办法是使用Edit - Rendering - Materials - Convert Selected Built-in Materials to URP或者在材质 Inspector 中手动替换为Universal Render Pipeline/Lit。另一个容易忽略的点是 Animator 的 Avatar 映射。如果模型从 Mixamo 下载导入 Unity 后一般能自动识别但如果是自己绑定骨骼的模型有时会出现骨骼错位。打开 Avatar 配置面板检查骨骼树节点颜色——全部绿色说明映射正确红色或黄色节点需要手动指定。6. 工程建议与最佳实践Pose Show 功能虽然看起来简单但在真实项目中往往要承担“角色卖相”的展示压力。如果只是 Demo 无所谓要上正式环境建议遵循以下几点。6.1 动画命名与资源管理姿势动画的命名应该能直接表达含义例如Pose_Stand、Pose_Win、Pose_Skill_01、Pose_Sit。不建议使用anim1、anim2这种命名因为后期动画一多维护成本会暴增。建议为每个角色创建一个专门的 Pose 动画文件夹按角色名分子目录Assets/ └── Animations/ └── Character_Hero/ ├── Idle.fbx ├── Pose_Win.fbx └── Pose_Skill_01.fbx如果是多角色项目每个角色的展示动画列表最好用 ScriptableObject 配置而不是直接暴露在 MonoBehaviour 上。这样可以做到“角色与数据分离”不同角色拥有不同的姿势集合不用为每个角色改场景。6.2 PoseShowController 扩展上面的示例只是基础版本实际项目中可能需要以下扩展姿势字幕切换姿势时显示当前姿势名称可以使用UnityEngine.Events.UnityEvent在切换时触发 UI 刷新。角色音效部分姿势附带给到语音或技能音效可以给 PoseClip 配置音频资源。展示模式需求可能是“展厅模式”即多个角色轮流上台展示这时候需要配合 Timeline 或协程做角色调度。换装系统Pose Show 常与换装联动切换到武器、翅膀、时装后需要刷新角色外观。这部分可以通过 SkinnedMeshRenderer 切换 mesh 和材质实现。6.3 相机与场景的性能控制Pose Show 很吃性能的地方往往是实时渲染负担而不是逻辑代码。实时阴影如果场景中角色较多建议优先使用烘焙光影或限制实时阴影的距离。粒子特效次元门粒子不宜太多移动端项目尤其要注意 Overdraw。LOD展示角色是视觉焦点不建议用低模 LOD但场景装饰、地面、背景模型需要 LOD 控制。相机后处理Bloom 常用来增强“次元感”但如果目标平台是低端机需要谨慎开启。6.4 交互兼容性鼠标拖拽旋转在 PC 上没问题但如果做手机项目需要用Input.touches实现单指旋转、双指缩放。建议把相机操作封装成单独的接口public interface ICameraInputHandler { bool IsDragging { get; } float DeltaX { get; } float DeltaY { get; } float ScrollValue { get; } }这样做的好处是PC 和移动端分别实现自己的输入逻辑相机脚本本身不用改。7. 总结与后续学习方向本文从零实现了一个 Pose Show 系统核心内容包括理解 Pose Show 在产品中的作用。使用 Unity 导入带骨骼的角色模型和姿势动画。通过 Animator 和 Animation Clip 控制姿势切换。使用 C# 脚本实现姿势切换、自动轮播。实现相机环绕、缩放交互。通过天空盒、灯光、粒子特效增强展示氛围。整理了常见报错和工程优化建议。如果你以前没有完整做过 Unity 角色展示建议按下面的顺序继续深入先把手头一个角色的 Pose Show 跑通熟悉模型导入和 Animator 流程。再尝试多角色配置用 ScriptableObject 管理每个角色的姿势列表。接着加入换装、特效、音效让展示更有“卖相”。最后做移动端适配处理触摸输入和性能优化。实际上Pose Show 的边界非常灵活。往简单了做它可能只是一个 ModelViewer往复杂了做它可以变成商城预览、抽卡演出、时装秀系统、虚拟展厅的一部分。核心还是先掌握“模型动画相机场景”这条链路。只要这条链路通了往上叠加任何业务功能都不会太难。如果你在配置过程中遇到报错建议先把 Unity 版本、渲染管线版本、模型来源这些信息整理好再排查。大多数姿势展示的问题都不是高深原理而是资源格式或组件引用没配对。希望这篇文章能帮你省下一些踩坑时间也欢迎在实际跑通后继续往多角色、换装、特效方向拓展试一下。