WebGL 各个空间的计算

1,623 阅读5分钟

渲染管线中的顶点变换 - 知乎 (zhihu.com)

渲染管线中MVP矩阵的推导 - 知乎 (zhihu.com)

概述

  • 各个空间
    • 顶点数据输入(物体的三维空间实际位置) object坐标系
    • 顶点着色器处理(只是做模型的偏移,相机在原点) 相机空间
    • 投影矩阵(做投影) 在裁剪空间
    • 透视除法 就是NDC空间了
    • 视口变换 就是屏幕空间(Screen-space Coordinates) 各个空间 image.png

在图形学渲染管线中,一个顶点坐标,大概要经历局部坐标系、世界坐标系、相机坐标系、裁剪坐标系,最后到窗口坐标系,显示在屏幕上。

在这些过程中,从一个坐标系到另一个坐标系,都需要进行一定的变换。下面,将介绍每次变换的方式。

注意,本文是针对OpenGL的。

局部空间->世界空间

这一变换过程,主要是将模型放置在世界空间中,进行一定的缩放、旋转或平移。这一步比较简单,只要将相应的矩阵作用到模型的局部空间坐标即可。

比如,对模型缩放 [公式] ,然后绕Z轴旋转 [公式] 度,再进行 [公式] 的平移。注意,这里的变换顺序是不能变的,即要先进行缩放,再进行旋转,最后进行平移。据此,我们可以构建模型变换矩阵。

[公式]

世界空间->相机空间

首先定义一下相机:

  • 坐标为 [公式]
  • 观察方向 [公式]
  • 向上方向 [公式]

示意图如下所示:

有一个性质注意一下:当相机和相机“看“到的物体一起变换时,相机”看“到的内容是不变的。 这样,可以将相机的坐标移动到世界坐标的原点,向上方向对齐世界坐标的Y轴,观察方向对齐世界坐标的-Z轴。然后,对物体进行相同的变换即可。

在数学上,这个过程大概这样:

  • 将相机移动到坐标原点
  • 旋转观察方向 [公式] 到-Z轴
  • 旋转向上方向 [公式] 到Y轴
  • 旋转( [公式] )到X轴

大体分为两步:先位移,后旋转。即 [公式] 。

平移部分:

[公式]

对于旋转部分,先补充一些知识点。对于二维空间来说:

[公式]

[公式]

根据定义,旋转 [公式] 角度和旋转 [公式] 角度是互逆的,即: [公式] 。

所以,对于旋转变换,可以得出旋转矩阵的逆等于它的转置,即:

[公式] 回到上面的旋转部分,直接求相机的坐标轴旋转到世界坐标轴的矩阵不是很方便,但是反过来,求世界坐标轴旋转到相机的坐标轴很容易:

[公式]

根据旋转矩阵的逆等于它的转置,得出:

[公式]

根据 [公式] ,不用考虑缩放,可以得出:

[公式]

相机空间->裁剪空间

由投影矩阵创建的观察箱(Viewing Box)被称为平截头体(Frustum),每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。将特定范围内的坐标转化到标准化设备坐标系的过程(而且它很容易被映射到2D观察空间坐标)被称之为投影(Projection),因为使用投影矩阵能将3D坐标投影(Project)到很容易映射到2D的标准化设备坐标系中。

这里要注意一下,OpenGL是右手坐标系的,但是在NDC中,是左手坐标系的,这里要特别注意!!!

正交投影

我们先定义一个正交投影的视锥体 [公式] (注意,n和f都是负数,f是远平面,所以f<n),它是一个长方体。我们需要做的,就是将正交投影的视锥体转换到标准立方体(即标准化设备坐标, [公式] )。**注意,这里 [公式] 映射到NDC中的[1,-1]。**注意下图第三幅图Z轴

这里,分成两个步骤:平移和缩放。注意:r-l>0,t-b>0,f-n<0 [公式]

透视投影

对于透视投影,分成两步操作:

  • 首先,“压扁”视锥体成一个长方体(n->n,f->f)( [公式] );
  • 然后,做正交投影操作( [公式] ,即上面的正交投影)。

观察下图:

根据相似三角形的关系,可以得出:

[公式]

类似的,可以得出:

[公式]

由此,可以得出下面的关系:

[公式]

下面,说一个齐次坐标的性质:在3D坐标系统中, [公式] , [公式] , [公式] 都表示相同的坐标--- [公式] 。例如: [公式] 和 [公式] 都表示坐标 [公式] 。

所以,有如下关系:

[公式]

更进一步的,可以得到:

[公式]

现在,还剩下第三列是未知的。

经过观察上面的透视投影视锥体,可以得出以下推论:

  1. 近平面上的点的坐标都不会改变;
  2. 远平面上的点,Z坐标不改变。

根据推论1,近平面上的点 [公式] 经过变换后,不会改变。即:

[公式]

根据:

[公式]

因为 [公式] 与x和y都没有关系,所以可以得出 [公式] 的第三列的形式是 [公式] 。

根据:

[公式]

可以得出:

[公式]

根据推论2,远平面的中心点 [公式] ,经过变换后,还是本身。如下:

[公式]

所以,可以得出:

[公式]

即:

[公式]

到这里,可以得出方程组:

[公式]

到这里,可以得出 [公式] :

[公式]

最终,透视投影矩阵:

[公式]

裁剪空间->窗口空间

  • 裁剪空间点的W值为1
  • 在裁剪空间的最后,所以的可见的点都在标准设备坐标系(NDC)中,即坐标坐落在范围 [-1 - 1] 内。
  • NDC到窗口空间,定义一个屏幕空间: [公式] 。平面左下角的坐标位 [公式] ,右上角的坐标为 [公式] 。对于X和Y坐标的变换,即从 [公式] 到 [公式] 。
  • 这里,经过两步变换,平移矩阵平移[1,1,1],缩放矩阵缩放[w/2,h/2,1/2]
    • [公式]
  • 对于Z坐标,从 [公式] 映射到了 [公式] 。先缩放再偏移.
  • [公式]

后处理的空间转换,获取视口空间下的Z值

  • 已知深度贴图depth,即可获取视口空间下的Z值
float viewZ = getViewZ( depth ); //这个viewZ是用来获取clipW用的
float getViewZ( const in float depth ) {
    #if PERSPECTIVE_CAMERA == 1
        return perspectiveDepthToViewZ( depth, cameraNear, cameraFar );
    #else
        return orthographicDepthToViewZ( depth, cameraNear, cameraFar );
    #endif
}
float perspectiveDepthToViewZ( const in float invClipZ, const in float near, const in float far ) {
    return ( near * far ) / ( ( far - near ) * invClipZ - far );
}
float orthographicDepthToViewZ( const in float linearClipZ, const in float near, const in float far ) {
    return linearClipZ * ( near - far ) - near;
}
  • 可以通过z值和相机矩阵获取到w值,正交情况下,w为1,投影情况下
float clipW = cameraProjectionMatrix[2][3] * viewZ + cameraProjectionMatrix[3][3];

后处理中的空间转换,获取视口空间下的坐标

  • 后处理vUV屏幕空间->裁剪空间 把vuv[0,1]区间转为[-1,1]间
    • -0.5后乘2
vec4 clipSpeace = vec4((vec3(vUV,depth) - 0.5)*2.0,1.0)
  • 裁剪空间->投影空间
vec4 projectionSpace = clipSpeace*w;
  • 投影空间->视口空间
vec4 viewSpace = projectionSpace*cameraInverseProjectionMatrix;

空间转换,获取屏幕空间的坐标

  • *0.5后+0.5
vec4 samplePointNDC = cameraProjectionMatrix * vec4( samplePoint, 1.0 ); //投影空间
samplePointNDC /= samplePointNDC.w; //透视除法
vec2 samplePointUv = samplePointNDC.xy * 0.5 + 0.5; //屏幕空间

补充

偶然间发现一张图,很清晰地描述了上述的变换过程,这里也记录一下: