熟悉Houdini Shader部分的同學應該多多少少也了解camera自身也可以設定自己的shader。其中polar panoramic shader 能夠非常方便的為藝術家渲染360全景視角的cg畫面,但是這樣渲染出來的畫面只是單眼所看到的環境,如果引入立體雙攝像機的渲染方法的話,默認的這個攝像機shader就會出現一個嚴重的問題,那就是所渲染出來的畫面是分別以各自兩台攝像機位置為原點所計算出來的。用文字說明可能有點繞口,看下圖:
圖片中我把攝像頭在一個水平軸向上移動了一點,渲染出來的結果發現垂直方向上粉紅圈圈確實是因為一定的位移看到了柱子后面的東西,但是前后是反的,而且最要命的是水平方向上還是被擋着的,主要原因就是因為渲染的采樣原點是攝像機自身中心點。這和我們實際旋轉頭部得到的影像是不一樣的。生活中如果要看我們左邊的事物,絕對不是兩眼珠子自己左轉九十度而是由我們的頭部旋轉來幫助眼睛看到目標。所以這種情況下實際上的兩只眼睛對身邊環境的成像是共享了同一個旋轉中心點,且中心點絕不會在任意眼珠上。
如下圖模型,O點才可能成為polar panoramic shader的旋轉點,而射線投射點的位置待會再細聊:
確定好正確的攝像機渲染原型之后就是怎樣把這個方法放入到Houdini的攝像機上,好在Hou很靈活的提供了camera自身的shader入口,而且shop下面的ASAD Lens節點給我們提供了一個非常好的shader模板,里面含有perspective/polar pano/ cylinder pano 的shader方法。打開節點的script能夠拿到當前polar全景的攝像機方法:
.................. else if (projection == "polar") { float xa = -PI*x; float ya = (0.5*PI)*y; float sx = sin(xa); float cx = cos(xa); float sy = sin(ya); float cy = cos(ya); P = 0; I = set(cx*cy, sy, sx*cy); } ...................
短短幾行,但是包含的內容實在太多了,我這里分別介紹一下不做太多擴展:
1:Houdini中camera shader的入口和出口
寫shader的都知道一定會有入口和出口的定義,攝像機shader也不例外。其中入口參數有x,y,Time 等等, 輸出端的參數則是P, I。具體對應什么攝像機的幫助文檔寫的比較詳細了,這里截下來比較關鍵的定義:
//float x – X screen coordinate in the range -1 to 1 // //float y – Y screen coordinate in the range -1 to 1 // //float Time – Sample time // //float dofx – X depth of field sample value // //float dofy – Y depth of field sample value // //float aspect – Image aspect ratio (x/y) // //export vector P – Ray origin in camera space // //export vector I – Ray direction in camera space // //export int valid – Whether the sample is valid for measuring
理解起來也不會太難,x,y都是攝像機橫軸縱軸的采樣點,是[-1,1]空間里給像素點定義的坐標系,P 設攝像機發射出射線的起始點位置,I 則是射線方向。
這里涉及到的問題就在 P = 0; 上。
2:球形坐標系(Spherical coordinates)與笛卡爾坐標系(Cartesian coordinates)之間的關系:
笛卡爾坐標系大家都熟悉,就是(x,y,z)三個軸向的數據確定空間的一個點。而球形坐標的參數則有點不一樣,我們拿地球做比,地球有經度與緯度,兩個度數就能確定地球球面的任何一個位置,准確來講是要加上地球半徑才真的定位到了球面上,只不過我們已經在球面上了也不會混淆說成地底下所以從來不會去碰地球半徑這個參數了。其實這就是球形坐標系的原型,緯度跨度有2π,經度跨度則是一個π。如下圖:
θ是緯度,φ是精度,ρ則是到原點的距離,由這三個數值我們就能建立球形坐標系在在笛卡爾坐標系中的表達了,另外考慮到houdini的攝像機空間是橫軸縱軸都是[-1,1]。所以可以得到上面代碼中的公式了:
x = cos(xa) * cos(ya)
y = sin(ya)
z = sin(xa) * cos(ya)
這些內容是為了理解攝像機的平面坐標到球形空間坐標的一個變換關系。如果還是覺得難以理解我把上面的方法直接通過vop運用到了一個grid上的每一個點上來觀察。其中grid是在xy平面上大小為2的正方形面板,反正我們這里先不考慮畫幅高寬的ratio。
grid上面的每一個點可以看成屏幕或者攝像機的每一個像素點,整個屏幕每個點投射出去的射線正好能組成一個圓球的所有方向,這就是polar panorama的奧秘了。
回到上面留下來的問題 P = 0, 這個等式直接就把射線的投射點固定在了一個位置上,所以我們只要改變它,使它隨着射線方向的變化而變化“位置”。
如圖,假如我們設定一個投射方向k:
那么兩只眼睛的連線必與射線k垂直,而PD則定義了我們人的瞳距。射線k我們知道那么正向和反向旋轉90度則能求出兩只眼睛在xz平面上的方向,最后乘以瞳距的一半便能求出眼睛在當前射線上的具體位置。
廢話了這么多基本上就是houdini 360全景雙眼渲染的方法了。再貼一下我在cvex里面實現的這個方法:
//float x – X screen coordinate in the range -1 to 1 // //float y – Y screen coordinate in the range -1 to 1 // //float Time – Sample time // //float dofx – X depth of field sample value // //float dofy – Y depth of field sample value // //float aspect – Image aspect ratio (x/y) // //export vector P – Ray origin in camera space // //export vector I – Ray direction in camera space // //export int valid – Whether the sample is valid for measuring #pragma hint x hidden #pragma hint y hidden #pragma hint Time hidden #pragma hint dofx hidden #pragma hint dofy hidden #pragma hint aspect hidden #pragma hint P hidden #pragma hint I hidden #pragma hint side oplist #pragma choice side 0 "right" #pragma choice side 1 "left" #pragma label offest "Pupil Distance" #include "math.h" cvex paronamaLens( // Inputs float x = 0; float y = 0; float Time = 0; float dofx = 0; float dofy = 0; float aspect = 1; float offest = 1; int side = 0; // Outputs export vector P = 0; export vector I = 0; ) { float halfPI = 0.5 * PI; float xa = -PI * x; float ya = halfPI * y; float sx = sin(xa); float cx = cos(xa); float sy = sin(ya); float cy = cos(ya); //correspondent position for eyes float px, pz, rotation; rotation = lerp(-halfPI, halfPI, side); px = cos(xa + rotation) * cos(ya); pz = sin(xa + rotation) * cos(ya); P = 0.5 * offest * set(px, 0 , pz); I = set(cx*cy, sy, sx*cy); }
最后我把視距拉大一點看看極端效果:
左眼:
右眼:
很好,四個方向都是真確的偏移。打完收工。