加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

基于Unity3D的模型像素着色的笔刷算法的实现

(2018-08-22 23:15:51)
分类: 游戏

基于Unity3D的模型像素着色的笔刷算法的实现

http://s9/mw690/003HtLHwzy7n2neAZwka8&690

    游戏场景制作经常会遇到模型与地形的贴图衔接问题,如《逆水寒》地形与边缘石头模型以及木桥模型等的纹理衔接。解决办法是模型的某个“自定义区域”和地形都用世界坐标采样同样的纹理贴图,而这个“自定义区域”为了制作方便往往是通过在Unity3D里绘制的一张图片来控制的。官方已有不少插件能实现此绘制功能,算法就是根据笔刷半径在图片中算出一个以raycastHit.textureCoord为中心正方形区域,然后改变区域内的像素,这种简易算法隐藏的前提是模型UV连续,比如Terrain转的Mesh,否则极易造成笔刷绘制区域内的某些像素画不上以及刷到了笔刷区域外的情况,而实际情况中模型的UV展法是很不确定的,即每刷一下对应图片中发生变化的像素可能会是东一块西一块比较杂乱的。本文讲述如何在Unity3D里像BodyPaint3D那样在任意模型上用像素级别的笔刷绘制所需图片,思路很明确——就是不断把每一步遇到的问题抽象成数学语言然后去拟定合适的算法。

 

    1.顶点的空间坐标转换

    要先做一些准备工作。

    构造一个类似CVC的笔刷空间:光标方向发一条射线,以raycastHit.point为中心建立一个大小跟笔刷半径有关的立方体,再等着去比较三角形在不在这个立方体中即可。由于采用一组单位正交基,一个基向量是raycastHit.normal,剩下两个向量再确定一个即可,我这里用的是Vector3.up和raycastHit.normal的外积,需要特殊处理下这两个向量共线的情况,因为共线的话由于sin(夹角)=0,而零向量是不能当基向量的。

    笔刷空间构建好了,下面要做的就是求模型的点在笔刷空间的坐标以方便下面的比较。由于可以瞬间写出笔刷空间到世界空间的坐标转换矩阵,所以偷懒的话直接求逆即可。API虽然直接提供了求逆矩阵的函数Matrix4x4.inverse,但并不太了解CPU求逆矩阵的底层运算,如果像人类一样求n阶方阵的n*n个代数余子式那就不合算了。所以这里可以优化算法:利用3x3正交矩阵的转置矩阵构造世界空间到笔刷空间的向量转换矩阵,再利用分块矩阵的运算规律改写矩阵的第4列,这样可以省了求矩阵的逆的运算。

    这一步是最最简单的,都是非常常规的处理方式。

 

    2.三角形剔除

    首先要解决的问题:一笔刷下去模型的哪些三角面受影响呢?

    用过ZBrush的都知道模型刷色的时候一般都要开启BackfaceMask(如下图),

                                                            

http://s6/mw690/003HtLHwzy7n2nnpLk965&690


相当于背面剔除,否则容易刷到背面,所以先遍历模型的每个三角形,如果三角形的法线与raycastHit.normal内积小于0则直接舍去。

    然后遍历筛完剩余的三角形的每个顶点,分两种情况:

    (1)至少有一个点落入笔刷立方体内,则该三角形肯定是受笔刷影响的。判断一个点是否在一个立方体内只需用比较坐标大小即可;

    (2)三个点都不在笔刷立方体内。这时为了简化算法我把判定的标准改成:三角形是否与以笔刷半径为半径的球体相交:

http://s12/small/003HtLHwzy7n2nAh0kHdb&690

    会有人质疑我忽略了这种情况:

http://s2/small/003HtLHwzy7n2nCezIZ81&690

    放心,没人会去注意角落的绘制情况,而且绘制区域都是强度从中心发散并减弱的圆形,角落里一般也不需要绘制。

    这里需要巧解一下该问题,首先raycastHit.triangleIndex的三角形肯定是要被绘制的。除去这个三角后,该问题可以转换为三角形的任意一条边只要有任意长度的一段落入笔刷球体内则可判定该三角形受到了笔刷的影响。于是问题转换成:三角形是否存在某条边(注意不是边所在的直线,这里的边是条线段)到原点的距离小于笔刷半径?对应的数学模型就是求点到线段的距离。

    空间中点向某条线段所在的直线投影分三种情况:

 http://s6/mw690/003HtLHwzy7n2nWSfIh05&690

    无论那种情况,未知数r的求法都相同。充分利用这里讨论的前提是三角形的三点已超出笔刷球体,所以只看满足r属于(0,1)的情况就行,再求出PC的长度比较与笔刷半径的大小关系即可确定该三角形是否与笔刷球体相交。

    这一步实际上是筛选出哪些三角形需要对其对应的UV线段围成的图案进行全部或部分像素填充。

 

    3.光栅化

    到了这一步才会去更改图片,对上一步筛完后符合要求的三角形进行像素填充。有人可能会立马想到现在该做的事情就是逐行遍历图片像素,然后用判断点是否在待填充三角形簇内的算法来决定是否改变该像素。但是很遗憾不行,因为待填充的三角形对应的UV形状可能根本就不是个三角形,可能是个点,还可能是条线,所以需要分情况讨论。

    首先先把顶点的UV坐标根据图片的长宽转换成像素坐标,如对于一张512*512的图片,横纵转换为(int)(0~511)离散型定义域。

    (1)点填充。

    当三角形三个顶点的像素坐标都一样时,说明此时已经缩成了一个点,美术经常会把一个纯色区域的UV缩放的相当小以节约UV面积。这种情况直接填充这个像素即可。

    (2)线填充

    这里需要再分两种情况讨论:一是三角形的三个顶点中有两个顶点重合,只需两两比较即可确定是否属于这种情况;二是不重合的三点共线,抽象成数学模型就是判断三点共线问题,可以以其中一点为起始点构建两个向量vec1(x1,y1)、vec2(x2,y2),进而转换成判断两个非零向量共线问题,即比较x1*y2和x2*y1。填充像素时以线段的一个端点为起点,沿着斜率方向每次增长delta长度,直到到达另一端点为止。对于情况一线段的两个端点是可以瞬间确定的,情况二需要先判断vec1、vec2的数量积是否小于0,如果是则说明选择的起始点在线段中间,其余两个点就是端点,否则则说明起始点为其中一个端点,再比较vec1、vec2的模即可确定另一个端点。

    (3)面填充

    终于到了三角形填充的讨论了。大致思路是:根据三角形的三个顶点的像素坐标最值建立一个包围框,在包围框内逐行扫描,判断每个像素是否在该三角形内。但是直接开始操作还是不行:有些被三角形三边穿过的像素,算法会得到其不在三角形内部的结论,但也需要被填充的,否则模型上就会出现锯齿。如图该三角形只会填充粉色标识的像素,发生了填充缺失现象:

 
http://s15/small/003HtLHwzy7n2obKmtwbe&690

    事实上由于游戏贴图经常需要mipmap,美术还希望UV边缘能溢出几个像素。所以我们需要把三角形向“外”略微扩展,面临的问题就是“外”的方向如何确定。想想三角形的那几个几何特征量,我选择了重心,重心本来就类似于中心的意思,它是三边中线的交点,而且一定在三角形内部,还非常好求:就是三个顶点坐标的几何平均值。所以让三个顶点沿着重心到各自构成的向量方向扩展即可。如图所示,扩展后的三角形会完全包含原来三角形覆盖的像素:

 http://s7/small/003HtLHwzy7n2obFGR0e6&690

    为避免同一个像素重复填充可记录已填充的像素坐标。

    现在就剩下一个数学模型了:如何判断一个点是否在三角形内部?

    通常的做法是选择一个起始点,用另外两个边向量表示起始点到该点的向量,如图:

http://s4/mw690/003HtLHwzy7n2ooZU7F53&690

    当求出的三个点的权重都在[0,1]内时则该点位于三角形内部。

    上面的算法确实能解决问题,但这里可以优化下算法。不妨反过来想,如果这个点在三角形内部的话,那么顶点属性就可以插值,坐标也是会满足插值关系的,所以可以推导出:

http://s15/mw690/003HtLHwzy7n2owTLoW0e&690

    相比前面的常规算法省去了很多边向量内积运算,执行效率要高一些。再判断a、b、c是否都在[0,1]内即可。

    最后根据点到笔刷原点的距离,以及类似于如下图ZBrush中的RgbIntensity、FocalShift、DrawSize等参数求出该点的笔刷强度进而求出该点填充的颜色色值。

http://s10/bmiddle/003HtLHwzy7n2oCwbNf79&690

    总的来说这套算法的实现都是基础数学问题,个人觉得文章写的已经够细了。搞完一共也就几百行代码,而且其中很多都是在处理插件UI的,所以跟编程水平关系并不大。

    最后放一个案例展示:

http://s3/mw690/003HtLHwzy7n2oMPI0G42&690

    平面代表地形,长方体代表与地形相接的石头模型,该模型的的UV拆的很碎而且布局是杂乱无章的:

http://s16/mw690/003HtLHwzy7n2oPU5Tpef&690

    要在石头模型上刷草皮纹理实现与地形的过渡,最终效果如下图:

http://s8/mw690/003HtLHwzy7n2oUeTun47&690

    插件绘制过程一分钟视频演示如下(内赠兔子简笔画教程^__^):

    https://v.youku.com/v_show/id_XMzc5MzQxOTg2OA==.html?spm=a2h3j.8428770.3416059.1

 

 

 

 

 

 

 

 

 

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有