17-立方体贴图

在本章中,我们将研究立方体贴图,这些立方体贴图基本上是以特殊方式解释的六个贴图的数组。 使用立方体贴图,我们可以轻松地构建天空或模型反射。
目标:
1.了解立方体贴图以及如何在HLSL代码中对它们进行采样。
2.发现如何使用DirectX纹理工具创建立方体贴图。
3.了解我们如何使用立方体贴图来模拟反射。
4.了解我们如何使用立方体贴图来渲染球体,以模拟天空和遥远的山脉。

立方体映射的想法是存储六个纹理,并将它们可视化为一个立方体的面,因此名称立方体以地图为中心并围绕某个坐标系统对齐轴。 由于立方体纹理是轴对齐的,每个面与沿着三个主轴的方向相对应; 因此,根据与面相交的轴方向(±X,±Y,±Z),在立方体图上引用特定面是很自然的。 为了识别立方体贴图面,Direct3D提供了D3D11_TEXTURECUBE_FACE枚举类型:

typedef enum D3D11_TEXTURECUBE_FACE {
D3D11_TEXTURECUBE_FACE_POSITIVE_X = 0,
D3D11_TEXTURECUBE_FACE_NEGATIVE_X = 1,
D3D11_TEXTURECUBE_FACE_POSITIVE_Y = 2,
D3D11_TEXTURECUBE_FACE_NEGATIVE_Y = 3,
D3D11_TEXTURECUBE_FACE_POSITIVE_Z = 4,
D3D11_TEXTURECUBE_FACE_NEGATIVE_Z = 5
} D3D11_TEXTURECUBE_FACE;

立方体贴图存储在包含六个元素的贴图数组中:
1.索引0指+X面。
2.索引1指-X面。
3.索引2指+Y面。
4.索引3指-Y面。
5.索引4指+Z面。
6.索引5指-Z面。
与2D纹理相反,我们不能再用2D纹理坐标来识别纹理元素。 为了识别立方体贴图中的纹理元素,我们使用3D纹理坐标,这些坐标定义了始发于原点的3D查找矢量v。 v相交的立方体贴图的纹理元素(见图17.1)是与v的三维坐标对应的纹理元素。第8章讨论的纹理过滤概念适用于v与纹理元素样本之间的点相交的情况。

Note:查找向量的大小并不重要,只有方向很重要。 两个方向相同但幅度不同的矢量将采样立方体贴图中的相同点。

在HLSL中,立方体纹理由TextureCube类型表示。 以下代码片段说明了我们如何对立方体贴图进行采样:

TextureCube gCubeMap;
SamplerState gTriLinearSam
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = Wrap;
AddressV = Wrap;
};
…
// in pixel shader
float3 v = float3(x,y,z); // some lookup vector
float4 color = gCubeMap.Sample(gTriLinearSam, v);


图17.1 为简单起见,我们用2D来说在3D中,正方形变成立方体。正方形表示以某个坐标系为中心和轴对齐的立方体贴图。我们从原点拍摄矢量v。纹素v相交是采样纹理。在此图中,v与相应于+ Y轴的立方体面相交。

Note:查找矢量应该与立方体贴图相对应的空间相同。 例如,如果立方体贴图相对于世界空间(即,立方体贴图与世界空间轴轴对齐),则查找矢量应该具有世界空间坐标。

17.2 环境图

立方体贴图的主要应用是环境贴图。 这个想法是将相机放置在场景中某个对象0的中心,并具有90°的视角(垂直和水平方向)。 然后让摄像机向下看正x轴,负x轴,正y轴,负y轴,正z轴和负z轴,拍摄场景图像(不包括对象0) 来自这六个观点的每一个。 由于视场角为90°,因此这6幅图像将从对象0的角度捕捉整个周围环境(见图17.2)。然后,我们将这六幅周围环境图像存储在立方体图中, 到名称环境地图。 换句话说,环境贴图是立方体贴图,其中立方体贴图存储环境的周围图像。

前面的描述表明,我们需要为每个要使用环境映射的对象创建一个环境映射。但是,环境映射通常只用于捕获遥远的“背景”信息。然后许多附近的对象可以共享相同的环境地图。在本章的演示中(图17.3),所有球体和头骨共享图17.2所示的环境地图。请注意,此环境地图不捕获场景的本地列或地板;它只捕捉远处的山脉和天空(即场景背景)。虽然背景环境地图在某种意义上是不完整的,但在实践中效果很好。

如果摄像头的轴线向下看去构建环境地图图像是世界空间轴,则据说环境地图是相对于世界空间生成的。当然,您可以从不同的方向捕捉环境(例如,物体的局部空间)。但是,查找矢量坐标必须位于立方体贴图相对的空间中。


图17.2 “展开”立方体贴图之后的环境贴图示例。 想象一下,将这六张脸重新折叠成一个3D盒子,然后想象一下盒子的中心位置。 从你看的每个方向看,你都会看到周围的环境。


图17.3 “Cube Map”演示的屏幕截图。

请注意,环境地图的六个图像不一定在Direct3D程序中使用,尽管它们可能是(我们将在第17.5节中看到如何实现)。 由于立方地图只存储纹理数据,其内容往往是由艺术家预先生成的(就像我们使用的2D纹理一样)。 因此,我们不需要使用实时渲染来计算立方体贴图的图像。 也就是说,我们可以在3D世界编辑器中创建一个场景,然后在编辑器中预先渲染六个立方体贴图人脸图像。 对于户外环境地图,Terragen(http://www.planetside.co.uk/)程序通常使用(免费供个人使用),并且可以创建逼真的户外场景。 我们为本书创建的环境地图(如图17.2所示)是用Terragen制作的。

Note:如果您选择尝试Terragen,则需要转到相机设置对话框并将缩放因子设置为1.0以实现90°视野。 此外,请务必将输出图像尺寸设置为相等,以便垂直和水平视场角都相同,即90°。
Note:在Web上有一个很好的Terragen脚本(https://developer.valvesoftware.com/wiki/Skybox_%282D%29_with_Terragen),它将使用当前的摄像头位置,并以90°视场渲染六张周围图像。

一旦使用某些程序创建了六个立方体贴图图像,我们需要创建一个立方体贴图贴图,它可以存储所有六个贴图。我们一直使用的DDS贴图图像格式很容易支持立方体贴图,我们可以使用DirectX贴图工具从我们的六种纹理中创建立方体贴图。打开DirectX纹理工具(附带DirectX SDK:C:\ DXSDKJune10 \ Utilities \ bin \ x86),首先进入文件菜单并选择新纹理。在弹出的对话框中(图17.4),选择Cubemap Texture作为纹理类型,输入与这六个图像的尺寸相匹配的尺寸,并选择一个曲面格式(使用高分辨率立方体以来没有像DXT1这样的alpha的压缩格式地图可以消耗大量的内存,因为有六个纹理被存储)。

现在我们有一个空的立方体地图。转到视图菜单,选择立方体贴图面,然后在窗口中沿着要查看的轴选取面(图17.5)。 (所有这些面都是空的。)选择任何一个面开始,然后进入文件菜单并选择打开到这个立方体贴图面,它将启动一个对话框,询问您要加载到该文件的文件当前选定的立方体贴图面;选择与该立方体贴图面对应的图像。对其余五个立方体贴图面重复此过程,以便每个立方体贴图面都可以插入所需的图像。完成后,将DDS保存到现在存储立方体贴图的文件中。


图17.4 使用DirectX纹理工具创建新的立方体纹理。


图17.5 选择立方体贴图的面部以在DirectX贴图工具中查看。

Note:NVIDIA提供Photoshop插件,用于在Photoshop中保存.DDS和立方体贴图; 请参阅http://developer.nvidia.com/nvidia-texture-tools-adobe-photoshop

17.2.1在Direct3D中加载和使用多维数据集地图

方便地,D3DX11CreateShaderResourceViewFromFile函数可以将存储立方体贴图的DDS文件加载到ID3D11Texture2D对象中,并为其生成着色器资源视图:

ID3D11ShaderResourceView* mCubeMapSRV;
HR(D3DX11CreateShaderResourceViewFromFile(device,
cubemapFilename.c_str(), 0, 0, &mCubeMapSRV, 0));

ID3D11Texture2D对象将立方体贴图的六个纹理作为纹理数组存储。

在我们获得ID3D11ShaderResourceView *到立方体纹理后,我们可以使用ID3DX11EffectShaderResourceVariable :: SetResource方法将其设置为效果文件中的TextureCube变量:

// .fx variable
TextureCube gCubeMap;
// .cpp code
ID3DX11EffectShaderResourceVariable* CubeMap;
CubeMap = mFX->GetVariableByName("gCubeMap")->AsShaderResource();
…
CubeMap->SetResource(cubemap);

17.3 塑造一个天空

我们可以使用环境地图来纹理天空。我们创建了一个围绕整个场景的椭球体(我们使用椭球体来创建更自然的平坦天空表面)。 为了在遥远的天空和天空中创造遥远的山脉幻觉,我们使用环境贴图通过图17.6所示的方法纹理椭球体。 通过这种方式,环境地图投影到椭球体的表面上。

我们假设天空椭球离无限远(即它以世界空间为中心,但具有无限的半径),因此无论摄像机在世界中如何移动,我们都不会看起来接近或远离表面 天空椭球。 为了实现这个无限遥远的天空,我们简单地将天空椭球围绕相机放在世界空间中,以便它始终以相机为中心。 因此,随着相机的移动,我们不会接近椭球的表面。 如果我们没有这样做,并且我们让摄像机靠近天空表面,那么整个错觉就会崩溃,因为我们用来模拟天空的技巧是显而易见的。

图17.6 为简单起见,我们用2D来说在3D中,正方形变成立方体,椭圆变成椭圆体。我们假设天空和环境地图以相同的原点为中心。然后纹理椭球体表面上的点,我们使用从原点到表面点的矢量作为立方体映射中的查找矢量。这将立方体贴图投影到椭球体上。
**

天空效果文件如下:

//=====================================================================
// Sky.fx by Frank Luna (C) 2011 All Rights Reserved.
//
// Effect used to shade sky dome.
//=====================================================================
cbuffer cbPerFrame
{
float4x4 gWorldViewProj;
};
// Nonnumeric values cannot be added to a cbuffer.
TextureCube gCubeMap;
SamplerState samTriLinearSam
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = Wrap;
AddressV = Wrap;
};
struct VertexIn
{
float3 PosL : POSITION;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosL : POSITION;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Set z = w so that z/w = 1 (i.e., skydome always on far plane).
vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj).xyww;
// Use local vertex position as cubemap lookup vector.
vout.PosL = vin.PosL;
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
return gCubeMap.Sample(samTriLinearSam, pin.PosL);
}
RasterizerState NoCull
{
CullMode = None;
};
DepthStencilState LessEqualDSS
{
// Make sure the depth function is LESS_EQUAL and not just LESS.
// Otherwise, the normalized depth values at z = 1 (NDC) will
// fail the depth test if the depth buffer was cleared to 1.
DepthFunc = LESS_EQUAL;
};
technique11 SkyTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS()));
SetRasterizerState(NoCull);
SetDepthStencilState(LessEqualDSS, 0);
}
}

Note:过去,应用程序会首先绘制天空,并将其用作清除渲染目标和深度/模板缓冲区的替代。 但是,“ATI Radeon HD 2000编程指南”(http://developer.amd.com/media/gpu_assets/ATI_Radeon_HD_2000_programming_guide.pdf)现在建议不要这是由于以下原因。 首先,需要明确地清除深度/模板缓冲区,以便内部硬件深度优化性能良好。 情况与渲染目标类似。 其次,通常大部分天空都被其他几何形状遮挡,如建筑物和地形。 因此,如果我们首先画出天空,那么我们通过绘制像素来浪费资源,这些像素稍后会被靠近相机的几何体覆盖。 因此,现在建议始终清除,并最后画上天空。

17.4 反射模型

如前一节所述,环境贴图对于纹理天空的作用效果很好。 然而,环境地图的另一个主要应用是模拟任意对象的反射(只有环境地图中的图像才被这种技术反映)。 图17.7说明了如何使用环境贴图进行反射。 表面就像一面镜子:眼睛看着p,看到环境反射出p。


图17.7 这里e是眼点,n是点p处的表面法线。被映射到表面上的点p的纹理元素由反射矢量v给出(即,我们使用v作为查找矢量),其是从表面到e的矢量的反射。这样,眼睛就能看到反射的环境。

我们计算每像素的反射向量,然后用它来对环境地图进行采样:

litColor = texColor*(ambient + diffuse) + spec;
if(gReflectionEnabled)
{
float3 incident = -toEye;
float3 reflectionVector = reflect(incident, pin.NormalW);
float4 reflectionColor = gCubeMap.Sample(
samAnisotropic, reflectionVector);
litColor += gMaterial.Reflect*reflectionColor;
}

通常,像素的颜色并不完全由反射的颜色决定(除非反射镜是100%反射的)。因此,我们修改我们的照明方程以包含反射项 mRcR m R ⊗ c R 。这里 cR c R 是从环境地图中采样的颜色, mR m R 是应用程序控制的材料值,指示表面有多少 cR c R 反射到眼睛中。例如,如果表面只反射红光,那么您可以设置 mR=(1,0,0) m R = ( 1 , 0 , 0 ) ,以便只有来自环境地图的红光才能进入眼睛。回想一下,我们的材料结构已经有一个反射属性,我们直到本章才使用它:

struct Material
{
float4 Ambient;
float4 Diffuse;
float4 Specular; // w = SpecPower
float4 Reflect;
};

将额外反射项纳入照明方程的一个问题是过饱和。 随着反射术语的增加,我们现在给像素添加更多的颜色,这可能会让它变得更加明亮。 基本上,如果我们从反射术语中添加额外的颜色,那么我们必须从其他术语中去除颜色以实现平衡。 这通常是通过缩小环境和漫射材料因子来实现的,从而使较少的环境光和漫射光反射离开表面。 另一种方法是将环境贴图中采样的颜色与通常照亮的像素颜色s进行平均:

f=tcR+(1t)s0t1 f = t c R + ( 1 − t ) s 0 ≤ t ≤ 1

通过这种方式,我们添加了从权重为t的环境地图中采样的颜色,我们同样从常用的照明像素颜色中拿走颜色以保持平衡。所以这里参数t控制表面的反射率。

图17.8显示通过环境映射的反射对于平坦表面不起作用。这是因为反射向量没有讲述整个故事,因为它没有包含位置;我们确实需要一个反射射线并将射线与环境贴图相交。射线有位置和方向,而矢量只有方向。从图中我们看到,两个反射射线r(t)= p + tv和r’(t)= p’+ tv与立方体贴图的不同纹理元素相交,因此应该有不同的颜色。然而,因为两个射线具有相同的方向矢量v,并且方向矢量v仅用于立方体映射查找,所以当眼睛分别处于e和e’时,相同的纹理元素被映射到p和p’。对于扁平物体,这种环境映射的缺陷非常明显。对于弯曲曲面,由于曲面的曲率导致反射向量变化,因此环境映射的这个缺点很难被忽略。请参阅[Brennan02]以获得针对此问题的近似解决方案。


图17.8 当眼睛位于位置e和e’时,反射向量分别对应于两个不同的点p和p’。

17.5 动态立方体贴图

到目前为止,我们已经描述了静态立方体贴图,其中存储在立方体贴图中的图像被预先制作和固定。 这适用于许多情况并且相对便宜。 但是,假设我们希望动画演员在我们的场景中移动。 使用预先生成的立方体贴图,您无法捕获这些动画对象,这意味着我们无法反射动画对象。 为了克服这个限制,我们可以在运行时建立立方体贴图。 也就是说,将相机放置在场景中的每一帧都将作为立方体贴图的原点,然后将场景沿着每个坐标轴方向渲染到每个立方体贴图面上六次(参见图17.9)。 由于立方体贴图会在每一帧中重建,因此它将捕获环境中的动画对象,并且反射也会动画(参见图17.10)。


图17.9 相机放置在场景中的位置0处,以我们想要生成相对于动态立方体贴图的对象为中心。 我们沿着每个坐标轴方向以90°的视角渲染场景6次,以便拍摄整个周围环境的图像。


图17.10 展示动态反射的“Dynamic CubeMap”演示的屏幕截图。 颅骨围绕中心球体运动,并在球体中反射。 此外,因为我们自己绘制立方体贴图,所以我们也可以模拟局部对象的反射,例如列,球体和地板。

NOTE:动态渲染立方体贴图非常昂贵。 它需要渲染场景到6个渲染目标! 因此,尽量减少场景中所需的动态立方体贴图的数量。 例如,也许只能使用场景中要显示或突出显示的关键对象的动态反射。 然后将静态立方体贴图用于较不重要的对象,在这些对象中动态反射可能会被忽视或不会被忽略。 通常,低分辨率立方体贴图用于动态立方体贴图,例如256×256,以节省填充率。

17.5.1构建多维数据集贴图并渲染目标视图

通过创建一个包含六个元素(每个面一个)的纹理数组并创建D3D11_TEXTURE2D_DESC :: MiscFlags的D3D11_RESOURCE_MISC_TEXTURECUBE来创建立方体贴图纹理,以便Direct3D知道将纹理数组解释为立方体贴图。

static const int CubeMapSize = 256;
//
// Cubemap is a special texture array with 6 elements. We
// bind this as a render target to draw to the cube faces,
// and also as a shader resource, so we can use it in a pixel shader.
//
D3D11_TEXTURE2D_DESC texDesc;
texDesc.Width = CubeMapSize;
texDesc.Height = CubeMapSize;
texDesc.MipLevels = 0;
texDesc.ArraySize = 6;
texDesc.SampleDesc.Count = 1;
texDesc.SampleDesc.Quality = 0;
texDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
texDesc.Usage = D3D11_USAGE_DEFAULT;
texDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
texDesc.CPUAccessFlags = 0;
texDesc.MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS |
D3D11_RESOURCE_MISC_TEXTURECUBE;
ID3D11Texture2D* cubeTex = 0;
HR(md3dDevice->CreateTexture2D(&texDesc, 0, &cubeTex));

我们还指定了D3D11_RESOURCE_MISC_GENERATE_MIPS标志。 该标志允许我们在立方体贴图上调用ID3D11DeviceContext :: GenerateMips方法,该方法使硬件生成纹理的较低mipmap级别。 这是必要的,因为我们只渲染纹理的顶层mipmap级别,所以较低的mipmap级别是未定义的,除非我们自己生成它们或使用GenerateMips方法。

接下来,我们需要为立方体贴图纹理数组中的每个元素创建一个渲染目标视图,以便我们可以渲染到每个立方体贴图面上。 这如下完成:

//
// Create a render target view to each cube map face
// (i.e., each element in the texture array).
//
ID3D11RenderTargetView* mDynamicCubeMapRTV[6];
D3D11_RENDER_TARGET_VIEW_DESC rtvDesc;
rtvDesc.Format = texDesc.Format;
rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY;
rtvDesc.Texture2DArray.MipSlice = 0;
// Only create a view to one array element.
rtvDesc.Texture2DArray.ArraySize = 1;
for(int i = 0; i < 6; ++i)
{
// Create a render target view to the ith element.
rtvDesc.Texture2DArray.FirstArraySlice = i;
HR(md3dDevice->CreateRenderTargetView(
cubeTex, &rtvDesc, &mDynamicCubeMapRTV[i]));
}

最后,我们为立方体贴图纹理创建着色器资源视图,以便在构建渲染反射后可以在像素着色器中对其进行采样。

//
// Create a shader resource view to the cube map.
//
ID3D11ShaderResourceView* mDynamicCubeMapSRV;
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
srvDesc.Format = texDesc.Format;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURECUBE;
srvDesc.TextureCube.MostDetailedMip = 0;
srvDesc.TextureCube.MipLevels = -1;
HR(md3dDevice->CreateShaderResourceView(
cubeTex, &srvDesc, &mDynamicCubeMapSRV));
// View saves reference.
ReleaseCOM(cubeTex);

17.5.2构建深度缓冲区和视口

通常,立方体贴图面将具有与主要后台缓冲区不同的分辨率。 因此,为了渲染到立方体贴图面,我们需要一个深度缓存,其尺寸与立方体贴图面的分辨率相匹配。 这是用下面的代码完成的(现在你应该很熟悉):

static const int CubeMapSize = 256;
ID3D11DepthStencilView* mDynamicCubeMapDSV;
D3D11_TEXTURE2D_DESC depthTexDesc;
depthTexDesc.Width = CubeMapSize;
depthTexDesc.Height = CubeMapSize;
depthTexDesc.MipLevels = 1;
depthTexDesc.ArraySize = 1;
depthTexDesc.SampleDesc.Count = 1;
depthTexDesc.SampleDesc.Quality = 0;
depthTexDesc.Format = DXGI_FORMAT_D32_FLOAT;
depthTexDesc.Usage = D3D11_USAGE_DEFAULT;
depthTexDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
depthTexDesc.CPUAccessFlags = 0;
depthTexDesc.MiscFlags = 0;
ID3D11Texture2D* depthTex = 0;
HR(md3dDevice->CreateTexture2D(&depthTexDesc, 0, &depthTex));
// Create the depth stencil view for the entire buffer.
D3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc;
dsvDesc.Format = depthTexDesc.Format;
dsvDesc.Flags = 0;
dsvDesc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
dsvDesc.Texture2D.MipSlice = 0;
HR(md3dDevice->CreateDepthStencilView(depthTex,
&dsvDesc, &mDynamicCubeMapDSV));
// View saves reference.
ReleaseCOM(depthTex);
Moreover, because the cube map faces will have a different resolution than the main back buffer, we need to define a new
viewport that covers a cube map face:
D3D11_VIEWPORT mCubeMapViewport;
mCubeMapViewport.TopLeftX = 0.0f;
mCubeMapViewport.TopLeftY = 0.0f;
mCubeMapViewport.Width = (float)CubeMapSize;
mCubeMapViewport.Height = (float)CubeMapSize;
mCubeMapViewport.MinDepth = 0.0f;
mCubeMapViewport.MaxDepth = 1.0f;

17.5.3设置魔方映射相机

回想一下,要生成立方体贴图的想法是将相机放置在具有90°视场角(垂直和水平方向)的场景中某个对象0的中心位置。 然后让摄像机向下看正X轴,负X轴,正Y轴,负Y轴,正Z轴和负Z轴,拍摄场景图像(不包括对象 0)来自这六个观点中的每一个。 为了实现这一点,我们生成了六个摄像头,每个人脸一个,以给定位置(x,y,z)为中心:

Camera mCubeMapCamera[6];
void DynamicCubeMapApp::BuildCubeFaceCamera(float x, float y, float z)
{
// Generate the cube map about the given position.
XMFLOAT3 center(x, y, z);
XMFLOAT3 worldUp(0.0f, 1.0f, 0.0f);
// Look along each coordinate axis.
XMFLOAT3 targets[6] =
{
XMFLOAT3(x+1.0f, y, z), // +X
XMFLOAT3(x-1.0f, y, z), // -X
XMFLOAT3(x, y+1.0f, z), // +Y
XMFLOAT3(x, y-1.0f, z), // -Y
XMFLOAT3(x, y, z+1.0f), // +Z
XMFLOAT3(x, y, z-1.0f) // -Z
};
// Use world up vector (0,1,0) for all directions except +Y/-Y.
// In these cases, we are looking down +Y or -Y, so we need a
// different "up" vector.
XMFLOAT3 ups[6] =
{
XMFLOAT3(0.0f, 1.0f, 0.0f), // +X
XMFLOAT3(0.0f, 1.0f, 0.0f), // -X
XMFLOAT3(0.0f, 0.0f, -1.0f), // +Y
XMFLOAT3(0.0f, 0.0f, +1.0f), // -Y
XMFLOAT3(0.0f, 1.0f, 0.0f), // +Z
XMFLOAT3(0.0f, 1.0f, 0.0f) // -Z
};
for(int i = 0; i < 6; ++i)
{
mCubeMapCamera[i].LookAt(center, targets[i], ups[i]);
mCubeMapCamera[i].SetLens(0.5f*XM_PI, 1.0f, 0.1f, 1000.0f);
mCubeMapCamera[i].UpdateViewMatrix();
}
}

17.5.4 绘制到立方体贴图

最后,我们的DrawScene方法首先将场景绘制到立方体贴图,除了我们正在生成立方体贴图的中心球体外,然后像往常一样绘制场景,并将动态立方体贴图应用于中心球体。

void DynamicCubeMapApp::DrawScene()
{
ID3D11RenderTargetView* renderTargets[1];
// Generate the cube map by rendering to each cube map face.
md3dImmediateContext->RSSetViewports(1, &mCubeMapViewport);
for(int i = 0; i < 6; ++i)
{
// Clear cube map face and depth buffer.
md3dImmediateContext->ClearRenderTargetView(
mDynamicCubeMapRTV[i],
reinterpret_cast<const float*>(&Colors::Silver));
md3dImmediateContext->ClearDepthStencilView(
mDynamicCubeMapDSV,
D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL, 1.0f, 0);
// Bind cube map face as render target.
renderTargets[0] = mDynamicCubeMapRTV[i];
md3dImmediateContext->OMSetRenderTargets(
1, renderTargets, mDynamicCubeMapDSV);
// Draw the scene with the exception of the
// center sphere, to this cube map face.
DrawScene(mCubeMapCamera[i], false);
}
// Restore old viewport and render targets.
md3dImmediateContext->RSSetViewports(1, &mScreenViewport);
renderTargets[0] = mRenderTargetView;
md3dImmediateContext->OMSetRenderTargets(
1, renderTargets, mDepthStencilView);
// Have hardware generate lower mipmap levels of cube map.
md3dImmediateContext->GenerateMips(mDynamicCubeMapSRV);
// Now draw the scene as normal, but with the center sphere.
md3dImmediateContext->ClearRenderTargetView(
mRenderTargetView,
reinterpret_cast<const float*>(&Colors::Silver));
md3dImmediateContext->ClearDepthStencilView(
mDepthStencilView,
D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL,
1.0f, 0);
DrawScene(mCam, true);
HR(mSwapChain->Present(0, 0));
}

此代码利用重载的DrawScene方法绘制实际对象; 这个重载方法需要一个Camera参数和一个布尔标志来指示是否绘制中心球体。

void DrawScene(const Camera& camera, bool drawCenterSphere);

所以我们在前面的代码中看到,我们将场景绘制六次,每次使用该面的相应相机对每个立方体贴图面绘制一次,而不是将中心球绘制到立方体贴图。 最后,我们再次绘制场景,绘制中心球体,并使用我们典型的“球员”相机。 当绘制中心球体时,它使用我们刚刚生成的立方体贴图,以便动画正确反映。

17.6带几何式阴影的动态立方体图

在前面的章节中,我们重绘了场景6次以生成立方体贴图 - 每个立方体贴图贴图都会生成一次。 绘制电话并不是免费的,我们应该尽量减少它们。 有一个名为“CubeMapGS”的Direct3D 10样本,几何着色器通过仅绘制一次场景来渲染立方体贴图。 (Direct3D 11是10的超集,因此这种工作方式也适用于Direct3D 11.)在本节中,我们重点介绍此示例的工作原理。

首先,它为整个纹理数组(不是每个单独的人脸纹理)创建一个渲染目标视图:

// Create the 6-face render target view
D3D10_RENDER_TARGET_VIEW_DESC DescRT;
DescRT.Format = dstex.Format;
DescRT.ViewDimension = D3D10_RTV_DIMENSION_TEXTURE2DARRAY;
DescRT.Texture2DArray.FirstArraySlice = 0;
DescRT.Texture2DArray.ArraySize = 6;
DescRT.Texture2DArray.MipSlice = 0;
V_RETURN(pd3dDevice->CreateRenderTargetView(
g_pEnvMap, &DescRT, &g_pEnvMapRTV));

此外,这种技术需要深度缓冲区的立方体贴图(每个面一张)。 深度缓冲区的整个纹理数组的深度模板视图创建如下:

// Create the depth stencil view for the entire cube
D3D10_DEPTH_STENCIL_VIEW_DESC DescDS;
DescDS.Format = DXGI_FORMAT_D32_FLOAT;
DescDS.ViewDimension = D3D10_DSV_DIMENSION_TEXTURE2DARRAY;
DescDS.Texture2DArray.FirstArraySlice = 0;
DescDS.Texture2DArray.ArraySize = 6;
DescDS.Texture2DArray.MipSlice = 0;
V_RETURN(pd3dDevice->CreateDepthStencilView(
g_pEnvMapDepth, &DescDS, &g_pEnvMapDSV));

然后它将此渲染目标和深度模板视图绑定到管道的OM阶段:

ID3D10RenderTargetView* aRTViews[ 1 ] = { g_pEnvMapRTV };
pd3dDevice->OMSetRenderTargets(sizeof(aRTViews)/sizeof(aRTViews[0]),aRTViews, g_pEnvMapDSV);

也就是说,我们已经绑定了一个视图来呈现一系列渲染目标和一个到OM阶段的深度模板缓冲区的视图,并且我们将同时渲染到每个阵列切片。

现在,场景被渲染一次,并且在常量缓冲区中有六个视图矩阵的阵列(每个视图矩阵的每个立方体映射面的相应方向)。 几何着色器复制输入三角形六次,并将三角形分配给六个呈现目标数组切片中的一个。 通过设置系统值SV_RenderTargetArrayIndex完成将三角形分配给渲染目标数组切片。 此系统值是一个整数索引值,只能将其设置为来自几何着色器的输出,以指定该基元应呈现到的呈现目标数组切片的索引。 只有渲染目标视图实际上是数组资源的视图时,才能使用此系统值。

struct PS_CUBEMAP_IN
{
float4 Pos : SV_POSITION; // Projection coord
float2 Tex : TEXCOORD0; // Texture coord
uint RTIndex : SV_RenderTargetArrayIndex;
};
[maxvertexcount(18)]
void GS_CubeMap(triangle GS_CUBEMAP_IN input[3],
inout TriangleStream<PS_CUBEMAP_IN> CubeMapStream)
{
// For each triangle
for(int f = 0; f < 6; ++f)
{
// Compute screen coordinates
PS_CUBEMAP_IN output;
// Assign the ith triangle to the ith render target.
output.RTIndex = f;
// For each vertex in the triangle
for(int v = 0; v < 3; v++)
{
// Transform to the view space of the ith cube face.
output.Pos = mul(input[v].Pos, g_mViewCM[f]);
// Transform to homogeneous clip space.
output.Pos = mul(output.Pos, mProj);
output.Tex = input[v].Tex;
CubeMapStream.Append(output);
}
CubeMapStream.RestartStrip();
}
}

因此我们看到,我们已经通过渲染场景而不是六次渲染场景到每个立方地图面。

NOTE:我们总结了此示例的主要思想,但请参阅“CubeMapGS”Direct3D 10样本以获取完整源代码以填写任何详细信息。

这个策略很有趣,并且演示了同时呈现的目标和SV_RenderTargetArrayIndex系统值;但是,这不是一个明确的胜利。有两个问题使得这种方法不具吸引力:
1.它使用几何着色器来输出一大组数据。回想一下第11章,当输出一大组数据时,几何着色器的效率低下。因此,使用几何着色器可能会损害性能。

2.在一个典型的场景中,一个三角形不会重叠多个立方体贴图面(见图17.9)。因此,当复制一个三角形并将其渲染到每个立方体面上时,它将被6个面中的5个剪裁,这是非常浪费的行为。无可否认,我们在§5.4中的演示也将整个场景呈现给每个立方体贴图面以简化。但是,在真实应用程序(非演示)中,我们将使用截锥体剔除(第15章),并仅将对象呈现为特定立方体贴图面。几何着色器实现无法完成对象级别的剔除。

另一方面,这种策略确实运行良好的情况是渲染场景周围的网格。 例如,假设你有一个动态的天空系统,云层移动,天空的颜色根据一天中的时间变化。由于天空在变化,我们不能使用预焙立方体贴图纹理来反射天空,所以我们必须 使用动态立方体贴图。由于天空网格围绕整个场景,所有六个立方体贴图面都可以看到它。 因此,以前的第二个项目符号不适用,并且几何着色器方法可以通过将绘制调用从六个减少为一个来获胜,假定几何着色器的使用不会太多地损害性能。

17.7总结

1.立方体贴图由六个纹理组成,我们将其视为立方体的面。在Direct3D 11中,立方体贴图可以由ID3D11Texture2D接口表示为具有附加标记D3D11_RESOURCE_MISC_TEXTURECUBE的贴图阵列。在HLSL中,立方体贴图由TextureCube类型表示。为了识别立方体贴图中的纹理元素,我们使用3D纹理坐标,这些坐标定义了起始于立方体贴图中心的3D查找矢量v。 v相交的立方体贴图的纹理元素是对应于v的3D坐标的纹理元素。

2.环境地图用六幅图像捕捉周围环境。这些图像可以存储在立方体贴图中。使用环境贴图,我们可以轻松地构建天空或近似反射。

3.使用DirectX纹理工具可以从六个单独的图像制作立方体贴图。然后可以将立方体贴图保存为DDS图像格式的文件。由于立方体贴图存储了六个2D纹理,可能会消耗大量内存,因此应使用压缩的DDS格式。 D3DX11CreateShaderResourceViewFromFile函数可以加载存储立方体贴图的DDS文件并为其创建着色器资源视图。

4.预焙立方体贴图不会捕获移动的对象或生成立方体贴图时不存在的场景中的对象。为了克服这个限制,我们可以在运行时建立立方体贴图。也就是说,对于每个帧,您将相机放置在要作为立方体贴图原点的场景中,然后将场景沿着每个坐标轴方向呈现到每个立方体贴图面上六次。由于立方体贴图每帧重建,它将捕获动画对象和环境中的每个对象。动态立方体贴图非常昂贵,它们的使用应该最小化为关键对象。

5.我们可以将渲染目标视图绑定到OM阶段的纹理阵列。而且,我们可以同时渲染纹理阵列中的每个阵列片。通过设置系统值SV_RenderTargetArrayIndex完成将三角形分配给渲染目标数组切片。纹理数组的渲染目标视图以及SV_RenderTargetArrayIndex系统值允许通过渲染场景一次而不是六次来动态生成立方体贴图。然而,这种策略可能并不总是通过截锥体扑杀六次来渲染场景。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值