虚空场景效果
显示上的要求是:
- 左右两边是虚空的太空场景
- 太空和可行走路径间有一个宽度的雾的效果。
- 中间是可行走路径
- 地板上的物品一部分是需要被裁剪的,一部分是不能裁剪的。
- 角色不能越界行走,怪物死亡也只能再区域内。
工具的要求是:
- 要能用笔刷方式画出可行走区域
- 自动生成虚空和边缘区域
- 边缘区域的透明和范围要可控
然后看看生成的场景的节点布局:
这里解释下:
Ground是地板物品
Hollow是虚空效果
HollowEdge是虚空边缘效果
TerrainMask是遮挡物
Collider是具体的碰撞体
这里可能会奇怪,为什么有两个地板遮挡物?为什么又这么多collider?后面会解释到
这里一个一个解决问题:
一:虚空的太空场景和雾的效果:
这个主要还是美术来提供效果了,之前给到的时一个完整的虚空和边缘效果,只是把他拆成两部分了,虚空是虚空的shader,雾是雾的shader。并配上他的宽度和透明的可调整度。其他倒没修改什么。
二:可行走区域
可行走区域的裁剪方式一般来说有两种:
第一种是在地板的shader上去获取贴图然后识别屏幕空间自己的像素是否再贴图透明度大于0内,如果在则显示,不在则隐藏。一般来说不会采用这种方式,因为我们的地板是分成很多个tile的,这个方式每个地板都要多采样一个贴图,带宽自然压力就大了。
第二种是用模板的方式去做,也就是设置虚空模板判断为小于等于0的情况下显示,然后可行走区域的遮挡是大于等于1的情况下显示。然后让地板先渲染,虚空和边缘后渲染。这样能得到被裁剪的正确效果。
这里有一点是需要有遮挡图,这个遮挡图一开始我是用一个真实的贴图做的。但是美术不希望用贴图来做,因为这样会导致他们每次做新图都要额外画遮挡图。希望更方便点。所以就有了后面会说到的笔刷工具。
要注意的一点是雾的效果要在最上面,也就是要能遮挡住地板,为什么这样做后面解释。
通过模板的方法我们已经可以形成一个遮挡路径了。
三:边缘模糊:
一开始没做边缘模糊导致效果很锐利,效果不理想:
然后想做一些边缘模糊的效果来让他舒服点。
边缘模糊这里也卡了我一段时间,经历过几段优化阶段
第一次做想从shader上来做,但是我也不太可能每个像素判断是否是边缘,因为他不是规则的图形,没办法知道边缘,而且就算你能用卷积等方式识别出边缘也要进行模糊处理,还得再采样。这样就一个像素都要采样很多次,不划算
第二次想着既然不能实时做,我就在编辑器下直接处理好这张图片。我的做法是在编辑器下识别出边缘,然后再边缘上往外扩,上下左右斜边,8个方向获取像素做alpha相加,这样越往外alpha值越低。因为加得少。这样也还不行,因为这样累加不会有模糊得效果,所以我后面做了一次高斯模糊。因为都是再编辑器下做的,所以性能不需要考虑太多。
往外扩是这样处理:
if (tColor.a > 0.01)
{
setColorRedSpaces.Add(new Vector2Int(i, j));
setColorSpaces.Add(new Vector2Int(i, j + edgeLine));
setColorSpaces.Add(new Vector2Int(i, j - edgeLine));
setColorSpaces.Add(new Vector2Int(i - edgeLine, j));
setColorSpaces.Add(new Vector2Int(i + edgeLine, j));
setColorSpaces.Add(new Vector2Int(i - edgeLine, j - edgeLine));
setColorSpaces.Add(new Vector2Int(i - edgeLine, j + edgeLine));
setColorSpaces.Add(new Vector2Int(i + edgeLine, j - edgeLine));
setColorSpaces.Add(new Vector2Int(i + edgeLine, j + edgeLine));
}
模糊是这样处理
TextureBlur(edgeMask, edgeLine);
TextureBlur(edgeMask, edgeLine * 2);
TextureBlur(edgeMask, edgeLine * 3);
private void TextureBlur(Texture2D edgeMask, int edgeLine)
{
Color[,] blurs = new Color[edgeMask.width, edgeMask.height];
for (int i = 0; i < edgeMask.width; i++)
{
for (int j = 0; j < edgeMask.height - 1; j++)
{
Color tColor = edgeMask.GetPixel(i, j);
//先做一次模糊=
Color lColor = edgeMask.GetPixel(i - edgeLine, j);
Color rColor = edgeMask.GetPixel(i + edgeLine, j);
Color topColor = edgeMask.GetPixel(i, j - edgeLine);
Color bColor = edgeMask.GetPixel(i, j + edgeLine);
Color ltColor = edgeMask.GetPixel(i - edgeLine, j - edgeLine);
Color lbColor = edgeMask.GetPixel(i - edgeLine, j + edgeLine);
Color rtColor = edgeMask.GetPixel(i + edgeLine, j - edgeLine);
Color rbColor = edgeMask.GetPixel(i + edgeLine, j + edgeLine);
Color outColor = Color.black;
outColor += lColor;
outColor += rColor;
outColor += topColor;
outColor += bColor;
outColor += ltColor;
outColor += lbColor;
outColor += rtColor;
outColor += rbColor;
outColor /= 9;
blurs[i, j] = new Color(outColor.r, outColor.g, outColor.b, outColor.a);
//edgeMask.SetPixel(i,j,new Color(outColor.r, outColor.g, outColor.b, outColor.a));
}
}
for (int i = 0; i < edgeMask.width; i++)
{
for (int j = 0; j < edgeMask.height; j++)
{
edgeMask.SetPixel(i, j, blurs[i, j]);
}
}
edgeMask.Apply(false, false);
}
这样的大的边缘就会比较模糊
第三次:在完成模糊效果后,一开始还是把边缘放在地板下面的。但是法线边缘还是很锐利。因为是地板锐利而不是虚空边缘锐利。所以又修改了下改为在地板上面显示了。然后我们需要把虚空边缘的中间给挖空。这个挖空就可以利用我们遮挡图中间的alpha为0来实现。这样才最终实现了比较理想的边缘效果
四:笔刷工具
之前实现的方式都是用美术一个贴图方式实现的,但是因为不希望额外提供一个图,所以我们需要提供一个笔刷工具。
我这里的整体思路是用linrenderer然后贝塞尔弧线连线生成一张遮罩图和碰撞区域。
一:笔刷底板:
做笔刷工具首先可刷的地板,我的思路是做一个半透明的地板
我们直接用UI/Default这个shader,然后颜色调整为半透明就好了
二:画线:
画线有几种方式,一种gl直接画,一种是用linerenderer。
我选择的是linerenderer,理由是他能帮我自动闭合整个连线。
只要把
这个设置好就行了。
然后点一个位置我就设置一次position。自然能连线了。但一开始用这个连线会有个问题,就是转角的锐利。
这个效果不理想,生成出来的图肯定会有问题
自然想到的就是用贝塞尔弧线来让他圆滑了。我们用四个点就能算出他们的圆滑的弧线。
Vector3 BezierPathCalculation(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
float tt = t * t;
float ttt = t * tt;
float u = 1.0f - t;
float uu = u * u;
float uuu = u * uu;
Vector3 B = new Vector3();
B = uuu * p0;
B += 3.0f * uu * t * p1;
B += 3.0f * u * tt * p2;
B += ttt * p3;
return B;
}
public void CreateCurve(ref List<Vector3> controlPoints)
{
if (controlPoints.Count < 4)
{
return;
}
segments = controlPoints.Count / 3;//以3为间隔进行平滑 算出分段数量
pointCount = controlPoints.Count;//这里最大点数就是本身
for (int s = controlPoints.Count - 4; s < controlPoints.Count - 3; s++)//以3为间隔遍历所有点
{
Vector3 p0 = controlPoints[s];
Vector3 p1 = controlPoints[s+1];
Vector3 p2 = controlPoints[s+2];
Vector3 p3 = controlPoints[s+3];
for (int p = 0; p < 3; p++)
{
float t = (1.0f / (pointCount/segments)) * p;
Vector3 point = new Vector3 ();
point = BezierPathCalculation (p0, p1, p2, p3, t);
controlPoints[p + s] = point;
}
}
}
上面是贝塞尔互选的运算,原理可以找找资源,有很多,我就不说了。
然后每次画就运算当前点与前三个点的关系,并修改当前三个点的位置。
这里有几个问题:
- 我们地图是分左右的。所以要知道左边什么时候画完,右边什么时候开始画。所以我们要定一个规则,为了尽量不要两手操作,所以就定义了鼠标中键点击后认为左边画完,让鼠标中键的x位置为左边终点位置,然后z轴就是我们上面说的透明画布的rect下边最大值的区域了。pos.z = meshRenderer.bounds.min.z;
- 右边起点自然就是左边画完再次点击鼠标左键的位置了。但是这里有一点需要关注的,因为我们有贝塞尔弧线,而且需要四个点一起起作用,如果我们直接按照鼠标左键来建立一个位置。那么就会导致前面的点都被动到,自然就连不上左边终点了。
所以我们在右边开始的时候要额外建立四个点放在x轴为鼠标左键的点,z轴为画布的最大值的点pos.z = meshRenderer.bounds.min.z;
这样才能链接上右边的区域:
解决完这个问题后就是右边终点的确定了,我们还是考虑单手操作的问题的我们识别右边结束就可以用鼠标右键的点击来决定了。
当鼠标右键点击后,我们需要做两步:
第一是把线连到x轴为鼠标右键的值,z轴为画布最上方的位置:
meshRenderer.bounds.max.z
第二是我们需要让linerenderer链接起来,自然就是设置loop为true了。(注意一开始不要设置为true,不然会很怪)
三:生成遮挡网格:
线画完了,这些都比较简单,接下来就是困扰我比较长时间的一个问题,建立网格。
首先我们要确认难点在哪
网格最少由三部分组成,顶点位置,uv和索引
顶点位置这个不用多说,可定就是画线的位置了。
(另外这里有一篇文章也说了凸多边形的创建:https://zhuanlan.zhihu.com/p/158043191)
Uv也比较简单,因为我们不设置具体的贴图展示用,只是做遮罩,所以所有顶点的uv都设置为0或1就好了。
难点在索引,索引影响到我们的三角形方向和建立。方向当然我们也可以暂时不管,我们完全可以双面渲染就解决了方向问题了。但是需要建立三角形还是需要索引的。索引顺序不同,结构不同,整个网格建立起来的结构也不同,
如果是014123134这样顺序就能得到正确的网格,如果是024134这样就是得到错误的结构,因为0和2链接起来了。
所以到索引这里我们得找规律。
一开始我是用左右两边数组来做得,相当于左边用两个数字,右边用一个数字组成一个三角形,然后右边两个数组,左边一个数字组成完成,一次类推直到结束。
但后面法线稍微有点扭曲的面就不行了,也会穿透顶点之外的区域:
后面考虑到要比较好的适应他的布局,用左右两边为数组不断链接其实是不太好的,因为他们有可能左边画的点多右边少,或右边多左边少,这样对不齐其实很容易导致这样的情况。
那么要对齐就是比较理想的方式了。我们可以建立两个遮罩图,第一个是在中间建立一些点,这些和左边的点一致,只是x在中间而已。第二个遮罩图是建立一些点让他和右边数组对齐,x轴也是在中间,也就是0.
这样建立网格就比较理想了
是两个遮罩图组成的。
当然他也由一定的局限,就是不能由漩涡的转角,也不能让凹进去的地形。
索引代码:
public static int[] CreateMaskMeshIndexs2(List<Vector3> borderPos, List<Vector3> extraLinePoss, int leftBlendLen)
{
int lposIndex = 0;
int rposIndex = 0;
int triangleNum = (borderPos.Count - 1) * 2;
int indexNum = triangleNum * 3;
int[] indexs = new int[indexNum];
for (int num = 0; num < triangleNum; num++)
{
int i = num * 3;
int topNum = borderPos.Count;
indexs[i] = topNum + rposIndex;
indexs[i + 1] = lposIndex;
indexs[i + 2] = lposIndex + 1;
num++;
i = num * 3;
indexs[i] = topNum + rposIndex;
indexs[i + 1] = lposIndex + 1;
indexs[i + 2] = topNum + rposIndex + 1;
rposIndex++;
lposIndex++;
}
return indexs;
}
四:生成碰撞网格:
生成碰撞网格其实我也考虑过两种方式,
第一种就是在左右两边直接建立一个碰撞体,这个碰撞体带宽度。后来法线这样的索引也比较难,规律不好找。先放弃了。
用第二种就比较简单,先建立一个碰撞片,这个碰撞片是在左边点的数组的y轴处向下20的位置建立同样数量的点,并且注意上下两边的碰撞要突出去一点,不然在边缘容易走出区域。这样建立一个碰撞片
但是如果只有一个碰撞片很容易导致角色移动的时候移出碰撞区域外,所以多建立几个然后紧贴着就好了
五:生成图片:
这一步就没上面太大的难度了
IEnumerator CreateTexture()
{
yield return new WaitForSeconds(0.2f);
RenderTexture prev = RenderTexture.active;
RenderTexture.active = mBrushTexture;
Texture2D png = new Texture2D(mBrushTexture.width, mBrushTexture.height, TextureFormat.ARGB32, false);
png.ReadPixels(new Rect(0, 0, mBrushTexture.width, mBrushTexture.height), 0, 0);
byte[] bytes = png.EncodeToPNG();
string contents = Application.dataPath + "/../" + TerrainManage.MaskTextureSavePath;
if (!Directory.Exists(contents))
Directory.CreateDirectory(contents);
FileStream file = File.Open(contents + "Brush_Mask_" + mSceneName + ".png", FileMode.Create);
BinaryWriter writer = new BinaryWriter(file);
writer.Write(bytes);
file.Close();
RenderTexture.active = prev;
TextureImporter texObj = AssetImporter.GetAtPath(TerrainManage.MaskTextureSavePath + "Brush_Mask_" + mSceneName + ".png") as TextureImporter;
texObj.mipmapEnabled = false;
texObj.isReadable = true;
AssetDatabase.Refresh();
}
然后我们就建立一个图片,不需要美术提供图了。最终的效果也比较理想
在设置上也比较少内容需要策划来设置的,还可以改变画板的大小。
基本就是一键生成了。
最终效果