一帧画面是怎么来的

UNIT 02灯光相机约 28 分钟 · 核心线

电脑怎么知道东西在哪、从哪看?

《拾光》里举起取景器,没拍中的时候它会提示:「再走近一些,或拉长焦距」。两种做法写在同一句里,像是一回事。拍出来的照片,真是一回事吗?这一课先弄清电脑怎么知道一样东西在哪、有多亮、从哪看,最后回来回答这一句。

02 · 1先说清它在哪

要把一棵古榕画进一帧,电脑得先知道它的每一个点在哪。最直接的办法:每个点给三个数 (x, y, z),说它从某个起点往右、往前、往上各走了多远。这三个数叫坐标(coordinate),那个起点叫原点。一个立方体的八个角,就是八组三个数。

那给每个点三个数就够了吗?三个数本身不带意思,得先约好三件事:原点在哪,哪根轴朝上,1 代表多长。还有一件:三根轴怎么排,叫右手系还是左手系。各家软件约得不一样:

三个你用过的地方,各自的约定
在哪朝上的轴1 代表
BlenderZ1 米(默认设置)
网页(glTF 格式、three.js)Y1 米(glTF 的规定;three.js 本身不定单位,惯例当米)
虚幻引擎Z1 厘米,而且是左手系(另外两家是右手系)

《拾光》撞过这堵墙。第一次把沙面导进虚幻引擎,整座岛缩成了 9 米长 C30。模型一个点都没坏:Blender 里按「米」写下的每个数,虚幻引擎原样收下,却当成「厘米」读,每样东西都小了一百倍。坏的是两边对「1 代表多长」的约定没对上。

后来的换算写成了一行,在《拾光》的 setup_shamian_play.py 里:

def ue(p):   # Blender 里的一个点 → 虚幻引擎里的一个点
    return unreal.Vector((p[0] - ORIGIN[0]) * 100.0,
                         -(p[1] - ORIGIN[1]) * 100.0,
                         (p[2] - ORIGIN[2]) * 100.0)

p[0]、p[1]、p[2] 就是这个点的 x、y、z。这一行做了三件事(朝上的轴两边都是 Z,不用换)。减去 ORIGIN:把沙面中间的一点当成新的原点(沙面在全城模型里的 x 约是 −8100 米,离全城的原点八公里多)。乘 100:米换成厘米,C30 那次漏的就是它。y 前面一个负号:把 Blender 的右手系翻成虚幻的左手系,不翻,整座岛会左右颠倒 C30。

02 · 2一个面有多亮,看两个方向

点在哪说清了,可一帧画面要的是每一格的颜色。同一堵墙,早上亮、傍晚暗;同一座屋子,向阳的一面亮、背阴的一面暗。亮不亮,看两个方向:这一面朝哪,光从哪来。

方向用一个箭头表示,叫向量(vector)。它也是几个数,说的却是「往哪走、走多远」,不管从哪出发。长度为 1 的箭头只表示朝向:一个面朝外的方向叫法线(normal),记作 n;从这个面指向光源的方向,记作 l。

最省事的想法是:照到的面就亮,照不到就黑。可正午和傍晚,地面都照得到,亮得却不一样。那是按角度比例变暗吗?先猜一个。

正午太阳在头顶,地面的亮度记作 1。太阳落到离地 30°(光斜着下来,和地面的朝向差了 60°),地面还剩多亮?

  1. 还是 1:照到就一样亮
  2. 约 0.33:差了 60°,按比例少掉三分之二
  3. 0.5
  4. 0:斜着就照不到了

一半。看下面那束光:同样宽的一束,正着照,落在 1.2 米长的一段上;斜着照,铺开到 2.4 米。光就那么多,铺开一倍,每一米分到一半。

铺开多少,是个直角三角形的事:光束的宽 w、落在面上的那一段 L,和光与面的朝向之间的夹角 θ,拼成一个直角三角形,w = L × cos θ。cos θ(余弦)是直角三角形里一个角的邻边 ÷ 斜边:θ 是 0° 时为 1,60° 时为 0.5,90° 时为 0。所以 L = w ÷ cos θ,每一米分到的光就是 cos θ 倍:亮度 = cos θ,cos 60° = 0.5。按比例算 0.33 的,把「转了多少度」和「铺开了多少」当成了一回事。

实验台是一张剖面图,只有左右、上下两个方向,所以每个箭头只写两个数。太阳离地 30° 时,指向它、长度为 1 的箭头 l 横着走 cos 30° ≈ 0.87、竖着走 sin 30° = 0.50(sin 是对边 ÷ 斜边),写成 l = (0.87, 0.50)。拖一下太阳,每一面上的数都跟着变。

实验台 2-1点积照亮一面
来自材料
亮度 = max(0, n·l) 这条规矩(LearnOpenGL「基础光照」);广州 9 月 26 日正午约 65°、18:00 约 4° 的太阳高度,是按广州片 sun.py 里的 NOAA 算法算的。
本实验的设定
剖面总是顺着阳光切:右墙正对太阳所在的方位;屋顶坡度 30°。只算太阳的直射,没算天空光、墙和地面互相反射的光,也没算傍晚阳光穿过更厚的大气变暗。地面上的影子是另算的:它只表示那一段被屋子挡住、照不到,n·l 本身不管挡没挡(第 6、7 课讲怎么算挡没挡)。
它能证明
一个面接到多少直射光,只取决于它朝哪、光从哪来,和它在画面哪里无关;背光的面接不到直射光。
它不能证明
真实画面里背光的面是黑的:它还会被天空和四周反射的光照亮(第 5 课);表面是什么做的,也会改变看起来多亮(第 4 课)。

电脑怎么算这个数?它不量角度,只做乘法和加法:

n·l = nx × lx + ny × ly + nz × lz
两个都是长度 1 的箭头时,n·l 正好等于它们夹角的余弦 cos θ(点积的一条性质;实验台的读数里两个数总是相等,可以自己对)。

三对数各乘一下、加起来,叫点积。每一格、每一盏灯都要算一次,乘三次、加两次,很便宜。点积小于 0,说明光在这一面的背后:这一面背光,直射亮度记 0,不是负的。所以规矩写成 亮度 = max(0, n·l)。把太阳拖到左边,右墙就变成 0。

这个式子也在你的游戏里。《拾光》判断你拍照的方向对不对,先算镜头朝向和规定方向的点积,得到夹角的余弦,再反推出夹角;取景器提示「它在你身后」时,判据是:从相机指向古榕的箭头和镜头朝向做点积,结果不是正数,也就是古榕不在镜头前面。

不看上面,用一句话说:同一束光,为什么斜着照的面更暗?

02 · 3从哪看:相机也要摆

坐标、亮暗都有了,还差一样:从哪看。相机也是场景里一个被摆放的东西,一个位置、一个朝向。画之前,电脑先把每个点换算成「相机自己的坐标」:相机在原点、朝着正前方。这等于把整个世界反着挪一遍、反着转一遍,让它正好摆在镜头前。

摆一个东西,相机也好、楼也好,要两步:转和移。这两步的顺序能换吗?

先转 90°,再右移 2 原点 (2, 0) 先右移 2,再绕原点转 90° (0, 2)
顺序不同,落点不同。绕原点转的时候,已经移出去的东西会被带着一起转。两边最后都朝上,位置差了一大截。

单个的转、移会做,一组合就在顺序上出错,是图形学课上被研究过的常见错误。它也藏在「坐标」里:整个场景共用的那套坐标叫世界坐标;一个角色自己还带着一套跟着它走、跟着它转的坐标(局部坐标)。两套之间,差的就是一次转加一次移。你的投篮课撞过:两骨 IK(给手的位置、倒推肘怎么弯)要在角色自己的坐标里算;在世界坐标里算,角色一转身,手就跑偏了 C46。

朝向本身怎么记?最直观的是三个角:左右摇、上下俯仰、绕镜头翻滚,叫欧拉角。它有个毛病:镜头俯到正下方时,镜头自己那根轴也竖了过来,和「左右摇」绕的那根竖轴成了同一根,三个角只剩两个在起作用,叫万向锁(gimbal lock)。另外,从一个朝向慢慢转到另一个,中间每一帧的角度要一点点算出来(插值);三个角各算各的,还可能绕远路。所以引擎里多用四个数的四元数(quaternion)记朝向,插值走最短的弧。投篮课里,程序先把角色的朝向设成存好的一组四元数,再单独去改其中一个欧拉角让它转身,姿势整个翻了过去;最后改成绕一根竖轴把整组转过去 C46。

02 · 4近大远小:除以距离

相机摆好了,最后一步:把立体的点压到平的画面上。同样高的东西,远的看着小。小多少?

两根一样高的护栏柱,一根离你 5 米,一根离你 20 米。近的那根在画面里高 400 格,远的那根高多少格?

  1. 300 格:远了 15 米,按比例少一截
  2. 100 格
  3. 25 格:按距离的平方变小
  4. 400 格:一样高的东西,画出来一样高
针孔 画面 h h ÷ 4 5 米 20 米 焦距
近大远小,是除法。从柱顶到针孔拉一条线,它穿过画面的高度 = 焦距 × 柱高 ÷ 距离。远 4 倍,画面上就是 1/4。

100 格。最简单的相机是一个开了小孔的暗盒:光从小孔(针孔)穿过去,落在画面上;画面离小孔的距离,就是焦距(focal length)。图里从柱顶到针孔的那条线,和柱子、地面围成一个三角形;它在画面这一段截出一个形状一样、小一号的三角形(相似三角形),所以 画面上的高 ÷ 焦距 = 柱高 ÷ 距离。20 米是 5 米的 4 倍,画面上就是 1/4。

猜 25 格的,把另一条规矩搬了过来:离灯远一倍,照到的光剩 1/4,那是亮度随距离变,不是大小。猜 300 格的用了减法;猜 400 格的,把真实的大小当成了画面上的大小。

写成式子,就是相机把点压到画面上的那一步:

画面上的位置 = 焦距 × 横向距离 ÷ 深度 (x = f × X ÷ Z)

X 是离镜头正中那条线有多远,Z 是顺着镜头朝向往前有多远,叫深度(刚才的「距离」就是它)。近大远小,就是除以深度。一排一样高的路灯往远处排,近处两盏差得多,远处几盏几乎一样高,也是这个除法。

02 · 5焦距只是裁切,站位才改远近

式子里的 f 就是焦距:24 mm,就是画面离镜头中心 24 毫米。能装下多宽,看画面本身多宽:《拾光》按 36 毫米宽的画面(画幅)算,一半是 18 毫米。从镜头中心看画面的一侧边缘,这个角的正切(对边 ÷ 邻边)是 18 ÷ 焦距;atan 是反过来由正切求角度,乘 2 就是左右合起来的视角:水平视角 = 2 × atan(18 ÷ 焦距),这是《拾光》代码里原样的一行。24 mm 看 73.7°,50 mm 看 39.6°,200 mm 只看 10.3°,滚轮在这一段里变焦。现在回到开头那句提示。

你在古榕大道上,离那棵古榕 150 米,用 24 mm,取景器提示「再走近一些,或拉长焦距」。A:走到离它 20 米,还用 24 mm;B:原地不动,推到 180 mm。两张里古榕差不多一样大,取景器都说够了。树后面 25 米那排 15 米高的老楼呢?

  1. 两张一模一样:古榕一样大,别的也就一样大
  2. A 里的楼显得更大:走得近,看得清
  3. B 里的楼显得大得多,像贴在树后
  4. B 里的楼更小:长焦把远处推得更远

B 里的楼大得多。画面上的高 = 焦距 × 高 ÷ 深度。古榕高 15.7 米,楼高 15 米:A 里「楼 ÷ 古榕」=(15 ÷ 45)÷(15.7 ÷ 20)≈ 0.42,B 里 =(15 ÷ 175)÷(15.7 ÷ 150)≈ 0.82,差不多翻了一倍。焦距在两边都约掉了。把 01 在 A、B 之间切换:古榕不动,楼一下子涌上来。

实验台 2-2焦距与站位
来自材料
视角公式、24–200 mm 的变焦范围、「百年榕荫下」这一刻的评分规则(把古榕当成一个半径 14 米的球,它在画面里要占画面高的 25%–180%,站位要在 15–160 米之间),都出自《拾光》的代码;古榕的尺寸(高 15.7 米、冠幅 22.6 米)出自广州片树库的报告。
本实验的设定
树后 25 米有一排 15 米高的楼,右手边每 3 米一根护栏柱,树前站着一个 1.7 米的人,都是为了比大小摆的;相机离地 1.6 米、平视。画面按针孔相机算,没有镜头畸变和景深。
它能证明
同一站位换焦距,整张画面等比例放大,等于从 24 mm 的画面里裁一块;远近两样东西的大小比,只随站位变。
它不能证明
真实镜头拍出来的样子。真镜头有畸变、有景深,广角的边缘会拉伸;这些都不改变「大小比只看站位」这一条。

原因在式子里。古榕和楼离你的深度之比,A 是 20 : 45,B 是 150 : 175。站得越远,两样东西的深度越接近,画面上的大小也越接近。只拨 02 焦距试试:画面在放大,下面那张 24 mm 画面上的框在缩小,「楼 ÷ 古榕」一动不动。焦距在式子里只是一个乘在整张画面上的数,所以同一站位换焦距,就是从同一张画面里裁一块:焦距是原来的几倍,框的宽和高就各是原来的几分之一。拨 03 站位,这个数才变。

所以摄影圈常说的「长焦压缩透视」,功劳记错了地方:压缩来自站得远,长焦只是把远处那一小块放大到满画面。打开 04 再拖焦距:相机边退边推,古榕一直一样大,楼却涌上来又退回去。电影里这叫滑动变焦(dolly zoom)。

既然换焦距等于裁切,那干脆都用 24 mm 拍、后面再裁,不就省事了?

省不了。裁出来的那块,内容和换长焦拍的一样,格子却少得多:焦距是 4 倍,框的宽、高各是 1/4,格子只剩 1/16。一张 1920 × 1080 裁完,只剩 480 × 270,再放回满屏,就是第 1 课那种把格子画大的糊。真换长焦,是用整张画面的格子去拍那一小块。

02 · 6光圈和快门

取景器上还有两样:Q 和 E 调光圈,景深跟着变;另一样是快门。光圈(aperture)是镜头里的那个孔,大小用「f 数」表示,写出来是大写 F 加一个数(F1.4、F2.8、F8……),数越小孔越大。孔大时,只有对焦的那个距离是清楚的,前后都糊,叫景深(depth of field)浅;孔小时前后都清楚。快门是一帧画面「开着」多久:开得越久,动的东西拖得越长,那是第 12 课的运动模糊。

带走一个问题

你想拍一张古榕,让背后的老楼显得又高又近,像压在树冠后面。该往哪站、用多长的焦距?要是只能站在原地,光换焦距做得到吗?用这一课的式子说出理由。

不看上面,写下这一课撞上的几堵墙,再展开对照
  1. 给每个点三个数就够了 → 还得约好原点、哪根轴朝上、1 代表多长;漏一步,沙面缩成 9 米。
  2. 照到就亮,或者按角度比例变暗 → 亮度 = max(0, n·l) = cos θ:同一束光铺开在更长的一段上;背光为 0。
  3. 先转先移都一样 → 顺序不同落点不同;三个角记朝向还会锁住一根轴。
  4. 远一截就小一截,或者按平方变小 → 除以深度:远一倍,小一半。
  5. 走近和拉长焦距是一回事、长焦压缩透视 → 焦距只裁切;站位才改变远近两样东西的大小比。