欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Perspective Correct Interpolation

程序员文章站 2022-03-26 13:40:17
...

线性插值的问题

OpenGL在进行光栅化时,对于每个fragment shader的input attribute作插值,这个插值默认情况下perspective correct的。插值默认是screen space进行的,如果使用简单的线性插值,对于texture mapping会得到错误的结果,来看下图(图片取自《Graphics Shaders 2nd》)。
Perspective Correct Interpolation
可以明显地看到采用线性插值的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同样有效。
Perspective Correct Interpolation
考虑上图中的线段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中作一个简单的实验就可以看到结果

Perspective Correct Interpolation
Perspective Correct Interpolation

上图是未进行透视除的结果,下图是进行透视除的结果。

测试代码如下

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中手动进行透视除。