Unity 程序化生成草地
前一阵看了对马岛之魂GDC上程序化草,效果很棒,正好项目有类似的需求,就在Unity中还原了一下,还有些待完善的地方,后续有时间会深入完善,简单说下实现思路。视频地址:Unity程序化草_哔哩哔哩_bilibili
Unity程序化生成的草地
一、初始化地块的数据
首先我们根据地形的Mask找出那些地方长草,以像素为单位把这些地块的坐标和索引存到一个Buffer里面,方便后续操作,Level是为后面根据远近生成不同数量的草准备的。比如我一个1024X1024的地形,Mask分辨率是512x512的,那么每个地块的边长就是2m。
struct CullArgs
{
float3 TilePos;
int Index;
int Level;
};二、初始化叶片的数据
第二步初始化叶片的数据,视频中作者用了一个长度为16的Buffer记录这些数据,这个可以根据需求自己添加需要的数据,这一步只是生成buffer并没有对buffer赋值。
Ghost of Tsushima 中每根叶片的数据
struct GrassArgs{
float3 Position; #位置
float2 Facing; #朝向
float WindStrength; #风的强度
float Hash; #哈希
float Height; #高度
float width; #宽度
float Tilt; #倾斜程度
float Bend; #弯曲程度
float SideCurve; #弯曲位置
float GrassType; #叶片类型
int GridIndex; #叶片索引
};三、生成叶片数据
接下来我们要在Computeshader中对叶片数据进行处理,我们要传一些初始的参数进来,叶片高度,宽度,随机程度等等,美术通过这些参数可以生成各种各样的草地。
float _PosOffsetToCenter; #距离每个地块中心的距离
float _voronoiOffset; #地块中心根据VoronoiNoise进行偏移的程度
float3 _GridRadius; #地块半径
float _heightOffset; #高度偏移
float _FacingOffset; #朝向偏移
float _FacingToCenter; #朝向中心的权重
float _HeightToCenter; #高度权重
float _HeightRandom; #高度偏移
float _TiltToCenter; #倾斜程度的权重
float _BendToCenter; #弯曲程度的权重
int _instanceCount; #每个地块生成多少叶片视频中单独解释了通过Voronoi Noise将整齐的地块进行随机偏移,每根叶片生成时会根据偏移后的地块中心的距离来决定它属于哪个地块,然后通过地块随机朝向高度等属性,生成出一簇一簇有层次感的草地。
Voronoi 偏移前
偏移后根据地块随机属性的草地
Voroni Noise我们直接用Unity官方ShaderGraph里面的就好,很方便。
Voronoi Node | Shader Graph | 6.9.2
我们把这些数据处理完成后填充到刚刚生成的叶片的Buffer中去,这一步就算完成了。
四、 视锥剔除
前三步我们是在游戏开始时预先生成好的,但一次性把这么大的Buffer传给GPU渲染肯定吃不消,我们在发送DrawCall时先进行一次剔除,舍弃掉那些不需要的地块,只保留我们能看到的。
由于草的数量太多,一根一根进行剔除也是很大的消耗,所以在这里我是以地块为单位进行剔除,保留能看到的地块,最后通过地块的Index去叶片的Buffer里找对应的叶片进行渲染。
视锥剔除的具体做法就不说了,网上有很多现成的。
然后我们把剔除结果存到一个Buffer里,发送给GPU进行渲染,同时我们根据摄影机的远近对之前地块的Level参数进行赋值,标记这个地块距离我很远或者很近方便后面渲染使用,我们还可以在这一步根据距离生成不同的LOD发送给GPU。
五、渲染叶片
前面的准备工作做好了之后,我们开始进行叶片的渲染。
首先是模型的准备工作,如图所示。
点序可以参考图片的顺序
首先我们通过InstanceID和每个地块叶片的总数可以拿到我们属于哪个地块,然后通过地块中的保存的索引找到我们事先生成好的叶片的数据。
int GrassID = TilePos.Index * instanceCount + (v.instanceID % instanceCount);拿到了数据之后我们就可以通过顶点来操作每根叶片的形态。
Tilt控制倾斜程度
bend控制贝塞尔控制点的远近
我们可以根据Tilt、bend 、position、height四个参数拿到贝塞尔曲线的p0 p1 p2三个参数,然后通过不同的顶点索引拿到不同的t,可以拿到每个顶点在贝塞尔曲线上的位置,通过操作bend和tilt我们可以后续通过风力图等等做出草的动画。
通过facing我们可以找到副切线的向量
朝副切线方向位移拉出宽度
然后根据叶片朝向和叶片宽度,把顶点横向位移拉出草的形态。
对贝塞尔曲线求导,叉乘出法向量
通过对贝塞尔曲线求导,可以很容易的拿到每个顶点的法线和切线。
贴图就是比较常规的粗糙度、颜色、厚度、AO
贴图方面颜色通道用了两张贴图一张黑白的纹理和一个可以变化的颜色通道,可以对叶片颜色进行随机偏移,做出一些细节变化。
圆柱型的法线
采用了圆柱形的法线,让每根叶片的体积感更强。
六、一些Tips
为了优化性能做了几个Tips,视频中有提到。
不同距离生成不同数量的草
根据每个地块的Level数值将远端的地块生成时剔除掉对应得数量得叶片,将顶点直接归零,会默认被GPU剔掉,每多一个level少生成一半数量的叶片,并且宽度增加一倍,因为我是根据地块生成得草,所以暂时没法在发送drawcall之前把这部分干掉,这块后面还有优化空间。
侧面加宽
叶片侧面对着屏幕时,向屏幕空间的两侧拉顶点
当叶片侧面对着屏幕时,会显得很稀疏,解决方法是当叶片侧面对着屏幕时,向屏幕空间的两侧拉顶点,增加覆盖面积,会看着更加饱满。
折叠叶片
当叶片高度低于一定高度时,顶点数会浪费,所以作者将一片叶片折叠成两片,增加覆盖面积,具体做法就是将中间的顶点作为原点向两侧延申。
根据距离减少粗糙度统一法线
远处的草减少粗糙度统一法线方向
远处的草动起来会显得噪点很多画面会显得很花,解决办法是将远处的法线朝向统一,减少粗糙度。
地面抖动阴影
地面抖动
每根草正常渲阴影消耗很高,视频中的做法是在有草的地方将地形阴影pass中的顶点向上位移草的高度,再进行抖动,可以表现出非常近似的草的阴影的效果。
屏幕空间接触阴影
屏幕空间接触阴影
近处细节用屏幕空间的接触阴影进行补充,增加层次感。
参考链接:
https://www.youtube.com/watch?v=Ibe1JBF5i5Y
https://outerra.blogspot.com/2012/05/procedural-grass-rendering.html 印象最深的就是那个地面顶点模拟阴影,简直鬼才[飙泪笑] 思路新奇! 转载就要说清楚转载 没明白你在说什么?文章是原创得,为啥要写转载呢。 ????
页:
[1]