利用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閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容

  • 在聊這一話題之前,我們先看看屏幕是如何顯示圖像的。 屏幕顯示圖像的原理 在最簡單的情況下,幀緩沖區只有一個,這時幀...
    muice閱讀 3,139評論 4 19
  • 原文地址 http://www.fx114.net/qa-75-172454.aspx 使用Profiler工具...
    IongX閱讀 5,882評論 1 11
  • 1 人不是堅固的巖石,人更像一塊濕漉漉的黃油。人不僅被自己的的經驗和閱讀所塑造,還被自己的語言和行為所塑造。不僅僅...
    alabiubiubiu閱讀 362評論 0 0
  • iOS 11 1. @available語法,判斷使用的API是否在當前系統存在。例如: if(@availabl...
    阿斯頓卡卡閱讀 507評論 0 0
  • 今天同事帶了幾袋小孩子看過的書,我在里面找了一袋給孩子看,孩子最喜歡看楊紅纓的書,這些書看完還可以捐給山區的孩子。...
    黃泳儀閱讀 123評論 0 0