案例学习——Interior Mapping 室内映射(假室内效果)

Posted 清清!

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了案例学习——Interior Mapping 室内映射(假室内效果)相关的知识,希望对你有一定的参考价值。

最近油管推荐了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 ⃗ \\vecd d
以“时间” t t t为自变量,我们就可以得到射线 L ( t ) = P + t d ⃗ L(t)=P+t\\vecd 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 ⃗ \\vecd 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;

	
	// 这个地方说明一下,也就是预先处理了一下倒数
	// t=(1-p)/view=1/view-p/view
	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 ⃗ \\vecd 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);
	// t=(1-p)/view=1/view-p/view
	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);以上是关于案例学习——Interior Mapping 室内映射(假室内效果)的主要内容,如果未能解决你的问题,请参考以下文章

internal,interior和inner的区别?

案例27:室内消火栓系统检测与验收案例分析

案例27:室内消火栓系统检测与验收案例分析

案例35:室内消火栓系统检查与维护保养案例分析

案例27:室内消火栓系统检测与验收案例分析

shadow mapping实现动态shadow实现记录