利用GPU实现无尽草地的实时渲染

0x00 前言
在游戏中展现一个写实的田园场景时,草地的渲染是必不可少的,而一提到高效率的渲染草地,很多人都会想起GPU Gems第七章
《Chapter 7. Rendering Countless Blades of Waving Grass》中所提到的方案。
现在国内很多号称“次世代”的手游甚至是一些端游仍或多或少的采用了这种方案。但是本文不会为这个方案着墨过多,相反,接下来的大部分内容是关于如何利用Geometry Shader在GPU生成新的独立草体的。
0x01 一个简单的星型
传统的方式,即将模型数据从CPU传递给GPU,GPU再根据这些数据进行渲染的方式在渲染大规模的草体时,往往会忽略单个草体的模型细节。因为单个草体的建模如果过于细致,则渲染大片的草地就需要传递很多多边形,从而造成性能的下降。
因此,一个渲染大片草地的方案往往需要满足以下条件:
- 单个草的多边形不能过多,最好一棵草只用一个quad来表示
- 从不同的角度观察,草都必须显得密集
- 草的排布不能过于规则,否则会不自然
综上,渲染草体时的经典结构——星形就出现了。

这样,简单的星形结构既满足了单棵草的面数很低同时也兼顾了从不同角度观察也能够显得密集。 而让草随风而动也很简单,只需要根据顶点的uv信息找出上面的几个顶点,按照自己规则让顶点移动就可以了。
if (o.uv.y > 0.5)
{
float4 translationPos =
float4(sin(_Time.x * _TimeFactor * Pi ), 0, sin(_Time.y * _TimeFactor * Pi ), 0);
v.vertex += translationPos * _StrengthFactor;
}
现在很多游戏在渲染草地时仍然使用了这种结构。

(图片来自:九州天空城3D)

(图片来自:剑网3)
但是,各位也都看到了,这种方式虽然简单,但是却并不自然,从上方俯视的时候各个面片也能看到清清楚楚,因此这种方式并不是我想要的。
0x02 更真实的草叶
我想要的效果是能够大规模实时渲染,并且每一颗草的叶片都能够随风摇曳的更真实自然的效果。在这方面,业内早有一些探索,例如Siggraph2006上的《Rendering Grass Terrains in
Real-Time with Dynamic Lighting》,以及Edward Lee的论文《REALISTIC REAL-TIME GRASS RENDERING》。
本文主要按照Edward Lee的论文中描述方式在Unity中实现GPU生成无尽草地随风摇曳的效果。
这里,我主要用到了Direct3D 10之后新引入的Geometry Shader来实现在GPU上创建单独草体叶片的逻辑。每个叶片根据LOD有3种组成方式,分别需要1个quad、3个quad以及5个quad。

(图片来自:Edward Lee)
而每颗草的位置则由CPU来随机决定,由于GS的输入是一个图元(point、line或triangle)而非顶点,所以我们在CPU中需要根据随机的位置创建point类型的图元作为这棵草的根位置。
ok,接下来就在GPU上通过一个根位置来制作草的叶子。
[maxvertexcount(30)]
void geom(point v2g points[1], inout TriangleStream<g2f> triStream)
{
float4 root = points[0].pos;
虽然位置是随机的,但是我们显然也希望叶子本身的高度和宽度也存在一些随机。
float random = sin(UNITY_HALF_PI * frac(root.x) + UNITY_HALF_PI * frac(root.z));
_Width = _Width + (random / 50);
_Height = _Height +(random / 5);
设置好叶子的属性之后,我们就可以根据这些属性来创建新的顶点模拟叶子的样子了。

画一个简图各位可以看到,组成一颗草的叶子需要12个不同的顶点,但是由于这里没有用index,所以最后总共要输出30个顶点来组成5个quad。
而根据这幅简图,我们还可以很方便的根据根的位置计算各个顶点的位置。
同时,还能发现偶数顶点对应的uv坐标是(0,v),而奇数顶点对应的uv坐标都是(1,v)——这里的v是uv坐标中的v——因此,我们又能很轻松的计算出各个顶点对应的uv坐标了。
最后,如果我们要计算实时光,则还需要获取顶点的法线信息,这里简单起见统一为(0, 0, 1)。
for (uint i = 0; i < vertexCount; i++)
{
v[i].norm = float3(0, 0, 1);
if (fmod(i , 2) == 0)
{
v[i].pos = float4(root.x - _Width , root.y + currentVertexHeight, root.z, 1);
v[i].uv = float2(0, currentV);
}
else
{
v[i].pos = float4(root.x + _Width , root.y + currentVertexHeight, root.z, 1);
v[i].uv = float2(1, currentV);
currentV += offsetV;
currentVertexHeight = currentV * _Height;
}
v[i].pos = UnityObjectToClipPos(v[i].pos);
}
这样,一个叶片的网格就在GPU上创建完成了。

接下来,我们需要处理一下草叶的纹理来渲染出符合我们预期的叶片。这里我用到了GPU Gem那篇文章中的草丛纹理的处理方法:

即叶片的颜色可以只用一个张单独表示叶片颜色的纹理来处理,比如我用的这张纹理:

而草体的具体轮廓则靠另一张纹理提供。但是这里没有使用alpha blend,而是使用了alpha to coverage,因为在处理重重叠叠的草叶时blend会有一些显示顺序上的问题,至于如何使用alpha to coverage各位可以参考SL-Blend。
SubShader
Tags{ "Queue" = "AlphaTest" "RenderType" = "TransparentCutout" "IgnoreProjector" = "True" }
Pass
AlphaToMask On

所以,现在我们只需要在fs内简单的取样输出就可以了。
half4 frag(g2f IN) : COLOR
{
fixed4 color = tex2D(_MainTex, IN.uv);
fixed4 alpha = tex2D(_AlphaTex, (IN.uv));
return float4(color.rgb, alpha.g);
}

0x03 生成覆盖地面的无尽草地
有了叶子之后,我们就可以考虑如何生成地形以及地面上覆盖的草了。为了地面的起伏轮廓自然真实,我们可以根据一张高度图来动态创建地面的网格。
由于Unity的网格顶点上限是65000,因此我决定让地面网格的尺寸为250 * 250:
for (int i = 0; i < 250; i++)
{
for (int j = 0; j < 250; j++)
{
verts.Add(new Vector3(i, heightMap.GetPixel(i, j).grayscale * 5 , j));
if (i == 0 || j == 0) continue;
tris.Add(250 * i + j);
tris.Add(250 * i + j - 1);
tris.Add(250 * (i - 1) + j - 1);
tris.Add(250 * (i - 1) + j - 1);
tris.Add(250 * (i - 1) + j);
tris.Add(250 * i + j);
}
}
...
Mesh m = new Mesh();
m.vertices = verts.ToArray();
m.uv = uvs;
m.triangles = tris.ToArray();
这样,一个自然而真实地面网格就创建好了。

之后就来生铺草吧。所谓的铺草无非就是我们需要生成一些顶点,作为草叶的根位置传入之前完成的GS。需要说明的是,由于草的密度要足够大,因此不止需要一个草地的mesh,例如我们要种200,000棵草的话就需要3个草地mesh。另外还要说明的一点,也是要吐槽Unity的地方就在于Unity的mesh实现默认是triangle,而非point(参考Invoking Geometry Shader for every vertex of a mesh)。因此创建记录草根位置的mesh的方法和之前创建地面稍有不同。
m.vertices = verts.ToArray();
m.SetIndices(indices,MeshTopology.Points, 0);
grassLayer = new GameObject("grassLayer");
mf = grassLayer.AddComponent<MeshFilter>();
grassLayer.AddComponent<MeshRenderer>();
创建好之后,可以看到草根的位置随机的分布在地面上,数量有上百万个。

把我们的shader应用于记录草根位置的mesh上。
wow,我们的草地出现了。

0x03 风的模拟
呆立的草虽然看上去比之前的纸片草好看了很多,但是静止而整齐的叶子毕竟还是很不自然。因此,我们要让草动起来也就是模拟风的效果。
思路仍然是利用三角函数来让草叶摇摆起来,同时根据草的根位置为三角函数提供初始相位然后再增加一些随机性在里面让效果更自然。
...伪代码
wind.x += sin(_Time.x + root.x);
wind *= random;
...
但是针对目前每一颗草都有独立的叶片网格,为了更加逼真的模拟风的效果,显然不同的叶片的不同部位受到风的影响是不同的。
距离叶子的顶端越近,则受到风的影响就越大。

因此在GS生成新顶点的逻辑中,增加风对顶点位置的影响,越高的顶点被影响的程度越大,这样一个更真实的无尽草地效果就实现了。

这个demo的代码各位可以在这里获取:
chenjd/Realistic-Real-Time-Grass-Rendering-With-Unity

当然,这不是手机上使用的技术,并且作为一个演示demo我并没有做过多的优化(不过在我的本子上跑起来还是很流畅)。
而且和我文章中的演示相比,要简化一些。
更多的渲染效果可以关注我的公众号:chenjd01
ref:
【1】《Chapter 7. Rendering Countless Blades of Waving Grass》
【2】《Rendering Grass Terrains in
Real-Time with Dynamic Lighting》
【3】《REALISTIC REAL-TIME GRASS RENDERING》
【4】《Programming Guide for Direct3D 11》
-EOF-
最后打个广告,欢迎支持我的书《Unity 3D脚本编程》

欢迎大家关注我的公众号慕容的游戏编程:chenjd01

利用GPU实现无尽草地的实时渲染的更多相关文章
- 利用GPU实现大规模动画角色的渲染(转)
原文: https://www.cnblogs.com/murongxiaopifu/p/7250772.html 利用GPU实现大规模动画角色的渲染 0x00 前言 我想很多开发游戏的小伙伴都希望自 ...
- 利用GPU实现大规模动画角色的渲染
0x00 前言 我想很多开发游戏的小伙伴都希望自己的场景内能渲染越多物体越好,甚至是能同时渲染成千上万个有自己动作的游戏角色就更好了. 但不幸的是,渲染和管理大量的游戏对象是以牺牲CPU和GPU性能为 ...
- 【原】实时渲染中常用的几种Rendering Path
[原]实时渲染中常用的几种Rendering Path 本文转载请注明出处 —— polobymulberry-博客园 本文为我的图形学大作业的论文部分,介绍了一些Rendering Path,比较简 ...
- 如何实现最佳的跨平台游戏体验?Unity成亮解密实时渲染
7月31日,2018云创大会游戏论坛在杭州国际博览中心103B圆满举行.本场游戏论坛聚焦探讨了可能对游戏行业发展有重大推动的新技术.新实践,如AR.区块链.安全.大数据等. Unity大中华区技术经理 ...
- 【实时渲染】实时3D渲染如何加速汽车线上体验应用推广
在过去,一支优秀的广告片足以让消费者对一辆汽车产生兴趣.完美的底盘线条或引擎的轰鸣声便会让潜在买家跑到经销商那里试驾.现在,广告还是和往常一样,并没有失去其特性,但86%的买家在与销售交流之前会在网上 ...
- 封装CIImage实现实时渲染
封装CIImage实现实时渲染 CIImage属于CoreImage里面的东东,用来渲染图片的,为什么要封装它呢?其实很简单,封装好之后使用更加方便. 如果直接使用CIImage渲染图片,使用的流程如 ...
- 利用GPU实现翻页效果(分享自知乎网)
https://zhuanlan.zhihu.com/p/28836892?utm_source=qq&utm_medium=social 首发于Runtime 写文章 利用GPU实现翻页效果 ...
- 利用动态图层实现数据的实时显示(arcEngine IDynamiclayer)
marine 原文利用动态图层实现数据的实时显示(arcEngine IDynamiclayer) 说明:最近一个项目用到这方面知识,文章主要来至网络,后期会加入自己的开发心得.(以下的代码实例中,地 ...
- 使用CSS3开启GPU硬件加速提升网站动画渲染性能
遇到的问题: 网站本身设计初衷就没有打算支持IE8及以下版本浏览器,并不是因为代码兼容性问题,而是真的不想迁就那些懒得更新自己操作系统和浏览器的用户,毕竟是我自己的网站,所以我说了算!哈哈~ 没有了低 ...
随机推荐
- 并发是个什么鬼之同步工具类CountDownLatch
扯淡 写这篇文章,我先酝酿一下,实不相瞒,脱离底层太久了,更确切的情况是,真没曾认真研究过.就目前来说,很多框架包括工具类已经把实现封装的很深,你只需轻轻的调用一下API,便不费半点力气.以至于大家会 ...
- MySQL DNS反查导致连接缓慢
场景 机器A上的一个模块连接机器B上的MySQL,在实验室网络环境下正常:同样A.B两台机器,网络环境切换为与外界隔离的一个小型局域网环境,A上的模块与B上MySQL建立连接非常慢. 环境 SuS ...
- linux Module驱动开发-一切刚刚开始
linux内核是可以高度定制的,通过配置编译选项达到定制的目的. 在配置kernel编译选项时驱动程序的编译选项一般有三种,不编译.编译为内核驱动.编译为模块驱动.所以linux驱动一般分为两类,内核 ...
- switch实现一个两数的运算
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- c语言中的文件格式化读写函数fscanf和fprintf函数
很多时候我们需要写入数据到文件中时都觉得很困扰,因为格式乱七八槽的,可读性太差了,于是我们就想有没有什么函数可以格式化的从文件中输入和输出呢,还真有.下面我将讲解一下fscanf和fprintf的强大 ...
- Android WebView基本使用
转载请注明出处: http://blog.csdn.net/lowprofile_coding/article/details/77928614 WebView介绍 Android WebView在A ...
- 自己动手写fullPage插件
仿造fullPage.js https://alvarotrigo.com/fullPage/#firstPage 自己参照网上教程写了一个,加了注释.主要是练习造轮子的能力,需求是不断变化的只拿来用 ...
- JAVA实用案例之邮件发送
最近有朋友问邮件怎么发送,就简单写了个demo,因为懒得找jar包,所以项目是创建的maven工程,具体的maven引用的jar如下: <dependency> <groupId&g ...
- css 弹性盒兼容性写法,直接复制粘贴
看这个定义弹性布局盒子display:-webkit-box; display: -moz-box; display: -ms-flexbox; display: -webkit-flex; disp ...
- §--------算法分界线--------§
如题 As said in the title~ 计算机的cpu计算从根源上由最基本的逻辑电路(晶体管)组成,由此衍生出最基本的数值运算:四则运算.而此后所有的高级算法都是建立在这个基本计算原理(逻辑 ...