需求
无论是2d或是3d游戏,换装都是比较受欢迎的游戏体验,也是游戏开发者经常需要面对的开发需求,本文主要介绍3D换装需求,关于2d换装(spine或龙骨)后续会向大家介绍。
从换装的方式分类,可以分为整体换装以及局部换装,整体换装较为简单,我们就不做讨论,本文主要介绍一下局部换装(其实理解过后也是非常的简单)。
从换装的模型分类, 主要分为两种类型:
一种类型是对于静态模型的换装,就是直接将身体需要换的Mesh更新即可。
另一种类型是动态模型的换装(有动作的模型)
本文主要介绍动态模型的换装实现。
效果展示
(素材仅用于学习交流)原理介绍
在开始描述换装前,首先要具备骨骼动画的知识,如果对骨骼动画的原理不熟悉,换装是比较难以理解的。换装的核心其实并不在换上,而是要理解为什么能换,而这些都和骨骼动画密不可分。
骨骼动画的组成:
图1(引用于shader实验室)网格(Mesh):
模型(Model)是由一个个三角形组成的,而这种三角形的学名则是网格(Mesh)
网格蒙皮数据(Skin Info)
顶点的Skin数据包括顶点受哪些骨骼影响以及这些骨骼影响该顶点时的权重(weight),另外对于每块骨骼还需要骨骼偏移矩阵(BoneOffsetMatrix)用来将顶点从Mesh空间变换到骨骼空间。可简单理解为:SkinMesh = Mesh+Skin Info
骨骼(Skeleton):
如图1,骨架由一系列具有层次关系的关节(骨骼)和关节链组成,是一种树结构,选择其中一个是根关节,其它关节是根关节的子孙,可以通过平移和旋转根关节移动并确定整个骨架在世界空间中的位置和方向。
骨骼的动画(关键帧)数据
骨骼动画是通过关键帧驱动骨骼运动,随之依次调整每块骨头的朝向和坐标,骨头再带动顶点运动,蒙皮信息就描述了每个顶点受哪些骨头的影响,以及他们的权重,这样骨骼动画就实现了运动以及形变。
实现思路
导入模型进入creator,可发现节点下含有SkinnedMeshRenderer组件,其中含有mesh属性,按照我的理解这里的Mesh特指SkinMesh = Mesh+Skin Info,而非普通的静态mesh。
动态模型换装需要更新SkinnedMeshRenderer组件的中SkinMesh,Skeleton(骨骼资源), SkinningRoot(骨骼根节点的引用——控制此模型的动画组件所在节点)。本案例中采取直接更换蒙皮网格渲染器组件(SkinnedMeshRenderer)的方式实现换装
实现步骤
骨骼动画及部位装备prefab的制作,核心——共享一套骨骼。动画师制作时,同一部位的不同装备绑定同一根骨骼,整体输出,在creator中将各部件装备制作为prefab后从主角删除,主角只保留一套默认装备。
主角节点需要关闭预烘焙功能,否则无法实时运算以实现换装功能。
初始化模型。建立Map<key-PartName, value-Node>,这一步是为了后续替换装备时可以检索到对应部位的节点
替换装备节点:
删除旧装备节点。检索Map,根据部位key-PartName获得OldNode引用,移除OldNode(保留骨骼根节点引用SkinningRoot,后续备用)。
增加新装备节点,加载部位A新装备prefab并实例化为NewNode,添加NewNode,
刷新部位key-PartName的value值为NewNode
刷新骨骼,取得步骤1中的SkinningRoot来刷新NewNode的SkinningRoot,完成。(我实现到这步,后续步骤为了节省性能大家可以研究)
合并mesh
合并贴图(贴图的宽高最好是2的N次方的值)
重新计算UV
核心代码
import { _decorator, Component, Node, resources, Prefab, instantiate, SkinnedMeshRenderer, EventTouch, SkeletalAnimation } from 'cc'; const { ccclass, property } = _decorator; @ccclass('ChangeCloth') export class ChangeCloth extends Component { @property({ type: Node }) modelNode!: Node; sex: string = "male"; bodyPart: string[] = ["hair", "top", "pants", "shoes"]; data: Map<string, Node> = new Map(); start() { this.initAllData(); } initAllData() { this.data.clear(); for (let i = 0; i < this.bodyPart.length; i++) { let partName = this.bodyPart[i]; let nodeName = `${this.sex}_${partName}-1`; let nodePart = this.modelNode.getChildByName(nodeName); if (nodePart) { console.debug("init part", nodeName); this.data.set(partName, nodePart); } } } changeCloth(partName: string, index: number) { resources.load(`prefab/${this.sex}_${partName}-${index}`, Prefab, (err, prefab) => { if (err) { console.debug(err); return; } let oldNode = this.data.get(partName); let oldModel = oldNode?.getComponent(SkinnedMeshRenderer); let newNode = instantiate(prefab); let newModel = newNode.getComponent(SkinnedMeshRenderer); if (oldModel?.skinningRoot && newModel) { newModel.skinningRoot = oldModel?.skinningRoot; oldNode?.removeFromParent(); this.modelNode.addChild(newNode); this.data.set(partName, newNode); } }) } onClickChange(touch: EventTouch, data: string) { console.debug("onClickChange", data); let params = data.split("-"); this.changeCloth(params[0], parseInt(params[1])); } onClickAnimation(touch: EventTouch, animationName: string) { console.debug("onClickAnimation", animationName); this.modelNode.getComponent(SkeletalAnimation)!.play(animationName); } update(deltaTime: number) { // [4] } }
小结
换装的核心是要理解为什么能换,理解了骨骼动画的原理以及构成,一旦弄清”为什么“?,换装的实现就会是非常简单的一件事了。
如果羽毛的理解存在错误,欢迎回复进行指导。
更多
今日技能你学废了吗?关注公众号回复“换装”获取源码