pbrt學習(二):攝像機建模
來自專欄 UE4遊戲開發技術
第6章 攝像機建模
攝像機空間(Camera space):以攝像機位置為原點,觀察方向為z軸,向上的方向為y軸,向右的方向為x軸的坐標系統。x軸的方向是觀察方向和向上方向的叉積,而得到的向量又與觀察方向計算叉積,得到向上的方向,以得到相互垂直的正交坐標系。在該坐標系統下,z<0的物體不可見,也是因為視域FOV(field of view)不可能大於180度。
屏幕空間(Screen space):將攝像機空間的對象投影到膠片平面(可看做近裁剪平面)上,深度z值範圍從0到1,依然是三維的。
規範化設備坐標空間(Normalized device coordinate space,NDC):渲染的圖片所使用的坐標系,左上角(0,0),右下角(1,1)。深度z和屏幕空間的一樣,由屏幕空間線性轉換得到。
光柵空間(Raster space):與NDC相同,除了坐標範圍從(0,0)到(resolution.x, resolution.y)之外。
6.1 攝像機基類
class Camera{ Camera(const AnimatedTransform &CameraToWorld, Float shutterOpen, Float shutterClose, Film *film, const Medium *medium);}
shutterOpen和shutterClose分別表示快門打開和關閉時間,可用於模擬真實攝像機的動態模糊(motion blur)效果。
成員函數:
GenerateRay: 傳入一個採樣點CameraSample,返回一個Ray。對應正交投影,Ray的起始點位於採樣點,方向指向z軸正方向;對於透視投影,起始點位於攝像機原點,也就是(0,0,0),方向指向採樣點。
GenerateRayDifferential: 除了生成跟GenerateRay()一樣的射線外,還生成2條輔助的射線,它們分別沿著x和y方向偏移一個像素,用於反走樣。
6.2 投影攝像機模型
class ProjectiveCamera: public Camera{ ProjectiveCamera(const AnimatedTransform &CameraToWorld, const Transform &CameraToScreen, const Bounds2f &screenWindow, Float shutterOpen, Float shutterClose, Float lensr, Float focald, Film *film, const Medium *medium) : Camera(CameraToWorld, shutterOpen, shutterClose, film, medium), CameraToScreen(CameraToScreen) {}}
繼承自Camera, 用於三維空間到二維空間的投影。正交投影和透視投影攝像機都繼承自該類。
CameraToScreen: 攝像機空間到屏幕空間的轉換矩陣,正交投影默認是Orthographic(0,1),透視投影默認是Perspective(fov, 1e-2f, 1000.f)(下面會介紹這兩個函數)。
screenWindow: 屏幕空間窗口的大小。
計算屏幕空間到光柵空間的轉換矩陣:
ScreenToRaster = Scale(film->fullResolution.x, film->fullResolution.y, 1) *Scale(1 / (screenWindow.pMax.x - screenWindow.pMin.x),1 / (screenWindow.pMin.y - screenWindow.pMax.y), 1) *Translate(Vector3f(-screenWindow.pMin.x, -screenWindow.pMax.y, 0));
以上,對於在屏幕空間的一個點,先平移使得屏幕左上角移動到原點位置,然後分別縮放x,y坐標使其範圍在0到1之間(也就是NDC坐標系),最後以光柵解析度分別放大x,y坐標,得到光柵空間的坐標。因為1 / (screenWindow.pMin.y - screenWindow.pMax.y)值為負,所以光柵空間的y坐標和屏幕空間的是相反的。
光柵坐標繫到屏幕坐標系:
RasterToScreen = Inverse(ScreenToRaster);
光柵坐標繫到攝像機坐標系:
RasterToCamera = Inverse(CameraToScreen) * RasterToScreen;
6.2.1 正交投影攝像機
用於將場景里的一個矩形區域內的物體投影到該矩形盒的前表面上。
class OrthographicCamera : public ProjectiveCamera { OrthographicCamera(const AnimatedTransform &CameraToWorld, const Bounds2f &screenWindow, Float shutterOpen, Float shutterClose, Float lensRadius, Float focalDistance, Film *film, const Medium *medium) : ProjectiveCamera(CameraToWorld, Orthographic(0, 1), screenWindow, shutterOpen, shutterClose, lensRadius, focalDistance, film, medium) {}}
默認攝像機空間到屏幕空間的轉換矩陣CameraToScreen為: Orthographic(0,1),也就是近裁剪平面z=0, 遠裁剪平面z=1。
Transform Orthographic(Float zNear, Float zFar){ return Scale(1, 1, 1 / (zFar - zNear)) * Translate(Vector3f(0, 0, -zNear));}
x,y坐標不變,沿著z軸平移,使近平面在z=0上,然後縮放z坐標,使遠平面在z=1上。
生成射線(Ray)的原點在膠片平面上,也就是近平面,方向是(0, 0,1):
Point3f pFilm = Point3f(sample.pFilm.x, sample.pFilm.y, 0);// 光柵空間的像素點轉換到攝像機空間的坐標點Point3f pCamera = RasterToCamera(pFilm);*ray = Ray(pCamera, Vector3f(0, 0, 1));
生成的2條微分射線(ray differentials)的方向不變,起始點是前面Ray的起始點分別向x和y偏移一個像素位置:
dxCamera = RasterToCamera(Vector3f(1, 0, 0));dyCamera = RasterToCamera(Vector3f(0, 1, 0));ray->rxOrigin = ray->o + dxCamera;ray->ryOrigin = ray->o + dyCamera;ray->rxDirection = ray->ryDirection = ray->d;
6.2.2 透視投影攝像機
投影某個體積空間下的物體到二維膠片平面上,透視攝像機和人眼以及真實攝像機最接近:觀察三維空間的物體時,會有近大遠小的視覺效果。
class PerspectiveCamera : public ProjectiveCamera{ PerspectiveCamera::PerspectiveCamera(const AnimatedTransform &CameraToWorld, const Bounds2f &screenWindow, Float shutterOpen, Float shutterClose, Float lensRadius, Float focalDistance, Float fov, Film *film, const Medium *medium) : ProjectiveCamera(CameraToWorld, Perspective(fov, 1e-2f, 1000.f), screenWindow, shutterOpen, shutterClose, lensRadius, focalDistance, film, medium) {}}
默認攝像機空間到屏幕空間的投影矩陣CameraToScreen為:Perspective(fov, 1e-2f, 1000.f),也就是近裁剪平面z=0.01,遠裁剪平面z=1000。
Transform Perspective(Float fov, Float n, Float f) { // Perform projective divide for perspective projection Matrix4x4 persp(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, f / (f - n), -f * n / (f - n), 0, 0, 1, 0); // Scale canonical perspective view to specified field of view Float invTanAng = 1 / std::tan(Radians(fov) / 2); return Scale(invTanAng, invTanAng, 1) * Transform(persp);}
投影矩陣:
齊次坐標(x, y, z, 1)經過上面的矩陣變換後,新的坐標為(x, y, f * (z- n) / (f - n), z),所以:
x = x / z
y = y / z
z = f * (z - n) / z * (f - n)
推導:
上圖表示將P點投影到圖像平面後得到P。三角形△ABC和△DEF相似,所以BC / EF = AB / DE, 假設攝像機到觀察面的距離是1,那麼BC = EF / DE,所以P點在觀察面的投影點P的坐標是:x = x / z,y = y / z。
假設投影后的z坐標為:
z = (z * m22 + m23) / z
因為z在近平面時z = 0,在遠平面時z = 1,帶入上面公式
(n * m22 + m23) / n = 0
( f * m22 + m23) / f = 1
求解二元一次方程得到:
m22 = f / (f - n)
m23 = -f * n / (f - n)
如果考慮FOV,那麼m00 = m11 = 1 / std::tan(Radians(fov) / 2)
目的通過放大或縮小x,y坐標,使FOV內所有可見對象的點坐標落在[-1:1]範圍內。
生成的射線起始點為(0, 0, 0),方向指向像素採樣點:
Point3f pFilm = Point3f(sample.pFilm.x, sample.pFilm.y, 0);Point3f pCamera = RasterToCamera(pFilm);// 因為pCamera是在攝像機空間的一個點,所以從原點到該點的方向也就是Vector3f(pCamera)*ray = Ray(Point3f(0, 0, 0), Normalize(Vector3f(pCamera)));
生成的2條微分射線起始點不變,方向分別向x,y偏移一個像素:
dxCamera = (RasterToCamera(Point3f(1, 0, 0)) - RasterToCamera(Point3f(0, 0, 0)));dyCamera = (RasterToCamera(Point3f(0, 1, 0)) - RasterToCamera(Point3f(0, 0, 0)));ray->rxOrigin = ray->ryOrigin = ray->o;ray->rxDirection = Normalize(Vector3f(pCamera) + dxCamera);ray->ryDirection = Normalize(Vector3f(pCamera) + dyCamera);
6.2.3 薄透鏡模型和景深(depth of field)
針孔攝像機模型
針孔越小,圖像越清晰,但需要更長的曝光時間,因為越小的針孔,經過的光子數就越少,這對於計算機建模的虛擬攝像機來說並不是個問題,但真實的攝像機因為需要較長的曝光時間,如果這期間移動攝像機或對象在移動,會產生模糊的圖片(與景深產生的模糊不同,景深是由於透鏡的聚焦產生的)。
為了獲取更多的光子進入針孔,需要大的孔徑,但這會導致圖像模糊,現代攝像機使用透鏡(Lens)替代針孔使光線重新聚焦於一點來解決這個問題:
一個點發散的光線經過透鏡折射聚焦在膠片上,在聚焦面之前或之後的點經過透鏡折射後的聚焦點在膠片之前或之後,光線在膠片上就會以圓盤的形狀分布,這個圓盤叫做模糊圈(circle of confusion)(事實上,只要模糊圈大小小於一個像素就認為對象是聚焦的),因而這個點的光線會分布在模糊圈上(同理,膠片上一個點上的光可能來自於場景中不同的點),這就產生了聚焦面上的點清晰周圍模糊的圖像。從鏡片到可聚焦對象的距離範圍叫做景深。
虛擬攝像機可通過圓盤採樣起始點(給定一個u,v坐標,將其映射到單位圓上的坐標,乘以鏡片半徑),射線方向指向原始射線與聚焦面的交點(這樣保證不管採樣點在哪,聚焦面上的點都是聚焦在膠片上的),獲得的多個射線模擬產生景深效果。
Point2f pLens = lensRadius * ConcentricSampleDisk(sample.pLens);Float ft = focalDistance / ray->d.z;// 原始射線在聚焦面(focalDistance範圍上的點)的交點Point3f pFocus = (*ray)(ft); ray->o = Point3f(pLens.x, pLens.y, 0);ray->d = Normalize(pFocus - ray->o);
應對每個像素使用多個射線進行採樣以獲得平滑的景深效果:
6.3 環境攝像機(ENVIRONMENT CAMERA)
以攝像機為原點,朝所有方向進行光線追蹤,可以得到在該點所有可見三維對象的二維圖像呈現。
Float theta = Pi * sample.pFilm.y / film->fullResolution.y;Float phi = 2 * Pi * sample.pFilm.x / film->fullResolution.x;Vector3f dir(std::sin(theta) * std::cos(phi), std::cos(theta), std::sin(theta) * std::sin(phi));*ray = Ray(Point3f(0, 0, 0), dir, Infinity, Lerp(sample.time, shutterOpen, shutterClose));
想像一個球面坐標,一個二維矩形圖像從上往下映射了θ從0到π,從左往右映射了φ從0到2π,因而對應於圖像上的每一個像素點,都可以由球面坐標計算得到一個射線方向dir.
這種類型的圖片非常有用,因為它表示了場景里一個點上所有的入射光,一個重要的使用就是環境光照(environment lighting)——一個基於圖像的場景光照渲染技術。
參考:Scratchapixel
推薦閱讀:
※NPR中的透明物體bloom
※Matrix and Transform Conversion 2/3
※炫舞引擎日常:截圖
※基於RTX的NVIDIA OptiX光線追蹤
※ZQlib都能幹些什麼之五:紋理合成