NoiseFloor 发表于 2022-11-23 12:04

类似模拟城市中建筑覆盖范围在Unity的实现方法

项目中有类似模拟城市中服务类建筑给住宅提升效果的功能,需要有个辐射范围(覆盖范围)的显示。
大约是这样:



选择服务建筑,显示此类建筑的覆盖范围

其实在项目初期策划就提出了这个功能!
不过最近提出了玩法和表现的修改,所以重新又翻了出来。
要实现的表现效果大致如下:

[*]选择此类服务建筑,显示此类服务建筑所有覆盖到的范围,被覆盖到的住宅变为绿色。
[*]如果被覆盖的住宅在范围边缘,则此住宅下也有住宅大小的范围框
[*]点选的某个服务建筑的覆盖范围,有边框高亮显示。
[*]覆盖范围显示需要是圆角
关键词是:范围延伸、边框高亮、圆角。
对于圆角和边框高亮的实现可以有2种方法,一种是用Shader画圆角矩形,一种是在C#中绘制圆角矩形面片,下面两种方法都分享一下,如果有不足或其他方法也请大家多多指教~
一.范围延伸

两种方法的基础其实差不多,思路主要是绘制覆盖范围的矩形面片+住宅本身的矩形面片,再用模板剔除来实现透明度和颜色的统一。
具体如何画矩形面片就不赘述了,网上还挺多的。
给个普通的透明材质,画出来后长这个样子:


嗯,对比一下要的效果图,不太对,一整块的颜色应该是一样的。
然后我们在SubShader的地方加上模板剔除:
……
      Stencil
      {
             Ref 0          //0-255
             Comp Equal   //default:always
             Pass IncrSat   //default:keep
             Fail Keep      //default:keep
             ZFail Keep   //default:keep
      }
……大致意思为。
设定模板参考值为0,比较当前缓冲区的值,若相等,则通过(渲染)。
若通过,这块像素模板缓冲值+1,再次画到这个像素时0与1不相等,不通过,则剔除这个像素。


然后上图就是剔除后的效果(-w-
第一个部分就是这样。
二.圆角和边框高亮

啊为什么这两个放一起呢,因为一开始Shader画圆角的高亮边框我不是按法线扩张法来得到边框的,后来试验感觉还是法线扩张得到的边框效果好一些,但是还是想大致说说思路。
1.矩形面片+Shader绘制
首先,按我平时的思维圆角矩形是这样画的:



[*]先减去矩形角上宽高相等为h的四个正方形-->该区域alpha =0
[*]在四个正方形的靠内顶点上画四个半径为h的圆形 -->该区域ahlpha=1
顺着这个思路我们会想到对称,如何得到对称呢?
在unity中uv坐标是左下角为原点,往右/上为u/v正轴,所以
float2 uv = abs(i.uv.xy - float2(0.5,0.5));uv减去0.5会使小于0.5的部分变为负值,大于0.5的部分值变小,此时再使用绝对值计算就会得到一个原点在中心的uv坐标。



修改过后的uv值

然后利用对称,使边缘宽度为h的部分透明度为0
float width =0.5-0.1; // 0.1是边缘挖空的宽度h
float rectX =step(uv.x,width);        //step(a,x) : x>=a?1:0使得输出颜色 col *=rectX 即可得到


同样对Y部分做处理,然后Y和X的部分叠加:
float rectY =step(uv.y,width);
col.a *=saturate(rectX+rectY);   //or max(rectX,rectY)

就把四个角减去啦。
接下来在角的位置画半径为h的圆
/// uv ---uv值
/// center --- 圆心点坐标
/// r --- 圆半径
float drawRound(float2 uv,float center,float r)
{       
        float2 p2center =uv -float2(center,center);
        float distance =length(p2center);

        float m = 1- step(r,distance);//d>=r?1:0
        return m;
}
fixed4 frag(v2f i) : SV_Target
{
        float2 uv = abs(i.uv.xy - float2(0.5,0.5));
        fixed4 col= _Color;

        float h =0.1;
        float width =0.5-h;       
        float rectX =step(uv.x,width);        //step(a,x) : x>=a?1:0
        float rectY =step(uv.y,width);
        float round = drawRound(uv,0.5-h,h);
      col.a *=saturate(round+rectX+rectY);
      return col;
}


基础圆角矩形



加上之前的边缘延伸

画完了圆角,可以开始画边框了,有两种画法,一种为内部覆盖,一种为法线扩张。两个方法都需要2个Pass来做剔除和边框的效果,但是法线扩张的边缘更好看一些。
不过还是想简单说一下方法的思路。
第一个Pass 绘制较小的圆角矩形,第二个Pass绘制较大的圆角矩形,然后利用模板测试剔除多余的颜色。
内部覆盖法
就是在面片内部绘制一个较小的圆角矩形,因为我们上面画的圆角矩形是贴着边的,所以作为第二个Pass使用。一般圆角矩形还有2个参数调节:边框宽度和圆角的大小,所以这里我们分了2个参数,一个是小圆角的半径R,一个是边框宽度h(或者width)。



h为边框宽度,R是小圆角半径,h+r为大圆角半径

由数学知识得知小矩形为:
float round = 0.5 - _Width;

float rx =step(uv.x,round);
float ry =step(uv.y,round);
float s = rx*ry;减去的四个角长宽为小圆角的位置则是圆心位置为0.5-(h+R),半径为R。



小圆角矩形,要记得clip哦

大圆角矩形的圆心位置则为0.5-h-R,半径为h+R;



大圆角矩形

绘制第一个Pass要记得clip掉透明的像素,不然第二个Pass边框的像素就会因为模板测试被丢弃。


看得出来这种内部覆盖的方法会因为Mesh的大小不同导致粗细不一致。
法线扩张
边框效果粗细会比较均匀。
实现上述方法大同小异,需要处理几个地方。
1.顶点法线需设定为矩形中心到顶点方向。
normals = (vertices -center) * normalFactor;
//顶点法线=(矩形边框顶点-矩形中心点)*法线长度。注意法线需要长度一致2.两个圆角矩形Pass的片元使用基础圆角矩形计算,计算相同(第一个仍需要clip)
第二个Pass中进行法线扩张
o.vertex = UnityObjectToClipPos(v.vertex+v.normal*_Width);

相对内部覆盖的方法来说更美观也更简洁一些。
2.圆角面片+模板测试
这个方法主要在于绘制圆角矩形的面片,Shader几乎没有计算。但是相较于第一种方法来说,每个圆角矩形比矩形多出25个左右的顶点,实际应用中约多出500个。



计算出圆点到边缘顶点的向量,通过对称或旋转得到4个方位对应点

大致思路如下:

[*]获取到对应的四个点的坐标和其中心坐标。
[*]根据圆角的半径得到A,B两点
[*]确定将圆角划分成几个三角形,然后计算其划分的夹角 及 mesh的顶点数。
//segments为单个圆角划分面数, num意为第几个圆角矩形
//计算总顶点和总三角形数,及其小圆角中每个小三角形夹角
vertices = new Vector3[( ( segments + 1 ) * 4 + 1 ) * ( buildingInfoList.Length )];
triangles = new int[( ( segments * 4 + 4 ) * 3 ) * ( buildingInfoList.Length )];
float preAngle = 90 / segments;

[*]按角度旋转CB向量,得到第一个小圆角的顶点
//弧形旋转绘制的圆点
Vector3 vectorTemp = new Vector3(vertices.x , yPos , vertices.z);
//用来计算弧形旋转的初始向量
Vector3 radRotVector = vertices - vectorTemp;
for (int i = 1; i < segments; i++)
{
      //初始向量按照角度旋转后的新向量
      vertices = Quaternion.Euler(0 , preAngle * i , 0) * radRotVector + vectorTemp;
}

[*]通过对称或旋转得到其余顶点
[*]每个顶点和中心点O连线组成三角形
[*]设定顶点法线方向为中心点→顶点的方向
[*]Shader使用普通的半透明+模板测试(不需要clip了),第二个Pass顶点延法线方向扩张
Shaded Wireframe视图效果如下图:


范围延伸+圆角+边缘高亮都实现啦。
回到策划的需求,未选中但是同类型的服务建筑范围也需要显示,只是没有高亮边框。
那么只要用上面不带边框的方法绘制出无边框部分的mesh,再绘制有边框的mesh即可。
逻辑部分就不过多展开了!
结语

以上就是覆盖范围功能的思路及实现方式。
实际操作中,服务建筑位置发生变化时才需要更新Mesh,每次计算需要剔除内部建筑避免绘制多余的顶点,CPU方面会计算得多一些,但是也还是可以接受的程度。考虑到主城渲染压力比较大,选择了Shader比较简单的画圆角矩形面片的方法。
应该还会有更巧妙的方法,也许是我关键词没找好或者这个功能比较简单…没有太多制作分享,所以写了这一篇。
谢谢观看~

ChuanXin 发表于 2022-11-23 12:12

大佬求shader分享下呀

ainatipen 发表于 2022-11-23 12:15

请问第二个pass的Stencil是怎么设置的,我用法线扩张加了第二个pass,第一个pass的颜色都被剔除掉了
页: [1]
查看完整版本: 类似模拟城市中建筑覆盖范围在Unity的实现方法