开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

开发者专栏

关注:2103

当前位置:游戏蛮牛 技术专区 开发者专栏

__________________________________________________________________________________
开发者干货区版块规则:

  1、文章必须是图文形式。(至少2幅图)
      2、文章字数必须保持在1500字节以上。(编辑器右下角有字数检查)
      3、本版块只支持在游戏蛮牛原创首发,不支持转载。
      4、本版块回复不得无意义,如:顶、呵呵、不错......【真的会扣分的哦】
      5、......
__________________________________________________________________________________
查看: 2177|回复: 163

[士郎] 比预想的更复杂——动态断肢实现

  [复制链接]  [移动端链接]
排名
3
昨日变化

5961

主题

6467

帖子

2万

积分

Rank: 16

UID
1231
好友
185
蛮牛币
10115
威望
30
注册时间
2013-7-29
在线时间
3045 小时
最后登录
2018-2-22

社区QQ达人活力之星原创精华达人突出贡献奖财富之证游戏蛮牛QQ群会员蛮牛妹VIP一掷千金

发表于 2018-1-15 15:44:04 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
为求参数录入方便(不想做编辑器)直接用骨骼作为分割的依据,指定一个骨骼节点以及子节点,只要权重中包含这些节点的顶点都是需要被“切断”的顶点。

1.png

[AppleScript] 纯文本查看 复制代码
var count = bones.Length;
//收集需要切掉的骨骼
HashSet<int> cutBoneIndexSet = new HashSet<int>();
for (int i = 0; i < count; i++)
{
Transform b = bones;
if (IsParentBone(b, cutBone))
cutBoneIndexSet.Add(i);
}


[align=left]//根据骨骼获得需要切掉的顶点[/align]
[align=left]HashSet<int> cutIndexs = new HashSet<int>();[/align]
[align=left]count = boneWeights.Length;[/align]
[align=left]for (int i = 0; i < count; i++)[/align]
[align=left]{[/align]
[align=left]BoneWeight bw = boneWeights;[/align]
[align=left]if (bw.weight0 > 0 && cutBoneIndexSet.Contains(bw.boneIndex0) || bw.weight1 > 0 && cutBoneIndexSet.Contains(bw.boneIndex1) ||[/align]
[align=left]bw.weight2 > 0 && cutBoneIndexSet.Contains(bw.boneIndex2) || bw.weight3 > 0 && cutBoneIndexSet.Contains(bw.boneIndex3))[/align]
[align=left]cutIndexs.Add(i);[/align]
[align=left]}

然后再遍历Mesh的三角索引数组,只要有一个点被Mark就认为是切断的部分,将三角数据拆开成两个数组,就能将一部分肢体分离显示了

[AppleScript] 纯文本查看 复制代码
List<int> newTriangles = new List<int>();
List<int> partTriangles = new List<int>();

count = triangles.Length;
for (int i = 0; i < count; i += 3)
{
      if (cutIndexs.Contains(triangles) || cutIndexs.Contains(triangles[i + 1]) || cutIndexs.Contains(triangles[i + 2]))
      {    //切下的部分
           partTriangles.Add(triangles);
           partTriangles.Add(triangles[i + 1]);
           partTriangles.Add(triangles[i + 2]);
      }
      else
      {    //留下的部分
           newTriangles.Add(triangles);
           newTriangles.Add(triangles[i + 1]);
           newTriangles.Add(triangles[i + 2]);
      }
}


但这样模型就会出现一个破洞


2.jpg


这个实现的主体就是这个破洞的缝补过程了。
过程如下:

1.寻找两部分的模型的交界线
交界线上,一边的三角形必定三个顶点都没有被Mark的,而交界线上的三角形一定有两个顶点是重合的,所以另一边的三角形肯定有两个顶点是未被Mark的,它又必须有一个点被Mark,那就只有下面这一种情况。

3.jpg


判断依旧就是有且只有两个顶点未Mark的三角形是边缘三角形,而且那两个未Mark的顶点就是边缘线段。
上面拆分顶点的代码改成:


[AppleScript] 纯文本查看 复制代码
count = triangles.Length;
for (int i = 0; i < count; i += 3)
{
    linkIndexInOneTriangles.Clear();
    if (!cutIndexs.Contains(triangles))
        linkIndexInOneTriangles.Add(triangles);
    if (!cutIndexs.Contains(triangles[i + 1]))
        linkIndexInOneTriangles.Add(triangles[i + 1]);
    if (!cutIndexs.Contains(triangles[i + 2]))
        linkIndexInOneTriangles.Add(triangles[i + 2]);

    if (linkIndexInOneTriangles.Count < 3)
    {
        partTriangles.Add(triangles);
        partTriangles.Add(triangles[i + 1]);
        partTriangles.Add(triangles[i + 2]);
        if (linkIndexInOneTriangles.Count == 2) 
        {
            //记录边缘线段
            findEdge.AddSegment(linkIndexInOneTriangles[0], linkIndexInOneTriangles[1]);
        }
    }
    else
    {
        newTriangles.Add(triangles);
        newTriangles.Add(triangles[i + 1]);
        newTriangles.Add(triangles[i + 2]);
    }
}


2.获得由这些线段首尾相连的最大回环
其实就是一个遍历。首先记录每个点与另一个点的相关性和距离,生成关系图linkDict。

[AppleScript] 纯文本查看 复制代码
class Item
{
    public int index;
    public float distance;
    public Item(int index, float distance)
    {
        this.index = index;
        this.distance = distance;
    }
}
Vector3[] vertices;//传入原来的顶点数据用于距离计算
Dictionary<int, List<Item>> linkDict;//关系图
public FindEdge(Vector3[] vertices)
{
    this.vertices = vertices;
    this.linkDict = new Dictionary<int, List<Item>>();
}
public void AddSegment(int p1, int p2)
{
    float distance = Vector3.Distance(vertices[p1], vertices[p2]);
    if (linkDict.ContainsKey(p1))
    {
        linkDict[p1].Add(new Item(p2, distance));
    }
    else
    {
        linkDict.Add(p1, new List<Item>() { new Item(p2, distance) });
    }
    if (linkDict.ContainsKey(p2))
    {
        linkDict[p2].Add(new Item(p1, distance));
    }
    else
    {
        linkDict.Add(p2, new List<Item>() { new Item(p1, distance) });
    }
}


然后随便遍历下整个关系图,只要保证路径不重复即可。就可以找出周长最长的顶点索引数组。

(并不是所有的线段都要加入结果中,孤立线段和hole是应该过滤掉的,这样处理它们自然也就不存在了)
(正常情况下,遍历都是单线没有分支的,时间复杂度并不高)
[AppleScript] 纯文本查看 复制代码
List<int> path;//当前路径
HashSet<int> pathSet;//当前路径的HashSet(方便判重)
HashSet<int> pathSetHistory;//记录全部遍历过的顶点
List<int> result;
float resultLength;
float curLength;
int startIndex;

public List<int> GetMaxLoop()
{
    path = new List<int>();
    pathSet = new HashSet<int>();
    pathSetHistory = new HashSet<int>();
    result = new List<int>();
    curLength = 0;
    resultLength = 0;
    foreach (var pair in linkDict)
    {
        if (!pathSetHistory.Contains(pair.Key))//凡是遍历过的顶点都不会再成为起点
        {
             startIndex = pair.Key;
             Find(pair.Key);//选择一个起点开始遍历
        }
    }
    return result;
}

void Find(int v)
{
    path.Add(v);
    pathSet.Add(v);
    pathSetHistory.Add(v);//遍历过的顶点只增加不减少
    foreach (Item next in linkDict[v])
    {
        curLength += next.distance;
        if (next.index == startIndex)
        {
            if (curLength > resultLength) //形成回环后,记录一个周长更长的结果
            {
                result.Clear();
                result.AddRange(path);
                resultLength = curLength;
            }
        }
        else if (!pathSet.Contains(next.index))
        {
            Find(next.index);//递归遍历下一个节点
        }
        curLength -= next.distance;
    }

    path.RemoveAt(path.Count - 1);
    pathSet.Remove(v);
}


但光这样还不行,因为通常模型都存在重复顶点,手臂之类的筒状物体接缝处其实是重合但没有任何相关性的点,这会导致回环无法完成。

4.jpg


所以需要删掉并转移和它相关的联系,修正linkDict数据,再进行上面的操作。
[AppleScript] 纯文本查看 复制代码
public void RemoveRepeatIndex()
{
    List<int> linkIndexs = new List<int>();
    foreach (var pair in linkDict)
    {
        linkIndexs.Add(pair.Key);
    }

    //记录重复顶点的对应关系
    Dictionary<int, int> repeatIndexDict = new Dictionary<int, int>();
    int count = linkIndexs.Count;
    for (int i = 0; i < count - 1; i++)
    {
        for (int j = i + 1; j < count; j++)
        {
            int v1 = linkIndexs;
            int v2 = linkIndexs[j];
            if (vertices[v1] == vertices[v2])
            {
                if (!repeatIndexDict.ContainsKey(v2))
                    repeatIndexDict.Add(v2, v1);
            }
        }
    }

    foreach (var pair in linkDict)
    {
        //通过之前记录的关系进行替换linkDict的内容
        foreach (Item v in pair.Value)
        {
            if (repeatIndexDict.ContainsKey(v.index))
            {
                v.index = repeatIndexDict[v.index];
            }
        }
        //如果键重复则合并
        if (repeatIndexDict.ContainsKey(pair.Key))
        {
            linkDict[repeatIndexDict[pair.Key]].AddRange(pair.Value);
            pair.Value.Clear();
        }
    }
}

3.根据获得的回环生成三角数据
首先要注意的是,我们的原数据不是离散顶点,而是Edge。而数据中出现凹多边形,当做离散点处理会出现下面的错误:

5.jpg

红色的错误连线
所以一般的Delaunay其实是不适用的,而且我们其实也并不在乎出现狭长的三角形。
这里用的是Unity Wiki上的一个算法(我稍微修正了下三角形正反朝向的问题
原理就是不断将相邻的三个顶点连成三角形并移除,同时保证生成的三角形内部没有其他的顶点(所以这个算法的先决条件就是形成回环而且没有hole)
但这个算法是2D的,所以要先把3D顶点映射到2D平面上。这里我直接用了断开处骨骼的切线平面——只要将顶点换算到骨骼空间就可以了,也就是直接乘以bindposes。
[AppleScript] 纯文本查看 复制代码
Matrix4x4 m = skinMeshRenderer.sharedMesh.bindposes[cutBoneStartIndex];
foreach (int index in linkIndexs)
{
      Vector3 boneSpaceVertice = m.MultiplyPoint3x4(vertices[index]);
      linkBoneSpaceVertices.Add(boneSpaceVertice);
}

切面的纹理Uv也可以从这个2D平面数据里获得,记录最大值和最小值,将x,y归一化到0,1就是Uv。
[AppleScript] 纯文本查看 复制代码
Dictionary<int, Vector2> NormalUv(Dictionary<int, Vector2> uvs)
{
    Vector4 uvBounds = new Vector4(float.MaxValue, float.MaxValue, float.MinValue, float.MinValue);
    foreach (var pair in uvs)
    {
        if (pair.Value.x < uvBounds.x)
            uvBounds.x = pair.Value.x;
        if (pair.Value.x > uvBounds.z)
            uvBounds.z = pair.Value.x;
        if (pair.Value.y < uvBounds.y)
            uvBounds.y = pair.Value.y;
        if (pair.Value.y > uvBounds.w)
            uvBounds.w = pair.Value.y;
    }
    uvBounds.z = uvBounds.z - uvBounds.x;
    uvBounds.w = uvBounds.w - uvBounds.y;
    Dictionary<int, Vector2> result = new Dictionary<int, Vector2>();
    foreach (var pair in uvs)
    {
        result.Add(pair.Key, new Vector2((pair.Value.x - uvBounds.x) / uvBounds.z, (pair.Value.y - uvBounds.y) / uvBounds.w));
    }
    return result;
}
4.将切面网格和原网格合成
这里用了subMesh和多重材质,可以少复制些数据。该多重纹理的时候还是该多重纹理。
切面三角形的正反面问题在生成三角形的时候就已经处理了,方法是判断相邻线段的叉乘方向。如果方向不对,输出时就Reverse一次。


6.jpg

这个做法大部分时候都是正确的,但因为坐标系转换的依据是Bone的方向而非切面的平均方向,假如关联骨骼比较杂乱,又或者蒙皮刷得比较奇葩,导致Bone方向和切面方向差得太远,在这里就会出错。


所以也可以直接使用双面材质。
7.jpg 8.jpg


切面图随便找的一张,演示用。可以看到纹理能正常呈现。

9.jpg


切面其实并不仅限于简单形状,这样的也是可以的,所以切面回环必须重新生成。


10.jpg



不过在“Bone方向和切面方向过于不统一”,“切面投影到平面是多层的”,“本来窟窿就有多个”的时候,三角形还是会生成失败。但只要切面比较规整,一般是没有问题的。

然而最后还存在一个问题
上面拆分顶点只拆了三角形索引,两个模型都有完整的顶点数据,断手的bakeMesh也是全模型执行的。效率问题是其次,更严重的问题是,如果想让断手有正常的物理表现就需要MeshCollider,而它的数据是根据顶点生成的(和三角形索引无关),会生成这样一个碰撞箱(按原模型生成)

11.jpg


所以必须过滤掉不在三角形索引内的顶点。

方法还是重映射,把三角形索引这样复制一下去重,数组下标就是新索引,数组内容是旧索引。

[AppleScript] 纯文本查看 复制代码
List<int> triConvertDict = new List<int>(new HashSet<int>(triangles));

然后重新生成所有顶点数据
[AppleScript] 纯文本查看 复制代码
//反向索引
Dictionary<int, int> triConvertInvDict = new Dictionary<int, int>();
//重排顶点
int count = triConvertDict.Count;
for (int i = 0; i < count;i++)
{
    int index = triConvertDict;
    vertices.Add(oldVertices[index]);
    uvs.Add(oldUvs[index]);
    colors.Add(oldColors[index]);
    normals.Add(oldNormals[index]);
    tangents.Add(oldTangents[index]);
    boneWeights.Add(oldBoneWeights[index]);
    triConvertInvDict.Add(index, i);
}
//重排三角索引
List<int> newTriangles = new List<int>(triangles.Count);
count = triangles.Count;
for (int i = 0;i < count;i++)
{
    newTriangles.Add(triConvertInvDict[triangles]);
}


便能得到正常的物理效果

12.jpg


1.1.gif





回复可得到Package和完整代码




游客,如果您要查看本帖隐藏内容请回复



[发帖际遇]: 清风 发帖时在路边捡到 1 蛮牛币,偷偷放进了口袋. 幸运榜 / 衰神榜

跟我念“站长妹纸萌萌哒!”我说站长,你说YO!爱你们么么哒~
回复

使用道具 举报

6蛮牛粉丝
1058/1500
排名
5784
昨日变化
2

6

主题

638

帖子

1058

积分

Rank: 6Rank: 6Rank: 6

UID
236677
好友
0
蛮牛币
1693
威望
0
注册时间
2017-8-9
在线时间
218 小时
最后登录
2018-2-22
发表于 2018-1-15 15:51:13 | 显示全部楼层
感谢分享

回复

使用道具 举报

5熟悉之中
669/1000
排名
4706
昨日变化

0

主题

182

帖子

669

积分

Rank: 5Rank: 5

UID
224429
好友
0
蛮牛币
115
威望
0
注册时间
2017-5-31
在线时间
235 小时
最后登录
2018-2-17
发表于 2018-1-15 16:02:29 | 显示全部楼层
感谢分享

回复

使用道具 举报

6蛮牛粉丝
1172/1500
排名
2441
昨日变化
2

3

主题

156

帖子

1172

积分

Rank: 6Rank: 6Rank: 6

UID
45560
好友
1
蛮牛币
33
威望
0
注册时间
2014-9-18
在线时间
517 小时
最后登录
2018-2-12
发表于 2018-1-15 16:02:53 | 显示全部楼层

感谢分享

回复

使用道具 举报

7日久生情
4086/5000
排名
278
昨日变化

51

主题

687

帖子

4086

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
120040
好友
0
蛮牛币
3498
威望
0
注册时间
2015-8-28
在线时间
1708 小时
最后登录
2018-2-21
发表于 2018-1-15 16:07:22 | 显示全部楼层
好吓人……

回复

使用道具 举报

6蛮牛粉丝
1334/1500
排名
23480
昨日变化
4

10

主题

700

帖子

1334

积分

Rank: 6Rank: 6Rank: 6

UID
158776
好友
1
蛮牛币
39
威望
0
注册时间
2016-7-26
在线时间
612 小时
最后登录
2018-2-9
发表于 2018-1-15 16:24:17 | 显示全部楼层
楼主牛逼啊,6666666666
[发帖际遇]: 一个袋子砸在了 Hao521314 头上,Hao521314 赚了 1 蛮牛币. 幸运榜 / 衰神榜

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1058/1500
排名
5784
昨日变化
2

6

主题

638

帖子

1058

积分

Rank: 6Rank: 6Rank: 6

UID
236677
好友
0
蛮牛币
1693
威望
0
注册时间
2017-8-9
在线时间
218 小时
最后登录
2018-2-22
发表于 2018-1-15 16:30:24 | 显示全部楼层
感谢分享

回复

使用道具 举报

2初来乍到
113/150
排名
15538
昨日变化
5

0

主题

46

帖子

113

积分

Rank: 2Rank: 2

UID
207687
好友
0
蛮牛币
4
威望
0
注册时间
2017-2-21
在线时间
31 小时
最后登录
2018-2-7
发表于 2018-1-15 16:31:34 | 显示全部楼层
复杂的算法就看看了,不写

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1120/1500
排名
2450
昨日变化

5

主题

135

帖子

1120

积分

Rank: 6Rank: 6Rank: 6

UID
172535
好友
2
蛮牛币
1227
威望
0
注册时间
2016-9-27
在线时间
484 小时
最后登录
2018-2-22
QQ
发表于 2018-1-15 16:50:48 | 显示全部楼层
感谢分享

回复

使用道具 举报

7日久生情
2428/5000
排名
469
昨日变化
1

1

主题

321

帖子

2428

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
2219
好友
1
蛮牛币
6067
威望
0
注册时间
2013-8-19
在线时间
768 小时
最后登录
2018-2-19
发表于 2018-1-15 16:54:21 | 显示全部楼层
问题是性能吧,有插件实现,但在手机上速度感人啊

回复 支持 反对

使用道具 举报

4四处流浪
463/500
排名
6516
昨日变化
1

0

主题

78

帖子

463

积分

Rank: 4

UID
132849
好友
0
蛮牛币
307
威望
0
注册时间
2015-12-28
在线时间
219 小时
最后登录
2018-2-8
发表于 2018-1-15 16:54:23 | 显示全部楼层
谢谢分享!!

回复

使用道具 举报

0

主题

4

帖子

21

积分

Rank: 1

UID
218093
好友
0
蛮牛币
48
威望
0
注册时间
2017-4-17
在线时间
20 小时
最后登录
2018-1-16
发表于 2018-1-15 17:05:28 | 显示全部楼层
感谢分享

回复

使用道具 举报

5熟悉之中
695/1000
排名
4067
昨日变化

1

主题

64

帖子

695

积分

Rank: 5Rank: 5

UID
123454
好友
0
蛮牛币
825
威望
0
注册时间
2015-9-22
在线时间
333 小时
最后登录
2018-2-2
发表于 2018-1-15 17:16:00 | 显示全部楼层
感谢楼主分享
[发帖际遇]: 稻米熟了吗 发帖时在路边捡到 1 蛮牛币,偷偷放进了口袋. 幸运榜 / 衰神榜

回复

使用道具 举报

3偶尔光临
209/300
排名
10188
昨日变化
2

1

主题

33

帖子

209

积分

Rank: 3Rank: 3Rank: 3

UID
213756
好友
0
蛮牛币
289
威望
0
注册时间
2017-3-22
在线时间
93 小时
最后登录
2018-2-11
发表于 2018-1-15 17:18:12 | 显示全部楼层
长知识了

回复

使用道具 举报

4四处流浪
488/500
排名
5687
昨日变化
49

0

主题

171

帖子

488

积分

Rank: 4

UID
229748
好友
0
蛮牛币
1695
威望
0
注册时间
2017-7-1
在线时间
119 小时
最后登录
2018-2-21
发表于 2018-1-15 17:23:39 | 显示全部楼层
NB啊 6666

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

关闭

站长推荐 上一条 /1 下一条

快速回复 返回顶部 返回列表