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

0x00 前言

在游戏中展现一个写实的田园场景时,草地的渲染是必不可少的,而一提到高效率的渲染草地,很多人都会想起GPU Gems第七章
《Chapter 7. Rendering Countless Blades of Waving Grass》中所提到的方案。
现在国内很多号称“次世代”的手游甚至是一些端游仍或多或少的采用了这种方案。但是本文不会为这个方案着墨过多,相反,接下来的大部分内容是关于如何利用Geometry Shader在GPU生成新的独立草体的。

0x01 一个简单的星型

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

  • 单个草的多边形不能过多,最好一棵草只用一个quad来表示
  • 从不同的角度观察,草都必须显得密集
  • 草的排布不能过于规则,否则会不自然

综上,渲染草体时的经典结构——星形就出现了。

fig07-04.jpg

这样,简单的星形结构既满足了单棵草的面数很低同时也兼顾了从不同角度观察也能够显得密集。 而让草随风而动也很简单,只需要根据顶点的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)


1无标题.png

(图片来自:剑网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。

LeeRealtimeGrassThesis.png

(图片来自:Edward Lee)
而每颗草的位置则由CPU来随机决定,由于GS的输入是一个图元(point、line或triangle)而非顶点,所以我们在CPU中需要根据随机的位置创建point类型的图元作为这棵草的根位置。

ok,接下来就在GPU上通过一个根位置来制作草的叶子。

    [maxvertexcount(40)]
    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);

设置好叶子的属性之后,我们就可以根据这些属性来创建新的顶点模拟叶子的样子了。

QQ截图20170922223330.png

画一个简图各位可以看到,组成一颗草的叶子需要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);

        }
QQ图片20170922225533.png

这样,一个叶片的网格就在GPU上创建完成了。

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

fig07-02.jpg

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

QQ截图20170922063324.png

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

        SubShader
            Tags{ "Queue" = "AlphaTest" "RenderType" = "TransparentCutout" "IgnoreProjector" = "True" }

            Pass
                AlphaToMask On
grassBladeAlpha2.png

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

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

        return float4(color.rgb, alpha.g);
    }
QQ图片20170923084241.png

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

这样,一个自然而真实地面网格就创建好了。

QQ截图20170923094148.png

之后就来生铺草吧。所谓的铺草无非就是我们需要生成一些顶点,作为草叶的根位置传入之前完成的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>();

创建好之后,可以看到草根的位置随机的分布在地面上,数量有上百万个。

QQ图片20170923101626.jpg

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

QQ图片20170923103751.jpg

0x04 风的模拟

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

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

但是针对目前每一颗草都有独立的叶片网格,为了更加逼真的模拟风的效果,显然不同的叶片的不同部位受到风的影响是不同的。
距离叶子的顶端越近,则受到风的影响就越大。


QQ图片20170923234027.png

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

QQ图片20170924000328.jpg

这个demo的代码各位可以在这里获取:
chenjd/Realistic-Real-Time-Grass-Rendering-With-Unity
当然,这不是手机上使用的技术,并且作为一个演示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》

-EOF-
最后打个广告,欢迎支持我的书《Unity 3D脚本编程》

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


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 在聊这一话题之前,我们先看看屏幕是如何显示图像的。 屏幕显示图像的原理 在最简单的情况下,帧缓冲区只有一个,这时帧...
    muice阅读 3,071评论 4 19
  • 原文地址 http://www.fx114.net/qa-75-172454.aspx 使用Profiler工具...
    IongX阅读 5,827评论 1 11
  • 1 人不是坚固的岩石,人更像一块湿漉漉的黄油。人不仅被自己的的经验和阅读所塑造,还被自己的语言和行为所塑造。不仅仅...
    alabiubiubiu阅读 345评论 0 0
  • iOS 11 1. @available语法,判断使用的API是否在当前系统存在。例如: if(@availabl...
    阿斯顿卡卡阅读 493评论 0 0
  • 今天同事带了几袋小孩子看过的书,我在里面找了一袋给孩子看,孩子最喜欢看杨红缨的书,这些书看完还可以捐给山区的孩子。...
    黄泳仪阅读 110评论 0 0