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

作者:陈嘉栋
2017-10-13
25 23 4

0x00 前言

在游戏中展现一个写实的田园场景时,草地的渲染是必不可少的,而一提到高效率的渲染草地,很多人都会想起 GPU Gems 第七章 Chapter 7. Rendering Countless Blades of Waving Grass 中所提到的方案。

现在国内很多号称 “次世代” 的手游甚至是一些端游仍或多或少的采用了这种方案。但是本文不会为这个方案着墨过多,相反,接下来的大部分内容是关于如何利用 Geometry Shader 在 GPU 生成新的独立草体的。
686199-20170924102926525-1829808194

0x01 一个简单的星型

传统的方式,即将模型数据从 CPU 传递给 GPU,GPU 再根据这些数据进行渲染的方式在渲染大规模的草体时,往往会忽略单个草体的模型细节。因为单个草体的建模如果过于细致,则渲染大片的草地就需要传递很多多边形,从而造成性能的下降。 因此,一个渲染大片草地的方案往往需要满足以下条件:

  • 单个草的多边形不能过多,最好一棵草只用一个 quad 来表示。
  • 从不同的角度观察,草都必须显得密集。
  • 草的排布不能过于规则,否则会不自然。
  • 综上,渲染草体时的经典结构——星形就出现了。
    686199-20170924100941165-1102863651
    这样,简单的星形结构既满足了单棵草的面数很低同时也兼顾了从不同角度观察也能够显得密集。 而让草随风而动也很简单,只需要根据顶点的 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;
    }
    

    现在很多游戏在渲染草地时仍然使用了这种结构。
    686199-20170924104011025-1229220212

    九州天空城3D

    686199-20170924101017275-1901699709

    剑网三

    但是,各位也都看到了,这种方式虽然简单,但是却并不自然,从上方俯视的时候各个面片也能看到清清楚楚,因此这种方式并不是我想要的。

    0x02 更真实的草叶

    我想要的效果是能够大规模实时渲染,并且每一颗草的叶片都能够随风摇曳的更真实自然的效果。
    在这方面,业内早有一些探索,例如 Siggraph2006 上的 Rendering Grass Terrains in
    Real-Time with Dynamic Lighting
    ,以及 Edward Lee 的论文 REALISTIC REAL-TIME GRASS RENDERING

    本文主要按照 Edward Lee 的论文中描述方式在 Unity 中实现 GPU 生成无尽草地随风摇曳的效果。

    这里,我主要用到了 Direct3D 10 之后新引入的 Programming Guide for Direct3D 11 .aspx) 来实现在 GPU 上创建单独草体叶片的逻辑。每个叶片根据 LOD 有 3 种组成方式,分别需要 1 个 quad、3 个 quad 以及 5 个 quad。
    686199-20170924102841103-1853404544
    而每颗草的位置则由 CPU 来随机决定,由于 GS 的输入是一个图元(point、line 或 triangle)而非顶点,所以我们在 CPU 中需要根据随机的位置创建 point 类型的图元作为这棵草的根位置。
    ok,接下来就在 GPU 上通过一个根位置来制作草的叶子。

    [maxvertexcount(30)]
    void geom(point v2g points[1], inout TriangleStream 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);
    

    设置好叶子的属性之后,我们就可以根据这些属性来创建新的顶点模拟叶子的样子了。
    686199-20170924103534931-995230500
    画一个简图各位可以看到,组成一颗草的叶子需要 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 上创建完成了。
    686199-20170924103552462-1948725041
    接下来,我们需要处理一下草叶的纹理来渲染出符合我们预期的叶片。这里我用到了 GPU Gem 那篇文章中的草丛纹理的处理方法:
    686199-20170924103616493-1088499603
    即叶片的颜色可以只用一个张单独表示叶片颜色的纹理来处理,比如我用的这张纹理:
    686199-20170924103640228-1555592219
    而草体的具体轮廓则靠另一张纹理提供。但是这里没有使用 alpha blend,而是使用了 alpha to coverage,因为在处理重重叠叠的草叶时 blend 会有一些显示顺序上的问题,至于如何使用 alpha to coverage 各位可以参考 SL-Blend。

    SubShader
    Tags{“Queue” = “AlphaTest” “RenderType” = “TransparentCutout” “IgnoreProjector” = “True” }
    Pass
    AlphaToMask On
    

    686199-20170924103650900-514532614
    所以,现在我们只需要在 fs 内简单的取样输出就可以了。

    half4 frag(g2f IN) : COLOR
    {
    fixed4 color = tex2D(_MainTex, IN.uv);
    fixed4 alpha = tex2D(_AlphaTex, (IN.uv));
    return float4(color.rgb, alpha.g);
    }
    

    686199-20170924103703978-1271841171

    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();
    

    这样,一个自然而真实地面网格就创建好了。
    686199-20170924104044931-519428717
    之后就来生铺草吧。所谓的铺草无非就是我们需要生成一些顶点,作为草叶的根位置传入之前完成的 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();
    grassLayer.AddComponent();
    

    创建好之后,可以看到草根的位置随机的分布在地面上,数量有上百万个。
    686199-20170924101912384-844143591
    把我们的 shader 应用于记录草根位置的 mesh 上。
    wow,我们的草地出现了。
    686199-20170924101930540-1482229106

    0x04 风的模拟

    呆立的草虽然看上去比之前的纸片草好看了很多,但是静止而整齐的叶子毕竟还是很不自然。因此,我们要让草动起来也就是模拟风的效果。
    思路仍然是利用三角函数来让草叶摇摆起来,同时根据草的根位置为三角函数提供初始相位然后再增加一些随机性在里面让效果更自然。

    … 伪代码
    wind.x += sin(_Time.x + root.x);
    wind *= random;
    …
    

    但是针对目前每一颗草都有独立的叶片网格,为了更加逼真的模拟风的效果,显然不同的叶片的不同部位受到风的影响是不同的。
    距离叶子的顶端越近,则受到风的影响就越大。
    686199-20170924102000728-980154312
    因此在 GS 生成新顶点的逻辑中,增加风对顶点位置的影响,越高的顶点被影响的程度越大,这样一个更真实的无尽草地效果就实现了。
    18608-post-1507775510
    这个 demo 的代码各位可以在这里获取:
    chenjd/Realistic-Real-Time-Grass-Rendering-With-Unity
    686199-20171001072751340-483931908

    当然,这不是手机上使用的技术,并且作为一个演示 demo 我并没有做过多的优化(不过在我的本子上跑起来还是很流畅)。
    而且和我文章中的演示相比,要简化一些。

    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 .aspx)

    近期点赞的会员

     分享这篇文章

    陈嘉栋 

    慕容小匹夫 微软MVP《Unity 3D脚本编程 》作者 公众号chenjd01 

    您可能还会对这些文章感兴趣

    参与此文章的讨论

    1. Oncle 2017-10-13

      干货集锦

    2. erufu 2017-10-13

      想起了地平线里的那些草

    3. Simidal 2017-10-13

      很实用的方法

    4. OTAKU牧师 2018-01-10

      牛逼坏了

    您需要登录或者注册后才能发表评论

    登录/注册