Perspective 3D 成像

大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 12 篇文章。本系列文章从 WebGL 基本运作机制以及使用的原理开始介绍,最後建构出绘制 3D、光影效果之网页。介绍完 WebGL 运作方式与 2D transform 後,本章节讲述的是建构并 transform 渲染成 3D 物件,如果在阅读本文时觉得有什麽未知的东西被当成已知的,可能可以在前面的文章中找到相关的内容

使用 perspective 3D 投影渲染物件时,相当於模拟现实生活眼睛、相机捕捉的光线形成的投影,也是大多 3D 游戏使用的投影方式,学会这种投影方式,才算是真正进入 3D 渲染的世界,那麽我们就开始吧

Orthogonal vs Perspective

先来回顾一下 orthogonal 投影,在这行产生的 transform 矩阵只是调整输入座标使之落在 clip space 内:

const viewMatrix = matrix4.projection(gl.canvas.width, gl.canvas.height, state.projectionZ);

为了让输入座标可以用萤幕长宽的 pixel 定位,这个转换只是做拉伸(0 ~ 萤幕宽高 => 0 ~ 2)及平移(0 ~ 2 => -1 ~ +1),Orthogonal 投影视觉化看起来像是这样:

orthogonal-projection-visualized

没错,orthogonal 投影其实是往 -z 的方向投影,也就是说在 clip space 是以 z = -1 的平面进行成像;还有另外一种讲法:使用者看者萤幕是看向 clip space 的 +z 方向

现实生活中比较可以找的到以 orthogonal 成像的范例就是影印机的扫描器,有一个大的面接收垂直於该面的光线,而眼睛、相机则是以一个小面积的感光元件,接收特定角度范围内的光线,这样的投影方式称为 perspective projection:

orthogonal-vs-perspective

上面这张图是从侧面看的,x 轴方向与萤幕平面垂直,而蓝色框起来表示可见、会 transform 到 clip space 的区域,在 orthogonal 是一个立方体;在 perspective 这个区域的形状叫做 frustum,这个形状 3D 的样子像是这样:

wiki-fustum

产生 Perspective transform 矩阵

什麽样的矩阵可以把 frustum 的区域 transform 成 clip space 呢?很不幸的,其实这样的矩阵不存在,因为这样的转换不是线性变换 (linear transformation),根据 3Blue1Brown 的 Youtube 影片 -- 线性变换与矩阵这边讲到的,线性变换後必须保持网格线平行并间隔均等,想像一下把上面 frustum 侧边的边拉成 clip space 立方体的平行线,这个 transform 就不是线性的

幸好在 vertex shader 输出的 gl_Position.w (等同於 gl_Position[3] ) 有一个我们一直没用到的功能:顶点位置在进入 clip space 之前,会把 gl_Position.x, gl_Position.y, gl_Position.z 都除以 gl_Position.w。有了这个功能,在距离相机越远的地方输出越大的 gl_Position.w,越远的地方就能接受更宽广的 xy 平面区域进入 clip space

产生矩阵的 function 接收以下几个参数:

matrix4.perspective(
  fieldOfView,
  aspect,
  near,
  far,
)

fieldOfView 表示看出去的角度有多宽,aspect 控制画面宽高比(宽/高),near 为靠近相机那面距离相机的距离,far 则为最远相机能看到的距离

产生 perspective transform 矩阵的 function 我们就叫它 matrix4.perspective(),网路上当然有许多现成的程序码/公式可以用,不过笔者认为这一个 transform 很关键,就算已经拿到实做,还是想尝试自己算一下了解这个公式是如何产生的,假设 matrix4.perspective() 要制作的矩阵为 M:

perspective-formula-1

FOV 为 fieldOfView 的缩写,接着令 A 表示 a_position 输入的向量(正确来说,是与 perspective 矩阵 M 相乘的向量),P 表示输出给 gl_Position 的向量,P' 表示 gl_Position 的 xyz 除以 w 的向量,也就是 clip space 中的位置:

perspective-formula-2

看下面这张图,经过 M transform 并且除以 gl_Position.w 之後,图中之 A1 应该要转换至 P1' ([1,1,-1]);而 A2 应该要转换至 P2' ([1,1,1]):

perspective-formula-3

定义 gl_Position.w 等於负的 A.z,使得在距离相机越远的地方接受更宽广的区域进入 clip space;FOV 表示看出去『画面上缘与下缘』之间的角度,也就是说 FOV 的直接作用对象是在 y 轴上;对於 x, y 轴来说,这样的 transform 理论上不会有旋转或是平移,从 AP 只有 scalar 做缩放,那麽就来算这两个 scalar:

perspective-formula-4

接着来算 z 轴的部份,从上方 A1 转换至 P1'A2 转换至 P2' 的图来看,z 轴会有平移产生,因此这样算:

perspective-formula-5

笔者也把公式输入线上公式视觉化工具来观察 near, far 与 z 轴输入输出的影响:https://www.desmos.com/calculator/dhsp5blfzg

看到这边读者应该也有发现,相机视角对着的方向为 -z,面向萤幕外(萤幕到使用者)的方向为 +z,原因笔者也不知道,在猜应该是业界的惯例

基於先前介绍 scale, translate 时各个数值要放在矩阵的哪个位置,得到矩阵(使用电脑上的行列排法):

perspective-formula-6

完整计算流程的 PDF 版本在此,并且把矩阵实做到 lib/matrix.js 内:

  perspective: (fieldOfView, aspect, near, far) => {
    const f = Math.tan(Math.PI / 2 - fieldOfView / 2);
    const rangeInv = 1.0 / (near - far);
    return [
      f / aspect, 0, 0, 0,
      0, f, 0, 0,
      0, 0, (near + far) * rangeInv, -1,
      0, 0, far * near * rangeInv * 2, 0,
    ]
  },

使用 perspective transform

来到主程序,把 viewMatrix 从原本 matrix4.projection() 改成 matrix4.perspective()fieldOfView 先给上 45 度:

  const viewMatrix = matrix4.perspective(45 * Math.PI / 180, gl.canvas.width / gl.canvas.height, 0.1, 2000);

存档重整後会看到一片惨白,没有错误,但是就是没东西。以现在来说,因为 matrix4.perspective() 是从原点出发向着 -z 的方向看,而当初规划 3D 模组的 z 轴是往 +z 的方向长的,更别说顶点时是用萤幕 pixel 为单位制作的,现在看不到东西其实很正常;要看到东西,就得『移动视角』让物件在 frustum 内,这就留到下篇再来继续实做

最後笔者也把 fieldOfView 的使用者控制实做进去取代原本的 projectionZ,完整程序码可以在这边找到:

後记:如果把 fieldOfView 拉到很大约 160 度以上,其实可以看到右上角出现东西:


<<:  [Tableau Public] day 12:调整好原始资料就来制作地图分布吧

>>:  2021-Day7. IntelliJ IDEA 内建的 Kotlin data class File from Json 功能

Day 18:数据蒐集、资料视觉化、数据分析

前言 在公司内部总是有大大小小的提案,每个提案都有拥护的人,但是大家各说各话,没有办法公平的做出决定...

[Day-25] math函式库(一)

今天要来练习的是 C++内建的函式库 首先要先引入函式库 #include <cmath>...

[DAY 19] 卡多利亚良食故事馆

卡多利亚良食故事馆 地点:台南市後壁区42-27号 时间:9:00~17:00 对於一个研替来说 最...

建立 Line Bot(1)

前几天已经建立好能读取信件内容中验证码的 GAS 专案,接下来就是要准备建置最重要的 Line Bo...

企划实现(6)

甚麽是第三方支付? 第三方支付是指电子商务企业或是具实力及信用保障的独立机构,与银行之间建立一个中立...