透视正确插值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 = a f a / w a + b f b / w b + c f c / w c a / w a + b / w b + c / w c ( 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) f=a/wa+b/wb+c/wcafa/wa+bfb/wb+cfc/wc(1)

其中 ( a , b , c ) (a,b,c) (a,b,c)为fragment对应的重心坐标, f i f_i fi为顶点处地attribute, w i w_i wi为顶点齐次坐标地最后一个分量。

为了简化讨论,我们这里在2D中进行讨论,结论对3D同样有效。
2D perspective projection
考虑上图中的线段AB,投影后的线段为A’B’,其中A’B’的中心 x = 0 x=0 x=0。我们可以容易地发现,A’B’中心并不对应AB中心,所以直接使用 0.5 0.5 0.5对AB属性进行线性插值会得到错误的结果。容易想到,如果我们通过screen space的插值系数,得到camera space的插值系数,那么就可以得到perspective correct的interpolation。

假设screen space的点P的插值系数为 t t t,A为 ( x 0 , y 0 ) (x_0,y_0) (x0,y0),B为 ( x 1 , y 1 ) (x_1, y_1) (x1,y1),near plane y = 1 y=1 y=1(不影响结果,为1可以简化讨论),可知

A x ′ = x 0 y 0 , B x ′ = x 1 y 1 A'_x = \frac{x_0}{y_0}, B'_x = \frac{x_1}{y_1} Ax=y0x0,Bx=y1x1

计算 O P OP OP A B AB AB的交点,我们可以得到camera space的插值系数为 s s s

s ( t ) = t y 1 ( 1 − t ) 1 y 0 + t 1 y 1 s(t) = \frac{\frac{t}{y_1}}{(1-t)\frac{1}{y_0}+t\frac{1}{y_1}} s(t)=(1t)y01+ty11y1t

若我们在A处属性为 a a a,B处属性为 b b b,那么使用 s ( t ) s(t) s(t)进行插值,结果为

P ( s ) = ( 1 − s ) a + s b = ( 1 − t ) a y 0 + t b y 1 ( 1 − t ) 1 y 0 + t 1 y 1 ( 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) P(s)=(1s)a+sb=(1t)y01+ty11(1t)y0a+ty1b(2)

如果使用标准透视矩阵,那么 ( 1 ) (1) (1)中的 w i = − z i w_i=-z_i wi=zi,我们可以看出公式 ( 2 ) (2) (2) ( 1 ) (1) (1)是一致的。

公式 ( 2 ) (2) (2)可以进一步表示为线性插值

P ( s ) = ( 1 − p ) a + p ⋅ b p = t 1 y 1 ( 1 − t ) 1 y 0 + t 1 y 1 \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} P(s)p=(1p)a+pb=(1t)y01+ty11ty11

gl_Position

也许有人会有疑问,我们能不能在vertex shader中进行透视除呢?即对于齐次坐标 ( x , y , z , w ) (x,y,z,w) (x,y,z,w),手动进行透视除,得到 ( x w , y w , z w , 1 ) (\frac{x}{w},\frac{y}{w},\frac{z}{w},1) (wx,wy,wz,1)并赋值给gl_Position。我们可以将 w = 1 w=1 w=1代入 ( 1 ) (1) (1),得到

f = a a + b + c f a + a a + b + c f b + a a + b + c f c = a f a + b f b + c f c \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} f=a+b+cafa+a+b+cafb+a+b+cafc=afa+bfb+cfc

显然这退化成为了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. 齐次坐标的 w w w对于进行perspective correct interpolation至关重要,不能在vertex shader中手动进行透视除。
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
多项式插值是一种在给定的数据点集上构建一个多项式函数的方法,使得该函数通过所有给定的数据点。通常情况下,这些数据点都是离散的,且是由某个真实函数在这些点上的取值所得到的。 多项式插值的基本思想是,假设要构建一个 $n$ 次多项式 $P(x)$ 来拟合给定数据点 $(x_i, y_i)$,那么可以通过求解一个线性方程组来确定 $P(x)$ 的系数。具体而言,我们需要找到 $n+1$ 个系数 $a_0, a_1, \cdots, a_n$,使得对于所有 $i=0,1,\cdots,n$,都有 $P(x_i) = y_i$。 我们可以用拉格朗日插值法来解决这个问题。具体而言,我们可以定义一个 $n$ 次拉格朗日插值多项式 $L(x)$,使得对于所有 $i=0,1,\cdots,n$,都有 $L(x_i) = \delta_{ij}$,其中 $\delta_{ij}$ 表示 Kronecker delta,即当 $i=j$ 时为 $1$,否则为 $0$。这样,我们就可以将 $P(x)$ 表示为 $P(x) = \sum_{i=0}^{n} a_i L_i(x)$,其中 $L_i(x)$ 表示拉格朗日插值多项式中的第 $i$ 项。 最后,我们需要求解的问题就转化为了求解一个线性方程组,即: $$ \begin{pmatrix} L_0(x_0) & L_1(x_0) & \cdots & L_n(x_0) \\ L_0(x_1) & L_1(x_1) & \cdots & L_n(x_1) \\ \vdots & \vdots & \ddots & \vdots \\ L_0(x_n) & L_1(x_n) & \cdots & L_n(x_n) \end{pmatrix} \begin{pmatrix} a_0 \\ a_1 \\ \vdots \\ a_n \end{pmatrix} = \begin{pmatrix} y_0 \\ y_1 \\ \vdots \\ y_n \end{pmatrix} $$ 其中左侧矩阵是一个 Vandermonde 矩阵,通常可以用高斯消元法等方法求解。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值