1. 程式人生 > 其它 >【跟著catlikecoding學渲染#2】紋理混合

【跟著catlikecoding學渲染#2】紋理混合

一, Detail Texture

紋理很好,但它們有侷限性。它們具有固定數量的紋素,無論它們以何種大小顯示。如果它們被渲染得很小,我們可以使用mipmap來保持它們看起來不錯。但是當它們被渲染得很大時,它們會變得模糊。

  我們不能憑空發明額外的細節,所以沒有辦法解決這個問題。還是有? 當然,我們可以使用更大的紋理。更多的紋素意味著更多的細節。但是紋理的大小是有限制的。而且,儲存大量僅在近距離可見的額外資料是一種浪費。

  增加紋理密度的另一種方法是平鋪紋理。然後你可以隨心所欲地變小,但你顯然會得到一個重複的模式。但是,這在近距離內可能並不明顯。畢竟,當你站著鼻子碰到一堵牆時,你只會看到整面牆的一小部分。

  因此,我們應該能夠通過將未切片的紋理與平鋪紋理相結合來新增細節。為了嘗試這一點,讓我們使用具有明顯圖案的紋理。這是一個方格網格。

Shader "Custom/Textured With Detail" {

	Properties {
		_Tint ("Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Texture", 2D) = "white" {}
	}

	SubShader {
		…
	}
}

  將材質指定給四邊形並檢視它。從遠處看,它看起來會很好。但是靠得太近,它會變得模糊和模糊。除了缺乏細節外,紋理壓縮引起的偽像也會變得明顯。

1.1 Multiple Texture Samples

我們獲取取樣一下紋理的顏色作為片元著色器的結果

float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
				float4 color = tex2D(_MainTex, i.uv) * _Tint;
				return color;
			}

因此,我們可以嘗試通過引入平鋪紋理來增加紋素密度

float4 color = tex2D(_MainTex, i.uv) * _Tint;
				color = tex2D(_MainTex, i.uv * 10);//好傢伙直接乘了個十倍
				return color;

我們可以嘗試將這兩個示例組合在一起。讓我們通過將它們相乘來做到這一點。但再一次,讓我們新增一個轉折點。使用完全相同的 UV 座標對紋理進行兩次取樣,合成巨大噴流

float4 color = tex2D(_MainTex, i.uv) * _Tint;
				color *= tex2D(_MainTex, i.uv);
				return color;

1.2 Separate Detail Texture

將兩個紋理相乘時,結果會變暗。除非至少有一個紋理是白色的。這是因為紋素的每個顏色通道都有一個介於 0 和 1 之間的值。向紋理新增細節時,您可能希望通過變暗和增亮來執行此操作。

   要使原始紋理變亮,您需要大於 1 的值。假設最多 2 個,這將使原始顏色加倍。這可以通過將細節樣本加倍,然後再將其與原始顏色相乘來支援。

color *= tex2D(_MainTex, i.uv * 10) * 2;

  乘以 1 不會改變任何東西。但是,當我們將詳細樣本加倍時,現在1/2也是如此。這意味著純灰色(而不是白色)紋理不會產生任何變化。所有低於 1/2 的值都會使結果變暗,而高於 1/2 的任何值都會使結果變亮。

因此,我們需要一個以灰色為中心的特殊細節紋理。這是網格的紋理。

Must detail textures be grayscale?

細節紋理不一定是灰度的,但通常是灰度的,灰度細節紋理可以通過增亮和變暗來嚴格調整原始顏色,使用非灰色顏色進行乘法會產生不太直觀的結果

  要使用此單獨的細節紋理,我們必須向著色器新增第二個紋理屬性。使用灰色作為其預設值,因為這不會改變主紋理的外觀。

Properties {
		_Tint ("Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Texture", 2D) = "white" {}
		_DetailTex ("Detail Texture", 2D) = "gray" {}
	}
sampler2D _MainTex, _DetailTex;
float4 _MainTex_ST, _DetailTex_ST;

 1.3 Using Two UV Pairs

我們k有直接再頂點著色器中計算最終的UV

struct Interpolators {
				float4 position : SV_POSITION;
				float2 uv : TEXCOORD0;
				float2 uvDetail : TEXCOORD1;
			};

新的細節 UV 是通過使用細節紋理的平鋪和偏移來變換原始 UV 來建立的。

Interpolators MyVertexProgram (VertexData v) {
				Interpolators i;
				i.position = mul(UNITY_MATRIX_MVP, v.position);
				i.uv = TRANSFORM_TEX(v.uv, _MainTex);
				i.uvDetail = TRANSFORM_TEX(v.uv, _DetailTex);
				return i;
			}

現在我們可以輸出給片元著色器了

float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
				float4 color = tex2D(_MainTex, i.uv) * _Tint;
				color *= tex2D(_DetailTex, i.uvDetail) * 2;
				return color;
			}

1.4 Fading Details

  新增細節的想法是,它們可以近距離或放大來改善材料的外觀。它們不應該在遠處可見或縮小,因為這會使平鋪變得明顯。因此,我們需要一種方法來隨著紋理顯示尺寸的減小而淡化細節。

我們可以通過將細節紋理淡化為灰色來做到這一點,因為這不會改變顏色。 我們以前做過這個!我們需要做的就是在細節紋理的匯入設定中啟用淡出 Mip maps。請注意,這還會自動將濾鏡模式切換到三線性,以便漸進到灰色是漸進的。

然後我們通過一個大理石的材質來演示這個效果

1.5 Linear Color Space

在 Gamma 色彩空間中渲染場景時,著色器工作正常,但如果切換到線性色彩空間,著色器就會出錯,Unity預設Gamma空間,而選擇線性空間時,引擎的渲染流程會再線性空間計算,理想狀況下專案使用線性空間的貼圖顏色,不需要勾選sRGB,如果勾選了,Unity會通過硬體特性取樣時進行線性轉換

詳細見語雀筆記https://www.yuque.com/docs/share/6806031f-351e-419b-b2fa-40506cbebc27?#

What is gamma space

伽瑪空間是指伽瑪校正顏色。伽瑪校正是對光強度的調整。最簡單的方法是將原始價值提高到一定程度,因此價值遊戲。γ 值為 1 表示沒有變化。灰度係數為 2 表示原始值是平方的。

   引入此轉換最初是為了適應CRT顯示監視器的非線性特性。另一個好處是,它也大致對應於我們的眼睛對不同光強度的敏感程度。我們注意到深色之間的差異多於明亮顏色之間的差異。因此,將數字數字的更多位專用於較暗的值而不是較亮的值是有意義的。冪允許我們通過在較大的範圍內拉伸較低的值,同時壓扁較高的值來做到這一點。

  Unity 假定紋理和顏色儲存為 sRGB。在 Gamma 空間中渲染時,著色器直接訪問原始顏色和紋理資料。這就是我們到目前為止的假設。 線上性空間中渲染時,情況不再如此。GPU 會將紋理樣本轉換為線性空間。

  此外,Unity 還會將材質顏色屬性轉換為線性空間。然後,著色器使用這些線性顏色進行操作。之後,片段程式的輸出將被轉換回伽瑪空間。 使用線性顏色的優點之一是它可以實現更真實的照明計算。這是因為光的相互作用在現實生活中是線性的,而不是指數級的。

由於我們將細節紋理樣本加倍,因此值為 1/2 會導致主紋理不會發生更改。然而,轉換為線性空間會將其更改為接近1/22.2≈0.22。翻倍大約是0.44,遠小於1。這解釋了變暗的原因。

  我們可以通過在細節紋理的匯入設定中啟用繞過sRGB取樣來解決此錯誤。這可以防止從 gamma 轉換為線性空間,因此著色器將始終訪問原始影象資料。但是,細節紋理是 sRGB 影象,因此結果仍然是錯誤的。

   最好的解決方案是重新對齊細節顏色,使它們再次以1為中心。我們可以通過乘以1 / 1 / 22.2≈4.59而不是2來做到這一點。但是,只有當我們線上性空間中渲染時,我們才能這樣做。

  幸運的是,UnityCG定義了一個統一變數,該變數將包含要乘以的正確數字。它是一個 float4,其 rgb 分量中包含 2 個或大約 4.59 個(視情況而定)。由於伽瑪校正未應用於 Alpha 通道,因此它始終為 2

color *= tex2D(_DetailTex, i.uvDetail) * unity_ColorSpaceDouble;

二.Texture Splatting

細節紋理的侷限性在於,整個表面使用相同的細節。這適用於均勻的表面,如大理石板。但是,如果您的材質沒有統一的外觀,則不希望在任何地方都使用相同的細節。 考慮一個大地形。它可以有草,沙子,岩石,雪等。

  您希望這些地形型別在近距離內相當詳細。但是,覆蓋整個地形的紋理永遠不會有足夠的紋素。您可以通過對每種表面型別使用單獨的紋理並平鋪這些紋理來解決此問題。但是你怎麼知道在哪裡使用哪種紋理呢?

  假設我們有一個具有兩種不同表面型別的地形。在每一點上,我們都必須決定使用哪種表面紋理。要麼是第一個,要麼是第二個。我們可以用布林值來表示它。如果設定為 true,我們將使用第一個紋理,否則使用第二個紋理

  。我們可以使用灰度紋理來儲存此選擇。值 1 表示第一個紋理,而值 0 表示第二個紋理。實際上,我們可以使用這些值在兩個紋理之間進行線性插值。然後,介於 0 和 1 之間的值表示兩種紋理之間的混合。這使得平滑過渡成為可能。 這樣的紋理被稱為splat貼圖。這就像您將多個地形特徵濺到畫布上一樣。由於插值,此map甚至不需要高解析度。

  將其新增到專案後,將其匯入型別切換為高階。啟用旁路 sRGB 取樣並指示其 mipmap 應線上性空間中生成。這是必需的,因為紋理不代表 sRGB 顏色,而是選擇。因此,線上性空間中渲染時不應對其進行轉換。另外,將其包裝模式設定為夾緊,因為我們不打算平鋪此map。

Shader "Custom/Texture Splatting" {

	Properties {
//		_Tint ("Tint", Color) = (1, 1, 1, 1)
		_MainTex ("Splat Map", 2D) = "white" {}
	}

	SubShader {

		Pass {
			CGPROGRAM

			#pragma vertex MyVertexProgram
			#pragma fragment MyFragmentProgram

			#include "UnityCG.cginc"

//			float4 _Tint;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			
			struct VertexData {
				float4 position : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct Interpolators {
				float4 position : SV_POSITION;
				float2 uv : TEXCOORD0;
			};

			Interpolators MyVertexProgram (VertexData v) {
				Interpolators i;
				i.position = mul(UNITY_MATRIX_MVP, v.position);
				i.uv = TRANSFORM_TEX(v.uv, _MainTex);
				return i;
			}

			float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
				return tex2D(_MainTex, i.uv); // * _Tint;
			}

			ENDCG
		}
	}
}

建立使用此著色器的新材質,並將 splat 貼圖指定為其主要紋理。由於我們尚未更改著色器,因此它只會顯示地圖。

2.1Adding Textures

為了能夠在兩種紋理之間進行選擇,我們必須將它們作為屬性新增到著色器中。讓我們將它們命名為 Texture1 和 Texture2。

Properties {
		_MainTex ("Splat Map", 2D) = "white" {}
		_Texture1 ("Texture 1", 2D) = "white" {}
		_Texture2 ("Texture 2", 2D) = "white" {}
	}

當然,我們為新增到著色器中的每個紋理提供了平鋪和偏移控制元件。我們確實可以單獨支援對每個紋理進行單獨的平鋪和偏移。但這需要我們將更多資料從頂點傳遞到片段著色器,或者計算畫素著色器中的 UV 調整。

  這很好,但通常地形的所有紋理都是相同的平鋪。而且 splat map根本沒有平鋪。因此,我們只需要一個平鋪和偏移控制元件的例項。 可以向著色器屬性新增特性,就像在 C# 程式碼中一樣。NoScaleOffset 屬性將按照其名稱建議執行。是的,它確實將平鋪和偏移稱為比例和偏移。它不是非常一致的命名。

  讓我們將此屬性新增到額外的紋理中,並保留主紋理的平鋪和偏移輸入。

	Properties {
		_MainTex ("Splat Map", 2D) = "white" {}
		[NoScaleOffset] _Texture1 ("Texture 1", 2D) = "white" {}
		[NoScaleOffset] _Texture2 ("Texture 2", 2D) = "white" {}
}

  然後我們再處理一下

sampler2D _MainTex;
			float4 _MainTex_ST;

			sampler2D _Texture1, _Texture2;
float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
				return
					tex2D(_Texture1, i.uv) +
					tex2D(_Texture2, i.uv);
			}

2.2 Using the Splat Map

為了對splat貼圖進行取樣,我們還必須將未修改的UV從頂點程式傳遞到片段程式。

struct Interpolators {
				float4 position : SV_POSITION;
				float2 uv : TEXCOORD0;
				float2 uvSplat : TEXCOORD1;
			};

			Interpolators MyVertexProgram (VertexData v) {
				Interpolators i;
				i.position = mul(UNITY_MATRIX_MVP, v.position);
				i.uv = TRANSFORM_TEX(v.uv, _MainTex);
				i.uvSplat = v.uv;
				return i;
			}

然後,我們可以先對 splat 貼圖進行取樣,然後再對其他紋理進行取樣。

float4 MyFragmentProgram (Interpolators i) : SV_TARGET {
				float4 splat = tex2D(_MainTex, i.uvSplat);
				return
					tex2D(_Texture1, i.uv) +
					tex2D(_Texture2, i.uv);
			}

我們決定值 1 表示第一個紋理。由於我們的 splat 對映是單色的,因此我們可以使用任何 RGB 通道來檢索此值。讓我們使用 R 通道並將其與紋理相乘。

return
    tex2D(_Texture1, i.uv) * splat.r +
    tex2D(_Texture2, i.uv);

 第一個紋理現在由 splat 貼圖調製。要完成插值,我們必須將另一個紋理乘以1 - R。

return
		tex2D(_Texture1, i.uv) * splat.r +
		tex2D(_Texture2, i.uv) * (1 - splat.r);

2.3 RGB Splat Map

  我們有一個功能 splat material,但它只支援兩種紋理。我們能支援更多嗎?我們只使用R通道,那麼我們是否也新增G和B通道呢?然後 (1,0,0) 表示第一個紋理,(0,1,0) 表示第二個紋理,(0,0,1) 表示第三個紋理。為了在這三者之間獲得正確的插值,我們只需要確保RGB通道的總和等於1。

  當我們只使用一個通道時,我們可以支援兩個紋理。這是因為第二個紋理的權重是通過1 - R得出的。同樣的技巧適用於任意數量的通道。因此,可以通過1 - R - G - B支援另一種紋理。

  這將導致具有三種顏色和黑色的splat地圖。只要三個通道加在一起不超過1,它就是一個有效的地圖。這是這樣一張地圖,抓住它並使用與以前相同的匯入設定。

What happens when R + G + B exceeds 1?

那麼前三個紋理的組合會太強。同時,第四個紋理將被減去而不是新增。如果錯誤很小,那麼您不會注意到,結果就足夠了。示例RGB地圖實際上並不完美,但您不會注意到。紋理壓縮引入了更多的錯誤,但同樣很難察覺。

Can we use the alpha channel as well?

確實可以!這意味著單個 RGBA 板圖最多可以支援五種不同的地形型別。但對於本教程,四個就足夠了。 如果要使用五個以上的紋理,則必須使用多個 splat 貼圖。雖然這是可能的,但你最終會得到很多紋理樣本。此時可以使用更好的技術,例如紋理陣列。

要支援 RGB 板塊貼圖,我們必須向著色器新增兩個額外的紋理。我為他們分配了大理石細節和測試紋理。

	Properties {
		_MainTex ("Splat Map", 2D) = "white" {}
		[NoScaleOffset] _Texture1 ("Texture 1", 2D) = "white" {}
		[NoScaleOffset] _Texture2 ("Texture 2", 2D) = "white" {}
		[NoScaleOffset] _Texture3 ("Texture 3", 2D) = "white" {}
		[NoScaleOffset] _Texture4 ("Texture 4", 2D) = "white" {}
	}

將所需的變數新增到著色器。不需要額外的_ST變數。

sampler2D _Texture1, _Texture2, _Texture3, _Texture4;

在片元著色器中,新增額外的紋理樣本。第二個樣本現在使用 G 通道,第三個樣本使用 B 通道。最終樣品用(1 - R - G - B)調製。

return
		tex2D(_Texture1, i.uv) * splat.r +
		tex2D(_Texture2, i.uv) * splat.g +
		tex2D(_Texture3, i.uv) * splat.b +
		tex2D(_Texture4, i.uv) * (1 - splat.r - splat.g - splat.b);

Why do the blended regions look different in linear color space?

我們的 splat 貼圖繞過了 sRGB 取樣,因此混合不應該取決於我們使用的色彩空間,對吧?splat地圖確實不受影響。但是發生混合的色彩空間確實會發生變化。 在伽瑪空間渲染的情況下,樣本在伽瑪空間中混合,僅此而已。

  但是線上性空間中渲染時,它們首先被轉換為線性空間,然後混合,然後轉換回伽馬空間。結果略有不同。線上性空間中,混合也是線性的。但在伽馬空間中,混合傾向於較深的顏色。

現在,您知道了如何應用細節紋理以及如何將多個紋理與 splat 貼圖混合。也可以將這些方法結合起來。 您可以向 splat 著色器新增四個細節紋理,並使用貼圖在它們之間進行混合。

  當然,這需要四個額外的紋理樣。 您還可以使用貼圖來控制細節紋理的應用位置以及省略細節紋理的位置。在這種情況下,您需要一個單色貼圖,它充當蒙版。當單個紋理包含表示不同材質的區域,但其比例不如地形大時,這很有用。例如,如果我們的大理石紋理也包含金屬片,那麼您不希望在那裡應用大理石細節。