传统高度图地形的制作方式不可避免会遇到峭壁拉伸严重的问题,另一方面峭壁要有分层的感觉。常见的解决方法主要有两种:一种是贴面,用石头的模型摆成悬崖,但是人力成本太高;另一种是用triplanar的uv,但是贴图采样次数过多。
一些新的程序化的方式包括:stochastic triplanar(Far Cry5),拆峭壁减少triplanar的贴图采样次数;启发式贴面Heruistic Quilting(Dauntless),程序化摆贴面模型,Ghost Recon目测大概也是这么做的。
不过triplanar仍然采样次数过多;而贴面意味着drawcall过多的问题。而要达到峭壁也有层积的效果,这里提出对峭壁mesh自动展UV的方法。
1. 建模
没什么好说的,houdini terrain程序化生成一下就好了,这里选择了一种大峡谷的地貌。方法就是经典的多层terrace然后erode,不是重点此处略过
2. 拆峭壁分块
转mesh然后remesh,transfer一下slope参数挑出来,然后用GameDevtool的AutoUV的Cluster直接分组先展UV
3. 分拆孤岛
我们只希望mesh是单片的而不是多块连通,要分拆孤岛,最简单的收缩扩张就好了,给一个参数向内收缩到孤岛再扩张
收缩用了一个loop
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 |
vector pos0 = point(0, "P", 0); vector uv0= point(0, "uv_transfered", 0); vector avgNormal = normalize(v@avgN); float step = 0.1; if(@weight <= step){ @weight = 0; return; } int neibs[] = neighbours(0, @ptnum); int neighborAllZero = 0; int neighborAllPosi = 1; int neighborHasZero = 0; float min_neib_weight = 10000; foreach(int neib ; neibs){ vector position = point(0, "P", neib); float weight = point(0, "weight", neib); if(weight < step){ neighborAllPosi = 0; } if(weight > 0){ neighborAllZero = 1; } if(weight == 0){ neighborHasZero = 1; } float distance = distance(position, @P); if(weight + distance < min_neib_weight){ min_neib_weight = weight+distance; } } if(neighborAllZero == 0){ @weight -= step; } else if(neighborAllPosi == 1){ return; } else if( neighborHasZero == 1){ f@weight -= step; } else{ f@weight = min_neib_weight - step; } |
扩张用了一个loop
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
if(@class > -0.5){ return; } int neibs[] = neighbours(0, @ptnum); int minclass = -1; foreach(int neib; neibs){ float thisclass= point(0, "class", neib); if(thisclass > minclass){ minclass = thisclass; } } if(minclass > -0.5){ @class = minclass; } |
这样UV就能拆成小片了
4 矫正UV
基本思路:我们假设uv已经展好,且应该接近平面形状。这是一组点在二维平面内的投影。之后我们将顶点投影到法平面上,这是同样一组点在二维平面内的另一个投影。我们希望找出一个旋转量,使得这两组投影尽可能接近。解析方法应该是用这个角度建立方程,求个导求值为0的解;采用的近似做法是,计算每个点的旋转角度求平均。

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 |
vector pos0 = point(0, "P", 0); vector uv0= point(0, "uv_transfered", 0); vector avgNormal = normalize(v@avgN); float anglesum = 0; vector center = getbbox_center(0) ; vector uvcenter = (0,0,0); for(int i = 1; i < @numpt; i++){ uvcenter += point(0, "uv_transfered", i); } uvcenter /= (@numpt - 1); vector up = (0,1,0); up.y = 1; vector avgPlaneX = cross(up, avgNormal); vector avgPlaneY = cross(avgNormal, avgPlaneX); v@avgX = avgPlaneX; v@avgY = avgPlaneY; for(int i = 1; i < @numpt; i++) { vector thispos = point(0, "P", i); vector thisuv = point(0, "uv_transfered", i); vector position_offset = (thispos - center); vector uv_offset = normalize(thisuv - uvcenter); float position_offset_project_on_angN = dot(avgNormal, position_offset); vector position_offset_project_on_angNPlane = position_offset - position_offset_project_on_angN * avgNormal; float onX = dot(avgPlaneX, position_offset_project_on_angNPlane); float onY = dot(avgPlaneY, position_offset_project_on_angNPlane); vector onAvgPlane = (0,0,0); onAvgPlane.x = onX; onAvgPlane.y = onY; float dot = dot(normalize(onAvgPlane), uv_offset); float angle = acos(dot); vector crossangle = cross(normalize(onAvgPlane), uv_offset); if(crossangle.z>0){ if(dot > 0){ angle = -angle; } else{ angle = 2*3.1416 - angle; } } anglesum += angle; setpointattrib(0, "errorness", i, dot); setpointattrib(0, "pos_offset", i, normalize(onAvgPlane)); setpointattrib(0, "uv_offset", i, uv_offset); } anglesum /= (@numpt - 1); @anglesum = anglesum * 360 / (2 * 3.14159); |
这样就基本矫正了UV.
以下是在UE4中创建的材质截图的效果
好文章
谢谢支持!
cool, 能把节点展示出来就更好了。
真棒
谢谢老板资瓷!
請問這樣UV交界處的貼圖不會有很明顯的垂直銜接嗎
确实会有一些。。。