翻譯20 視差和法線、高度圖回顧

1 視差紋理

    因爲視角的緣由,當調整攝像機位置時,觀察到的事物的相對位置會發生變化,這種視覺現象稱爲視差。在坐火車高速行駛看窗外的景物,附近的物體看起來很大而且移動很快,而遠處的背景看起來很小而且移動較慢。
渲染時,相機使用透視模式時,也會出現視差。html

    以前(翻譯6翻譯9)使用過法線貼圖將表面不規則感添加到平滑表面。 它會影響照明,但不會影響表面的實際形狀。 所以,該效果視差不明顯,經過實現法線貼圖基於視野深度的幻覺有許多限制。這一篇的目的就是解決該限制。算法

1.1 法線貼圖效果回顧

    下面給出許多albedo map 和 normal map差別對比app

image image

albedo map 和 normal map函數

   若是沒有法線貼圖,表面看起來很平坦。 添加法線貼圖會使它看起來好像具備不規則的表面。 可是,高度海拔差別看起來不明顯。 當從入射視角與表面的夾角越趨於0,高度差越不明顯。若是高程差別較大,則表面特徵的相對視覺位置應因爲視差而發生很大變化,但不會發生變化。 咱們看到的視差是平坦的表面this

 image

平坦的視角編碼

    雖然能夠增長法線貼圖的強度,但這不會改變視差。一樣,當法線貼圖變得太強時,它會看起來很奇怪。它影響了平坦表面的光線的明暗變換,而視差效果它們確實是平的。因此法線貼圖只適用於小的變化,但不會表現出明顯的視差。spa

image

法線貼圖的光線變化.翻譯

要得到真正的深度視差感,首先須要肯定深度應該是多少。法線貼圖不包含這些信息。因此咱們須要一個高度圖。這樣,咱們就能夠建立一個基於高度信息的假視差效果,就像法線貼圖建立一個假斜率同樣。下面的貼圖也稱它是灰度圖,黑色表明最低點,白色表明最高點。由於咱們將使用這個貼圖來建立一個視差效果,也稱爲視差圖。3d

image

高度圖code

確保在導入時禁用sRGB(顏色紋理),這樣在使用線性空間渲染時數據就不會被弄亂

1.2 Shader參數

爲了可以使用視差貼圖,咱們必須爲它添加一個屬性到着色器。也會給它一個強度參數來縮放效果。由於視差效果至關強,咱們將其範圍設置爲0-0.1。

[NoScaleOffset] _ParallaxMap ("Parallax", 2D) = "black" {}
_ParallaxStrength ("Parallax Strength", Range(0, 0.1)) = 0

[NoScaleOffset] _OcclusionMap ("Occlusion", 2D) = "white" {}
_OcclusionStrength ("Occlusion Strength", Range(0, 1)) = 1

視差貼圖是一個着色器特性,咱們將啓用_PARALLAX_MAP關鍵字。將必需的編譯器指令添加到base pass、additive pass和deferred pass。

#pragma shader_feature _NORMAL_MAP
#pragma shader_feature _PARALLAX_MAP
爲何不在ShadowCaster增長視差貼圖?
當使用albedo貼圖的alpha通道的透明度時,視差貼圖只會影響陰影。即便是這樣,在陰影貼圖中的視差效果也很難被注意到。因此它一般不值得額外的計算時間。可是若是願意,也能夠將它添加到陰影施法者通道中。

爲了訪問新的屬性,給個人照明添加相應的變量

sampler2D _ParallaxMap;
float _ParallaxStrength;

sampler2D _OcclusionMap;
float _OcclusionStrength;

爲了可以自定義配置材質,在Extend ShaderGUI擴展中增長相應Enable與Disanble key的方法。

void DoParallax () {
	MaterialProperty map = FindProperty("_ParallaxMap");
	Texture tex = map.textureValue;
	EditorGUI.BeginChangeCheck();
	editor.TexturePropertySingleLine
	(
		MakeLabel(map, "Parallax (G)"), map,
		tex ? FindProperty("_ParallaxStrength") : null
	);
	if (EditorGUI.EndChangeCheck() && tex != map.textureValue) {
		SetKeyword("_PARALLAX_MAP", map.textureValue);
	}
}

image

1.3 座標匹配

經過在fragment程序中調整紋理座標,讓平坦表面的某些部分看起來高低交錯。建立一個應用視差函數,給它一個inout插值器參數。

void ApplyParallax (inout Interpolators i) {
}

在fragment程序使用插入的數據以前調用視差函數。會有點異常是LOD衰落,由於這取決於屏幕位置。先不調整這些座標。

FragmentOutput MyFragmentProgram (Interpolators i) {
	UNITY_SETUP_INSTANCE_ID(i);
	#if defined(LOD_FADE_CROSSFADE)
		UnityApplyDitherCrossFade(i.vpos);
	#endif

	ApplyParallax(i);

	float alpha = GetAlpha(i);
	#if defined(_RENDERING_CUTOUT)
		clip(alpha - _Cutoff);
	#endif
	…
}

經過簡單地向U座標添加視差強度來調整紋理座標。作一次偏移計算

void ApplyParallax (inout Interpolators i) {
	#if defined(_PARALLAX_MAP)
		i.uv.x += _ParallaxStrength;
	#endif
}

改變視差強度會致使紋理偏移。增長U座標會使紋理向負的U方向移動,V座標同理。這看起來不是視差效果,由於這是一個與視角無關的均勻位移。

shifting[3]

u座標移動

1.4 隨視角方向移動

視差是由相對於觀察者的透視投影,因此必須改變紋理座標。這意味着必須基於視圖的方向來移動座標,而視圖的方向對於表面上每一個片斷都是不一樣的。

 imageView direction varies across a surface.

    紋理座標存在於切線空間中。爲了調整這些座標,須要知道視圖在切線空間中的方向。這須要矩陣乘法對空間進行轉換。在fragment-程序已經有了一個切線空間矩陣,可是它是用於從切線空間到世界空間的轉換。在本例中,須要從對象空間轉到切線空間

    視圖方向向量定義爲從表面到攝像機,須要歸一化。咱們能夠在vertex程序中肯定這個向量,轉換它並將它傳遞給fragment程序。可是爲了最終獲得正確的方向,須要推遲歸一化,直到插值完成後。添加切線空間視圖方向做爲一個新的插值成員變量。

struct InterpolatorsVertex {
	…

	#if defined(_PARALLAX_MAP)
		float3 tangentViewDir : TEXCOORD8;
	#endif
};

struct Interpolators {
	…

	#if defined(_PARALLAX_MAP)
		float3 tangentViewDir : TEXCOORD8;
	#endif
};
寄存器數量限制是多少?
model 1與model 2都只支持8個Texture Coordinate Register ->Texcoord[0-7]。當使用model 3時,可使用TEXCOORD8。若硬件不支持model 3其機能也就不是很強大,因此不要使用視差映射。

首先, 使用mesh網格數據中的原始頂點切向量和法向量,在頂點程序中建立一個從對象空間到切線空間的轉換矩陣。由於咱們只用它來變換一個向量而不是一個位置咱們用一個3×3矩陣就足夠了

InterpolatorsVertex MyVertexProgram (VertexData v) {
	…

	ComputeVertexLightColor(i);

	#if defined (_PARALLAX_MAP)
		float3x3 objectToTangent = float3x3(
			v.tangent.xyz,
			cross(v.normal, v.tangent.xyz) * v.tangent.w,
			v.normal
		);
	#endif

	return i;
}

而後,可使用ObjSpaceViewDir函數獲得對象空間中頂點位置的視圖方向,再用矩陣變換它咱們就獲得了咱們須要的切線空間下視圖方向。

#if defined (_PARALLAX_MAP)
	float3x3 objectToTangent = float3x3
	(
		v.tangent.xyz,
		cross(v.normal, v.tangent.xyz) * v.tangent.w,
		v.normal
	);
	i.tangentViewDir = mul(objectToTangent, ObjSpaceViewDir(v.vertex));
#endif
 
ObjSpaceViewDir內部實現?
ObjSpaceViewDir函數是在UnityCG中定義的。它先將攝像機位置轉換到對象空間,而後減去對象空間下頂點位置獲得一個從頂點指向攝像機的向量,注意它尚未標準化.
inline float3 ObjSpaceViewDir (float4 v)
{
    float3 objSpaceCameraPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos.xyz, 1)).xyz;
    return objSpaceCameraPos - v.xyz;
}

最後,咱們能夠在ApplyParallax函數使用切線空間視圖方向了。首先,對它進行規格化normalize,把它變成一個合適的方向向量。而後,添加它的XY組件到紋理座標,再由視差強度縮放。

void ApplyParallax (inout Interpolators i) {
	#if defined(_PARALLAX_MAP)
		i.tangentViewDir = normalize(i.tangentViewDir);
		i.uv.xy += i.tangentViewDir.xy * _ParallaxStrength;
	#endif
}
 

這能有效地將視圖方向投影到紋理表面上。當以90度角直視表面時,在切空間中的視圖方向等於表面法線(0,0,1),這不會致使位移。視角越淺,投影越大,位移效果也越大。

 image影視圖方向用做UV偏移

全部這一切的影響是表面彷佛被拉向上的切線空間,看起來比它實際上更高,基於視差強度。The effect of all of

shiftinguv

隨投影視角方向移動UV.

1.5 基於高度滑動

    在基於高度這一點上,咱們可讓表面看起來更高,但它仍然是一個均勻位移。下一步是使用視差貼圖來縮放位移。採樣貼圖,使用它的G通道做爲高度,應用視差強度,並使用它來調節位移。

i.tangentViewDir = normalize(i.tangentViewDir);
float height = tex2D(_ParallaxMap, i.uv.xy).g;
height *= _ParallaxStrength;
i.uv.xy += i.tangentViewDir.xy * height;

shiftingbyHeight

由高度調整的移動

    低的區域如今保持不變,而高的區域被向上拉。standard shader抵消了這種效果,因此低的區域也向下移動,而在中間的區域保持他們原來的位置。這是經過從原始高度數據中減去差值來實現的。

float height = tex2D(_ParallaxMap, i.uv.xy).g;
height -= 0.5;
height *= _ParallaxStrength;
 

shiftingbyStrength

視差貼圖效果 由合理到過量

這就產生了咱們想要的視差效果,但它只在低強度下有效。不足的是位移位移變換的很快,會撕裂表面。

1.6 偏移視差映射算法

    咱們目前使用的視差映射技術被稱爲帶偏移限制的視差映射。咱們只是使用了視圖方向的XY部分,它的最大長度是1。所以,紋理偏移量是有限的。這種效果不錯,但不能表明正確的透視投影。

    一個更精確的計算偏移量的物理方法是將高度場視爲幾何圖形表面下的體積,並經過它拍攝一個視圖射線。光線從相機發射到表面,從上面進入高度場體積,並持續發射直到它到達由場定義的表面。

    若是高度場均勻爲零,那麼射線就會一直持續到體積的底部。它與物體的距離取決於光線進入物體時的角度。它沒有限制。角度越淺,越遠。最極端的狀況是當視角趨於0時,光線射向無窮大。

 image光線投射到底部,有限且正確

    爲了找到合適的偏移量,咱們必須縮放視圖方向向量,經過除以它本身的Z份量來使它的Z份量變成1。由於咱們之後不須要用Z,咱們只須要用X和Y除以Z。

i.tangentViewDir = normalize(i.tangentViewDir);
i.tangentViewDir.xy /= i.tangentViewDir.z;

    雖然這樣能夠獲得一個更正確的投影,但它確實會使淺視角的視差效果惡化。standard着色器經過增長0.42誤差到Z減輕淺視角的視差效果惡化,因此它永遠不會接近零。這扭曲了透視圖,但使工件更易於管理。咱們再加上這個誤差.

i.tangentViewDir.xy /= (i.tangentViewDir.z + 0.42);

image
視差貼圖像標準着色器

    經過上述多個步驟修正後, 如今咱們的着色器與標準着色器支持一樣的視差效果。視差映射能夠應用於任何表面,投影假設切線空間是均勻的。曲面具備彎曲的切線空間,所以會產生物理上不正確的結果。只要視差強度和曲率很小,你就能夠擺脫它。

image球面視差貼圖

    一樣,陰影座標不會受到這個效果的影響。所以,陰影在強烈的視差的組合下看起來很奇怪,好像漂浮在表面上。

image 陰影不受視差影響

1.7 Parallax Configuration

    你不一樣意Unity使用0.42的偏移值嗎?或者你想使用一個不一樣的值,仍是讓它保持在0?或者你想用偏移限制代替嗎?它是能夠配置!

   當你想使用偏移限制,定義PARALLAX_OFFSET_LIMITING在着色器。或者,經過定義PARALLAX_BIAS來設置要使用的誤差。

void ApplyParallax (inout Interpolators i) {
	#if defined(_PARALLAX_MAP)
		i.tangentViewDir = normalize(i.tangentViewDir);
		#if !defined(PARALLAX_OFFSET_LIMITING)
			i.tangentViewDir.xy /= (i.tangentViewDir.z + PARALLAX_BIAS);
		#endif
		…
	#endif
}
 

    當沒有定義時,假設誤差是0.42。在ApplyParallax 中定義它。注意,宏定義不關心函數做用域,它們老是全局的。

#if !defined(PARALLAX_OFFSET_LIMITING)
	#if !defined(PARALLAX_BIAS)
		#define PARALLAX_BIAS 0.42
	#endif
	i.tangentViewDir.xy /= (i.tangentViewDir.z + PARALLAX_BIAS);
#endif
 

    如今咱們能夠經過着色器的CGINCLUDE塊來微調咱們的視差效果。添加無誤差和限制偏移的選項,但將它們轉換爲註釋,以堅持默認選項。

CGINCLUDE
	#define BINORMAL_PER_FRAGMENT
	#define FOG_DISTANCE

//	#define PARALLAX_BIAS 0
//	#define PARALLAX_OFFSET_LIMITING

ENDCG

1.8 Detail UV

視差貼圖能夠在主貼圖上工做,可是咱們尚未注意到副貼圖。咱們必須應用紋理座標偏移到細節UV上。

首先,下面是一個包含網格模式的詳細地圖。它能夠很容易地驗證效果是否正確地應用於細節。

image細節網格紋理

使用這個紋理做爲材質的細節albedo貼圖。設置二級貼圖的平鋪爲10×10。這代表,細節紫外線確實仍然不受影響。

 image image
細節UV不受影響

Standard也簡單地添加了UV偏移到細節UV,這是存儲在UV插值器的ZW組件。

float height = tex2D(_ParallaxMap, i.uv.xy).g;
height -= 0.5;
height *= _ParallaxStrength;
float2 uvOffset = i.tangentViewDir.xy * height;
i.uv.xy += uvOffset;
i.uv.zw += uvOffset;

細節可能有所變化,可是它們確定還不匹配視差效果。 那是由於咱們平鋪了二級紋理。 這樣會將細節UV縮放10倍,使視差偏移量變弱十倍。 咱們還必須將細節拼貼應用到偏移量。

i.uv.zw += uvOffset * _DetailTex_ST.xy;

實際上,縮放應該相對於主UV平鋪,以防它被設置爲1×1之外的一些東西。

i.uv.zw += uvOffset * (_DetailTex_ST.xy / _MainTex_ST.xy);
 

image
正確的UV

2 Ray Marching-光線步進

    然而,除了上述的偏移視差映射還有另外的視差算法:發射射線與高度場體積相交,肯定其交點在表面上的位置,而後對該位置採樣。 它經過在射線進入體積時的交點,對高度圖進行一次採樣。 可是,當看向任意一個角度時,這並不能準確告訴射線實際上與高度場相交的高度。

image 實際與預測的高度對比

  先假設入口點的高度與交點的高度相同,但這實際上只有在入口點和交點具備相同的高度時纔是正確的。當偏移量不大且高度場變化不大時,它的效果仍然很好。可是,當偏移量太大或高度變化太快時,該算法就會出現問題,而這極可能是錯誤的。這就會形成表面撕裂。

    若是咱們能算出射線實際到達的高度場的位置,那麼總能找到真正的可見表面點。這不能經過單個紋理樣原本實現,咱們必須沿着視圖射線逐步移動,並每次都採樣高度場,直到射線到達表面。該技術是RayMarching。

image 隨視圖射線前進

    有各類不一樣的視差貼圖使用raymarching。常見的是陡視差映射Steep Parallax Mapping、地形映射Relief Mapping和視差遮擋映射Parallax Occlusion Mapping。與使用單一紋理樣本相比,它們能經過高度場來建立更好的視差效果。除此以外,它們還能夠應用額外的陰影和技術來改進該算法。當咱們作的匹配這些方法時,我會調用它。

2.1 自定義視差函數

    標準着色器僅支持簡單的偏移視差映射。 如今,咱們要在本身的着色器中添加對視差光線Ray marching的支持。 可是,咱們還要繼續支持這種簡單方法。 二者都須要採樣height字段,所以將採樣代碼行放在單獨的GetParallaxHeight函數中。 並且,兩種方法的投影視圖方向和偏移量的最終應用都相同。 所以,將偏移量計算也單獨爲一個函數。 它僅須要原始UV座標和已處理的視圖方向做爲參數,結果返回要應用的UV偏移。

float GetParallaxHeight (float2 uv) {
	return tex2D(_ParallaxMap, uv).g;
}

float2 ParallaxOffset (float2 uv, float2 viewDir) {
	float height = GetParallaxHeight(uv);
	height -= 0.5;
	height *= _ParallaxStrength;
	return viewDir * height;
}

void ApplyParallax (inout Interpolators i) {
	#if defined(_PARALLAX_MAP)
		i.tangentViewDir = normalize(i.tangentViewDir);
		#if !defined(PARALLAX_OFFSET_LIMITING)
			#if !defined(PARALLAX_BIAS)
				#define PARALLAX_BIAS 0.42
			#endif
			i.tangentViewDir.xy /= (i.tangentViewDir.z + PARALLAX_BIAS);
		#endif

		float2 uvOffset = ParallaxOffset(i.uv.xy, i.tangentViewDir.xy);
		i.uv.xy += uvOffset;
		i.uv.zw += uvOffset * (_DetailTex_ST.xy / _MainTex_ST.xy);
	#endif
}

如今,咱們將應用視差函數宏替換對視差偏移的硬編碼調用,從而使視差方法更加靈活。若是沒有定義它,咱們將它設置爲使用偏移量方法。

void ApplyParallax (inout Interpolators i) {
	#if defined(_PARALLAX_MAP)
		…
		#if !defined(PARALLAX_FUNCTION)
			#define PARALLAX_FUNCTION ParallaxOffset
		#endif
		float2 uvOffset = PARALLAX_FUNCTION(i.uv.xy, i.tangentViewDir.xy);
		i.uv.xy += uvOffset;
		i.uv.zw += uvOffset * (_DetailTex_ST.xy / _MainTex_ST.xy);
	#endif
}
 

爲RayMarching方法建立一個新函數。與ParallaxOffset函數相似的參數和返回類型。

float2 ParallaxOffset (float2 uv, float2 viewDir) {
    …
}

float2 ParallaxRaymarching (float2 uv, float2 viewDir) {
    float2 uvOffset = 0;
    return uvOffset;
}

如今能夠經過定義PARALLAX_FUNCTION來改變着色器中的視差方法。

#define PARALLAX_BIAS 0
//#define PARALLAX_OFFSET_LIMITING
#define PARALLAX_FUNCTION ParallaxRaymarching

2.2 相交計算

    爲了找到視圖射線到達高度場的點,咱們須要對射線上的多個點進行採樣並計算出在表面下方的位置。第一個採樣點在頂部,咱們在這裏輸入高度量,就像使用偏移方法同樣。最後一個採樣點就是射線到達體積底部的地方。咱們會在這些端點之間均勻地添加額外的採樣點。

    假設每條射線進行10次採樣。這意味着咱們將對高度圖採樣10次而不是一次,因此這不是一個便宜計算方法。由於咱們用了10個樣本,因此步長是0.1。這是咱們沿着視圖射線移動的因子,也就是UV偏移增量。

float2 ParallaxRaymarching (float2 uv, float2 viewDir) {
    float2 uvOffset = 0;
    float stepSize = 0.1;
    float2 uvDelta = viewDir * stepSize;
    return uvOffset;
}
 

爲了應用視差強度,咱們能夠調整每一步採樣的高度。可是縮放UV delta也有一樣的效果,只須要計算一次。

float2 uvDelta = viewDir * (stepSize * _ParallaxStrength);
 

    經過這種方式,不管視差強度如何,咱們均可以繼續使用0–1做爲高度場的範圍。 所以,射線的第一步高度始終爲1。低於或高於該高度的表面點的高度由高度場定義。

float stepSize = 0.1;//步長
float2 uvDelta = viewDir * (stepSize * _ParallaxStrength);
float stepHeight = 1;//步高
float surfaceHeight = GetParallaxHeight(uv);

    如今咱們要沿着射線迭代。首先,每一步咱們都會增長UV偏移量。視圖向量指向攝像機,但咱們是在向表面移動,因此咱們須要減去UV delta。而後咱們用步高來減少步長。而後咱們再次對高度圖採樣。使用while循環重複上述步驟,直到採樣完畢。

float stepHeight = 1;
float surfaceHeight = GetParallaxHeight(uv);

while (stepHeight > surfaceHeight)
{
    uvOffset -= uvDelta;
    stepHeight -= stepSize;
    surfaceHeight = GetParallaxHeight(uv + uvOffset);
}

  當編譯時,會獲得一個編譯器警告和錯誤。這個警告告訴咱們在循環中使用了梯度指令。這指的是循環中的紋理採樣。GPU必須弄清楚使用哪一個mipmap級別,它須要比較相鄰片斷使用的UV座標。只有當全部片斷執行相同的代碼時,它才能對比。對於循環來講,這是不可能的,由於它能夠提早終止,每一個片斷均可能不一樣。所以編譯器將展開循環,這意味着它將一直執行全部9個步驟,而無論邏輯是否能夠提早中止。相反,它隨後使用肯定性邏輯選擇最終結果。

    編譯失敗是由於編譯器沒法肯定循環的最大迭代次數。它不知道這個最可能是9。經過將while循環轉換爲執行限制的for循環來明確這一點。

for (int i = 1; i < 10 && stepHeight > surfaceHeight; i++)
{
    uvOffset -= uvDelta;
    stepHeight -= stepSize;
    surfaceHeight = GetParallaxHeight(uv + uvOffset);
}
 
image 

Raymarching 步進10次 無誤差, 無限制.

    與簡單的視差偏移方法相比,視差效果更加明顯。較高的區域如今也正確地阻擋了咱們後面較低區域的視野。咱們還獲得了明顯的圖層,總共10層。

2.3 更多步進

    這個基本的光線行進方法最適合陡峭的視差貼圖。效果的質量是由咱們的樣本分辨率決定的。一些方法根據視角使用可變的步驟。較淺的角度須要更多的步長,由於光線較長。但咱們的樣本量是固定的,因此咱們不會這樣作。

提升質量的明顯方法是增長採樣的次數,所以讓其可配置。使用PARALLAX_RAYMARCHING_STEPS,默認值爲10,而不是固定的步長和迭代次數。

float2 ParallaxRaymarching (float2 uv, float2 viewDir) {
    #if !defined(PARALLAX_RAYMARCHING_STEPS)
        #define PARALLAX_RAYMARCHING_STEPS 10
    #endif
    float2 uvOffset = 0;
    float stepSize = 1.0 / PARALLAX_RAYMARCHING_STEPS;
    float2 uvDelta = viewDir * (stepSize * _ParallaxStrength);

    float stepHeight = 1;
    float surfaceHeight = GetParallaxHeight(uv);

    for (
        int i = 1;
        i < PARALLAX_RAYMARCHING_STEPS && stepHeight > surfaceHeight;
        i++
    ) {
        uvOffset -= uvDelta;
        stepHeight -= stepSize;
        surfaceHeight = GetParallaxHeight(uv + uvOffset);
    }

    return uvOffset;
}
 

如今咱們能夠在着色器中控制步數。對於真正的高質量,將PARALLAX_RAYMARCHING_STEPS定義爲100。

#define PARALLAX_BIAS 0
//#define PARALLAX_OFFSET_LIMITING
#define PARALLAX_RAYMARCHING_STEPS 100
#define PARALLAX_FUNCTION ParallaxRaymarching
 
image

Raymarching 100次採樣

    這讓咱們知道了它的效果能有多好,但它計算量太大了,通常不適合手機。因此把樣本數設爲10後,咱們仍然能夠看到視差效果看起來連續和平滑。然而,由視差遮擋引發的輪廓老是鋸齒狀的,MSAA並不能消除這一點,由於它只適用於幾何圖形的邊緣,而不是紋理效果。只要不依賴深度緩衝區,後處理抗鋸齒技術能解決。

不能按片斷寫入深度緩衝區嗎?
這在足夠先進的硬件上確實是可能的,使它可以正確地與高度場相交併應用陰影。不過,它計算量太大。

    咱們當前的方法是沿着射線步進,直到到達表面如下的點,或者到達射線末端可能的最低點。而後咱們用UV偏移處理那個點。但隱藏在表面之下的這個點,極可能會出現錯誤。這就是致使表面撕裂的緣由。

    增長步長數只會減小最大偏差。使用足夠的步驟,錯誤會變得更小,以致於咱們沒法再看到它。因此當一個表面老是從遠處看,你能夠用更少的步驟。距離越近,視角越小,須要的樣本就越多。

image2.4 步長之間插值

    提升質量的一種方法是根據經驗預測光線真正到達表面的位置。好比第一步在表面之上,下一步在表面之下。在這兩步之間的某個點射線必定到達了表面。

    兩個射線點、和兩個射線點到表面最近的點,能定義兩條線段。由於光線和表面碰撞,這兩條線段會相交。因此若是咱們跟蹤前面的步驟,咱們能夠在循環以後執行直線交叉。咱們能夠用這個信息來近似出真正的交點。

image
執行直線交叉

    在for循環內,咱們必須跟蹤以前的UV偏移量、步長高度和表面高度。通常來講,這些等於循環以前的第一個樣本。

float2 prevUVOffset = uvOffset;
float prevStepHeight = stepHeight;
float prevSurfaceHeight = surfaceHeight;

for (
    …
) {
    prevUVOffset = uvOffset;
    prevStepHeight = stepHeight;
    prevSurfaceHeight = surfaceHeight;
    …
}
 

    在循環以後,咱們計算這些線的交點。咱們可使用這個插值之間的前點和後點的UV偏移。

forfloat prevDifference = prevStepHeight - prevSurfaceHeight;
float difference = surfaceHeight - stepHeight;
float t = prevDifference / (prevDifference + difference);
uvOffset = lerp(prevUVOffset, uvOffset, t);

return uvOffset;
 
數學原理:
這兩個線段定義在兩個樣本步驟之間的空間內。咱們將這個空間的寬度設置爲1。從前一步到最後一步的直線由點(0,a)和點(1,b)定義,其中a是前一步的高度,b是後一步的高
度。所以,能夠用線性函數'v(t) = a + (b - a)t'來定義視圖線。一樣地,面線由點(0,c)和(1,d)定義,函數's(t) = c + (d - c)t'。

交點存在於s(t) = v(t)'處。那麼t的值是多少?
c + (d - c)t` = a + (b - a)t`

(d - c)t` - (b - a)t` = a - c

(a - c + d - b)t` = a - c

`t = (a - c) / (a - c + d - b)
注意:a - c是在t = 0處直線高度的絕對差。d - b是t = 1處的絕對高度差。

image線段交點

    實際上,在這種狀況下,咱們可使用插值器來縮放咱們要添加到上一點上的UV偏移量。它能夠歸結爲相同的東西,只是用了更少的數學。

float t = prevDifference / (prevDifference - difference);
uvOffset = prevUVOffset - uvDelta * t;
 

image

    效果看起來好多了。咱們如今假設表面在樣本點之間是線性的,這能夠防止最明顯的分層假象。然而,它不能幫助咱們檢測咱們是否錯過了步驟之間的交集。咱們仍然須要不少的樣原本處理小的特徵,輪廓和淺角度。

    有了這個技巧,咱們的方法相似於視差遮擋映射。雖然這是一個相對便宜的改進,但經過定義PARALLAX_RAYMARCHING_INTERPOLATE,咱們讓它成爲可選的。

#if defined(PARALLAX_RAYMARCHING_INTERPOLATE)
    float prevDifference = prevStepHeight - prevSurfaceHeight;
    float difference = surfaceHeight - stepHeight;
    float t = prevDifference / (prevDifference + difference);
    uvOffset = prevUVOffset - uvDelta * t;
#endif

在shader內定義PARALLAX_RAYMARCHING_INTERPOLATE。

#define PARALLAX_BIAS 0
//#define PARALLAX_OFFSET_LIMITING
#define PARALLAX_RAYMARCHING_STEPS 10
#define PARALLAX_RAYMARCHING_INTERPOLATE
#define PARALLAX_FUNCTION ParallaxRaymarching

2.5 步長搜索

    經過在兩個步長之間進行線性插值,咱們假定表面在兩個步長之間是筆直的。 可是,一般狀況並不是如此。 爲了更好地處理不規則的高度場,咱們必須在兩個步長之間搜索實際的交點。 或至少接近它。

    完成循環後,不要使用最後的偏移量,而是將偏移量調整到最後兩個步長的中間位置。對該點的高度進行採樣。若是咱們結束在表面如下,向表面之上方向移動四分之一,並再次採樣。若是咱們在表面上結束,向表面之下方向移動四分之,並再次採樣。不斷重複這個過程。

image

愈來愈接近交點

    上述方法是二分查找的一個應用。它與地形測繪方法最匹配。每走一步,路程減半,直到到達目的地。在咱們的例子中,咱們將簡單地作固定次數,以達到預期的解決方案。一步,獲得0.5。兩步,獲得0.2五、0.75。三步,是0.12五、0.37五、0.62五、0.875。注意,從第二步開始,每次採樣提高分的辨率將翻倍。

The above approach is an application of binary search. It best matches the Relief Mapping approach. Each step the covered distance halves, until the destination is reached. In our case, we'll simply do this a fixed number of times, arrived at a desired resolution. With one step, we always end up halfway between the last two points, at 0.5. With two steps, we end up at either 0.25 or 0.75. With three steps, it's 0.125, 0.375, 0.625, or 0.875. And so on. Note that, starting at the second step, the effective resolution doubles per sample.

    爲了控制是否使用此方法,咱們定義PARALLAX_RAYMARCHING_SEARCH_STEPS。默認狀況下將其設置爲零,這意味着咱們根本不進行搜索。若是它被定義爲大於0,咱們將不得不使用另外一個循環。注意,這種方法與PARALLAX_RAYMARCHING_INTERPOLATE不兼容的,由於咱們不能再保證表面是交叉的最後兩個步驟。當咱們搜索的時候,禁用插值。

for#if !defined(PARALLAX_RAYMARCHING_SEARCH_STEPS)
    #define PARALLAX_RAYMARCHING_SEARCH_STEPS 0
#endif
#if PARALLAX_RAYMARCHING_SEARCH_STEPS > 0
    for (int i = 0; i < PARALLAX_RAYMARCHING_SEARCH_STEPS; i++) {
    }
#elif defined(PARALLAX_RAYMARCHING_INTERPOLATE)
    float prevDifference = prevStepHeight - prevSurfaceHeight;
    float difference = surfaceHeight - stepHeight;
    float t = prevDifference / (prevDifference + difference);
    uvOffset = prevUVOffset - uvDelta * t;
#endif

    此循環也執行與原始循環相同的基本工做。調整偏移量和步高,而後採樣高度字段。

for (int i = 0; i < PARALLAX_RAYMARCHING_SEARCH_STEPS; i++) {
    uvOffset -= uvDelta;
    stepHeight -= stepSize;
    surfaceHeight = GetParallaxHeight(uv + uvOffset);
}

但每次迭代,UV增量和步長減半。

for (int i = 0; i < PARALLAX_RAYMARCHING_SEARCH_STEPS; i++)
{
    uvDelta *= 0.5;
    stepSize *= 0.5;
    uvOffset -= uvDelta;
    stepHeight -= stepSize;
    surfaceHeight = GetParallaxHeight(uv + uvOffset);
}

一樣,若是點在表面之下,咱們必須朝相反的方向移動。

uvDelta *= 0.5;
stepSize *= 0.5;
if (stepHeight < surfaceHeight) {
    uvOffset += uvDelta;
    stepHeight += stepSize;
}
else {
    uvOffset -= uvDelta;
    stepHeight -= stepSize;
}
surfaceHeight = GetParallaxHeight(uv + uvOffset);

調整着色器,因此它使用三個搜索步驟

#define PARALLAX_BIAS 0
//#define PARALLAX_OFFSET_LIMITING
#define PARALLAX_RAYMARCHING_STEPS 10
#define PARALLAX_RAYMARCHING_INTERPOLATE
#define PARALLAX_RAYMARCHING_SEARCH_STEPS 3
#define PARALLAX_FUNCTION ParallaxRaymarching

image

10步長加上3個二分查找的最終效果

    結果看起來至關不錯,但仍不完美。二分法搜索能夠比簡單的插值處理較淺的角度,但仍然須要至關多的搜索步驟,以擺脫分層。因此這是一個試驗的問題,找出哪一種方法在特定狀況下最有效,須要多少步驟。

2.6 縮放對象和動態批處理

    儘管咱們的視差映射方法彷佛可行,但存在一個隱藏的錯誤。 並且還把錯誤顯示出來了。它顯示了什麼時候使用動態批處理來組合已縮放的對象。 例如,給咱們的四邊形一個像(10,10,10)的比例,而後複製它,將副本移到它下面一點。 假設在播放器設置中啓用了此選項,這將觸發Unity動態批處理四邊形。
    批處理開始時,視差效果將扭曲。 旋轉相機時,這一點很是明顯。 可是,這僅發生在遊戲視圖和構建中,而不發生在場景視圖中。 請注意,standard着色器也存在此問題,可是當使用弱偏移視差效果時,您可能不會當即注意到它。

image

動態批處理會產生奇怪的結果

    在批處理將它們合併到一個單一的網格中以後,Unity不能標準化處理後的幾何法向量和切向量。所以頂點數據正確的假設再也不成立。

    頂點法向量和切向量沒有規範化不是什麼大的問題,由於咱們在頂點程序中將視圖向量轉換到切線空間。對於其餘全部內容,數據在使用以前都要標準化。

   解決方法是在構造對象轉換到切線矩陣以前對向量進行歸一化。 由於只有動態批處理的縮放幾何才須要此選項,因此根據是否認義了PARALLAX_SUPPORT_SCALED_DYNAMIC_BATCHING,將其設爲可選。

#if defined (_PARALLAX_MAP)
    #if defined(PARALLAX_SUPPORT_SCALED_DYNAMIC_BATCHING)
        v.tangent.xyz = normalize(v.tangent.xyz);
        v.normal = normalize(v.normal);
    #endif
    float3x3 objectToTangent = float3x3(
        v.tangent.xyz,
        cross(v.normal, v.tangent.xyz) * v.tangent.w,
        v.normal
    );
    i.tangentViewDir = mul(objectToTangent, ObjSpaceViewDir(v.vertex));
#endif
#define PARALLAX_BIAS 0
//#define PARALLAX_OFFSET_LIMITING
#define PARALLAX_RAYMARCHING_STEPS 10
#define PARALLAX_RAYMARCHING_INTERPOLATE
#define PARALLAX_RAYMARCHING_SEARCH_STEPS 3
#define PARALLAX_FUNCTION ParallaxRaymarching
#define PARALLAX_SUPPORT_SCALED_DYNAMIC_BATCHING

動態批量與正確的結果

相關文章
相關標籤/搜索