【OpenGL】透视和ZBuffer

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/stalendp/article/details/50775450

转发,请保持地址:http://blog.csdn.net/stalendp/article/details/50775450

本文介绍OpenGL中的投影,同时讲解一下ZBuffer。先看一下古人是怎么进行透视作画的:


Perspective drawing in the Renaissance: “Man drawing a lute” by Albrecht Dürer, 1525

这么严谨!肯定是德国人了 ;-) 

除了Graphic Pipeline外,ZBuffer是我最想介绍的部分了,因为有一些很酷实现都依赖于此(比如Shadow、Field Of View、SSOA)。为了具有说服力,我们以公式推导的形式给出结论。公式推导利用了中学的知识“相似三角形法则”,虽然有些繁琐但并不复杂。另外,文章的组织流程大致是沿着Graphic Pipeline的顺序进行的,其中剖析了一个正方形面片的显示过程。这个过程大致为:点->三角形->光栅化->透视矩阵->属性插值。关于ZBuffer的理解,可以带着如下几个问题进行探索:ZBuffer是什么环节下的产物,有什么特性,可以用在哪些地方。

     3D模型在经过Model Transform和View Transform之后(以后介绍),会进入到本文介绍的Projection(投影);投影把View(可视区域,也叫“截头椎体”)转化为一个边长为2的立方体(中心点在立方体的中央,xyz轴的范围都为[-1,1]),这个立方体叫做Normalized device coordinates,简称为NDC。以透视投影为例:

View (“截头椎体”)  ============>     NDC(Normalized device coordinates)
图1

从空间到平面

这篇文章中,我们将解释在透视摄像机下的一个带有贴图的正方形是怎么被正确地显示的,如下图:

图2
在摄像机视角将看到如下图(先去掉贴图),发现一个正方形是由两个三角形组成的:

图3
OpenGL的渲染单位是三角形,现在先考虑靠其中一个三角形的显示,如下:
图4
三角形是由3个点组成的,下面先看一个点是怎么被映射的。下图中,空间中的p点会被映射为截面上的q点。

图5
上面的可视区域(“截头椎体”), 近截面为n,远截面为f;(分别为单词near和far的缩写),C表示Camera。p是摄像机视野中的一点,q是p在n上的一个投影。
简化,并添加相关参数如下:

图6
Q的坐标为(x, -n),P的坐标为(Px, Py); 我们利用相似三角形法则,得到等式(1):
$$x=-\frac{n}{P_z}P_x  \text{   and   } y=-\frac{n}{P_z}P_y$$ (1)
x的范围为[l,r];把x映射成[-1,1]的公式如(2),

$$x'=(x-l)\frac{2}{r-l}-1$$ (2)
同理得到y‘
$$y'=(y-b)\frac{2}{t-b}-1$$ (3)
把(2),(3)分别代入(1),得到公式(4),(5)
$$x'=\frac{2n}{r-l}\left(-\frac{P_x}{P_z}\right)-\frac{r+l}{r-l}$$ (4)
$$y'=\frac{2n}{t-b}\left(-\frac{P_y}{P_z}\right)-\frac{t+b}{t-b}$$ (5)
这样,一个3D点就可以被映射到一个2D的投影面上了。回到图4,我们也很容易可以得到三角形的三个点,接下来就是怎么把这个三角形显示出来。显示三角形,其实就是显示三角形内部的颜色或者图案,也就是确定三角形内部的每个点的颜色。要完成这个任务,首先要对三角形进行分割;对三角形的分割的专有名词叫:光栅化(Rasterization).光栅化的结果如图7,会得到三角形内部的所有点(根据不同设备进行光栅化,由于分辨率的原因,得到的点的数量也不一样。图7是很低分辨率的结果,为的是更好的说明问题)。

光栅化(Rasterization)

图7
如果用线性差值的方式来获取贴图的颜色,得到下图:

图8
用同样的方法处理另外一个三角形,如图9(感觉好像不对!):
图9
导致这个现象的原因是:在透视中,屏幕贴图的采样不是线性的,不能用线性插值得到,图10很好地解释这个现象。当我们在截平面上线性地取点时(可以理解为像素点),对应到3d中的点,不是线性的,而是随着z的变大而变大。"一叶障目不见泰山" 很好地描述了这个现象,在人的眼睛里,近处的叶子和远处的泰山一样大。具体来说属性点的插值和z呈反比例关系,具体会在下文的  透视下的插值中讨论。

透视矩阵


图10
通过光栅化中遇到的问题中的讨论知道,NDC中的Z和View中的Z呈反比例关系,我们计算NDC中的Z如下:

$$z'=\frac{A}{z}+B$$ (6)
View中的Z的范围为[n, f],对应NDC中为[1, -1],代入(6)整理得到如下公式:

$$-1=\frac{A}{-n}+b\text{  and  }1=\frac{A}{-f}+B.$$ (7)
求出A,B:
$$A=\frac{2nf}{f-n}\text{  and  } B=\frac{f+n}{f-n}.$$ (8)
把AB代入(6)得到
$$z'=-\frac{2nf}{f-n}\left(-\frac{1}{P_z}\right)+\frac{f+n}{f-n}.$$ (9)
为了方便运算,把(4)(5)(9)等式两边乘以-Pz,整理得到:
$$
\begin{equation}
  \begin{cases}
-x'P_z&=&\frac{2n}{r-l}&P_x+&\frac{r+l}{r-l}&P_z,\\
-y'P_z&=&\frac{2n}{t-b}&P_y+&\frac{t+b}{t-b}&P_z,\\
   -z'P_z&=-&\frac{f+n}{f-n}&P_z-&\frac{2nf}{f-n}\\
  \end{cases}
\end{equation}
$$
(10)
这里的点(x',y',z'), 是NDC中的点,为了方便计算,现在定义一个P':
$$P'=\langle-x'P_z, -y'P_z, -z'P_z, -P_z\rangle$$ (11)

注意:这里的z‘是NDC中的值,范围是[-1,1]。在之后的操作中,OpenGL把z’的范围变为[0,1]后,存到ZBuffer中,参考下文“Z-Buffer的特性

根据(11)生成矩阵,P’的计算如下:

(12)
这样我们就可以根据Pz的信息,在透视下对相关属性进行正确的插值了,结果如下:

图11

图12

透视下的插值(Perspective-Correct Interpolation)

       OpenGL显示3D模型是以三角形为单位来处理的。我们在屏幕上看到的模型,其实是三角形内部的所对应的像素包含的属性(包括颜色、uv、法线等,参考Built-in Vertex Input Parameters),结合外部光照等其它因素综合计算得到。在显示空间三角形时,首先要更具三个点计算其在屏幕上的投影,然后对三角形内部进行填充,根据算法得到每个像素点的颜色。填充过程的第一步是一个光栅化,具体到某一像素点的颜色,其实就是根据当前像素点的属性,通过相应的算法得到颜色。这种取色的途径可以直接在贴图上取色,或则还要通过某种算法得到(比如光照算法)。取色算法是要依赖于当前像素点的信息的,比如uv,法线等。这些值也是通过插值得到的。

      下面将展示插值的计算方法并解释属性差值的方法。

假设有如下的直线:
$$ax+bz=c,$$ (13)


图13
利用相似三角形法则得到:
$$\frac{P}{x}=\frac{-e}{z}$$ (14)
把(14)代入(13)得到:

$$\left(-\frac{ap}{e}+b\right)z=c$$ (15)
整理得到:
$$\frac{1}{z}=-\frac{ap}{ce}+\frac{b}{c}.$$ (16)
我们发现,在16中, p和z的确呈反比例关系
假设z3在区间[z1,z2]中,则z3的插值如下:

(17)
整理(17)得到:
$$z_3=\frac{1}{\frac{1}{z_1}(1-t)+\frac{1}{z_2}t}$$ (18)

我们用b来表示其中的某一个需要插值的属性(比如颜色、uv、法线),现在要对两点间的属性进行插值的推导如下:
Z3为[Z1,Z2]中的一点:
$$\frac{b_3-b_1}{b_2-b_1}=\frac{z_3-z_1}{z_2-z_1}$$ (19)
整理得到:
$$b_3=\frac{b_1z_2(1-t)+b_2z_1t}{z_2(1-t)+z_1t}$$ (20)
等式右边上下除以z1z2,得到:
$$b_3=\frac{\frac{b_1}{z_1}(1-t)+\frac{b_2}{z_2}t}{\frac{1}{z_1}(1-t)+\frac{1}{z_2}t}=z_3\left[\frac{b_1}{z_1}(1-t)+\frac{b_2}{z_2}t\right]$$ (21)
所以对属性插值,其实是对每个顶点的属性除以的z的值的插值(即对b/z进行插值,参考 Cg Programming/Rasterization)。

NDC中Z的特性

ZBuffer是OpenGL的一个很重要的Buffer,和贴图一样,它的每一个像素的范围为[0,1],其浮点数是16位或24位(更高配置的可能为32位的)。
ZBuffer是由NDC中的Z经过处理后生成的。空间的点通过公式(12)的计算,我们得到了P'(参考公式11)。其中NDC中的z的计算如下: z= P'.z/P'.w 。NDC中的z的范围为[-1,1],和公式(6)(7)的方法一样,很容易使其范围变为[0,1];
NDC中的z和View中的z呈反比例关系,具体参考如下:

Z-Buffer的应用

使用Z-Buffer的技术的有Shadow、Field Of View、SSOA等,我做一个简单的介绍。等写完相应的文章之后,将会添加链接。
1)Shadow:以灯光的位置渲染得到depthMap(其中的每个像素表示到灯光的最近距离的值,也就是如果这个像素点中所覆盖的点的距离大于这个值,就在阴影中),并把这个保存为Texture形式;在摄像机角度进行渲染,每个点都转换到灯光的那个空间,并对比depth值看是否处于阴影中。
2)Field Of View:a)把场景渲染到RenderTexture上,记为RT0;b)对RT0进行模糊得到RT1;c)根据深度值融合RT0和RT1(当然深度值可以从Z-Buffer中获取,这个时候需要注意这个值为非线性的,所以要进行适当处理,使其变为线性的;“辅助函数”部分有相应的介绍)。
3)SSOA:a)根据屏幕的每个点的uv以及对应的Z-Buffer的值,计算出View中的点(参考“辅助函数”中的方法);b)运用特定的半球sample方法,计算出每个点的OA;

辅助函数

将介绍一些Cg(特别是Unity中)常用的相关函数。
inline float4 ComputeScreenPos (float4 pos) {
	float4 o = pos * 0.5f;
	#if defined(UNITY_HALF_TEXEL_OFFSET)
	o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w * _ScreenParams.zw;
	#else
	o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
	#endif
	
	o.zw = pos.zw;
	return o;
}

// Z buffer to linear 0..1 depth (0 at eye, 1 at far plane)
inline float Linear01Depth( float z )
{
	return 1.0 / (_ZBufferParams.x * z + _ZBufferParams.y);
}
// Z buffer to linear depth
inline float LinearEyeDepth( float z )
{
	return 1.0 / (_ZBufferParams.z * z + _ZBufferParams.w);
}


// Depth pass vertex shader
output.vPositionCS = mul(input.vPositionOS, g_matWorldViewProj);
output.vDepthCS.xy = output.vPositionCS.zw;

// Depth pass pixel shader (output z/w)
return input.vDepthCS.x / input.vDepthVS.y;

// Function for converting depth to view-space position
// in deferred pixel shader pass.  vTexCoord is a texture
// coordinate for a full-screen quad, such that x=0 is the
// left of the screen, and y=0 is the top of the screen.
float3 VSPositionFromDepth(float2 vTexCoord)
{
    // Get the depth value for this pixel
    float z = tex2D(DepthSampler, vTexCoord);  
    // Get x/w and y/w from the viewport position
    float x = vTexCoord.x * 2 - 1;
    float y = (1 - vTexCoord.y) * 2 - 1;
    float4 vProjectedPos = float4(x, y, z, 1.0f);
    // Transform by the inverse projection matrix
    float4 vPositionVS = mul(vProjectedPos, g_matInvProjection);  
    // Divide by w to get the view-space position
    return vPositionVS.xyz / vPositionVS.w;  
}

总结

本文介绍了投影矩阵的推导、简单介绍了光栅化,最后介绍了在透视中的插值以及带来的问题。
最后要强调两点,
1)NDC中的Z是非线性的,与View中的Z与呈反比例关系
2)View中的点经过矩阵运算之后,得到的是P‘,见公式(11)。P'是一个类型为Vector4的变量,其中P'.w为-Pz!(Pz为View中的Z值)

参考:

1)《Mathematics for 3D Game Programming and Computer Graphics(3rd Edition)》Chapter 5.5 Projections.

2)《Real-Time Rendering(3rd Edition)》Chapter 4.6 Projections

3)  Cg Programming/Vertex Transformations

4)  Cg Programming/Rasterization

5)Cg Programming/Unity/Debugging of Shaders

6)Using Depth Texture

7)Built-in shader variables (也可以参考官方buildin shader中的 “UnityShaderVariables.cginc”)

8)Scintillating Snippets: Reconstructing Position From Depth

9)Reconstructing pixel 3D position from depth

10) Camera’s Depth Texture

猜你喜欢

转载自blog.csdn.net/stalendp/article/details/50775450