案例學習——Interior Mapping 室內對映(假室內效果)
最近油管推薦了Interior Mapping的教程,發現很有意思
好像各種資料都比較零散,於是到處蒐集了一些,有了這篇文章彙總,一起學習學習
0 效果展示
因為有點長,所以先展示一些效果
以一張2d投影圖為基底,用alpha值存深度&手動指定深度,在圖集中隨機生成,以及加上窗框,
菲涅爾效應
,窗戶汙垢等等
幾個樣例效果展示(場景中只有基本幾何形狀,是沒有那些室內模型的)
深度的調整
1 背景介紹——虛假的窗戶
什麼是Interior Mapping?我們先從遊戲裡的窗戶開始說起
這是GTA IV的中的一個傢俱店
我們仔細看看,可以發現
它在美術上很好看,在視覺上的顏色啊搭配啊也很有趣,適合傢俱店的主題和位置,氛圍感也很好……只是有些不對勁。
透過窗戶,我們能看到的只是一張商店的圖片,就像玻璃上的貼紙一樣,直接拍到了窗玻璃上。 在轉角的不同部分上的各個視窗之間,並沒有透視差異。 即使相機與牆成一定角度,內部的視野也始終是朝向正面的。 這種透視缺失的效果大大削弱了氛圍感。
讓房間出現在窗戶後面,最簡單的方法就是用實際的模型填充每個房間。
這對小規模的場景來說是適用的,但對於大型遊戲顯然是不切實際的。
這些內部模型三角形面片所產生的消耗,對於大型遊戲來說實在太誇張了,尤其是我們在遊玩時往往只會偶爾看到少數幾個房間的一小部分。
那麼,如何來平衡效能和效果呢?答案正如最開始提到的,是Shader上的小Trick
2 怎麼模擬窗戶?
2。1 通用——視差對映(Parallax Mapping)
Shader是將幾何資訊作為輸入,一頓操作輸出顏色,我們唯一關心的只是最終輸出顏色在場景中看起來正確,中間發生的事情並不重要。
所以如果我們能偏移輸出顏色,使它看起來像輸入了幾何模型一樣偏移,不就達到目的了嘛。
如果輸出的偏移不均勻,則可以使渲染的影象看起來好像在某種程度上發生了歪斜,扭曲或變形。
怎麼偏移呢?我們會馬上想到這方面通用的偏移技術,
視差對映(Parallax Mapping)
。
下圖是現實生活中利用視差效果的典例,類似一種投影
這裡是之前學習LearnOpenGL上視差對映的筆記。
在使用視差對映時,我們輸入紋理座標,根據觀察者角度和每個畫素的“深度”值進行偏移。透過確定相機射線與表面高度相交的點,我們可以建立相當於3D投影出來的2D影象。
視差對映能實現室內效果嗎?雖然看起來非常適合我們的需求,也就是在2D平面表示3D效果的需求。
但作為通用解決方案的視差對映,在針對室內場景做的特定情況下使用時,似乎又不太行。
視差對映在較平滑的高度圖上效果最佳。 如果高度圖上紋素的高度差異過大會造成奇怪的視覺失真,有一些替代方法可以解決此問題(例如“陡峭視差對映”),但替代方法基本是迭代的,並且隨著深度與迭代次數之比的增加,會產生階梯狀效果。
除了迭代次數的消耗大,我們也發現窗戶的體積是在內部的,視差通常模擬的是表面的凹凸程度,而不是對於內部的模擬。
當通用解決方案失敗時,我們需要考慮可能滿足的簡單的特定解決方案。
2。2 特定——室內對映(Interior Mapping)
具體問題具體分析,我們要窗戶的shader如何進行偏移呢?
在我們的情況下,我們希望將矩形房間插入顯示到我們的窗戶中。
視差貼圖的通用性意味著必須使用迭代數值方法,因為沒有分析方法可以確定我們的相機射線在哪裡與表面高度相交。
如果我們僅將問題限制在矩形盒子的房間上,那麼,只需將房間體積內的相交點對映到紋理,然後輸出正確的顏色即可。
Joost van Dongen在2008年CGI會議上發表了一篇論文:Interior Mapping: A new technique for rendering realistic buildings,作為該技術的起源(這是作者提供的演示demo)。
和之前體繪製shader的思路很像,論文中,Interior Mapping考慮建築物本身不包含任何額外的幾何形狀,內部的體積僅是虛假的存在於著色器中。
其把建築網格的mesh劃分為很多“房間”,並對每個房間窗戶的紋理畫素進行raycast。 使用相機射線與房間box之間的交點處的座標,來取樣一組“房間紋理”。
以盒狀房間為例,一個房間有六個面,四面牆加天花板加地板,但我們只要考慮看得到的三個面就行了。 計算射線與這3個平面中每個平面的交點,如P‘,然後,我們使用交點P’作為
紋理座標
來查詢畫素P的顏色
於是,類似於視差貼圖,它偏移了輸入紋理的座標,給每個隱藏的“房間box”提供牆壁天花板和地板紋理的投影。
在不增加其他幾何形狀和材質複雜性的情況下,它較好的表示了內部空間。
其技術廣泛運用在如今的遊戲裡,像下面這些窗戶中“以假亂真”的房間
在《漫威蜘蛛俠》,角色在建築物上爬牆的演示影片中,似乎能“透過”玻璃看到建築內部 但其實在拐角處的時候能發現奇怪的地方
這是另一個《漫威蜘蛛俠》中的演示影片,也可以發現房間實際上並不存在於幾何體中,轉角的玻璃有一扇門,但那裡顯然應該有一個窗戶
在《七大罪》的技術分享中,演講者分享了一種稱之為FakeInterior的技術,技術人員用其來模擬室內的效果
在《守望先鋒》中玩家發現了一面神奇的窗戶
還有很多遊戲中的例子,儘管所有這些都表明,它們這些透過窗戶看到的房間是偽造的,但Interior Mapping的應用讓它們在透視上是完全正確的,並且具有真實的深度。
3 實現方式
3。1 物件空間/切線空間
在論文的實現中,內部的房間是在物件空間或世界空間中定義的。
這確實很容易直接使用,但其在帶有傾斜或彎曲牆壁的建築物上表現不太好
房間受到建築幾何形狀的限制,可能導致不平坦或房間截斷,就像下圖,論文作者的演示demo中的例子
在現實中,房間幾乎總是與建築物的外部對齊。
所以我們更願意讓所有的房間與mesh對齊,然後向內擠壓,向建築的中心延伸。
為了做到這一點,我們可以計算尋找一個替代的座標系,它與我們的表面一致,也就是我們可以到切線空間去做raycast計算。
即使世界空間是彎曲的,但是切線空間永遠是軸對齊的。
在切線空間計算後,我們的房間可以隨建築物的曲率而變化,並且始終具有與建築物外部平行的整面牆。
3。2 房間貼圖
對於房間的貼圖,論文中要求為牆壁、地板和天花板提供單獨的紋理。
這雖然能用,但很難操作。要保持這三種紋理的同步一致,將多個房間的紋理放在一起,是比較困難的。
於是人們也提出了一些其他的方法
3。2。1 立方體貼圖——《七大罪》
Zoe J Wood在 “Interior Mapping Meets Escher”中用立方體貼圖替代了原本的貼圖。
在《七大罪》的技術分享中,也使用了這個方式,他們室內用了cubeMap,然後加上一張窗戶的貼圖,以及提供深度調節景深
但立方體貼圖也意味著,人們基本不可能人為對貼圖進行繪製微調,這對藝術家構建多種的內部貼圖資產不太友好。
3。2。2 預投影2D貼圖——《模擬城市5》
《模擬城市5》的開發者Andrew Willmott在“From AAA to Indie: Graphics R&D”的演講中提到,他們在《模擬城市5》中為內部地圖使用了預先投影的內部紋理,這是當時的PPT。
這個方式是比較好的,具有很高的創作性,易於使用,並且展示的結果僅比完整的立方體貼圖差一點。
因為是基於一張投影圖做的對映,所以只有在該圖片原先渲染的角度才能獲得最完美的效果。 換到其他角度或者隨意改變深度對映什麼的,都會造成些微的扭曲和失真
而且它可以在每個建築物的基礎上構建大量的室內圖集,隨機選擇,達到僅使用一個
紋理資源
,建築物就可以保持具有隨機變化的內部場景風格。
只能說和cubemap的方法各有利弊吧
3。2。3 預投影2D貼圖——《極限競速:地平線4》
同樣的,Playground Games的技術美術總監Gareth Harwood在The Gamasutra Deep Dives的一篇訪談中也提到,《極限競速:地平線4》中也使用了預投影2D貼圖來實現室內對映,製作街景的窗戶
訪談提到有幾個重要部分:
窗戶+窗框+內部形成三層紋理;地圖集的使用;夜間與白天的紋理切換;小角度的處理;內部紋理
建模
的注意事項;規則化擺放內部紋理;效能最佳化……
下面摘錄一些訪談的內容,比較具有參考意義,翻譯了一下可以瞭解瞭解
建立逼真的世界一直是《極限競速:地平線》系列遊戲的一項優勢,而我們實現這一目標的一個方面就是在建築物中增加室內裝飾。
在《極限競速:地平線4》中,我們知道愛丁堡是如此的密集和細緻,以至於物理建模的內部空間將超出我們的預算,因此我們研究了另一種稱為
視差紋理
的技術。
這給我們帶來了巨大的優勢,可以創造出一種內飾,因為將其烘焙成一種紋理,而不受多邊形數或材料複雜性的限制。 由於渲染這些物體比建立幾何體便宜,因此我們可以在遊戲中擁有更多的內飾。 以前,由於預算問題可能需要關閉窗簾或讓窗戶變暗,現在我們有了完整的視差內部。
視差內部材料由三層組成,藝術家可以為每個視窗獨立選擇這些層。 由於每個圖層都有幾個不同的選項,因此即使僅使用少量的
Atlas紋理
,這也為每個視窗提供了數百種可能的視覺效果。
第一層很簡單,包含窗框和玻璃。 這增加了細微差別,例如窗玻璃的細節或窗戶上的窗框,這在較舊的英國建築中尤為突出。該層具有漫反射,阿爾法,粗糙度,金屬性和法線。 法線透過在每個玻璃窗格中包含變化,以增加玻璃反射角的真實變化,這在我們的古董窗格中更為明顯。
第二層是窗簾或百葉窗。 這些具有漫反射和Alpha,但還具有透射紋理,該紋理在晚上用於顯示窗簾的厚度。 我們有不讓光透過的厚實的窗簾,半透明的百葉窗,甚至是華麗的窗簾,我們用花邊部分製成的窗簾允許一些光線透過。
最後一層是著色器產生神奇效果的地方,因為這是模擬3D內部的平面紋理。
首先使用統一的比例尺以3D模型對內部進行建模,然後將其渲染為室內對映著色器支援的獨特紋理格式。 像其他圖層一樣,我們建立了地圖集,該地圖集包含多達八個相同樣式的不同內部裝飾,以減少繪製呼叫。
我們有一些農村地區的地圖集,它們在農村家庭中共享,城鎮房屋的地圖集以及我們的商店,飯店和典型的英式酒吧中的一些商業地圖集。 我們也有兩個主要的內部深度。 一種是用於標準間,另一種是用於淺窗陳列。 當對每個視窗設定材質屬性時,藝術家可以為建築物的每個視窗選擇所需的地圖集的一部分。
著色器計算了您在房間中看到的角度,並將調整UV,以便您只能看到從您的視角可見的內部區域。 該技術有一些我們要解決的侷限性。
首先,著色器的trick在非常淺的角度變得明顯。在這些邊緣情況下,我們增加了玻璃的
自然菲涅爾效果
,以顯示比室內更多的反射影象。
其次,隨著角度的增加,房間中央的細節開始彎曲。我們透過將興趣吸引到牆壁和角落以及使地板變暗來減少這種情況。 與現實生活中一樣,透過使線條匯聚到達某個點,可以幫助實現視差效果。
在我們的影象中,我們儘可能地使用了平行線來進行遊戲:平鋪,書架,木地板和帶圖案的牆紙。 每個內部紋理在夜間也具有輔助光紋理,再次烘焙紋理的好處是可以根據需要使用盡可能多的光,因為它們最終都將被合併為一個紋理。 我們為每個建築物都有開和關的時間,以增加開燈時的變化。
藝術家可以自由選擇他們喜歡的任何型別的窗戶,窗簾和室內組合。 但是他們遵循一些簡單的規則:臥室通常出現在較高的樓層,我們不會在每個窗戶上重複相同的房間,也不會在樓梯上放上上升的樓梯頂樓。
我們還研究了每個房間的可行性。 例如,我們不可能有一個小型的兩居室小屋,其中有十個或更多的窗戶可以看到不同的房間,因此藝術家設定了可以看到同一房間不同部分的窗戶。
最後,出於效能原因;當玩家離開建築物時,我們不僅需要降低網格的複雜性,而且還需要降低著色器和材質的複雜性。 為此,我們淡化了
視差效果
,並用平面圖像替換了視窗,該影象是一張具有視窗,窗簾和內部的圖片。 當視窗在螢幕上很小且播放器不注意到時,就會發生這種情況。
關於《極限競速:地平線4》所用到的高畫質圖,可以去製作者的A站這個連結下載
4 動手試試
實現上,借用了Unity商城裡的免費資源Fake Interiors FREE裡面的模型和部分貼圖,並參考了其中的
shader結構
,以及參考了Unity論壇的討論,colin大大的實現
核心的思想就是,如何取樣虛擬的房間
因為我們是在窗戶(朝著我們的這個面上)執行shader,那麼從窗戶看進去的一根光線會打中後面虛擬房間box的哪個點呢?
也就是光線與AABB(軸對齊包圍盒)的相交問題
一個包圍盒有6個矩形面,把兩個互相平行的矩形看成一塊板,那麼問題就轉化為求射線與互相垂直的3塊板的相交
另外,射線與各種形狀的相交演算法可以看看這個網頁
4。1 立方體貼圖
首先實現一個立方體貼圖的方法 比如用這麼個cubemap
4。1。1 ObjectSpace的方法
我們先不管切線空間,先從在object空間實現的方法開始, 為避免分散注意,先只展示shader的主幹思想
v2f
vert
(
appdata
v
)
{
v2f
o
;
o
。
pos
=
UnityObjectToClipPos
(
v
。
vertex
);
// slight scaling adjustment to work around “noisy wall”
// when frac() returns a 0 on surface
o
。
uvw
=
v
。
vertex
*
_RoomCube_ST
。
xyx
*
0。999
+
_RoomCube_ST
。
zwz
;
// get object space camera vector
float4
objCam
=
mul
(
unity_WorldToObject
,
float4
(
_WorldSpaceCameraPos
,
1。0
));
o
。
viewDir
=
v
。
vertex
。
xyz
-
objCam
。
xyz
;
// adjust for tiling
o
。
viewDir
*=
_RoomCube_ST
。
xyx
;
return
o
;
}
o。UVW也就是該畫素在模型空間的位置,我們之後會用來取樣 這裡配合tilling的影響加上了ST的引數,以及為了避免極值,乘了一下0。999,調整一下UVW值 根據相機和當前位置,計算視線方向(也就是公式推導裡的$\vec{d}$)
相機位置,永遠是在平面的上面,可以看到平面的正面(反面就被剔除了)
fixed4
frag
(
v2f
i
)
:
SV_Target
{
// room uvws
float3
roomUVW
=
frac
(
i
。
uvw
);
// raytrace box from object view dir
// transform object space uvw( min max corner = (0,0,0) & (+1,+1,+1))
// to normalized box space(min max corner = (-1,-1,-1) & (+1,+1,+1))
float3
pos
=
roomUVW
*
2。0
-
1。0
;
// for axis aligned box Intersection,we need to know the zoom level
float3
id
=
1。0
/
i
。
viewDir
;
// k means normalized box space depth hit per x/y/z plane seperated
// (we dont care about near hit result here, we only want far hit result)
float3
k
=
abs
(
id
)
-
pos
*
id
;
// kmin = normalized box space real hit ray length
float
kMin
=
min
(
min
(
k
。
x
,
k
。
y
),
k
。
z
);
// normalized box Space real hit pos = rayOrigin + kmin * rayDir。
pos
+=
kMin
*
i
。
viewDir
;
// randomly flip & rotate cube map for some variety
float3
flooredUV
=
floor
(
i
。
uvw
);
float3
r
=
rand3
(
flooredUV
。
x
+
flooredUV
。
y
+
flooredUV
。
z
);
float2
cubeflip
=
floor
(
r
。
xy
*
2。0
)
*
2。0
-
1。0
;
pos
。
xz
*=
cubeflip
;
pos
。
xz
=
r
。
z
>
0。5
?
pos
。
xz
:
pos
。
zx
;
// sample room cube map
fixed4
room
=
texCUBE
(
_RoomCube
,
pos
。
xyz
);
return
fixed4
(
room
。
rgb
,
1。0
);
}
roomUVW = frac(i。uvw); 擷取小數部分當做取樣的UV值
對虛擬房間進行標準化,原本(0,0,0) ~ (+1,+1,+1)的UVW進行*2-1之後,變為(-1,-1,-1) ~ (+1,+1,+1)
在這裡再回憶一下公式
因為三個軸的計算方法都是一樣的,所以對於出點,我們可以得到程式碼中的 float kMin = min(min(k。x, k。y), k。z);
然後再根據射線公式,我們就能得到交點的位置,(後面做了一些隨機旋轉和選擇的操作,可以不管)然後就可以用它來取樣CubeMap了
可以發現,能在內部看到box
但是對於ObjectSpace的方法,內部房間只能嚴格按照軸對齊排列,在曲面上顯得很奇怪
4。1。2 TangentSpace的方法
為了讓box在曲面也能表現良好,我們到切線空間中進行求交,程式碼整體上差不多
v2f
vert
(
appdata
v
)
{
v2f
o
;
o
。
pos
=
UnityObjectToClipPos
(
v
。
vertex
);
// uvs
o
。
uv
=
TRANSFORM_TEX
(
v
。
uv
,
_RoomCube
);
// get tangent space camera vector
float4
objCam
=
mul
(
unity_WorldToObject
,
float4
(
_WorldSpaceCameraPos
,
1。0
));
float3
viewDir
=
v
。
vertex
。
xyz
-
objCam
。
xyz
;
float
tangentSign
=
v
。
tangent
。
w
*
unity_WorldTransformParams
。
w
;
float3
bitangent
=
cross
(
v
。
normal
。
xyz
,
v
。
tangent
。
xyz
)
*
tangentSign
;
o
。
viewDir
=
float3
(
dot
(
viewDir
,
v
。
tangent
。
xyz
),
dot
(
viewDir
,
bitangent
),
dot
(
viewDir
,
v
。
normal
)
);
// adjust for tiling
o
。
viewDir
*=
_RoomCube_ST
。
xyx
;
return
o
;
}
TRANSFORM_TEX方法就是將模型頂點的uv和Tiling、Offset兩個變數進行運算,計算出實際顯示用的uv
切線空間的轉換,做一下視線乘以TBN矩陣就可以
fixed4
frag
(
v2f
i
)
:
SV_Target
{
// room uvs
float2
roomUV
=
frac
(
i
。
uv
);
// raytrace box from tangent view dir
float3
pos
=
float3
(
roomUV
*
2。0
-
1。0
,
1。0
);
float3
id
=
1。0
/
i
。
viewDir
;
float3
k
=
abs
(
id
)
-
pos
*
id
;
float
kMin
=
min
(
min
(
k
。
x
,
k
。
y
),
k
。
z
);
pos
+=
kMin
*
i
。
viewDir
;
// randomly flip & rotate cube map for some variety
float2
flooredUV
=
floor
(
i
。
uv
);
float3
r
=
rand3
(
flooredUV
。
x
+
1。0
+
flooredUV
。
y
*
(
flooredUV
。
x
+
1
));
float2
cubeflip
=
floor
(
r
。
xy
*
2。0
)
*
2。0
-
1。0
;
pos
。
xz
*=
cubeflip
;
pos
。
xz
=
r
。
z
>
0。5
?
pos
。
xz
:
pos
。
zx
;
#endif
// sample room cube map
fixed4
room
=
texCUBE
(
_RoomCube
,
pos
。
xyz
);
return
fixed4
(
room
。
rgb
,
1。0
);
}
片元著色器大同小異,基本一樣 在定義pos的時候,讓Z=1(Z也就是TBN裡的N),朝內取樣 float3 pos = float3(roomUV * 2。0 - 1。0, 1。0);
得到效果,在曲面上表現良好
4。1。3 深度
深度如何實現呢?
我們指定一個深度值,然後在片元著色器前面加上深度對映的程式碼,我們把深度乘上視線的Z
// Specify depth manually
fixed
farFrac
=
_RoomDepth
;
//remap [0,1] to [+inf,0]
//->if input _RoomDepth = 0 -> depthScale = 0 (inf depth room)
//->if input _RoomDepth = 0。5 -> depthScale = 1
//->if input _RoomDepth = 1 -> depthScale = +inf (0 volume room)
float
depthScale
=
1。0
/
(
1。0
-
farFrac
)
-
1。0
;
i
。
viewDir
。
z
*=
depthScale
;
我們如果在原本的基礎上放大了視線Z的倍率,也就是
視線向量
更加朝著遠處走了,取樣的時候就好像更深了
上面的做法是根據z移動的距離來進行在取樣上的縮放,會導致四周的貼圖失真
也可以用偏移來表達房間的深度,也就是整體前後移動,由此造成能偏移的深度就很有限了,只能是box的深度
可以看這位大大的實現
4。1。4 程式碼
整合起來就是這樣
// Upgrade NOTE: replaced ‘mul(UNITY_MATRIX_MVP,*)’ with ‘UnityObjectToClipPos(*)’
Shader
“MyShaders/InteriorMapping_CubeMap”
{
Properties
{
_RoomCube
(
“Room Cube Map”
,
Cube
)
=
“white”
{}
[
Toggle
(
_USEOBJECTSPACE
)]
_UseObjectSpace
(
“Use Object Space”
,
Float
)
=
0。0
_RoomDepth
(
“Room Depth”
,
range
(
0。001
,
0。999
))
=
0。5
}
SubShader
{
Tags
{
“RenderType”
=
“Opaque”
}
LOD
100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma shader_feature _USEOBJECTSPACE
#include
“UnityCG。cginc”
struct
appdata
{
float4
vertex
:
POSITION
;
float2
uv
:
TEXCOORD0
;
float3
normal
:
NORMAL
;
float4
tangent
:
TANGENT
;
};
struct
v2f
{
float4
pos
:
SV_POSITION
;
#ifdef _USEOBJECTSPACE
float3
uvw
:
TEXCOORD0
;
#else
float2
uv
:
TEXCOORD0
;
#endif
float3
viewDir
:
TEXCOORD1
;
};
samplerCUBE
_RoomCube
;
float4
_RoomCube_ST
;
float
_RoomDepth
;
// psuedo random 偽隨機
float3
rand3
(
float
co
)
{
return
frac
(
sin
(
co
*
float3
(
12。9898
,
78。233
,
43。2316
))
*
43758。5453
);
}
v2f
vert
(
appdata
v
)
{
v2f
o
;
o
。
pos
=
UnityObjectToClipPos
(
v
。
vertex
);
#ifdef _USEOBJECTSPACE
// slight scaling adjustment to work around “noisy wall” when frac() returns a 0 on surface
o
。
uvw
=
v
。
vertex
*
_RoomCube_ST
。
xyx
*
0。999
+
_RoomCube_ST
。
zwz
;
// get object space camera vector
float4
objCam
=
mul
(
unity_WorldToObject
,
float4
(
_WorldSpaceCameraPos
,
1。0
));
o
。
viewDir
=
v
。
vertex
。
xyz
-
objCam
。
xyz
;
// adjust for tiling
o
。
viewDir
*=
_RoomCube_ST
。
xyx
;
#else
// uvs
o
。
uv
=
TRANSFORM_TEX
(
v
。
uv
,
_RoomCube
);
// get tangent space camera vector
float4
objCam
=
mul
(
unity_WorldToObject
,
float4
(
_WorldSpaceCameraPos
,
1。0
));
float3
viewDir
=
v
。
vertex
。
xyz
-
objCam
。
xyz
;
float
tangentSign
=
v
。
tangent
。
w
*
unity_WorldTransformParams
。
w
;
float3
bitangent
=
cross
(
v
。
normal
。
xyz
,
v
。
tangent
。
xyz
)
*
tangentSign
;
o
。
viewDir
=
float3
(
dot
(
viewDir
,
v
。
tangent
。
xyz
),
dot
(
viewDir
,
bitangent
),
dot
(
viewDir
,
v
。
normal
)
);
// adjust for tiling
o
。
viewDir
*=
_RoomCube_ST
。
xyx
;
#endif
return
o
;
}
fixed4
frag
(
v2f
i
)
:
SV_Target
{
// Specify depth manually
fixed
farFrac
=
_RoomDepth
;
//remap [0,1] to [+inf,0]
//->if input _RoomDepth = 0 -> depthScale = 0 (inf depth room)
//->if input _RoomDepth = 0。5 -> depthScale = 1
//->if input _RoomDepth = 1 -> depthScale = +inf (0 volume room)
float
depthScale
=
1。0
/
(
1。0
-
farFrac
)
-
1。0
;
i
。
viewDir
。
z
*=
depthScale
;
#ifdef _USEOBJECTSPACE
// room uvws
float3
roomUVW
=
frac
(
i
。
uvw
);
// raytrace box from object view dir
// transform object space uvw( min max corner = (0,0,0) & (+1,+1,+1))
// to normalized box space(min max corner = (-1,-1,-1) & (+1,+1,+1))
float3
pos
=
roomUVW
*
2。0
-
1。0
;
// for axis aligned box Intersection,we need to know the zoom level
float3
id
=
1。0
/
i
。
viewDir
;
// k means normalized box space depth hit per x/y/z plane seperated
// (we dont care about near hit result here, we only want far hit result)
float3
k
=
abs
(
id
)
-
pos
*
id
;
// kmin = normalized box space real hit ray length
float
kMin
=
min
(
min
(
k
。
x
,
k
。
y
),
k
。
z
);
// normalized box Space real hit pos = rayOrigin + kmin * rayDir。
pos
+=
kMin
*
i
。
viewDir
;
// randomly flip & rotate cube map for some variety
float3
flooredUV
=
floor
(
i
。
uvw
);
float3
r
=
rand3
(
flooredUV
。
x
+
flooredUV
。
y
+
flooredUV
。
z
);
float2
cubeflip
=
floor
(
r
。
xy
*
2。0
)
*
2。0
-
1。0
;
pos
。
xz
*=
cubeflip
;
pos
。
xz
=
r
。
z
>
0。5
?
pos
。
xz
:
pos
。
zx
;
#else
// room uvs
float2
roomUV
=
frac
(
i
。
uv
);
// raytrace box from tangent view dir
float3
pos
=
float3
(
roomUV
*
2。0
-
1。0
,
1。0
);
float3
id
=
1。0
/
i
。
viewDir
;
float3
k
=
abs
(
id
)
-
pos
*
id
;
float
kMin
=
min
(
min
(
k
。
x
,
k
。
y
),
k
。
z
);
pos
+=
kMin
*
i
。
viewDir
;
// randomly flip & rotate cube map for some variety
float2
flooredUV
=
floor
(
i
。
uv
);
float3
r
=
rand3
(
flooredUV
。
x
+
1。0
+
flooredUV
。
y
*
(
flooredUV
。
x
+
1
));
float2
cubeflip
=
floor
(
r
。
xy
*
2。0
)
*
2。0
-
1。0
;
pos
。
xz
*=
cubeflip
;
pos
。
xz
=
r
。
z
>
0。5
?
pos
。
xz
:
pos
。
zx
;
#endif
// sample room cube map
fixed4
room
=
texCUBE
(
_RoomCube
,
pos
。
xyz
);
return
fixed4
(
room
。
rgb
,
1。0
);
}
ENDCG
}
}
}
4。2 預投影2d貼圖
對於立方體形狀的房間,若後壁的大小為可見瓷磚的1/2
此時如果將它們渲染出來,要使用
水平FOV
為53。13度從開口向後的攝像機。
我們於是可以規定這種情況下的深度單位為
標準深度
,也就是_RoomDepth = 0。5 → depthScale = 1
比如下面這個圖,就是用這種情況下渲染出來的
房間深度
可以儲存在圖集紋理的
Alpha通道
中(標準深度也就是alpha通道為128)
也可以做很多圖集
當然也可以不管alpha通道,手動來自己調整深度,這兩種指定方式程式碼會說明
也可以去地平線4製作者的A站連結下載圖集
我們的整體思路不變,先在軸對齊包圍盒中求交點,然後我們要把原本要取樣cubemap的三維空間的交點,對映到2d圖片的UV上,取樣預投影2d圖片
4。2。1 實現
頂點著色器功能和之前一樣,圍繞切線空間進行
v2f
vert
(
appdata
v
)
{
v2f
o
;
o
。
pos
=
UnityObjectToClipPos
(
v
。
vertex
);
o
。
uv
=
TRANSFORM_TEX
(
v
。
uv
,
_RoomTex
);
// get tangent space camera vector
float4
objCam
=
mul
(
unity_WorldToObject
,
float4
(
_WorldSpaceCameraPos
,
1。0
));
float3
viewDir
=
v
。
vertex
。
xyz
-
objCam
。
xyz
;
float
tangentSign
=
v
。
tangent
。
w
*
unity_WorldTransformParams
。
w
;
float3
bitangent
=
cross
(
v
。
normal
。
xyz
,
v
。
tangent
。
xyz
)
*
tangentSign
;
o
。
tangentViewDir
=
float3
(
dot
(
viewDir
,
v
。
tangent
。
xyz
),
dot
(
viewDir
,
bitangent
),
dot
(
viewDir
,
v
。
normal
)
);
o
。
tangentViewDir
*=
_RoomTex_ST
。
xyx
;
return
o
;
}
片元著色器程式碼如下
// psuedo random
float2
rand2
(
float
co
)
{
return
frac
(
sin
(
co
*
float2
(
12。9898
,
78。233
))
*
43758。5453
);
}
fixed4
frag
(
v2f
i
)
:
SV_Target
{
// room uvs
float2
roomUV
=
frac
(
i
。
uv
);
float2
roomIndexUV
=
floor
(
i
。
uv
);
// randomize the room
float2
n
=
floor
(
rand2
(
roomIndexUV
。
x
+
roomIndexUV
。
y
*
(
roomIndexUV
。
x
+
1
))
*
_Rooms
。
xy
);
//float2 n = floor(_Rooms。xy);
roomIndexUV
+=
n
;
// get room depth from room atlas alpha
// fixed farFrac = tex2D(_RoomTex, (roomIndexUV + 0。5) / _Rooms)。a;
// Specify depth manually
fixed
farFrac
=
_RoomDepth
;
//remap [0,1] to [+inf,0]
//->if input _RoomDepth = 0 -> depthScale = 0 (inf depth room)
//->if input _RoomDepth = 0。5 -> depthScale = 1
//->if input _RoomDepth = 1 -> depthScale = +inf (0 volume room)
float
depthScale
=
1。0
/
(
1。0
-
farFrac
)
-
1。0
;
// raytrace box from view dir
// normalized box space‘s ray start pos is on trinagle surface, where z = -1
float3
pos
=
float3
(
roomUV
*
2
-
1
,
-
1
);
// transform input ray dir from tangent space to normalized box space
i
。
tangentViewDir
。
z
*=
-
depthScale
;
float3
id
=
1。0
/
i
。
tangentViewDir
;
float3
k
=
abs
(
id
)
-
pos
*
id
;
float
kMin
=
min
(
min
(
k
。
x
,
k
。
y
),
k
。
z
);
pos
+=
kMin
*
i
。
tangentViewDir
;
// remap from [-1,1] to [0,1] room depth
float
interp
=
pos
。
z
*
0。5
+
0。5
;
// account for perspective in “room” textures
// assumes camera with an fov of 53。13 degrees (atan(0。5))
// visual result = transform nonlinear depth back to linear
float
realZ
=
saturate
(
interp
)
/
depthScale
+
1
;
interp
=
1。0
-
(
1。0
/
realZ
);
interp
*=
depthScale
+
1。0
;
// iterpolate from wall back to near wall
float2
interiorUV
=
pos
。
xy
*
lerp
(
1。0
,
farFrac
,
interp
);
interiorUV
=
interiorUV
*
0。5
+
0。5
;
// sample room atlas texture
fixed4
room
=
tex2D
(
_RoomTex
,
(
roomIndexUV
+
interiorUV
。
xy
)
/
_Rooms
);
return
fixed4
(
room
。
rgb
,
1。0
);
}
選擇房間UV和隨機的部分很正常
深度值可以從圖的alpha通道里獲得也可以手動指定,指定depthScale時,把0 ~ 1的深度輸入對映到0 ~ +inf
從貼圖中獲取深度的程式碼也就是註釋的那一小段
// fixed farFrac = tex2D(_RoomTex, (roomIndexUV + 0。5) / _Rooms)。a;
因為背面剔除的關係,能看到的相機永遠在平面的正面,所以視線方向永遠是指向平面內的
之前cubemap的切線空間裡,我們pos的Z值指定為1,該平面位於標準化正方體的頂面,所以視線方向不用調整
現在我們把pos的Z值指定為-1
float3 pos = float3(roomUV * 2 - 1, -1);
該平面位於標準化正方體的底面 所以要把
i。tangentViewDir。z *= -depthScale;
,把視線的Z反轉一下
那為什麼這次要把pos的Z指定為-1呢?這和平面到立體的對映有關,也就是這一段程式碼做的事情
// remap from [-1,1] to [0,1] room depth
float
interp
=
pos
。
z
*
0。5
+
0。5
;
// account for perspective in “room” textures
// assumes camera with an fov of 53。13 degrees (atan(0。5))
// visual result = transform nonlinear depth back to linear
float
realZ
=
saturate
(
interp
)
/
depthScale
+
1
;
interp
=
1。0
-
(
1。0
/
realZ
);
interp
*=
depthScale
+
1。0
;
// iterpolate from wall back to near wall
float2
interiorUV
=
pos
。
xy
*
lerp
(
1。0
,
farFrac
,
interp
);
interiorUV
=
interiorUV
*
0。5
+
0。5
;
因為有參考論壇的程式碼實現,但不理解,於是畫圖走了一遍程式碼,把程式碼每一步的效果都標了出來,看看它做了什麼 (從結果倒回去推了半天,依然不甚明白是怎麼得到這樣做的方法,只是懂個意境了)
結果如圖
4。2。2 程式碼
該部分著色器如下
// Upgrade NOTE: replaced ’mul(UNITY_MATRIX_MVP,*)‘ with ’UnityObjectToClipPos(*)‘
Shader
“MyShaders/InteriorMapping_2D”
{
Properties
{
_RoomTex
(
“Room Atlas RGB (A - back wall fraction)”
,
2
D
)
=
“white”
{}
_Rooms
(
“Room Atlas Rows&Cols (XY)”
,
Vector
)
=
(
1
,
1
,
0
,
0
)
_RoomDepth
(
“Room Depth”
,
range
(
0。001
,
0。999
))
=
0。5
}
SubShader
{
Tags
{
“RenderType”
=
“Opaque”
}
LOD
100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include
“UnityCG。cginc”
struct
appdata
{
float4
vertex
:
POSITION
;
float2
uv
:
TEXCOORD0
;
float3
normal
:
NORMAL
;
float4
tangent
:
TANGENT
;
};
struct
v2f
{
float4
pos
:
SV_POSITION
;
float2
uv
:
TEXCOORD0
;
float3
tangentViewDir
:
TEXCOORD1
;
};
sampler2D
_RoomTex
;
float4
_RoomTex_ST
;
float2
_Rooms
;
float
_RoomDepth
;
v2f
vert
(
appdata
v
)
{
v2f
o
;
o
。
pos
=
UnityObjectToClipPos
(
v
。
vertex
);
o
。
uv
=
TRANSFORM_TEX
(
v
。
uv
,
_RoomTex
);
// get tangent space camera vector
float4
objCam
=
mul
(
unity_WorldToObject
,
float4
(
_WorldSpaceCameraPos
,
1。0
));
float3
viewDir
=
v
。
vertex
。
xyz
-
objCam
。
xyz
;
float
tangentSign
=
v
。
tangent
。
w
*
unity_WorldTransformParams
。
w
;
float3
bitangent
=
cross
(
v
。
normal
。
xyz
,
v
。
tangent
。
xyz
)
*
tangentSign
;
o
。
tangentViewDir
=
float3
(
dot
(
viewDir
,
v
。
tangent
。
xyz
),
dot
(
viewDir
,
bitangent
),
dot
(
viewDir
,
v
。
normal
)
);
o
。
tangentViewDir
*=
_RoomTex_ST
。
xyx
;
return
o
;
}
// psuedo random
float2
rand2
(
float
co
)
{
return
frac
(
sin
(
co
*
float2
(
12。9898
,
78。233
))
*
43758。5453
);
}
fixed4
frag
(
v2f
i
)
:
SV_Target
{
// room uvs
float2
roomUV
=
frac
(
i
。
uv
);
float2
roomIndexUV
=
floor
(
i
。
uv
);
// randomize the room
float2
n
=
floor
(
rand2
(
roomIndexUV
。
x
+
roomIndexUV
。
y
*
(
roomIndexUV
。
x
+
1
))
*
_Rooms
。
xy
);
//float2 n = floor(_Rooms。xy);
roomIndexUV
+=
n
;
// get room depth from room atlas alpha
// fixed farFrac = tex2D(_RoomTex, (roomIndexUV + 0。5) / _Rooms)。a;
// Specify depth manually
fixed
farFrac
=
_RoomDepth
;
//remap [0,1] to [+inf,0]
//->if input _RoomDepth = 0 -> depthScale = 0 (inf depth room)
//->if input _RoomDepth = 0。5 -> depthScale = 1
//->if input _RoomDepth = 1 -> depthScale = +inf (0 volume room)
float
depthScale
=
1。0
/
(
1。0
-
farFrac
)
-
1。0
;
// raytrace box from view dir
// normalized box space’s ray start pos is on trinagle surface, where z = -1
float3
pos
=
float3
(
roomUV
*
2
-
1
,
-
1
);
// transform input ray dir from tangent space to normalized box space
i
。
tangentViewDir
。
z
*=
-
depthScale
;
float3
id
=
1。0
/
i
。
tangentViewDir
;
float3
k
=
abs
(
id
)
-
pos
*
id
;
float
kMin
=
min
(
min
(
k
。
x
,
k
。
y
),
k
。
z
);
pos
+=
kMin
*
i
。
tangentViewDir
;
// remap from [-1,1] to [0,1] room depth
float
interp
=
pos
。
z
*
0。5
+
0。5
;
// account for perspective in “room” textures
// assumes camera with an fov of 53。13 degrees (atan(0。5))
// visual result = transform nonlinear depth back to linear
float
realZ
=
saturate
(
interp
)
/
depthScale
+
1
;
interp
=
1。0
-
(
1。0
/
realZ
);
interp
*=
depthScale
+
1。0
;
// iterpolate from wall back to near wall
float2
interiorUV
=
pos
。
xy
*
lerp
(
1。0
,
farFrac
,
interp
);
interiorUV
=
interiorUV
*
0。5
+
0。5
;
// sample room atlas texture
fixed4
room
=
tex2D
(
_RoomTex
,
(
roomIndexUV
+
interiorUV
。
xy
)
/
_Rooms
);
return
room
;
}
ENDCG
}
}
FallBack
“Diffuse”
}
5 豐富效果
基礎技術就如上所示,自行拓展一下可以得到一些結果
下圖用2d投影圖為基底,用alpha值存深度&手動指定深度,在圖集中隨機生成,以及加上窗框,菲涅爾效應,窗戶汙垢等等
另外也找到一些其他效果,比如這篇文章(Interior Mapping – Part 3)中,其用窗框的SDF圖模擬光照效果
總之還有蠻有意思的一個案例(中間那段根據深度變換UV的程式碼思路,希望能有大神指教了,想不明白Orz)