利用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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM