Basic Character Customization Tutorial (基础角色定制教程)

昨儿做UI的时候想起来,之前搞角色定制找资料挺费劲的,很多东西都不知道啥是啥。Unity的例子帮了大忙,但还是会有 “啥玩意儿啊”, 这种感觉。所以想着把自己的经验在这分享一下。

代码在最下面。

Yesterday I rearranged UI of character customization scene of the under-developing-but-open-for-testing game .

And I remembered that, back to a few month ago it was a hard time for me to get information about how to achieve Character Customization. Unity’s example, and the community helped a lot. But I still spent hours to figure out what is what.

So I think I should share my experience here. Hope this can help others who were like me. You can download the source file at the bottom of this post.


Prepare Assets

俺的模型啥的是用Blender做的。直接用 Unity的例子 的模型也行,都一样。

Before we start, I’ve created these assets in Blender. You can create your own, or simply use assets from Unity’s example, the fundamentals are same.


Customize the Character

我把所有部件的Prefab都链到Designer里了,然后用个byte数组表示配置。配置里的每个数字代表一个部件。

I linked all my assets in to a GameObject, used a byte[] to specify which parts the player have chosen (called ‘config’).

Now just set up the UI and let the player customize his/her character.

1
2
3
4
5
6
7
8
9
10
11
12
13
public SkinnedMeshRenderer[] skins;
public SkinnedMeshRenderer[] hairs;
public SkinnedMeshRenderer[] eyebrows;
public SkinnedMeshRenderer[] eyes;
public SkinnedMeshRenderer[] mouths;
public SkinnedMeshRenderer[] clothes;
public SkinnedMeshRenderer[] pants;
private SkinnedMeshRenderer[][] elements;

// character configuration array,
// [0 skin,1 hairs, 2 eyebrow, 3 eye, 4 mouth, 5 cloth, 6 pants, 7 accessories]
public byte[] config; // = new byte[]{0, 0, 0, 0, 0, 0, 0};
byte[] GetCurrentConfig() { return config; }

Combine SkinnedMeshRenderers

“You should use only a single skinned mesh renderer for each character. Unity optimizes animation using visibility culling and bounding volume updates and these optimizations are only activated if you use one animation component and one skinned mesh renderer in conjunction……”
—— Unity Official Doc - Modeling Characters for Optimal Performance

SkinnedMesh 就是关联骨骼的mesh,哪个vertex受哪根骨头影响百分之多少,然后骨头一动皮也就跟着动了。每一个部件都是一个SkinnedMesh,但是这东西很费,很影响性能,所以越少越好。一般一个角色内的SkinnedMesh要把它们合起来。

骨头:在Unity里就是Transform

Skinned Meshes are meshes linked with ‘Bones’, Each bone is associated with some vertices of the mesh (with different weights). During animation, the skinned mesh is deformed ( I’ve read somewhere that new mesh are generated for each frame, heavy, so heavy ).

So in short, skinned meshes kill performance, and we definitely want less of them in the scene if possible.
Yes, we should combine them.

In case you are wondering what are Bones, they are just Transforms in Unity.

用下面的代码,根据之前玩家选择的配置,把各个部件粘起来。

In the last Section, we have a byte[] config chosen by the player, now we are going to build a character with single mesh using this config,

and here is the function to do this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// go is a root GameObject, in my case, I used 'PH.base' shown in the up-right image, it has Bones

GameObject GenerateWithConfig (GameObject go, byte[] config)
{
if (!go.GetComponent<SkinnedMeshRenderer>()) { go.AddComponent<SkinnedMeshRenderer>(); }
SkinnedMeshRenderer smr = go.GetComponent<SkinnedMeshRenderer>();


// CombineInstance Struct used to describe meshes to be combined using Mesh.CombineMeshes.
List<CombineInstance> combineInstances = new List<CombineInstance>();

// We must put all materials used by all renderers into the final combined renderer,
// I combined all the textures into in big texture, in order to have only one material
materialList = new List<Material>();
materialList.Add(smr.material);

// Same, boneWeights should be copied into the final SkinnedMesh
boneWeightsList = new List<BoneWeight>();
Transform[] bones = smr.bones;

// Now for each SkinnedMeshRenderer specified by config
for (int i = 0; i < config.Length; i++ )
{
SkinnedMeshRenderer smr1 = (SkinnedMeshRenderer)Object.Instantiate( elements[ i ][ config[i] ] );

// Add meshes to combineinstances
for (int sub = 0; sub < smr1.sharedMesh.subMeshCount; sub++) {
CombineInstance ci = new CombineInstance();
ci.mesh = smr1.sharedMesh;
ci.transform = smr1.transform.localToWorldMatrix;
ci.subMeshIndex = sub;
combineInstances.Add(ci);

foreach (Material mat in materialList) {
if (mat.name != smr1.material.name) {
materialList.Add(smr1.material);
}
}
}

boneWeightsList.AddRange(smr1.sharedMesh.boneWeights);

Object.Destroy(smr1.gameObject);
}

// Combine meshes
smr.sharedMesh = new Mesh();
smr.sharedMesh.CombineMeshes(combineInstances.ToArray(), true, false);
smr.sharedMesh.boneWeights = boneWeightsList.ToArray();
// Combine meshes
smr.bones = bones;
smr.materials = materialList.ToArray();

// Update bindposes
// " The bind pose is the pose that the skeleton is in when you bind skin.
// When you pose a character’s skeleton after skinning, the skeleton’s actions cause deformations
// to the skin. The only pose that does not cause deformations to the skin is the bind pose. "
// [Source: Maya User Guide]

List<Matrix4x4> bindposes = new List<Matrix4x4>();
for (int i = 0; i < bones.Length; i++)
{
bindposes.Add(bones[i].worldToLocalMatrix * transform.localToWorldMatrix);
}
smr.sharedMesh.bindposes = bindposes.ToArray();

// This is for Blender exported fbx, not sure how other program should rotate
go.transform.Rotate(Vector3.right, -90);

// (Optional)
go.GetComponent<Animation>().Play();

// DONE, this GameObject have the combined SkinnedMeshRenderer
return go;
}

搞定!

代码在下面:

That is it!

Download the source code below:

phcharacterdesigner.cs

分享