这篇文章取代并更新了以前的文章名“镜面反射立方体贴图的提示、技巧和指南”,其信息包含在SIGGRAPHE 2012演讲“基于局部图像的照明和视差校正立方体贴图”中。
基于图像的照明(IBL)在今天的游戏中很常见,可以模拟环境照明,它与基于物理的渲染(PBR)非常匹配。cubemap参数化由于其硬件效率而成为主要用途,这篇文章主要关注cubemap的环境镜面照明。在游戏中有许多不同的镜面反射立方体贴图的用法,这篇文章将描述其中的大部分,并包括一种新方法的描述,该方法使用基于局部图像的照明来模拟环境镜面反射照明。即使这篇文章的大部分内容都是专门为这种新方法设置的工具,它们也可以很容易地在其他上下文中重用。这篇文章将首先描述环境镜面照明的不同IBL策略,然后给出一些节省镜面立方体贴图内存的技巧。其次,它将详细介绍从兴趣点(POI)收集最近立方体贴图的算法,计算每个立方体贴图的贡献,并在GPU上有效混合这些立方体贴图。它将通过几种算法来完成对立方体贴图的视差校正,并通过视差立方体贴图方法应用于局部IBL。我使用术语“镜面反射立方体贴图”来指代经典镜面反射立方体贴图和预过滤mipmapped radiance环境贴图。一如既往,欢迎任何反馈或意见。
环境镜面照明的IBL策略
在本节中,我将讨论用于环境镜面照明的立方体贴图的几种策略。选择正确的方法取决于游戏环境和引擎架构。为清楚起见,我需要介绍一些定义:
我将立方体贴图分为两类:
–无限立方体贴图:这些立方体贴图用于表示无限远的照明,它们没有位置。它们可以通过游戏引擎生成,也可以手工编写。它们非常适合表示低频照明场景,如室外照明(即光线在水平面上相当平滑)。
–局部立方体贴图:这些立方体贴图具有位置并表示有限的环境照明。它们大多是基于关卡中的一个示例位置使用游戏引擎生成的。生成的照明仅位于生成立方体贴图的位置,所有其他位置必须是近似位置。此外,由于立方体贴图定义为一个无限大的长方体,因此存在视差问题(反射的对象不在正确的位置),需要对其进行补偿。用于室内照明等中高频照明场景。匹配场景照明条件所需的局部立方体贴图数量随着照明复杂性的增加而增加(即,如果有许多不同的灯光影响场景,则需要在多个位置对照明进行采样,以便能够模拟原始照明条件)。
由于我们经常需要混合多个立方体贴图,我将定义不同的立方体贴图混合方法:
–在主着色器中采样K个立方体贴图并进行加权和。昂贵的
–在CPU上混合立方体贴图,并在着色器中使用生成的立方体贴图。昂贵取决于分辨率和避免GPU暂停所需的双缓冲资源。
–在GPU上混合立方体贴图,并在着色器中使用生成的立方体贴图。快速的
–仅适用于延迟或轻型预分级发动机:通过加权添加剂混合应用K立方贴图。将每个立方体贴图边界体积渲染到屏幕,并使用G-Buffer中的法线+粗糙度对立方体贴图进行采样。
在下面描述的所有策略中,我将不讨论视觉逼真度,而是讨论问题和优势。
基于对象的立方体映射
每个对象都链接到一个局部立方体贴图。对象获取放置在标高中最近的立方体贴图,并将其用作镜面反射环境光源。这是半衰期2[1]为其世界镜面照明所采用的方式。
后台对象将脱机链接到最近的立方体贴图,动态对象将在运行时执行动态查询。立方体贴图的范围可以不影响其边界以外的对象,它们可以仅影响背景对象、仅影响动态对象或两者兼而有之。
基于对象的立方体贴图的主要问题是使用不同的立方体贴图照亮相邻对象之间的接缝。
在此屏幕截图上,您可以看到立方体贴图脱机链接到多个对象(红线)。当将部分地面指定给不同的立方体贴图时,地面上会出现照明接缝。这在这个屏幕截图中很明显,因为照明频率很高。另一个问题是,当网格由两个不同的环境共享时,需要将其拆分为多个部分,这对于墙和楼板等对象很常见。它在网格上添加生产约束。请注意,最近的立方体贴图并不总是正确的选择。在某些情况下,应测试可见性。
此处[2]描述了使用此方法放置立方体贴图的一些建议:
如果一个立方体地图是为NPC或玩家设计的,那么env_立方体地图应该放在离地高度。这样,立方体地图将从站立生物的角度最准确地代表世界。
如果立方体贴图用于静态世界几何体,则环境立方体贴图应与所有笔刷曲面保持相当的距离。
在视觉对比度不同的区域,应使用不同的立方体贴图。亮黄色灯光的走廊需要有自己的环境立方体贴图,尤其是在靠近低蓝光房间的情况下。如果没有两个env_立方体贴图实体,其中一个区域中的实体和世界几何体上的反射和镜面反射高光似乎不正确。
由于曲面必须通过立方体贴图近似其周围环境,因此在小区域中使用过多立方体贴图可能会在移动时造成明显的视觉不连续。对于高反射率区域,通常在曲面中心放置一个立方体贴图更为正确,而不是更多。这样可以避免视图更改时出现接缝或弹出。
另一个问题是需要跟踪哪个立方体贴图影响动力学对象。由于动态对象可以交换其立方体贴图,因此会出现弹出。通过混合动态对象的K最近立方体贴图,可以减少弹出。这是一个相当昂贵的解决方案,您可以在着色器中混合K立方体贴图,添加大量指令和获取。但是,当存在超过K个立方体贴图时,即使混合K个最近立方体贴图也不能通过切换最小贡献立方体贴图来防止爆裂诱导。
基于区域的立方体映射
为标高的每个分区指定一个无限立方体贴图。当摄影机进入某个分区时,该分区中的无限立方体贴图将应用于所有对象。杀手地带2/3,任务召唤:黑色行动,猎物2号样本使用此方法[3][7][19]。
该方法易于实现,适合任何引擎体系结构。对象之间没有照明接缝,因为所有对象都使用相同的立方体贴图,并且立方体贴图需要在分区之间混合以避免弹出。Cubemap可以在CPU/GPU上有效地混合,或者以延迟的方式与同时处理的2个Cubemap混合。
全局和局部立方体贴图不重叠
默认情况下,所有对象都使用全局(本例中为无限)立方体贴图。一些局部立方体贴图放置在没有重叠范围的标高中。当相机或播放器到达局部立方体贴图的范围时,所有对象都使用该局部立方体贴图而不是全局立方体贴图。
该方法与基于分区的立方体贴图在精神上类似,只是“分区”的定义不同。Cubemap可以在CPU/GPU上有效地混合,或者以延迟的方式与同时处理的2个Cubemap混合。冻伤引擎(战场2)仅使用全局立方体贴图来表示天光[4]。
我不确定在Frosbite 2引擎(战场3)中用于表示室外(全局)/室内(局部)照明的方法[5]。但他们使用天空可见性来混合室内立方体贴图和室外立方体贴图,这看起来与此策略类似。
全局和局部立方体映射重叠
全局(本例中为无限)和局部立方体贴图仅影响其范围内的对象(由艺术家定义)。对象可能会受到多个重叠立方体贴图的影响,例如全局立方体贴图和局部立方体贴图。
此策略只能通过延迟或轻预处理渲染体系结构有效地实现。
这是Cryengine 3(Crysis 2)[6]使用的方法。这种方法很容易在延迟上下文中实现,cubemap以延迟方式混合。立方体贴图的边界处可能有照明接缝。
基于兴趣点(POI)的立方体地图(Siggraph 2012 talk)
在标高中放置了几个局部立方体贴图。局部立方体贴图的数量取决于照明的复杂性。然后混合POI的K-最近立方体贴图,并将结果应用于所有对象。
POI取决于上下文,它可以是摄影机、播放器或场景中的任何角色。它可以是一个虚拟的位置,由游戏中电影的轨迹设置动画。
我为具有复杂照明场景的正向渲染器开发了此方法,因为由于引擎架构的限制,我无法依赖全局/局部重叠方法。该方法已在Siggraph 2012演讲中介绍:“基于局部图像的照明,具有视差校正立方体贴图”。
使用这种方法,根本没有接缝,因为K立方体贴图始终是混合的。然后,始终只有一个立方体贴图应用于对象。与基于对象方法中的动态对象类似,当存在超过K个立方体贴图时,切换最小的立方体贴图时可能会出现弹出(艺术家也应该能够避免)。这种方法的两个主要问题是局部立方体贴图引起的视差问题,当混合多个立方体贴图时,视差问题会更严重,以及远距离对象上的照明不准确;照明精度随着距离POI的距离而降低,因为近似照明环境是在POI处完成的。
视差伪影用本文末尾描述的方法修复,远距离物体上的不准确照明通过以下方法之一减少(也可用于全局和局部立方体贴图无重叠策略):
在对象位置使用环境照明信息(如光照贴图或球面谐波),并根据与POI的距离将其与当前立方体贴图的贡献相混合(类似的技术用于节省内存,在本文后面的“重用可用立方体贴图”一节中进行了描述)。
根据与POI的距离应用镜面反射贡献的衰减。对于几乎镜像对象(如PBR上下文中的纯金属),这可能是一个问题,因为漫反射颜色将为黑色。在这种情况下,应提供一个选项来禁用此对象的衰减,并返回到上述方法。请注意,这可以通过共享LOD技术进行共享,并有助于提高性能。
覆盖立方体贴图(请参见下文)。
几种策略的共同点
对于所有策略,对象都应该能够强制使用预定义的立方体映射。这允许在镜像对象[8]上设置高分辨率立方体贴图,修复始终距离镜像对象的视觉瑕疵,或任何其他类似需要。
对于基于区域的方法(基于区域以及全局和局部立方体贴图无重叠),立方体贴图的混合可以基于时间,而不是基于区域距离。
补充说明:
在PBR方面:
–使用灯光预过程渲染器,延迟立方体贴图将无法访问菲涅尔方程所需的镜面反射颜色项。
–使用正向渲染器,立方体贴图与对象同时渲染,可以访问所有材质属性。
–使用延迟渲染器,立方体贴图可以在G-Buffer时间前渲染(使用G-Buffer中的发射存储),也可以在以后延迟渲染,并且可以访问G-Buffer中存储的所有材质属性。
–使用延迟cubemap,我们无法访问ddx/ddy texcoord,因此我们无法执行mipmap优化,该优化包括在手动lod和硬件lod之间采用较小的lod。
通过打开/关闭立方体贴图,可以为照明环境添加一些动态响应。必须在不同的照明条件下生成多个立方体贴图,并且可以根据上下文进行切换,甚至混合。
镜面立方体贴图内存占用
拥有大量立方体贴图需要大量存储空间。辐照度立方体贴图的分辨率可能非常低(16x16x6或8x8x6),与此不同,镜面反射立方体贴图的分辨率相当中等(64x64x6或128x128x6)。镜像对象甚至可能需要更高的分辨率(256x256x6或512x512x6)。本节将讨论一些有助于减少cubemap内存占用的方法。
纹理压缩与流媒体
当谈到使用纹理节省内存时,最明显的建议是实现流式纹理系统并选择一种积极的纹理压缩格式。
流式立方体贴图是一个比我们一开始想象的更复杂的问题。最简单的策略是让无限/本地立方体贴图链接到一个区域,并用该区域加载它们。在需要时仅流式处理cubemap的高mipmap假设您知道何时需要它,这不是小事,这需要硬件cubemap布局知识。
关于压缩格式,cubemap应该是HDR(如果您在游戏引擎中生成cubemap,请记住关闭后处理,那么您可能需要在捕获新cubemap时不在对象上应用cubemap)。当我们谈论DX9/XBOX360/PS3一代的游戏时,我们最多必须适合RGBA8bit格式。
几乎没有候选者:YCoCg[24]、LogLuv、Luv、RGBE、任何类型的RGBM[9][11][12]或Cryengine 3 RGBK(如RGBM)[6];所有存储为DXT5,需要额外的说明。由于我需要大量中等质量的立方体贴图,而额外的指令可能更少,因此我选择了[10]中描述的激进HDR格式。一个好的替代方案是让艺术家选择压缩格式,并自动默认为最适合所需范围的压缩格式(如RGBE适用于非常大的范围)。对于激进的格式,HDR cubemap压缩到一个紧凑的sRGB DXT1(任何DXT5 RGBM成本的一半),并且解压只需要很少的指令。
该方法是计算立方体贴图的所有texel的每个通道(R、G和B)的最大值,将其钳制到阈值。将每个纹理除以此最大值。然后将结果存储在sRGB DXT1中:
// R通道的示例,但其他通道的代码相同。
//MaxPixelColor是整个立方体贴图中3个RGB通道的最大值
StorageColor.R = Clamp(SrcPtr[0], 0.0f, MAX_HDR_CUBEMAP_INTENSITY);
StorageColor.R = MaxPixelColor > 0.0f ? StorageColor.R / MaxPixelColor : 0.0f;
StorageColor.R = ConvertTosRGB(StorageColor.R);
压缩质量取决于立方体贴图内容和最大HDR立方体贴图强度。我选择8作为最大HDR立方贴图强度,有效地提供MDR而不是HDR。结果很好,满足了我的需要。简单比较:1024×1024 DXT1纹理占用的内存与十个128x128x6 DXT1立方体贴图相同。
在着色器中,只需执行乘法即可恢复原始范围。乘法在硬件sRGB解压缩后在线性空间中完成:
half3 CubeColor = texCUBElod(CubeTexture, half4(CubeDir, MipmapIndex)).rgb;
half3 FinalCubeColor = CubeColor * MaxPixelColor;
建议:您可以选择使用自己的通道最大值压缩每个通道,而不是使用三个通道中的最大值。在这种情况下,必须注意通道最大值之间的差异(即三个最大值是非常不同的值,如R:8、G:1、B:1)。当最大值之间的差异较大时,单独压缩通道会导致感知色调错误。下面是一个在场景中生成的lightprobe示例,该场景具有高亮度红光,导致最大颜色为R:8、G:1.5、B:1.5。除灯光外,光探头应为灰色(此处未显示):
此处,将红色通道压缩8会在解压比较中引入错误,这会导致红移(与下图比较)。如果您选择三个压缩值中的最大值(即8),则会得到以下结果:
这在视觉上最接近正确的结果,因为所有通道都使用相同的值进行压缩并引入相同的错误,因此没有色调偏移,但是解压缩的精度较低。综上所述,三通道压缩更准确,更好地保持亮度,但如果最大值之间的差异较大,则会导致色调偏移;使用三通道的最大值进行压缩精度较低,但不会产生色调偏移。更好的办法是把控制权交给艺术家。
重用可用的cubemap
节省内存的最好方法是重用cubemap。立方体贴图的确切内容小于其平均颜色和强度[10]。因此,我们希望对原始立方体贴图进行一些修改,以匹配给定位置的照明环境。这些修改允许在多个位置重用cubemap,而不是为每个新位置生成新的cubemap。有几种方法可以做到这一点:
在预处理中“规格化”环境贴图(将其除以平均值),然后将其乘以着色器中的漫反射环境。使用的漫反射环境值应在所有法线方向上平均,如果是球面谐波,则为0阶SH系数。
–乘以环境光照比:
所有立方体贴图必须在引擎中生成。在生成时,获取漫反射环境光(如上定义)。在新位置重用立方体贴图时,获取新的漫反射环境,然后按比例(“新漫反射环境”/“旧漫反射环境”)调制立方体贴图。
–基于光泽的混合:
Brian Karis在这篇文章的评论中描述了这种方法:对于低光泽值,使用光照贴图的颜色立方体贴图的强度。高光泽度值,与光照贴图的强度立方体贴图的颜色相反。这两种情况都使用标准化立方体映射和调制。
–使立方体贴图去饱和:
将立方体贴图转换为灰度亮度,然后使用漫反射环境光(环境光的平均颜色)重新定焦。请注意,使用此方法,您只有一个通道来存储cubemap。战场3[5]使用这种方法。他们将一些黑白立方体贴图存储在一个RGBA8立方体纹理中(一个用于玻璃,一个用于武器等),并使用动态光能传递系统提供的环境照明进行调制。
–对立方体贴图进行颜色校正[3]:
应用允许匹配新位置的任何颜色校正。颜色校正可以采取曲线(高光、中区、阴影)、简单的颜色调制、去饱和度等形式…
Killzone 2[15]可以指定一个颜色倍增器和一个分区去饱和因子,以应用于cubemap样本(它们也可以以类似于以前方法的方式乘以SH样本)。
漫反射环境光通常取自SH Lightprobe或动态光能传递,但也可以取自光照贴图中的像素。
所有这些方法都有类似的缺点。当复杂的场景照明由许多不同颜色的灯光组成时,它们都会失败。当然,失败并不太难,玩家可能不会注意到,但要完全准确,唯一的选择是生成更多的立方体贴图。这些方法也可以用于使用基于POI的方法最小化伪影。
局部立方体映射混合权重计算
一些基于局部立方体映射的策略,如基于POI的立方体映射方法,需要检索POI的K个最近的局部立方体映射并将它们混合在一起。获得K最近立方图的问题类似于SH辐照度体积问题。[13] [20][21]给出解决该问题的插值方法示例。然而,对于镜面立方体贴图,由于您的关卡中不能有这么多的lightprobe,所以更简单的算法更可取。我用八叉树来表示这个。如果您希望避免弹出,混合计算可能会更复杂,本节将介绍我为本地立方体贴图(即具有位置的立方体贴图)开发的一种方法。
该算法旨在处理与八叉树中每个立方体映射存储相关联的影响体积。这将允许有效地收集靠近POI的立方体贴图。如果当前POI超出影响体积,则不考虑立方体贴图。这意味着为了平滑过渡,影响体积必须重叠。我们为艺术家提供盒子和球体。仅使用球体可能会导致我们希望避免的较大重叠。想象一下不同楼层的几个U形走廊。
该图显示了一个玩具级的俯视图,其中3个立方体贴图体积以绿色重叠。一个正方形,两个球体。红色圆圈表示内部范围。当POI在内部范围内时,它将获得立方体贴图的100%贡献。如果未定义内部范围,则这是影响体积的中心。内部范围是艺术家的要求。
为了避免任何灯光弹出,我们定义了一组算法必须遵循的规则,包括艺术家的要求。立方体贴图在内部范围的边界处具有100%的影响,在外部范围的边界处具有0%的影响。因此,影响体积权重遵循一种类似于距离场的模式,但进行了反转和归一化。
我们还添加了一条规则,即一个较小的卷在另一个较大的卷中必须对最终结果做出更大的贡献,但应尊重以前的约束。这使艺术家能够提高局部区域的精确度。
在下文中,我们将变量“NDF”(标准化距离场)定义为内范围边界处0,外范围边界处1,如果在内范围内,则小于0。该算法首先收集与POI位置相交的所有影响体积。对于每个影响体积,它计算体积影响权重(NDF值)。然后,选定的影响体积将按影响最大的进行排序。然后,对于每个选定的影响体积,我们计算体积影响权重之和以及逆权重之和。然后使用这些总和得到两个结果。第一个结果强制执行边界处的贡献率为0%的规则,第二个结果强制执行中心处的贡献率为100%的规则,无论重叠原语的数量如何。然后我们调整这两个结果。最后,对所有混合因子进行规格化。以下是伪代码:
Box::GetInfluenceWeights()
{
//从世界空间转换到局部框(无需缩放,因此我们可以测试扩展框)
Vector4 LocalPosition = InfluenceVolume.WorldToLocal(SourcePosition);
//在盒子的左上角工作。
Vector LocalDir = Vector(Abs(LocalPosition.X), Abs(LocalPosition.Y), Abs(LocalPosition.Z));
LocalDir = (LocalDir - BoxInnerRange) / (BoxOuterRange - BoxInnerRange);
//取所有轴的最大值
NDF = LocalDir.GetMax();
}
Sphere::GetInfluenceWeights()
{
Vector SphereCenter = InfluenceVolume->GetCenter();
Vector Direction = SourcePosition - SphereCenter;
const float DistanceSquared = (Direction).SizeSquared();
NDF = (Direction.Size() - InnerRange) / (OuterRange - InnerRange);
}
void GetBlendMapFactor(int Num, CubemapInfluenceVolume* InfluenceVolume, float* BlendFactor)
{
//第一次计算NDF和InvDNF之和,以规范化值
float SumNDF = 0.0f;
float InvSumNDF = 0.0f;
float SumBlendFactor = 0.0f;
//算法如下
//基本体有一个标准化的距离函数,中心为0,边界为1
//混合多个基本体时,我们希望遵守以下约束:
//A-原语中心的100%(全重),无论原语重叠的数量如何
//B-基元边界处的0%(零权重),无论基元重叠的数量是多少
//为此,我们计算两个重量并调整它们。
//权重0是使用NDF计算的,并允许遵守约束B
//Weight1是带有反向NDF的calc,即(1-NDF)并允许遵守约束A
//强制执行约束的是0的特例,它一次乘以另一个值就是0。
//对于权重0,0将强制边界始终为0%,但中心不总是100%
//对于权重1,0将强制中心始终为100%,但边界不始终为0%
//调整权重0和权重1,然后重新规范化将允许同时考虑A和B。
//两者之间不是线性的,而是一个令人愉快的结果。
//在实践中,该算法在离开原语的内部范围时无法避免弹出
//它包含在至少2个其他基本体中。
//由于这是一个罕见的案例,我们需要处理它。
for (INT i = 0; i < Num; ++i)
{
SumNDF += InfluenceVolume(i).NDF;
InvSumNDF += (1.0f - InfluenceVolume(i).NDF);
}
//权重0=标准化NDF,反转为中心为1,边界为0。
//当我们反转时,我们需要除以Num-1以保持标准化(否则总和大于1)。
//尊重约束B。
//权重1=归一化的反向NDF,所以我们在中心有1,在边界有0
//尊重约束A。
for (INT i = 0; i < Num; ++i)
{
BlendFactor[i] = (1.0f - (InfluenceVolume(i).NDF / SumNDF)) / (Num - 1);
BlendFactor[i] *= ((1.0f - InfluenceVolume(i).NDF) / InvSumNDF);
SumBlendFactor += BlendFactor[i];
}
//规范化混合因子
if (SumBlendFactor == 0.0f) //可以定制重量
{
SumBlendFactor = 1.0f;
}
float ConstVal = 1.0f / SumBlendFactor;
for (int i = 0; i < Num; ++i)
{
BlendFactor[i] *= ConstVal;
}
}
// Main code
for (int i = 0; i < NumPrimitive; ++i)
{
if (In inner range)
EarlyOut;
if (In outer range)
SelectedInfluenceVolumes.Add(InfluenceVolumes.GetInfluenceWeights(LocationPOI));
}
SelectedInfluenceVolumes.Sort();
GetBlendMapFactor(SelectedInfluenceVolumes.Num(), SelectedInfluenceVolumes, outBlendFactor)
下面是不同情况下的结果。每个影响体积由一种颜色表示(两个圆圈:红色、绿色和一个方框:蓝色)。权重由每个影响体积的相应颜色的混合表示。纯红色表示100%的贡献,50%红色表示红色影响体积的50%贡献。内部范围由较小的影响体积表示,在相同颜色的较大影响体积内有一条白线。单击以获得高分辨率:
上面的图片突出显示,在一般情况下不会出现弹出窗口,我们完全尊重我们制定的规则。该算法甚至提供了令人愉快的过渡。然而,在一些我们很少遇到的压力条件下,它失败了。如果你想玩它,这里有一个我用来生成这个图像的RenderMonkey项目的链接:Siggraph_NormalizedDistance(这是一个.pdf文件,因为wordpress不支持.zip文件,右键单击并选择“将链接另存为”,下载后将文件重命名为.rfx)。
这里还有一个视频显示了影响体积版本和相关调试工具:基于局部图像的照明和视差校正立方体贴图:影响体积。
补充说明:
可以使用其他更复杂的方法,如Delauney三角剖分[13][21]或Voronoi图,但我发现这一方法简单有效。
样本的杀伤区2使用简单的基于距离的插值[14]进行SH lightprobe插值。
感谢我的同事安托万·扎努蒂尼(Antoine Zanuttini)为我提供了用于生成图片的基本渲染器项目。
高效GPU立方体映射混合
在正向渲染器的上下文中,基于POI的立方体贴图方法需要一种混合多个立方体贴图(包括mipmap)的有效方法。在处理多个立方体贴图时,在对象着色器中混合这些立方体贴图的成本可能会很高(在处理经过视差校正的立方体贴图时,成本会更高,如本文末尾所述)。相反,我们选择在主渲染之前的一个步骤中分别混合CPU或GPU上的立方体贴图。
在本节中,我将描述一种伪DX9实现方法,用于在GPU上高效地混合多个立方体贴图。对于更真实的用例,代码附带了我使用的HDR cubemap格式。这允许查看与此特定格式关联的性能。
目标是基于K加权立方体映射生成一个新的立方体映射。对于此方法,所有立方体贴图都需要相同的大小(此处为128x128x6)。算法很简单:
For each face
For each mipmap of the resulting cubemap
Setup the current mipmap face as the current rendertarget
Render the weighted sum of current mipmap of the K cubemaps
我不会详细介绍立方体贴图的DX9创建。您需要创建立方体纹理,并在立方体贴图的每个面的每个mipmap上保存曲面指针。以下伪实现描述了主混合循环:
const int CubemapSize = 128;
int SizeX = CubemapSize;
const int NumMipmap = 7;
for (int FaceIdx = 0; FaceIdx < CubeFace_MAX; FaceIdx++)
{
//mipmap在内存中相互跟随,因此使用此顺序
for (INT MipmapIndex = 0; MipmapIndex < NumMipmap; ++MipmapIndex)
{
SizeX = CubemapSize >> MipmapIndex;
Direct3DDevice->SetRenderTarget(BlendCubemapTextureCube->CubeFaceSurfacesRHI[FaceIdx * NumMipmap + MipmapIndex]);
//没有alpha混合,没有深度测试或写入,没有模具测试或写入,没有背面剔除。
Direct3DDevice->SetRenderState(...)
//将rendertarget作为sRGB写入本地的agressive HDR格式
Direct3DDevice->SetRenderState(D3DRS_SRGBWRITEENABLE,TRUE);
SetBlendCubemapShader(FaceIdx, MipmapIndex);
DrawQuad(0, 0, SizeX, SizeX);
}
}
为了提高性能,cubemap rendertarget目标是RGBA8bit,因此我们需要压缩结果。我使用硬件sRGB从混合的HDR cubemap读取数据,并写入生成的HDR cubemap,这样可以保存一些指令。为了提高效率,我为立方体贴图的每个面生成了一个着色器(每个面都有不同的FACEIDX定义,请参见下面的着色器代码)。上面代码中的SetBlendCubemapShader将为当前面选择着色器并设置当前mipmap索引。着色器代码负责对K立方体贴图的每个纹理进行采样并混合。代码如下:
#define FACE_POS_X 0
#define FACE_NEG_X 1
#define FACE_POS_Y 2
#define FACE_NEG_Y 3
#define FACE_POS_Z 4
#define FACE_NEG_Z 5
void BlendCubemapVertexMain(
in float4 InPosition : POSITION,
out float4 OutPosition : POSITION
)
{
OutPosition = InPosition;
}
half4 VPosScaleBias;
half MipmapIndex;
samplerCUBE BlendCubeTexture1;
samplerCUBE BlendCubeTexture2;
samplerCUBE BlendCubeTexture3;
samplerCUBE BlendCubeTexture4;
half MaxPixelColor1;
half MaxPixelColor2;
half MaxPixelColor3;
half MaxPixelColor4;
//从给定面的立方体纹理获取方向。x和y在[-1,1]范围内。
half3 GetCubeDir(half x, half y )
{ //根据面设置方向。
//注:无需标准化,因为我们在立方体贴图中采样
#if FACEIDX == FACE_POS_X
return half3(1.0, -y, -x);
#elif FACEIDX == FACE_NEG_X
return half3(-1.0, -y, x);
#elif FACEIDX == FACE_POS_Y
return half3(x, 1.0, y);
#elif FACEIDX == FACE_NEG_Y
return half3(x, -1.0, -y);
#elif FACEIDX == FACE_POS_Z
return half3(x, -y, 1.0);
#elif FACEIDX == FACE_NEG_Z
return half3(-x, -y, -1.0);
#endif
}
void BlendCubemapPixelMain(
float2 ScreenPosition: VPOS,
out half4 OutColor : COLOR0
)
{
float2 xy = VPosScaleBias.xy * ScreenPosition.xy + VPosScaleBias.zw;
half3 CubeDir = GetCubeDir(xy.x, xy.y);
//我们从sRGB到线性,然后应用乘法器
half3 CubeColor1 = texCUBElod(BlendCubeTexture1, half4(CubeDir, MipmapIndex)).rgb;
half3 CubeColor = CubeColor1 * MaxPixelColor1;
half3 CubeColor2 = texCUBElod(BlendCubeTexture2, half4(CubeDir, MipmapIndex)).rgb;
CubeColor += CubeColor2 * MaxPixelColor2;
half3 CubeColor3 = texCUBElod(BlendCubeTexture3, half4(CubeDir, MipmapIndex)).rgb;
CubeColor += CubeColor3 * MaxPixelColor3;
half3 CubeColor4 = texCUBElod(BlendCubeTexture4, half4(CubeDir, MipmapIndex)).rgb;
CubeColor += CubeColor4 * MaxPixelColor4;
OutColor = half4(CubeColor / 8, 1.0f); //在RenderGet中写入时,将完成到sRGB的转换
}
FACEIDX是要混合的当前面的定义集,每个不同的值表示不同的着色器。
MaxPixelColorX是硬件解压缩sRGB值后用于解压缩HDR立方体映射的常量。请注意着色器末尾的除以8(这是恒定的MAX_HDR_CUBEMAP_强度)。为了保存指令,我只需除以允许的最大范围,结果存储在RGBA8中(但您可以使用所有MaxPixelColor的最大值以获得更高的精度)。因此,当在其他着色器中使用混合结果时,MaxPixelColor必须设置为8。
MipmapIndex是当前混合的mipmap。
VPosScaleBias是四个值,允许从VPOS寄存器转换为GetDir()函数所需的[-1…1]间隔。这可以在着色器中完成,但为了提高效率,在VPosScaleBias中烘焙变换。VPOS寄存器和GetDir()允许检索用于对混合立方体贴图进行采样的方向。以下是VPosScaleBias设置的代码:
//我们需要在X轴和Y轴上变换[-1,1]范围内的VPO。
float InvResolution = 1.0f / float(128 >> MipmapIndex);
//128是立方体贴图的大小
//从[0..res-1]转换为[-(1-1/res)…(1-1/res)]
//VPos寄存器左0,右0,上0,下0
//(对于DX9,UsePixelCenterOffset为0.5,其他为0)
Vector4 ScaleBias = Vector4(2.0f * InvResolution, 2.0f * InvResolution, -1.0f + (UsePixelCenterOffset * 2.0f * InvResolution), -1.0f + (UsePixelCenterOffset * 2.0f * InvResolution));
下面是一个选项卡,显示了在PS3和XBOX360上使用此方法可以获得的性能。它需要了解PS3上的硬件布局,我在这里无法详述。
此代码的另一种方法是从以原点(0,0,0)为中心的结果立方体贴图的6个视图中渲染一个无限长方体。每个立方体贴图与其相应的重量进行额外混合。示例代码:
#define HALF_WORLD_MAX 262144.0
Matrix CubeLocalToWorld = ScaleMatrix(HALF_WORLD_MAX);
SetShader(...)
SetAdditiveBlending()
for each view of the blended cubemap
{
for each mipmap
{
for each cubemap to blend
{
SetEnvMapScale(EnvMapScale * Blendweights);
SetMipmapIndex(...);
SetViewProjectionMatrix(CalcCubeFaceViewMatrix() * ProjectionMatrix);
DrawUnitBox(...)
}
}
}
// In the shader
float4 MipmapIndex;
float4x4 LocalToWorld;
float4x4 ViewProjectionMatrix;
sampler2D BlendCubeTexture;
float3 EnvMapScale;
void BlendCubemapVertexMain(
in float4 InPosition : POSITION,
out float4 OutPosition : POSITION,
out float3 UVW : TEXCOORD0
)
{
float4 WorldPos = mul( LocalToWorld, InPosition);
float4 ScreenPos = mul( ViewProjectionMatrix, WorldPos);
OutPosition = ScreenPos;
UVW = WorldPos.xyz;
}
void BlendCubemapPixelMain(
in float3 UVW : TEXCOORD0,
out float44 OutColor : COLOR0
)
{
float3 CubeSample = texCUBElod(BlendCubeTexture, float4(UVW, MipmapIndex.x)).xyz;
OutColor = half4(EnvMapScale.xyz * CubeSample / 8.0f, 1.0f);
}
结果是一样的。此外,即使看起来更简单,一个cubemap的性能是相同的,但是当使用多个cubemap时,在PS3上使用前面的方法会更快。因此,这取决于上下文/平台。
局部立方体贴图的视差校正
如第一节所述,立方体映射根据定义表示一个无限长方体。当用作局部立方体贴图时,存在视差问题(反射的对象不在正确的位置)。本节介绍一些可用于纠正局部立方体贴图的视差问题的技术,以更好地匹配反射对象的放置。
每个视差校正技术的共同点是定义围绕局部立方体贴图的几何体(我们称之为几何体代理)的近似值。近似值越简单,以精度为代价的算法效率就越高。几何体代理的示例包括球体体积[16]、长方体体积[17]或立方体深度缓冲区[22]。下面是一个立方体贴图的示例,其中关联的长方体体积为白色。
在着色器中,我们在反射向量和几何体代理之间执行交集。然后使用该交点将原始反射向量校正为新方向。由于交互必须在着色器中执行,因此可以看到性能如何链接到几何体代理的选择。
以下是一个简单AABB卷的算法示例:
阴影线是反射地面,黄色形状是环境几何体。一个立方体地图已经在位置C生成。一个摄像头正在观察地面。曲面法线R反射的视图向量通常用于对立方体贴图进行采样。美工人员使用长方体体积定义立方体贴图周围几何体的近似值。这是图中的黑色矩形。应该注意的是,长方体中心不需要与立方体贴图中心匹配。然后我们找到P,向量R和盒子体积的交集。我们使用向量CP作为新的反射向量R’对立方体贴图进行采样。
下面是代码,这是一个简单的方框交叉点,有一些简化:
float3 DirectionWS = PositionWS - CameraWS;
float3 ReflDirectionWS = reflect(DirectionWS, NormalWS);
//下面是视差校正代码
//查找光线与长方体平面的交点
float3 FirstPlaneIntersect = (BoxMax - PositionWS) / ReflDirectionWS;
float3 SecondPlaneIntersect = (BoxMin - PositionWS) / ReflDirectionWS;
//沿着光线获取这些交点中最远的一个
//(可以,因为x/0给出+inf和-x/0给出-inf)
float3 FurthestPlane = max(FirstPlaneIntersect, SecondPlaneIntersect);
//找到最近的远交点
float Distance = min(min(FurthestPlane.x, FurthestPlane.y), FurthestPlane.z);
//获取交叉点位置
float3 IntersectPositionWS = PositionWS + ReflDirectionWS * Distance;
//得到正确的反射
ReflDirectionWS = IntersectPositionWS - CubemapPositionWS;
//结束视差校正代码
return texCUBE(envMap, ReflDirectionWS);
AABB卷相当有限,最好使用OBB卷。以下是OBB卷的实现示例:
float3 DirectionWS = normalize(PositionWS - CameraWS);
float3 ReflDirectionWS = reflect(DirectionWS, NormalWS);
//与OBB转换为单位框空间的交点
//局部单位视差立方体空间中的变换(缩放和旋转)
float3 RayLS = MulMatrix( float(3x3)WorldToLocal, ReflDirectionWS);
float3 PositionLS = MulMatrix( WorldToLocal, PositionWS);
float3 Unitary = float3(1.0f, 1.0f, 1.0f);
float3 FirstPlaneIntersect = (Unitary - PositionLS) / RayLS;
float3 SecondPlaneIntersect = (-Unitary - PositionLS) / RayLS;
float3 FurthestPlane = max(FirstPlaneIntersect, SecondPlaneIntersect);
float Distance = min(FurthestPlane.x, min(FurthestPlane.y, FurthestPlane.z));
//直接使用WS中的距离恢复交点
float3 IntersectPositionWS = PositionWS + ReflDirectionWS * Distance;
float3 ReflDirectionWS = IntersectPositionWS - CubemapPositionWS;
return texCUBE(envMap, ReflDirectionWS);
对于立方体贴图的每个纹理,我们将相应的视图向量转换为一个单位框空间来执行相交。注意将结果转换回世界空间以获得最终校正向量时的优化步骤。示例结果:
有关球体的示例,请参阅[16]。
存在更便宜的具有视差效果的技巧。它还需要调整。它在[18][23]中有描述,在[8]中也有使用,似乎起源于2000年出版的Advanced Renderman图书。
它包括从立方体贴图位置C到正在绘制Pp的对象上的点添加矢量的缩放版本。实现很简单:
ReflDirectionWS= EnvMapOffset * (PositionWS - CubemapPositionWS) + ReflDirectionWS;
当我们进入立方体映射时,不需要进行规范化。EnvMapOffset是艺术家的可调值,它取决于对象大小、环境大小等。Brennan在[23]中使用(1/radius),其中radius是球体几何体代理的半径。下面是一个调优参数的示例。第一个图像是默认图像,第二个图像是优化了EnvMapOffset的图像:
本着同样的精神,在对立方体贴图进行采样时,杀伤区2/3使用偏移量来更改显示在对象内的立方体贴图的“大小”[3]。
高效的GPU局部视差校正立方体映射混合
现在,我将使用基于POI的方法应用前面两节的结果。这就是我所说的:带有视差校正立方体贴图的局部IBL方法。这种方法是由我和我的同事Antoine Zanuttini共同开发的。
正如“高效GPU cubemap混合”一节中的详细说明,我们有一个专门的cubemap混合步骤。我们正在寻找视差校正立方体地图时,混合他们。但是,在这个混合步骤中,我们无法访问像素位置。为了解决这个问题,我们进行了以下观察:
左图显示了我们之前使用的一个简单长方体作为视差校正的几何体代理的情况。在中间的图上,我们观察到,如果我们添加反射相机V’的视图向量,它与反射视图向量R匹配。我们可以看到向量V’和R都与同一点P相交。该点P可以像以前一样使用,以获得新的反射向量R’对立方体贴图进行采样。右图是将此推理应用于立方体贴图的每个视图方向的结果。我们现在可以在不需要任何像素位置的情况下对整个立方体贴图进行视差校正。我们只需要一个反射平面来代替像素的位置。然而,这将限制我们对平面对象的视差校正方法。
下面是一个混合步骤的代码示例,该步骤还使用box geometry proxy处理视差校正,正如我们刚才看到的。该代码仅处理混合立方体贴图的一个面和一个立方体贴图(几个立方体贴图与“高效GPU立方体贴图混合”部分相同):
float4 VPosScaleBias;
float MipmapIndex;
float4x4 WorldToLocal;
float3 ReflCameraWS;
samplerCUBE BlendCubeTexture;
float3 EnvMapScale;
void BlendCubemapPixelMain(
float2 ScreenPosition: VPOS,
out half4 OutColor : COLOR0
)
{
float2 xy = VPosScaleBias.xy * ScreenPosition.xy + VPosScaleBias.zw;
half3 CubeDir = GetCubeDir(xy.x, xy.y);
//与OBB的交点转换为单位框空间
half3 RayWS = normalize(GetCubeDir(xy.x, xy.y)); // Current direction
half3 RayLS = mul((half3x3)WorldToLocal, RayWS);
half3 ReflCameraLS = mul(WorldToLocal, ReflCameraWS); // Can be precalc
//与之前相同的代码,但用于反射和带单位框
half3 Unitary = half3(1.0f, 1.0f, 1.0f);
half3 FirstPlaneIntersect = (Unitary - ReflCameraLS) / RayLS;
half3 SecondPlaneIntersect = (-Unitary - ReflCameraLS) / RayLS;
half3 FurthestPlane = max(FirstPlaneIntersect, SecondPlaneIntersect);
float Distance = min(FurthestPlane.x, min(FurthestPlane.y, FurthestPlane.z));
//直接使用WS中的距离恢复交点
half3 IntersectPositionWS = ReflCameraWS + RayWS * Distance;
half3 ReflDirectionWS = IntersectPositionWS - CubemapPositionWS;
half3 CubeColor = texCUBElod(BlendCubeTexture, half4(ReflDirectionWS, MipmapIndex)).rgb;
CubeColor = CubeColor * EnvMapScale.xyz;
OutColor = half4(CubeColor / 8, 1.0f);
}
在“高效GPU立方体映射混合”部分中,我们有另一种方法来获得相同的结果。此代码的另一种方法是从反射摄影机(而不是原点)的角度,从生成立方体贴图的6个视图渲染长方体几何体代理(而不是无限长方体)。每个立方体贴图与其相应的重量进行额外混合。但在这种情况下,我们可以做得更好。为什么要限制我们在一个盒子里渲染呢。通过光栅化,我们可以摆脱对形状的限制,开始处理凸面体积!由于隐藏的特征,凹面体积仍然存在瑕疵。为此,我们允许美工人员定义BSP几何体(在示例编辑器中使用笔刷工具),并将BSP转换为三角形。然后渲染这个三角形列表,而不是简单的框。
下面是一个适合环境的凸面卷使用示例(右图),它代替了长方体卷(中间图)。左图显示了原始的未更正立方体贴图:
这是对凸面体进行视差校正的代码。着色器代码用于立方体贴图的一个面。
// C++
SetShader(...)
SetAdditiveBlending()
for each view of the blended cubemap
{
for each mipmap
{
for each cubemap to blend
{
Matrix Mirror = CreateMirrorMatrix(ReflectionPlaneWS of this cubemap);
Vector ReflectedViewPositionWS = Mirror.TransformVector(ViewPositionWS);
SetEnvMapScale(EnvMapScale * Blendweights);
SetMipmapIndex(...);
SetViewProjectionMatrix(CalcCubeFaceViewMatrix(ReflectedViewPositionWS) * ProjectionMatrix);
DrawConvexVolume(...);
}
}
}
// Shader
float4 MipmapIndex;
float4x4 LocalToWorld;
float4x4 ViewProjectionMatrix;
sampler2D BlendCubeTexture;
float3 EnvMapScale;
float3 CubemapPos;
void BlendCubemapVertexMain(
in float4 InPosition : POSITION,
out float4 OutPosition : POSITION,
out float3 UVW : TEXCOORD0
)
{
float4 WorldPos = mul( LocalToWorld, InPosition);
float4 ScreenPos = mul( ViewProjectionMatrix, WorldPos);
OutPosition = ScreenPos;
UVW = WorldPos.xyz - CubemapPos; // Current direction
}
void BlendCubemapPixelMain(
in float3 UVW : TEXCOORD0,
out float44 OutColor : COLOR0
)
{
float3 CubeSample = texCUBElod(BlendCubeTexture, float4(UVW, MipmapIndex.x)).xyz;
OutColor = half4(EnvMapScale.xyz * CubeSample / 8.0f, 1.0f);
}
请注意,即使我们使用bsp作为视差校正的几何体代理,我们仍然使用简单的长方体或球体作为影响体积,以避免聚集立方体贴图算法过载。
有了所有这些,我们现在可以应用“带视差校正立方体贴图的局部IBL方法”。这些步骤是:
–借助影响体积从POI中检索最近的立方体贴图
–计算混合权重,如“局部立方体贴图混合权重计算”
–使用上面的凸面视差校正混合代码在GPU上执行混合步骤。
–像往常一样在主通道中应用结果。
下面是一个视频示例,其中包含3个立方体贴图和3个球体影响体积以及长方体几何体代理:带有视差校正立方体贴图的局部IBL方法
现在让我们看看一些性能结果。在DXT1中,将立方体贴图与128x128x6分辨率的凸视差校正进行混合,得到以下结果:
将这些数字与直接在对象着色器中应用视差校正的实际成本(即在没有混合步骤的主过程中)进行比较非常有用。左边的数字表示25%的屏幕覆盖率,右边的数字表示75%的屏幕覆盖率:
第二个表的数量是通过减去原始着色器的视差校正着色器的成本获得的。阅读这个结果需要一些解释。使用每像素校正(即第二个表),结果取决于场景对象的屏幕覆盖率。对于25%的屏幕覆盖率,每个要混合的新立方体贴图约为0.25ms,对于75%的屏幕覆盖率,PS3上的立方体贴图约为0.75ms。这与混合步骤法相比,混合步骤法的附加立方贴图仅为0.08ms。可以观察到,对于每像素校正,XBOX在使用一个立方体贴图时表现更好,但通过增加立方体贴图的数量,下降速度更快。综上所述,我们的混合方法可以更好地缩放多个立方体贴图。
最后,上述视差校正步骤仅适用于镜面。处理光泽材质的法线扰动意味着我们可以访问反射平面下方的下半球。这意味着反射相机的视图向量将错过与长方体体积的交点。为避免此问题,艺术家必须确保反射的摄影机始终保持在长方体体积内:
此外,对于光泽材质,我们引入了与真实结果相比的失真。该效果随反射平面法线和扰动法线之间的角度而增加。
这种失真的根源在于我们生成视差校正立方体贴图的方式。在我们的算法中,视差校正立方体贴图仅对法线垂直于反射平面的像素有效。
在左图中,针对不同的摄影机视图计算了几个不同的交点。整个地表面具有与反射平面垂直的相同法线。在右图中,我们以单个位置为例,在不同方向上扰动其法线。在这种情况下,需要移动相机位置。在这种情况下,我们以前的视差校正相机无法提供正确的值。我们可以为每个可能的法线生成该位置的视差正确立方体贴图,但对于其他位置,结果将是错误的。
参考
[1] McTaggarts, “Half-Life 2 Valve Source Shading” http://www2.ati.com/developer/gdc/D3DTutorial10_Half-Life2_Shading.pdf
[2] http://developer.valvesoftware.com/wiki/Cubemaps
[3] Van Beek, “Killzone lighting pipeline” not available publicly
[4] Andersson, Tatarchuck, “Rendering Architecture and Real-time Procedural Shading & Texturing Techniques” http://www.slideshare.net/repii/frostbite-rendering-architecture-and-realtime-procedural-shading-texturing-techniques-presentation
[5] Magnusson, “Lighting you up in Battlefield 3” http://www.slideshare.net/DICEStudio/lighting-you-up-in-battlefield-3
[6] Sousa, Kasyan, Schulz, “Secrets of CryENGINE 3 Graphics Technology” http://advances.realtimerendering.com/s2011/SousaSchulzKazyan%20-%20CryEngine%203%20Rendering%20Secrets%20((Siggraph%202011%20Advances%20in%20Real-Time%20Rendering%20Course).ppt
[7] Karis, “comment” http://blog.selfshadow.com/2011/07/22/specular-showdown/#comments
[8] Gotanda, “Real-time Physically Based Rendering – Implementation” http://research.tri-ace.com/Data/cedec2011_RealtimePBR_Implementation.pptx
[9] “Unity RGBM toy” http://beta.unity3d.com/jcupisz/rgbm/index.html
[10] Hoffman, “Crafting Physically Motivated Shading Models for Game Development” and “Background: Physically-Based Shading” http://renderwonk.com/publications/s2010-shading-course/
[11] Kaplanyan, “CryENGINE 3: Reaching the Speed of Light” http://advances.realtimerendering.com/s2010/index.html
[12] Karis, “RGBM Color encoding” http://graphicrants.blogspot.com/2009/04/rgbm-color-encoding.html
[13] Cupisz, “LightProbe” http://blogs.unity3d.com/2011/03/09/light-probes/
[14] van der Leeuw, “The playstation3 spus in the real world” http://www.slideshare.net/guerrillagames/the-playstation3s-spus-in-the-real-world-a-killzone-2-case-study-9886224
[15] Personal communication with Michal Valient of Guerrilla game
[16] Bjorke, “Image Based-Lighting” http://http.developer.nvidia.com/GPUGems/gpugems_ch19.html
[17] behc, “Box Projected Cubemap Environment Mapping” http://www.gamedev.net/topic/568829-box-projected-cubemap-environment-mapping/
[18] Mad Mod Mike demo, “The Naked Truth Behind NVIDIA’s Demos” http://ftp.up.ac.za/mirrors/www.nvidia.com/developer/presentations/2005/SIGGRAPH/Truth_About_NVIDIA_Demos.pdf
[19] Lazarov, “Physically Based Lighting in Call of Duty: Black Ops”, http://advances.realtimerendering.com/s2011/Lazarov-Physically-Based-Lighting-in-Black-Ops%20(Siggraph%202011%20Advances%20in%20Real-Time%20Rendering%20Course).pptx
[20] Hall, Edwards, “Rendering in Cars 2”, http://advances.realtimerendering.com/s2011/Hall,%20Hall%20and%20Edwards%20-%20Rendering%20in%20Cars%202%20(Siggraph%202011%20Advances%20in%20Real-Time%20Rendering%20Course).pptx
[21] Cupisz, “Light probe interpolation using tetrahedral tessellations”, http://robert.cupisz.eu/stuff/Light_Probe_Interpolation-RobertCupisz-GDC2012.pdf
[22] Szirmay-Kalos, Aszódi, Lazányi, and Premecz, “Approximate Ray-Tracing on the GPU with Distance Impostors.”, http://sirkan.iit.bme.hu/~szirmay/ibl3.pdf
[23] Brennan, “Accurate Environment Mapped Reflections and Refractions by Adjusting for Object Distance.”, http://developer.amd.com/media/gpu_assets/ShaderX_CubeEnvironmentMapCorrection.pdf
[24] Van Waveren, Castaño, “Real-Time YCoCg-DXT Compression”, http://developer.download.nvidia.com/whitepapers/2007/Real-Time-YCoCg-DXT-Compression/Real-Time%20YCoCg-DXT%20Compression.pdf