最近油管推荐了Interior Mapping的教程,发现很有意思,发现各种资料都比较零散
于是到处搜集学习了一些资料,有了这篇文章汇总,大家一起学习学习
案例学习——Interior Mapping 室内映射(假室内效果)
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(轴对齐包围盒)的相交问题
先以2D为例子,对于给定的一个光线
我们可以分别求出它与竖直和水平面的交点,也就是分别在X轴和Y轴上求相交
我们取所有进入时间(
t
m
i
n
t_{min}
tmin)里的max,出去的时间
t
m
a
x
t_{max}
tmax里的min,
于是得到了进入和出去包围盒的
t
t
t 的值
对于3D盒子的处理也是一样,通用解法如下
假设出发点某个点,为
P
P
P点,视线的方向向量为
d
⃗
\vec{d}
d
以“时间”
t
t
t为自变量,我们就可以得到射线
L
(
t
)
=
P
+
t
d
⃗
L(t)=P+t\vec{d}
L(t)=P+td
对它与轴对齐包围盒
(
B
m
i
n
,
B
m
a
x
)
(B_{min},B_{max})
(Bmin,Bmax)做相交测试
一个包围盒有6个矩形面,把两个互相平行的矩形看成一块板,那么问题就转化为求射线与互相垂直的3块板的相交
以X轴的为例(如图b),我们可以得到在X轴上进入的时间
t
0
x
t_0x
t0x和
t
1
x
t_1x
t1x
Y和Z轴同理,然后在XYZ的
t
0
t_0
t0里取最大可以得到入点,在XYZ的
t
1
t_1
t1里取最小就可以得到出点了
在具体应用的时候,根据需要再小作调整,可以看下面代码的实现过程
另外,射线与各种形状的相交算法可以看看这个网页
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值
根据相机和当前位置,计算视线方向(也就是公式推导里的 d ⃗ \vec{d} 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)
- 在这里再回忆一下公式
因为我们是在窗户表面运行shader,所以推导中的 P P P就是代码中标准化后的 P o s Pos Pos
我们只要考虑光线打出去的点,也就是我们只需要知道 t 1 t_1 t1(代码中为 k k k)的值是多少
进行标准化后, b m a x = ( 1 , 1 , 1 ) b_{max}=(1,1,1) bmax=(1,1,1),也就不用乘了;而 d ⃗ \vec{d} d 就是 v i e w D i r viewDir viewDir - 因为三个轴的计算方法都是一样的,所以对于出点,我们可以得到代码中的
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值指定为-1float3 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)", 2D) = "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值存深度&手动指定深度,在图集中随机生成,以及加上窗框,菲涅尔效应,窗户污垢等等
另外也找到一些其他效果,比如这篇文章中,其用窗框的SDF图模拟光照效果
总之还有蛮有意思的一个案例(中间那段根据深度变换UV的代码思路,希望能有大神指教了,想不明白Orz)