DirectX11 With Windows SDK--22 立方体映射:静态天空盒的读取与实现

阅读目录 前言 立方体映射(Cube Mapping) 环境映射(Environment Maps) 使用DXTex构建天空盒 使用代码读取天空盒 1.读取天空盒纹理 CreateWICTextureFromFileEx函数--使用更多的参数,从文件中读取WIC纹理 ID3D11DeviceContext::GenerateMips--为纹理资源视图创建完整的mipmap链 2.创建包含6个纹理的数组 3.选取原天空盒纹理的6个子正方形区域,拷贝到该数组中 D3D11_BOX结构体 ID3D11DeviceContext::CopySubresourceRegion方法--从指定资源选取区域复制到目标资源特定区域 4.创建纹理立方体的着色器资源视图 绘制天空盒 新的深度/模板状态 HLSL代码 模型的反射 SkyRender类 项目演示 回到顶部 前言 从现在开始可以说算是要进入到高级主题部分了。这一章我们主要学习由6个纹理所构成的立方体映射,以及用它来实现一个静态天空盒。 DirectX11 With Windows SDK完整目录 Github项目源码 欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。 回到顶部 立方体映射(Cube Mapping) 一个立方体(通常是正方体)包含六个面,对于立方体映射来说,它的六个面对应的是六张纹理贴图,然后以该立方体建系,中心为原点,且三个坐标轴是轴对齐的。我们可以使用方向向量(±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; 可以看出: 索引0指向+X表面; 索引1指向-X表面; 索引2指向+Y表面; 索引3指向-Y表面; 索引4指向+Z表面; 索引5指向-Z表面; 使用立方体映射意味着我们需要使用3D纹理坐标进行寻址。 在HLSL中,立方体纹理用TextureCube来表示。 回到顶部 环境映射(Environment Maps) 关于立方体映射,应用最广泛的就是环境映射了。为了获取一份环境映射,我们可以将摄像机绑定到一个物体的中心(或者摄像机本身视为一个物体),然后使用90°的垂直FOV和水平FOV(即宽高比1:1),再让摄像机朝着±X轴、±Y轴、±Z轴共6个轴的方向各拍摄一张不包括物体本身的场景照片。因为FOV的角度为90°,这六张图片已经包含了以物体中心进行的透视投影,所记录的完整的周遭环境。接下来就是将这六张图片保存在立方体纹理中,以构成环境映射。综上所述,环境映射就是在立方体表面的纹理中存储了周围环境的图像。 由于环境映射仅捕获了远景的信息,这样附近的许多物体都可以共用同一个环境映射。这种做法称之为静态立方体映射,它的优点是仅需要六张纹理就可以轻松实现,但缺陷是该环境映射并不会记录临近物体信息,在绘制反射时就看不到周围的物体了。 注意到环境映射所使用的六张图片不一定非得是从Direct3D程序中捕获的。因为立方体映射仅存储纹理数据,它们的内容通常可以是美术师预先生成的,或者是自己找到的。 一般来说,我们能找到的天空盒有如下三种: 已经创建好的.dds文件,可以直接通过DDSTextureLoader读取使用 6张天空盒的正方形贴图,格式不限。(暂不考虑只有5张的) 1张天空盒贴图,包含了6个面,格式不限,图片宽高比为4:3 对于第三种天空盒,其平面分布如下: 对于其余两种天空盒,这里也提供了3种方法读取。 回到顶部 使用DXTex构建天空盒 准备6张天空盒的正方形贴图,如果是属于上述第三种情况,可以用截屏工具来截取出6张贴图,但是要注意按原图的分辨率来进行截取。 打开放在Github项目中Utility文件夹内的DxTex.exe,新建纹理: Texture Type要选择Cubemap Texture Dimensions填写正方形纹理的像素宽度和高度,因为1024x1024的纹理最多可以生成11级mipmap链,这里设置成11.但如果你不需要mipmap链,则直接指定为1. 对于Surface/Volume Format,通常情况下使用Unsigned 32-bit: A8R8G8B8格式,如果想要节省内存(但是会牺牲质量),可以选用Four CC 4-bit: DXT1格式,可以获得6:1甚至8:1的压缩比。 创建好后会变成这样: 可以看到当前默认的是+X纹理。 接下来就是将这六张图片塞进该立方体纹理中了,选择View-Cube map Face,并选择需要修改的纹理: 在当前项目的Texture文件夹内已经准备好了有6张贴图。 选择File-Open To This Cubemap Face来选择对应的贴图以加载进来即可。每完成当前的面就要切换到下一个面继续操作,直到六个面都填充完毕。 最后就可以点击File-Save As来保存dds文件了。 这种做法需要比较长的前期准备时间,它不适合批量处理。但是在读取上是最方便的。 回到顶部 使用代码读取天空盒 对于创建好的DDS立方体纹理,我们只需要使用DDSTextureLoader就可以很方便地读取进来: HR(CreateDDSTextureFromFile( device.Get(), cubemapFilename.c_str(), nullptr, textureCubeSRV.GetAddressOf())); 然而从网络上能够下到的天空盒资源经常要么是一张天空盒贴图,要么是六张天空盒的正方形贴图,用DXTex导入还是比较麻烦的一件事情。我们也可以自己编写代码来构造立方体纹理。 将一张天空盒贴图转化成立方体纹理需要经历以下4个步骤: 读取天空盒的贴图 创建包含6个纹理的数组 选取原天空盒纹理的6个子正方形区域,拷贝到该数组中 创建立方体纹理的SRV 而将六张天空盒的正方形贴图转换成立方体需要经历这4个步骤: 读取这六张正方形贴图 创建包含6个纹理的数组 将这六张贴图完整地拷贝到该数组中 创建立方体纹理的SRV 可以看到这两种类型的天空盒资源在处理上有很多相似的地方。 在d3dUtil.h中,提供了CreateWICTextureCubeFromFile的重载函数,原型如下: // // 纹理立方体相关函数 // // 根据给定的一张包含立方体六个面的纹理,创建纹理立方体 // 要求纹理宽高比为4:3,且按下面形式布局: // . +Y . . // -X +Z +X -Z // . -Y . . // 该函数默认不生成mipmap(即等级仅为1),若需要则设置generateMips为true Microsoft::WRL::ComPtr CreateWICTextureCubeFromFile( Microsoft::WRL::ComPtr device, Microsoft::WRL::ComPtr deviceContext, std::wstring cubemapFileName, bool generateMips = false); // 根据按D3D11_TEXTURECUBE_FACE索引顺序给定的六张纹理,创建纹理立方体 // 要求纹理是同样大小的正方形 // 该函数默认不生成mipmap(即等级仅为1),若需要则设置generateMips为true Microsoft::WRL::ComPtr CreateWICTextureCubeFromFile( Microsoft::WRL::ComPtr device, Microsoft::WRL::ComPtr deviceContext, std::vector cubemapFileNames, bool generateMips = false); 1.读取天空盒纹理 CreateWICTextureFromFileEx函数--使用更多的参数,从文件中读取WIC纹理 HRESULT CreateWICTextureFromFileEx( ID3D11Device* d3dDevice, // [In]D3D设备 ID3D11DeviceContext* d3dContext, // [In]D3D设备上下文(可选) const wchar_t* szFileName, // [In].bmp/.jpg/.png文件名 size_t maxsize, // [In]默认填0,否则图片会根据该像素大小进行缩放 D3D11_USAGE usage, // [In]D3D11_USAGE枚举值类型,指定CPU/GPU读写权限 unsigned int bindFlags, // [In]绑定标签,指定它可以被绑定到什么对象上 unsigned int cpuAccessFlags, // [In]CPU访问权限标签 unsigned int miscFlags, // [In]杂项标签 unsigned int loadFlags, // [In]WIC_LOADER_FLAGS枚举值类型,用于指定SRGB ID3D11Resource** texture, // [Out]获取创建好的纹理(可选) ID3D11ShaderResourceView** textureView);// [Out]获取创建好的纹理资源视图(可选) } 关于纹理的拷贝操作可以不需要从GPU读到CPU再进行,而是直接在GPU之间进行拷贝,因此可以将usage设为D3D11_USAGE_DEFAULT,cpuAccessFlags设为0. 现在先不演示使用方法。由于通过该函数读取进来的纹理mipmap等级只有1,如果还需要创建mipmap链的话,还需要用到下面的方法。 ID3D11DeviceContext::GenerateMips--为纹理资源视图创建完整的mipmap链 void ID3D11DeviceContext::GenerateMips( ID3D11ShaderResourceView *pShaderResourceView // [In]需要创建mipamp链的SRV ); 比如一张1024x1024的纹理,经过该方法调用后,就会生成剩余的512x512, 256x256 ... 1x1的子纹理资源,加起来一共是11级mipmap。 但是在调用该方法之前,需要确保所使用的纹理bindFlags需要同时设置D3D11_BIND_RENDER_TARGET和D3D11_BIND_SHADER_RESOURCE标签,然后在miscFlags中设置为D3D11_RESOURCE_MISC_GENERATE_MIPS标签,否则调用无效。 无论是否需要生成mipmap链,D3D11_BIND_SHADER_RESOURCE标签是必须的,因为它很大可能会用在着色器资源的绑定。我们可以在CreateWICTextureFromFile函数的实现中看到: HRESULT DirectX::CreateWICTextureFromFile(ID3D11Device* d3dDevice, ID3D11DeviceContext* d3dContext, const wchar_t* fileName, ID3D11Resource** texture, ID3D11ShaderResourceView** textureView, size_t maxsize) { return CreateWICTextureFromFileEx(d3dDevice, d3dContext, fileName, maxsize, D3D11_USAGE_DEFAULT, D3D11_BIND_SHADER_RESOURCE, 0, 0, WIC_LOADER_DEFAULT, texture, textureView); } 在了解上面这些内容后,我们就可以开始加载天空盒纹理了,然后在用户指定了需要创建mipmap链时再调用ID3D11DeviceContext::GenerateMips方法。现在演示的是单张天空盒纹理的加载: ComPtr srcTex; ComPtr srcTexSRV; // 该资源用于GPU复制 HR(CreateWICTextureFromFileEx(device.Get(), deviceContext.Get(), cubemapFileName.c_str(), 0, D3D11_USAGE_DEFAULT, D3D11_BIND_SHADER_RESOURCE | (generateMips ? D3D11_BIND_RENDER_TARGET : 0), 0, (generateMips ? D3D11_RESOURCE_MISC_GENERATE_MIPS : 0), WIC_LOADER_DEFAULT, (ID3D11Resource**)srcTex.GetAddressOf(), (generateMips ? srcTexSRV.GetAddressOf() : nullptr))); // (可选)生成mipmap链 if (generateMips) { deviceContext->GenerateMips(srcTexSRV.Get()); } 注意srcTex和srcTexSRV都指向同一份资源。 至于读取六张正方形贴图的操作也是一样的,这里就不赘述了。 2.创建包含6个纹理的数组 接下来需要创建一个新的纹理数组。首先需要填充D3D11_TEXTURE2D_DESC结构体内容,这里的大部分参数可以从天空盒纹理取得。 这里以单张天空盒贴图的为例: D3D11_TEXTURE2D_DESC texDesc, texCubeDesc; srcTex->GetDesc(&texDesc); // 确保宽高比4:3 assert(texDesc.Width * 3 == texDesc.Height * 4); UINT squareLength = texDesc.Width / 4; texCubeDesc.Width = squareLength; texCubeDesc.Height = squareLength; // 例如64x48的天空盒,可以产生7级mipmap链,但天空盒的每个面是16x16,对应5级mipmap链,因此需要减2 texCubeDesc.MipLevels = (generateMips ? texDesc.MipLevels - 2 : 1); texCubeDesc.ArraySize = 6; texCubeDesc.Format = texDesc.Format; texCubeDesc.SampleDesc.Count = 1; texCubeDesc.SampleDesc.Quality = 0; texCubeDesc.Usage = D3D11_USAGE_DEFAULT; texCubeDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; texCubeDesc.CPUAccessFlags = 0; texCubeDesc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE; // 标记为TextureCube ComPtr texCube; HR(device->CreateTexture2D(&texCubeDesc, nullptr, texCube.GetAddressOf())); D3D11_BIND_SHADER_RESOURCE和D3D11_RESOURCE_MISC_TEXTURECUBE的标签记得不要遗漏。 3.选取原天空盒纹理的6个子正方形区域,拷贝到该数组中 D3D11_BOX结构体 在进行节选之前,首先我们需要了解定义3D盒的结构体D3D11_BOX: typedef struct D3D11_BOX { UINT left; UINT top; UINT front; UINT right; UINT bottom; UINT back; } D3D11_BOX; 3D box使用的是下面的坐标系,和纹理坐标系很像: 由于选取像素采用的是半开半闭区间,如[left, right),在指定left, top, front的值时会选到该像素,而不对想到right, bottom, back对应的像素。 对于1D纹理来说,是没有Y轴和Z轴的,因此需要令back=0, front=1, top=0, bottom=1才能表示当前的1D纹理,如果出现像back和front相等的情况,则不会选到任何的纹理像素区间。 而2D纹理没有Z轴,在选取像素区域前需要置back=0, front=1。 3D纹理(体积纹理)可以看做一系列纹理的堆叠,因此front和back可以用来选定哪些纹理需要节选。 ID3D11DeviceContext::CopySubresourceRegion方法--从指定资源选取区域复制到目标资源特定区域 void ID3D11DeviceContext::CopySubresourceRegion( ID3D11Resource *pDstResource, // [In/Out]目标资源 UINT DstSubresource, // [In]目标子资源索引 UINT DstX, // [In]目标起始X值 UINT DstY, // [In]目标起始Y值 UINT DstZ, // [In]目标起始Z值 ID3D11Resource *pSrcResource, // [In]源资源 UINT SrcSubresource, // [In]源子资源索引 const D3D11_BOX *pSrcBox // [In]指定复制区域 ); 例如现在我们要将该天空盒的+X面对应的mipmap链拷贝到ArraySlice为0(即D3D11_TEXTURECUBE_FACE_POSITIVE_X)的目标资源中,则可以像下面这样写: D3D11_BOX box; box.front = 0; box.back = 1; for (UINT i = 0; i < texCubeDesc.MipLevels; ++i) { // +X面拷贝 box.left = squareLength * 2; box.top = squareLength; box.right = squareLength * 3; box.bottom = squareLength * 2; deviceContext->CopySubresourceRegion( texCube.Get(), D3D11CalcSubresource(i, D3D11_TEXTURECUBE_FACE_POSITIVE_X, texCubeDesc.MipLevels), 0, 0, 0, srcTex.Get(), i, &box); // 此处省略其余面的拷贝... // 下一个mipLevel的纹理宽高都是原来的1/2 squareLength /= 2; } 至于天空盒的六张正方形贴图的话,我们不需要对原贴图进行裁剪,但还是需要将子资源逐个转移到纹理数组中。为了拷贝整个纹理子资源,需要指定pSrcBox为nullptr: for (int i = 0; i < 6; ++i) { for (UINT j = 0; j < texCubeDesc.MipLevels; ++j) { deviceContext->CopySubresourceRegion( texCube.Get(), D3D11CalcSubresource(j, i, texCubeDesc.MipLevels), 0, 0, 0, srcTex[i].Get(), j, nullptr); } } 4.创建纹理立方体的着色器资源视图 到这一步就简单的多了: D3D11_SHADER_RESOURCE_VIEW_DESC viewDesc; viewDesc.Format = texCubeDesc.Format; viewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURECUBE; viewDesc.TextureCube.MostDetailedMip = 0; viewDesc.TextureCube.MipLevels = texCubeDesc.MipLevels; ComPtr texCubeSRV; HR(device->CreateShaderResourceView(texCube.Get(), &viewDesc, texCubeSRV.GetAddressOf())); 回到顶部 绘制天空盒 尽管天空盒是一个立方体,但是实际上渲染的是一个很大的"球体"(由大量的三角形逼近)表面。使用方向向量来映射到立方体纹理对应的像素颜色,同时它也指向当前绘制的"球"面上对应点。另外,为了保证绘制的天空盒永远处在摄像机能看到的最远处,通常会将该球体的中心设置在摄像机所处的位置。这样无论摄像机如何移动,天空盒也跟随摄像机移动,用户将永远到不了天空盒的一端。可以说这和公告板一样,都是一种欺骗人眼的小技巧。如果不让天空盒跟随摄像机移动,这种假象立马就会被打破。 天空球体和纹理立方体的中心一致,不需要管它们的大小关系。 实际绘制的天空球体 绘制天空盒需要以下准备工作: 将天空盒载入HLSL的TextureCube中 在光栅化阶段关闭背面消隐 在输出合并阶段的深度/模板状态,设置深度比较函数为小于等于,以允许深度值为1的像素绘制 新的深度/模板状态 在RenderStates.h引进了一个新的ID3D11DepthStencilState类型的成员DSSLessEqual,定义如下: D3D11_DEPTH_STENCIL_DESC dsDesc; // 允许使用深度值一致的像素进行替换的深度/模板状态 // 该状态用于绘制天空盒,因为深度值为1.0时默认无法通过深度测试 dsDesc.DepthEnable = true; dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL; dsDesc.DepthFunc = D3D11_COMPARISON_LESS_EQUAL; dsDesc.StencilEnable = false; HR(device->CreateDepthStencilState(&dsDesc, DSSLessEqual.GetAddressOf())); 在绘制天空盒前就需要设置该深度/模板状态: deviceContext->OMSetDepthStencilState(RenderStates::DSSLessEqual.Get(), 0); HLSL代码 现在我们需要一组新的特效来绘制天空盒,其中与之相关的是Sky.hlsli, Sky_VS.hlsl和Sky_PS.hlsl,当然在C++那边还有新的SkyEffect类来管理,需要了解自定义Effect的可以回看第13章。 // Sky.hlsli TextureCube texCube : register(t0); SamplerState sam : register(s0); cbuffer CBChangesEveryFrame : register(b0) { matrix gWorldViewProj; } struct VertexPos { float3 PosL : POSITION; }; struct VertexPosHL { float4 PosH : SV_POSITION; float3 PosL : POSITION; }; // Sky_VS.hlsl #include "Sky.hlsli" VertexPosHL VS(VertexPos vIn) { VertexPosHL vOut; // 设置z = w使得z/w = 1(天空盒保持在远平面) float4 posH = mul(float4(vIn.PosL, 1.0f), gWorldViewProj); vOut.PosH = posH.xyww; vOut.PosL = vIn.PosL; return vOut; } // Sky_PS.hlsl #include "Sky.hlsli" float4 PS(VertexPosHL pIn) : SV_Target { return texCube.Sample(sam, pIn.PosL); } 注意: 在过去,应用程序首先绘制天空盒以取代渲染目标和深度/模板缓冲区的清空。然而“ATI Radeon HD 2000 Programming Gudie"(现在已经404了)建议我们不要这么做。首先,为了获得内部硬件深度优化的良好表现,深度/模板缓冲区需要被显式清空。这对渲染目标同样有效。其次,通常绝大多数的天空会被其它物体给遮挡。因此,如果我们先绘制天空,再绘制物体的话会导致二次绘制,还不如先绘制物体,然后让被遮挡的天空部分不通过深度测试。因此现在推荐的做法为:总是先清空渲染目标和深度/模板缓冲区,天空盒的绘制留到最后。 回到顶部 模型的反射 关于环境映射,另一个主要应用就是模型表面的反射(只有当天空盒记录了除当前反射物体外的其它物体时,才能在该物体看到其余物体的反射)。对于静态天空盒来说,通过模型看到的反射只能看到天空盒本身,因此还是显得不够真实。至于动态天空盒就还是留到下一章再讲。 下图说明了反射是如何通过环境映射运作的。法向量n对应的表面就像是一个镜面,摄像机在位置e,观察点p时可以看到经过反射得到的向量v所指向的天空盒纹理的采样像素点: 首先在之前的Basic.hlsli中加入TextureCube: // Basic.hlsli Texture2D texA : register(t0); Texture2D texD : register(t1); TextureCube texCube : register(t2); SamplerState sam : register(s0); // ... 然后只需要在Basic_PS.hlsl添加如下内容: float4 litColor = texColorA * ambient + texColorD * diffuse + spec; if (gReflectionEnabled) { float3 incident = -toEyeW; float3 reflectionVector = reflect(incident, pIn.NormalW); float4 reflectionColor = texCube.Sample(sam, reflectionVector); litColor += gMaterial.Reflect * reflectionColor; } litColor.a = texColorD.a * gMaterial.Diffuse.a; return litColor; 然后在C++端,将采样器设置为各向异性过滤: // 在RenderStates.h/.cpp可以看到 ComPtr RenderStates::SSAnistropicWrap; D3D11_SAMPLER_DESC sampDesc; ZeroMemory(&sampDesc, sizeof(sampDesc)); // 各向异性过滤模式 sampDesc.Filter = D3D11_FILTER_ANISOTROPIC; sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP; sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP; sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP; sampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER; sampDesc.MaxAnisotropy = 4; sampDesc.MinLOD = 0; sampDesc.MaxLOD = D3D11_FLO
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信