基于 Cocos Creator 3.0 的 3D 換裝
需求
從換裝的方式分類,可以分為整體換裝以及局部換裝。整體換裝較為簡單,我們就不做討論,本文主要介紹一下局部換裝(其實理解過后也是非常的簡單)。
從換裝的模型分類, 主要分為兩種類型:
一種類型是對于靜態(tài)模型的換裝,就是直接將身體需要換的 Mesh 更新即可。
另一種類型是動態(tài)模型的換裝(有動作的模型)。
效果展示

原理介紹

網(wǎng)格(Mesh):
模型(Model)是由一個個三角形組成的,而這種三角形的學名則是網(wǎng)格(Mesh)
網(wǎng)格蒙皮數(shù)據(jù)(Skin Info)
頂點的 Skin 數(shù)據(jù)包括頂點受哪些骨骼影響以及這些骨骼影響該頂點時的權重(Weight),另外對于每塊骨骼還需要骨骼偏移矩陣(BoneOffsetMatrix)用來將頂點從Mesh空間變換到骨骼空間。可簡單理解為:SkinMesh = Mesh+Skin Info
骨骼(Skeleton):
如圖 1,骨架由一系列具有層次關系的關節(jié)(骨骼)和關節(jié)鏈組成,是一種樹結構,選擇其中一個是根關節(jié),其它關節(jié)是根關節(jié)的子孫,可以通過平移和旋轉根關節(jié)移動,并確定整個骨架在世界空間中的位置和方向。
骨骼的動畫(關鍵幀)數(shù)據(jù)
實現(xiàn)思路
實現(xiàn)步驟
骨骼動畫及部位裝備 Prefab 的制作,核心——共享一套骨骼。動畫師制作時,同一部位的不同裝備綁定同一根骨骼,整體輸出,在 Creator 中將各部件裝備制作為 Prefab 后從主角刪除,主角只保留一套默認裝備。

主角節(jié)點需要關閉預烘焙功能,否則無法實時運算以實現(xiàn)換裝功能。

初始化模型。建立 Map<key-PartName, value-Node>,這一步是為了后續(xù)替換裝備時可以檢索到對應部位的節(jié)點。
替換裝備節(jié)點:
刪除舊裝備節(jié)點。檢索 Map,根據(jù)部位 key-PartName 獲得 OldNode 引用,移除 OldNode(保留骨骼根節(jié)點引用 SkinningRoot,后續(xù)備用)。 增加新裝備節(jié)點,加載部位 A 新裝備 Prefab 并實例化為 NewNode,添加 NewNode。 刷新部位 key-PartName 的 value 值為 NewNode。 刷新骨骼,取得步驟 1 中的 SkinningRoot 來刷新 NewNode 的 SkinningRoot,完成(我實現(xiàn)到這步,后續(xù)步驟為了節(jié)省性能大家可以研究)。
合并 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]
}
}
小結



