本文转载自https://my.oschina.net/huliqing/blog/880113?hmsr=toutiao.io
如果本文涉及侵权行为,请原作者联系博主邮箱,我将及时进行删除处理
博主邮箱:yibiandao@aliyun.com
前言
本文将详细讲解3D游戏中换装的原理及换装中的一些重点问题,先粗略看一下换装的简单原理:
没错,看起来很简单吧!!!
那么接下来,开始讲复杂的地方!
在阅读本文之前如果你能够了解或知道以下一些基础知识,可以帮助你更好的了解3D换装原理,因为后面的讲解或多或少会引用或涉及到这些内容。
- Blender - 开源的3D建模工具,或了解过其它如:3dmax,maya之类的建模工具都可以。了解一些其中的基础建模、骨骼动画和蒙皮知识
- JME3 - 开源3D游戏引擎,全名:JMonkeyEngine,基于Java。了解一些Java基础编程,文章后面将在这个游戏引擎中用代码示例如何换装。
- MakeHuman - 开源的人物建模工具,可帮你快速建立人物模型及标准骨骼.
- ogre3D - blender导出插件,我们将用它来导出ogre格式的模型文件
- 落樱之剑 - 我开发的免费Android游戏,没错,打了个软广告,还加粗了呢! :) 用于换装演示,含换装脱装、换武器、换眼镜、换面罩、换角、换耳朵、换发形,...
版本要求: Blender2.69, JME3.0, ogre插件v0.6.0
你可以偿试更新的版本来学习这个教程,但建议使用与我一样的版本。
一、准备
在整个讲解过程中需要以下一些东西来进行说明,我们将通过各种工具一步一步来完成以下物件,并最终使用这些物件在游戏中演示如何进行换装。
- 一个标准的人物骨骼 - 用于控制角色基本身形、装备以及角色动画
- 一套基本身形 - 用于模拟人物角色的皮肤,即脱光装备后的样子,这里以内衣装示例,后面我称为“身形”或“身体”
- 一套角色装备 - 用于换装示例,后面我统称为“装备”
注:角色基本身形和装备的本质是一样的,切换原理也是一样的, 这里先区分开来,以方便后面进行说明。由于文章的重点是讲解“换装”原理,以及换装过程中可能遇到的重要问题,所以繁琐的"建模"过程我可能会简单略过。
二、确定和建立自己的标准骨骼
首先,你可能也知道一些骨骼动画的基本知识,我这里不秀概念,尽量讲清楚明白。
就像人体的骨骼和皮肤的关系一样,当人的手在动的时候,实际上就是手上的骨头带动了手上的皮肤在动。原理一样,这里的骨骼和人体的骨骼一样,3D中的骨骼在动画过程中通过各个骨头来带动皮肤(或装备)模型中的各个顶点进行运动就形成了骨骼动画。模型中的每个顶点都可能受一个或多个骨头影响,而这些影响可能不一样,有一些影响大一些,有一些影响小一些,这个影响值就是权重。
因此在开始讲解换装之前,我们需要先建立一个骨骼(或称骨架),尽量建得标准一些,确定一个标准的骨骼非常重要,因为后面我们的游戏角色动画都可以使用这一套骨骼,身体模型和装备也都是要以这一套骨骼来建模的。下面我称这套骨骼为标准骨骼
关于标准骨骼你可以自己手动在blender或其它3D建模工具中手动一根一根建立,或使用一些建模插件或是从一些开源软件中获得,比如从MakeHuman中建立一个模型,然后给模型配置一个骨骼,如下图:
根据自己的需要和模型动画的复杂度,选择不同的骨骼,如果你的模型需要包含眼部、嘴部或手指动画等,你就可以选择包含眼部、嘴部和手指等骨头的骨骼。当然,骨骼和模型越复杂,对后续游戏性能影响也就越大,根据需要适当选择。
在MakeHuman中建立了模型之后导出为mhx,再把它导入到blender中,就可以在blender中获得一套比较标准的骨骼了。
下面的讲解我将以我所开发的游戏落樱之剑中使用的标准骨骼来进行说明:
骷骼正面(手指处的名称太杂乱,我没有显示出来)
骨骼侧面
注意,给骨骼中的骨头起个有意义的名称也很重要。
三、建立基本身形并切割身形
下面是根据标准骨骼建立的基本身形,你也可以在建立模型后再根据模型确定自己的标准骨骼,根据自己需要而定。 你也可以直接把从MakeHuman中导出的模型来作为基本身型使用,但是一般情况还是需要根据自己的需要调节一下,比如导出的模型面数或顶点数可能达到1万多,在大部分情况下对于游戏来说精度太高,会影响游戏性能, 建模是一个繁琐的过程,也不是我们要讲的重点,这里就不详细介绍了。 下面是落樱之剑中主角的基本身形,看一下是如何和标准骨骼匹配的。
注:这个基本身形非常重要,因为后面的装备都是以这个身形为基础建立的。
现在我们需要把身型切割成一个一个的部分,因为在后面的游戏中我们需要对不同部位进行替换,来达到“换装”的效果。 在落樱之剑中我是这样切割的:
脚(foot),下身(lowerBody), 上身(upperBody), 手部(hand), 脸部(face), 眼睛(eye), 耳朵(ear),头发(hair),眼罩(blinder),角(horn),脸罩(mouthmask),...
注:左手和右手是作为一个整体切割出来的。
由于篇幅和模型复杂度的关系一部分基本身型我没有显示出来,并且也不打算在这里讲解如何切割模型,如果你懂得一些建模的知识,你应该很清楚如何做,切割模型很简单,在明白了原理之后你也可以根据自己实际的需要切割成更多或更细的部分。
为了方便说明,我把切割后的模型各部分都移动了一下,实际切割后的模型应该保留原来的位置不变,以便后续的操作。特别是切割后的模型分界线,
这里以手部的分界线为例,上身和手部的分界线是一样的,分界线上的这些顶点位置不能移动,特别是在制作装备的时候,应该尽量保持这些分界线上的顶点位置不变动,否则“换装”后的整个模型就可能出现裂缝, 因为后续可能会制作很多的装备,这些装备需要能够和身形以及其它装备进行无缝匹配,那么严格遵守这个规范就很重要。
四、将身形绑定到骨骼和调整边界权重
在切割了身形之后,我们需要为身形的各个部分分配骨骼权重,因为身形和装备最终都是要由标准骨骼来控制的,所以身形的各部分需要各自绑定到骨骼中去,在最终的产出物中身形的各个部分是各自独立的,并且这些身形和装备都附带有骨骼权重信息,这些信息最终在游戏中会被用到主骨骼(标准骨骼)中去。
现在,假如我把身形切分成“脚”,“下身”,“上身”,“手”,“脸”。 先以“脚”为例来进行说明,其它部分操作原理是一样的。
首先给“脚”分配骨骼和确定“连接骨骼”
从标准骨骼中复制1个骨骼出来,记得保留原始标准骨骼,因为后面我们需要用它来作为控制角色的主骨骼,这个临时复制出来的骨骼只是为了给“脚”部分配权重,后续在游戏中我们是不需要这些临时骨骼的,如图所示,
将复制出来的骨骼删除掉一些不必要的骨头,因为脚部非常小,需要绑定的骨骼也很少,所以shin.L和shin.R的父骨骼部分都可以不需要。
然而为什么需要shin.L和shin.R部分的骨头呢?因为脚很小,所以不是只要foot.L和foot.R及其子骨骼部分就可以了吗? 因为需要一些有特殊意义的骨头,为了处理角色在“换装”后可能出现的“裂缝”现象,需要使用这些特殊骨头,我把这些为了处理模型边界裂缝、在两个连接模型中都出现、同时连接两个身体模型的骨头称为“连接骨骼”。 比如(参考下图):
- 连接“脚”和“下身”的骨头: shin.L,shin.R
- 连接“下身”和“上身”的骨头: hips
- 连接“手”和“上身”的骨头: hand.L, hand.R
- 连接“上身”和“脸”的骨头: neck
一般情况下,这些“连接骨骼”需要根据自己模型的切割方式来定义,下图是我在游戏“落樱之剑”中对模型各个部分分配骨骼后的划分参考!
通常来说,一旦确定了身形各个部分和骨骼各个部分的划分之后,这些定义(或称规范)都可以通用到你后续装备的制作部分,所以你可以把身形和相关骨骼部分备份起来以便后续制作装备时使用,特别是在制作装备的时候要尽量保持“装备”各部分的分界点与划分身形时各部分的分界点一致(不移动,不增删顶点),以做到无缝匹配。
给“脚”分配权重
身形的其它部分操作与脚相似,划分及确定连接骨骼请参考上图。接着需要把身形的各部分分别绑定到各自所分配的骨骼上去,仍然以“脚”为例来说明。
操作: 右键选择“脚” -> 按住Shift不放再选择脚部骨骼 -> 按Ctr+P -> 选择“With Automaic Weights”来自动分匹配权重。如图:
在绑定后你就可以使用骨骼来控制脚部或制作动画了,但是在这里我们不需要为特定身形或装备来制作动画,我们需要的只是身形(或装备)与骨骼的权重信息就可以,所有的动画我们将在“标准骨骼”中制作。现在我们还有一个更重要的问题要处理 - 处理“裂缝”
处理“裂缝”
在给“脚”绑定了骨骼之后,由于是使用自动分配权重(With Automatic Weight), 自动分配权重是个好东西,但是可能在“换装”游戏中由于模型边界处顶点权重的分布不一致而导致角色皮肤或装备出现裂缝的现象。现在我们需要为这些身形确定一些权重分配规则,以及通过这些规则来调整一下权重分配,有可能你的规范与我的不一样,下面是我在落樱之剑中定义的规则。
- 身形边界点(边界处的顶点)只接受连接骨骼的影响,并且影响权重是1.0
- 身形边界点不接受其它骨骼的权重影响,所有其它非连接骷髅对于边界点的权重都要清0.
- 其它部分按默认系统自动分配的权重或适当按需调整
现在说明为什么要这样做,我们以“下身”左边和左“脚”的边界点来说明,由于两个边界都是以连接骨骼"shin.L"进行连接的,shin.L对“下身”和“脚”边界的权重影响都是1.0,并且这些边界点不受其它骨骼的影响,这样当shin.L在运动的时候就能保证对边界处顶点的完全控制,使它们的运动行为一致而不会出现裂缝。具体参考下图:
或许对于边界裂缝的处理方式不同的人会有不同的处理方式,但是遵守一定的规则是有意义的,特别是在后续装备的制作。身形的其它部分对裂缝的处理方式与“脚”和“下身”的处理方式是一样的,这里就不一一详细说明。
五、根据基本身形建立装备模型
在建立基本身形并绑定了权重之后,现在我们来制作一套“装备”,以便后续在游戏中演示如何进行“换装”。
装备的性质实际与基本身形是一样的,只是看起来像是一套装甲而已,建立过程与基本身形差不多,所以一般也是在基本身形上建模而成的,并且为了确保与基本身形以及其它装备之间的无缝匹配(不出现边界处的裂缝现象),我们在制作装备的时候需要确保边界处的顶点尽量不要被编辑到,即不移动也不增删顶点,虽然这并不是绝对必须遵守的原则,比如一些比较宽大的装备或许可以遮住分界处的裂缝,这个时候边界处的顶点即使稍微移动或改变也可能不会有什么大的影响,但是始终遵守边界顶点不被编辑可以减少模型制作时候的很多麻烦。
现在使用我在落樱之剑中为角色创建的一些“装备”来示例说明:
这些“装备”是从基本身形中修改而来的,建模过程就略了,重点注意装备各部分箭头所示处的顶点与原始身形的位置是一致的,这些是原始身形的边界点,为了与原始身形无缝嵌接,这些顶点在建模装备的时候必须保持与原来的位置一致(这里为了方便示例,我对各个部分做了一些移动)。
在建立了装备后,同样需要给装备绑定骨骼,绑定骨骼和分配权重的过程与前面“基本身形”的处理过程一样遵守相同的规则,即原始边界点处只接受连接骨骼的影响(权重1.0),非连接骨骼不影响边界处的顶点。如下图:
六、使用Ogre3D插件导出骨骼、基本身形、装备等物件
在经过前面的过程后,现在我们在blender中获得了这些东西:
- 一套标准骨骼(可带动画,也可不带)
- 一套基本身形(包含身形的各部分,并已经全部绑定了骨骼)
- 一套可换的装备(包含装备各部分,并已经全部绑定了骨骼)
现在我们需要把这些东西从blender中导出,我这里使用ogre3D插件来导出,你可以从ogre3D官网中找到相关插件(详细看说明,注意版本的兼容)。
使用ogre插件分别导出以下相应物件:
- 导出标准骨骼
- 导出身形的各个部分,“脚”,“下身”,“上身”,“手”,“脸”...等
- 导出装备的各个部分,“脚”,“下身”,“上身”,“手” 装备部分我们只要这一些就可以。
注:在导出标准骨骼时,ogre3D可能不允许单独导出骨骼,这时可以随便找个模型(如简单的立方体)绑定到骨骼中去,再进行导出。
在使用ogre3D导出后,我们将获得这些文件(文件名称依据物体在blender中的名称而定):
1.标准骨骼文件:
ske.skeleton.xml, ske.mesh.xml, ske.material
2.基本身形各部分:
foot0.skeleton.xml, foot0.mesh.xml, foot0.material lowerBody0.skeleton.xml, lowerBody0.mesh.xml, lowerBody0.material upperBody0.skeleton.xml, upperBody0.mesh.xml, upperBody0.material hand0.skeleton.xml, hand0.mesh.xml, hand0.material face0.skeleton.xml, face0.mesh.xml, face0.material ...
3.装备各部分:
foot1.skeleton.xml, foot1.mesh.xml, foot1.material lowerBody1.skeleton.xml, lowerBody1.mesh.xml, lowerBody1.material upperBody1.skeleton.xml,upperBody0.mesh.xml, upperBody1.material hand1.skeleton.xml, hand1.mesh.xml, hand1.material
七、将ogre3D格式的文件转换为j3O文件
接下来的讲解需要你了解过一些JME3游戏引擎的知识,因为这一部分的说明会与特定引擎和环境有关,并非所有游戏引擎通用,但是了解一下也有益处,毕竟道理和原理是差不多通用的。
从ogre3D导出的文件为ogre格式,现在需要把这些文件转换为j3o格式,j3o是JME3游戏引擎默认使用的模型文件格式。因此在JME3使用这些模型之前需要将它们转为j3o格式。
操作
- 打开JME3的SDK,然后创建或打开你的JME项目.(JME3官网下载SDK,请使用JME3.0稳定版)
- 将所有刚导出的ogre格式的模型文件(标准骨骼,身形,装备)一起拷贝到JME项目的asset目录下,如: "assets/Textures/demo"
- 在SDK中,将所有xxx.mesh.xml文件一个一个转换为j3o文件,操作: 右键选择xxx.mesh.xml文件 -> 转换为j3o
现在我们将得到这些文件(省略号部分自己脑补):
标准骨骼: ske.mesh.j3o
基本身形: foot0.mesh.j3o, lowerBody0.mesh.j3o, upperBody0.mesh.j3o, ... 装 备: foot1.mesh.j3o, lowerBody1.mesh.j3o, upperBody1.mesh.j3o, ...
八、调整标准骨骼,身形和装备
在把标准骨骼、身形和装备转换为j3o格式的模型之后我们还需要对其做一些调整,以便在游戏中使用。
调整标准骨骼
如果从blender中导出标准骷髅的时候我们给骨骼绑定了任何模型,这时转换后的ske.mesh.j3o文件中可能包含这些模型,然而我们并不需要它们, 因为标准骨骼只包含骨骼就可以,在游戏中“换装”的时候模型(身形或装备)是动态添加上去的。下面对骨骼中的多余模型进行删除操作:
在SDK中选择模型(ske.mesh.j3o) -> 右键选择“Edit in SceneComposer” -> 在SceneExplorer Window中将多余的Geometry删除掉,如图:
调整身形及装备
所有身形和装备的调整过程是一样的,所以这里统一进行说明。
身形和装备的调整刚好与骨骼的调整相反,由于身形和装备最终是添加(attach)到标准骷髅中去的,由标准骨骼进行控制,所以身形和装备自身就不需要包含骨骼。但是从blender中导出身形和装备的时候,由于我们需要骨骼与权重的相关信息,所以导出并转换为j3o后的模型中包含了骨骼,所以现在需要把这些东西处理掉,只保留权重信息就可以,这里稍微有一些复杂。
由于在blender中我们使用了不完整的骨骼去绑定身形和装备的各个部分。 所以在导出并转换后的身形或装备(如:foot0.mesh.j3o)中所包含的骨骼索引和标准骨骼中的索引是有可能不一样的。举个例子,标准骨骼(ske.mesh.j3o)中名称为foot.L的骨头的索引值可能是10,而脚(foot0.mesh.j3o)自身骨骼中的foot.L的骨头的索引值可能是其它值,这种不一致最终会导致在执行角色动画的时候,无法从“标准骨骼”中找到正确的用于控制“脚”部动画的骨头。
现在我们需要执行一些操作来重定向“脚”(foot0.mesh.j3o)中顶点和骨骼之间的权重信息中的骨骼索引,使它指向标准骨骼中正确的骨头,然后“脚”中的骨骼就可以不要了,顺便各种Control也可以丢掉,只保留网格(Geometry)就行,因为权重等信息保存在Geometry的mesh中,所以丢掉其它东西是没有问题的。这一段说得有点绕,操作也有些复杂,在明白原理之后你可以自己进行处理,也可以使用下面我提供的方法来处理骨骼索引的重定向(注:骨骼索引重定向这一步是可以有其它方法绕过的,看附录)。
package name.huliqing.luoying.utils.modifier;import com.jme3.animation.AnimControl;
import com.jme3.animation.Skeleton;
import com.jme3.animation.SkeletonControl;
import com.jme3.asset.AssetManager; import com.jme3.scene.Geometry; import com.jme3.scene.Mesh; import com.jme3.scene.Spatial; import com.jme3.scene.VertexBuffer; import java.nio.ByteBuffer; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import name.huliqing.luoying.LuoYing; import name.huliqing.luoying.utils.GeometryUtils; import name.huliqing.luoying.utils.ModelFileUtils; /** * * @author huliqing */ public class OutfitUtils { private final static Logger logger = Logger.getLogger(OutfitUtils.class.getName()); private final static String REDIRECT_BONE_INDEX_OK = "redirect_bone_index_ok"; private final static String RIG_SKE_PATH = "Models/actor/ske.mesh.j3o"; public static void redirectBoneIndex(String outfitFile, String rigSkeFile) { AssetManager am = LuoYing.getAssetManager(); Spatial outfit = am.loadModel(outfitFile); if (outfit.getUserData(REDIRECT_BONE_INDEX_OK) != null) { logger.log(Level.WARNING, "Outfit name={0} has already Redirect bone index!", outfit.getName()); return; } AnimControl outfitAC = outfit.getControl(AnimControl.class); SkeletonControl outfitSC = outfit.getControl(SkeletonControl.class); if (outfitSC == null) { return; } // 移除control outfit.removeControl(outfitAC); outfit.removeControl(outfitSC); // 重定向boneIndex Skeleton rigSke = am.loadModel(rigSkeFile).getControl(SkeletonControl.class).getSkeleton(); // skin中可能存在多个Geometry,每一个都要进行处理. List<Geometry> geos = GeometryUtils.findAllGeometry(outfit); Skeleton outfitSke = outfitSC.getSkeleton(); // skin中的骨骼(已丢弃) for (Geometry geo : geos) { // 找到Mesh中的骨骼索引 // 这里需要检测并初始化一次就可以, 不能重复做,即使skin是重新load进来的 // 因为geometry或mesh可能进行了缓存,所以即使重新Loader.loadSkin(),可能 // 载入的对象仍然引用相同的mesh.所以这里需要通过判断,避免对skin mesh // 中的骨骼索引重定向多次,只有第一次是正确的,第二次及后续一定错误,因为数据覆盖了. Mesh mesh = geo.getMesh(); logger.log(Level.INFO, "==MaxNumWeights={0}", mesh.getMaxNumWeights()); VertexBuffer indices = mesh.getBuffer(VertexBuffer.Type.BoneIndex); if (!indices.getData().hasArray()) { // Prepares the mesh for software skinning by converting the bone index and weight buffers to heap buffers. // 另参考: SkeletonControl => void resetToBind() mesh.prepareForAnim(true); } // 重定向 ByteBuffer ib = (ByteBuffer) indices.getData(); ib.rewind(); byte[] fib = ib.array(); for (int i = 0; i < fib.length; i++) { int bIndex = fib[i] & 0xff; // bIndex是skin中子骨骼 // 这里一般不会发生, 除非做了第二次骨骼索引的重定向, // 否则skin中的初始骨骼索引不可能会大于或等于它的骨骼数(最大索引为BoneCount-1) if (bIndex >= outfitSke.getBoneCount()) { logger.log(Level.WARNING, "SkinSke bone index big than boneCount, bIndex={0}, totalBone={1}" , new Object[] {bIndex, outfitSke.getBoneCount()}); continue; } String boneName = outfitSke.getBone(bIndex).getName(); // 从父骨骼中找出与skin中当前骨头相同名称的骨头. int rootBoneIndex = rigSke.getBoneIndex(boneName); if (rootBoneIndex != -1) { logger.log(Level.INFO, "update bone index, skin={0}, index update: {1} to {2}" , new Object[]{outfit.getName(), fib[i], rootBoneIndex}); fib[i] = (byte) rootBoneIndex; } else { // 如果skinNode中的骨骼没有在父骨骼中找到,则随便直接绑定到父骨骼的根节点中. // 出现这种情况主要是skin中存在额外的骨骼,这个骨头不知道要绑定到哪里?!!? fib[i] = 0; logger.log(Level.WARNING, "SkinSke found a extra bone, but not know where to bind to! boneName={0}" , boneName); } } indices.updateData(ib); } outfit.setUserData(REDIRECT_BONE_INDEX_OK, "1"); ModelFileUtils.saveTo(outfit, outfitFile); } // 1.注意:如果skin中存在骨骼及动画信息,这些信息 // 将被丢弃,相应的骨骼索引信息会重定向到actor中的主骨骼上.这要求skinNode // 骨骼中的所有骨头的名称都应该与主rig中骨骼中的骨头名称相匹配. // 2.需要重定向skin中的骨骼索引,这里稍微复杂,需要详细记住: // 比如:主骨骼存在 root - hips - hand 这三个骨头, 而skin中的骨骼可能只有 // hand这个骨头, 为了把skin绑定到主骨骼中,也就是是将skin中的mesh的控制权 // 从skin自身的hand骨头交由主骨骼中的hand骨头来控制,这样skin就不需要AnimControl // 和SkeletonControl. // 因为skin自身的mesh中包含有权重和骨骼索引的信息. // 但是因为skin自身的骨骼索引和主骨骼索引是不一致的,如上:主骨骼中的hand // 的索引为2, 而skin因为hand只有一个骨头,索引为0, 所以在丢弃了skin中的 // AnimControl和SkeletonControl之后,需要把skin中hand的骨头索引重定向到 // 主骨骼中的hand的索引.而权重信息不需要改变,也就是如上:skin中mesh的关于 // hand的骨骼索引需要从0重定向到主骨骼中的hand索引2. // 这样主骨骼中的SkeletonControl在控制skin执行变换的时候才能正确从主骨骼 // 中找到hand的骨头,(否则找到的就是root了). // 关于骨骼与Mesh的具体逻辑可参考: SkeletonControl.controlRenderSoftware public static void main(String[] args) { // 重要:模型(j3o)只需要重定向一次就可以了,不能多次重定向,除非模型修改后重新转换成j3o. redirectBoneIndex("Models/actor/female/eye.000.mesh.j3o", RIG_SKE_PATH); } }
redirectBoneIndex
中有几个比较简单的外部引用,由于文章篇幅关系我没有列出代码.
AssetManager am = Common.getAssetManager(); // 获取资源管理器,可以自己从application中获得引用。
GeometryUtils.findAllGeometry(outfit); // 找出一个节点中的所有子"Geometry"
ModelFileUtils.saveTo(outfit, outfitFile); // 保存文件
这几个方法很简单,请自行实现。
下面是调用这个方法来处理身形和装备中骨骼索引的示例(你可以在自己的代码中动态调用):
public static void main(String[] args) { redirectBoneIndex("xxx/foot0.mesh.j3o", "xxx/ske.mesh.j3o"); redirectBoneIndex("xxx/lowerBody0.mesh.j3o", "xxx/ske.mesh.j3o"); redirectBoneIndex("xxx/upperBody0.mesh.j3o", "xxx/ske.mesh.j3o"); redirectBoneIndex("xxx/hand0.mesh.j3o", "xxx/ske.mesh.j3o"); //... redirectBoneIndex("xxx/foot1.mesh.j3o", "xxx/ske.mesh.j3o"); redirectBoneIndex("xxx/lowerBody1.mesh.j3o", "xxx/ske.mesh.j3o"); redirectBoneIndex("xxx/upperBody1.mesh.j3o", "xxx/ske.mesh.j3o"); redirectBoneIndex("xxx/hand1.mesh.j3o", "xxx/ske.mesh.j3o"); }
xxx代表你的资源路径,请自行脑补。在处理前你的模型应该像是下面这样的:
在处理后,你的模型应该像是下面这样的:
示例图中的foot.000名称根据你的模型在blender中的名称不同而有所区别。身形和装备中的其它部分处理过程都是一样的,这里不一一详述。
九、在游戏中载入,并演示换装
在经过前面的处理后,剩下的问题已经不是问题了,我们获得了这些东西:
标准骨骼: ske.mesh.j3o (已经去掉了多余的模型)
基本身形: foot0.mesh.j3o, lowerBody0.mesh.j3o, upperBody0.mesh.j3o, ...(已经重定向骨骼索引,并去掉了Control和骨骼) 装 备: foot1.mesh.j3o, lowerBody1.mesh.j3o, upperBody1.mesh.j3o, ...(同身形一样)
现在使用一段代码来示例如何在游戏中进行换装:
@Override
public void simpleInitApp() { // 载入标准骨骼 Node ske = (Node) getAssetManager().loadModel("Models/actor/ske.mesh.j3o"); rootNode.attachChild(ske); rootNode.addLight(new AmbientLight()); // 载入基本皮肤 Spatial foot = getAssetManager().loadModel("Models/actor/female/foot.000.mesh.j3o"); Spatial lowerBody = getAssetManager().loadModel("Models/actor/female/lowerBody.000.mesh.j3o"); Spatial upperBody = getAssetManager().loadModel("Models/actor/female/upperBody.000.mesh.j3o"); Spatial hand = getAssetManager().loadModel("Models/actor/female/hand.000.mesh.j3o"); Spatial face = getAssetManager().loadModel("Models/actor/female/face.000.mesh.j3o"); // 组装角色(基本皮肤) ske.attachChild(foot); ske.attachChild(lowerBody); ske.attachChild(upperBody); ske.attachChild(hand); ske.attachChild(face); // 换装示例,比如换上“脚”装备 Spatial footOutfit = getAssetManager().loadModel("Models/actor/female/foot.001.mesh.j3o"); ske.detachChild(foot); // 移除基本皮肤 ske.attachChild(footOutfit);// 换上装备 }
没错,跟其它节点的切换一样简单,只是attach和detach而已。当标准骨骼(上例中的ske节点)在执行动画的时候,它的SkeletonControl会从子节点(皮肤或装备)中获取顶点、顶点与骨骼索引、权重的相关信息来计算动画(注:皮肤和装备中的Mesh中保存了顶点与骨骼索引和权重的关系)。
好了,文章就写到这里,下面是一些额外补充!
十、附录
1.上下连身装备的处理
有时候我们可能会需要一些上下连身的装备,如法师或牧师的长袍,这些没有什么特别的,只是把lowerBody和upperBody合起来(在blender中不要切割开来就可以),在游戏中换上长袍的时候同时把基本皮肤中的lowerBody和upperBody移除即可。
2.不需要蒙皮的皮肤或装备
有一些皮肤或装备是可以不需要蒙皮的,比如头发,手里的武器(如剑),或一些静态饰品(如眼镜),这些直接导出成完全静态(不需要骨骼和动画)的物体即可,然后在游戏中(这里以JME3示例)直接挂接到某块骨头上就可以,比如头发:
// 载入标准骨骼
Node ske = (Node) getAssetManager().loadModel("Models/actor/ske.mesh.j3o");
SkeletonControl skeControl = ske.getControl(SkeletonControl.class);
// 获得“头骨”的节点
Node headNode = skeControl.getAttachmentsNode("headBone")
// 把头发添加到头骨的节点下(注意与上面的差别) headNode.attachChild(hairNode);
这可以节省性能,因为蒙皮的物体在动画过程中每个顶点都需要经过骨骼和权重来计算顶点的位置,一个顶点还可能受多个骨头的影响,所以计算量比一般动画要大得多。
3.不需要骨骼索引重定向的方法
在前面我们使用了一些特殊代码来重定向皮肤和装备中权重信息中的骨骼索引,但是这一步是可以超过的,只要在blender中为皮肤或装备(如脚)绑定骨骼的时候使用复制出来的完整的标准骨骼就可以,这样导出blender并转为j3o后的foot0.mesh.j3o中的骨骼索引就和标准骨骼中的完全一样(同样的要删除掉皮肤和装备中的Control). 只不过使用这种方法后,以后调整标准骨骼的时候(比如添加或减少某些骨头)你的皮肤和装备可能需要重新使用新的标准骨骼来绑定并重新导出。