透視正確插值Perspective Correct Interpolation

線性插值的問題

OpenGL在進行光柵化時,對於每個fragment shader的input attribute作插值,這個插值默認情況下perspective correct的。插值默認是screen space進行的,如果使用簡單的線性插值,對於texture mapping會得到錯誤的結果,來看下圖(圖片取自《Graphics Shaders 2nd》)。
Linear Interpolation vs Perspective Correct
可以明顯地看到採用線性插值的quad中兩個triangles的邊界。

Perspective Correct Interpolation的簡單推導

OpenGL 4.5 spec 14.6 p456中給出地插值公式如下

f=afa/wa+bfb/wb+cfc/wca/wa+b/wb+c/wc(1)f = \frac{af_a/w_a+bf_b/w_b+cf_c/w_c}{a/w_a+b/w_b+c/w_c}\quad (1)

其中(a,b,c)(a,b,c)爲fragment對應的重心座標,fif_i爲頂點處地attribute,wiw_i爲頂點齊次座標地最後一個分量。

爲了簡化討論,我們這裏在2D中進行討論,結論對3D同樣有效。
2D perspective projection
考慮上圖中的線段AB,投影后的線段爲A’B’,其中A’B’的中心x=0x=0。我們可以容易地發現,A’B’中心並不對應AB中心,所以直接使用0.50.5對AB屬性進行線性插值會得到錯誤的結果。容易想到,如果我們通過screen space的插值係數,得到camera space的插值係數,那麼就可以得到perspective correct的interpolation。

假設screen space的點P的插值係數爲tt,A爲(x0,y0)(x_0,y_0),B爲(x1,y1)(x_1, y_1),near plane y=1y=1(不影響結果,爲1可以簡化討論),可知

Ax=x0y0,Bx=x1y1 A'_x = \frac{x_0}{y_0}, B'_x = \frac{x_1}{y_1}

計算OPOPABAB的交點,我們可以得到camera space的插值係數爲ss

s(t)=ty1(1t)1y0+t1y1s(t) = \frac{\frac{t}{y_1}}{(1-t)\frac{1}{y_0}+t\frac{1}{y_1}}

若我們在A處屬性爲aa,B處屬性爲bb,那麼使用s(t)s(t)進行插值,結果爲

P(s)=(1s)a+sb=(1t)ay0+tby1(1t)1y0+t1y1(2) P(s)=(1-s)a+sb=\frac{(1-t)\frac{a}{y_0}+t\frac{b}{y_1}}{(1-t)\frac{1}{y_0}+t\frac{1}{y_1}} \quad(2)

如果使用標準透視矩陣,那麼(1)(1)中的wi=ziw_i=-z_i,我們可以看出公式(2)(2)(1)(1)是一致的。

公式(2)(2)可以進一步表示爲線性插值

P(s)=(1p)a+pbp=t1y1(1t)1y0+t1y1 \begin{aligned} P(s)&= (1-p)a+p \cdot b \\ p&=\frac{t\frac{1}{y_1}}{(1-t)\frac{1}{y_0}+t\frac{1}{y_1}} \\ \end{aligned}

gl_Position

也許有人會有疑問,我們能不能在vertex shader中進行透視除呢?即對於齊次座標(x,y,z,w)(x,y,z,w),手動進行透視除,得到(xw,yw,zw,1)(\frac{x}{w},\frac{y}{w},\frac{z}{w},1)並賦值給gl_Position。我們可以將w=1w=1代入(1)(1),得到

f=aa+b+cfa+aa+b+cfb+aa+b+cfc=afa+bfb+cfc \begin{aligned} f&=\frac{a}{a+b+c}f_a+\frac{a}{a+b+c}f_b+\frac{a}{a+b+c}f_c \\ &=af_a+bf_b+cf_c \end{aligned}

顯然這退化成爲了screen space的線性插值,所以必然最後結果是不正確的。在Unity中作一個簡單的實驗就可以看到結果

Without Manual Perspective Divide
With Manual Perspective Divide

上圖是未進行透視除的結果,下圖是進行透視除的結果。

測試代碼如下

Shader "Unlit/VertexColor"
{
	Properties
	{
		_MainTex ("Main Texture", 2D) = "white" {}
		[Toggle] _ManualPerspectiveDivision ("Manual Perspective Division", Float) = 0
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			GLSLPROGRAM

			#include "UnityCG.glslinc"

			#ifdef VERTEX

			uniform int _ManualPerspectiveDivision;

			out vec2 vST;

			void main()
			{
				vST = gl_MultiTexCoord0.st;
				gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
				if (_ManualPerspectiveDivision != 0) {
					gl_Position /= gl_Position.w;
				}
			}

			#endif

			#ifdef FRAGMENT

			in vec2 vST;
			uniform sampler2D _MainTex;

			void main()
			{
				gl_FragColor = texture(_MainTex, vST);
			}

			#endif
			
			ENDGLSL
		}
	}
}

結論

  1. 要進行perspective correct interpolation,不能直接在screen space利用重心座標直接進行插值。
  2. 齊次座標的ww對於進行perspective correct interpolation至關重要,不能在vertex shader中手動進行透視除。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章