这一章咱们主要学习由6个纹理所构成的立方体映射,以及用它来实现一个静态天空盒。html
可是在此以前先要消除两个误区:git
我提出这两个误区,是由于看到有些人的做品直接贴了六个立方体,就说本身用到了天空盒技术,可是当你真正学这一章的话会发现此天空盒非彼天空盒,并且该篇教程除了天空盒的技术实现外,还有其他的一些干货值得学习,建议认真研读。github
在此以前还须要回顾一下里面有关纹理子资源的部分:数组
章节回顾 |
---|
深刻理解与使用2D纹理资源(重点了解纹理子资源、纹理数组和纹理天空盒) |
DirectX11 With Windows SDK完整目录网络
Github项目源码app
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。ide
一个立方体(一般是正方体)包含六个面,对于立方体映射来讲,它的六个面对应的是六张纹理贴图,而后以该立方体建系,中心为原点,且三个坐标轴是轴对齐的。咱们可使用方向向量(±X,±Y,±Z),从原点开始,发射一条射线(取方向向量的方向)来与某个面产生交点,取得该纹理交点对应的颜色。wordpress
注意:函数
- 方向向量的大小并不重要,只要方向一致,那么无论长度是多少,最终选择的纹理和取样的像素都是一致的。
- 使用方向向量时要确保所处的坐标系和立方体映射所处的坐标系一致,如方向向量和立方体映射同时处在世界坐标系中。
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;
能够看出:
使用立方体映射意味着咱们须要使用3D纹理坐标进行寻址。
在HLSL中,立方体纹理用TextureCube
来表示。
关于立方体映射,应用最普遍的就是环境映射了。为了获取一份环境映射,咱们能够将摄像机绑定到一个物体的中心(或者摄像机自己视为一个物体),而后使用90°的垂直FOV和水平FOV(即宽高比1:1),再让摄像机朝着±X轴、±Y轴、±Z轴共6个轴的方向各拍摄一张不包括物体自己的场景照片。由于FOV的角度为90°,这六张图片已经包含了以物体中心进行的透视投影,所记录的完整的周遭环境。接下来就是将这六张图片保存在立方体纹理中,以构成环境映射。综上所述,环境映射就是在立方体表面的纹理中存储了周围环境的图像。
因为环境映射仅捕获了远景的信息,这样附近的许多物体均可以共用同一个环境映射。这种作法称之为静态立方体映射,它的优势是仅须要六张纹理就能够轻松实现,但缺陷是该环境映射并不会记录临近物体信息,在绘制反射时就看不到周围的物体了。
注意到环境映射所使用的六张图片不必定非得是从Direct3D程序中捕获的。由于立方体映射仅存储纹理数据,它们的内容一般能够是美术师预先生成的,或者是本身找到的。
通常来讲,咱们能找到的天空盒有以下三种:
DDSTextureLoader
读取使用对于第三种天空盒,其平面分布以下:
对于其他两种天空盒,这里也提供了3种方法读取。
准备6张天空盒的正方形贴图,若是是属于上述第三种状况,能够用截屏工具来截取出6张贴图,可是要注意按原图的分辨率来进行截取。
打开放在Github项目中Utility文件夹内的DxTex.exe,新建纹理:
Texture Type
要选择Cubemap Texture
Dimensions
填写正方形纹理的像素宽度和高度
若是你须要自动生成mipmaps,则指定mipmap Level为1.若是你须要手工填充mipmaps,因为1024x1024的纹理mipmap最大数目为11,你能够指定mipmap Level为2-11的值。
对于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来选择对应的贴图以加载进来便可。每完成当前的面就要切换到下一个面继续操做,直到六个面都填充完毕。此时填充的是Mipmap Level为0的子资源:
若是你须要自动生成纹理,则能够点击下面的选项生成,要求建立时MipMap Level为1:
最后就能够点击File-Save As来保存dds文件了。
这种作法须要比较长的前期准备时间,它不适合批量处理。可是在读取上是最方便的。
对于建立好的DDS立方体纹理,咱们只须要使用DDSTextureLoader
就能够很方便地读取进来:
HR(CreateDDSTextureFromFile( device.Get(), cubemapFilename.c_str(), nullptr, textureCubeSRV.GetAddressOf()));
然而从网络上可以下到的天空盒资源常常要么是一张天空盒贴图,要么是六张天空盒的正方形贴图,用DXTex导入仍是比较麻烦的一件事情。咱们也能够本身编写代码来构造立方体纹理。
将一张天空盒贴图转化成立方体纹理须要经历如下4个步骤:
而将六张天空盒的正方形贴图转换成立方体须要经历这4个步骤:
能够看到这两种类型的天空盒资源在处理上有不少类似的地方。
有关天空盒读取的代码实现若是你想了解,须要回到开头了解"深刻理解与使用2D纹理资源"这章
尽管天空盒是一个立方体,可是实际上渲染的是一个很大的"球体"(由大量的三角形逼近)表面。使用方向向量来映射到立方体纹理对应的像素颜色,同时它也指向当前绘制的"球"面上对应点。另外,为了保证绘制的天空盒永远处在摄像机能看到的最远处,一般会将该球体的中心设置在摄像机所处的位置。这样不管摄像机如何移动,天空盒也跟随摄像机移动,用户将永远到不了天空盒的一端。能够说这和公告板同样,都是一种欺骗人眼的小技巧。若是不让天空盒跟随摄像机移动,这种假象立马就会被打破。
天空球体和纹理立方体的中心一致,不须要管它们的大小关系。
实际绘制的天空球体
绘制天空盒须要如下准备工做:
在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);
如今咱们须要一组新的特效来绘制天空盒,其中与之相关的是Sky.hlsli
, Sky_VS.hlsl
和Sky_PS.hlsl
,固然在C++那边还有新的SkyEffect
类来管理,须要了解自定义Effect的能够回看第13章。
// Sky.hlsli TextureCube g_TexCube : register(t0); SamplerState g_Sam : register(s0); cbuffer CBChangesEveryFrame : register(b0) { matrix g_WorldViewProj; } 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), g_WorldViewProj); vOut.PosH = posH.xyww; vOut.PosL = vIn.PosL; return vOut; }
// Sky_PS.hlsl #include "Sky.hlsli" float4 PS(VertexPosHL pIn) : SV_Target { return g_TexCube.Sample(g_Sam, pIn.PosL); }
注意: 在过去,应用程序首先绘制天空盒以取代渲染目标和深度/模板缓冲区的清空。然而“ATI Radeon HD 2000 Programming Gudie"(如今已经404了)建议咱们不要这么作。首先,为了得到内部硬件深度优化的良好表现,深度/模板缓冲区须要被显式清空。这对渲染目标一样有效。其次,一般绝大多数的天空会被其它物体给遮挡。所以,若是咱们先绘制天空,再绘制物体的话会致使二次绘制,还不如先绘制物体,而后让被遮挡的天空部分不经过深度测试。所以如今推荐的作法为:老是先清空渲染目标和深度/模板缓冲区,天空盒的绘制留到最后。
关于环境映射,另外一个主要应用就是模型表面的反射(只有当天空盒记录了除当前反射物体外的其它物体时,才能在该物体看到其他物体的反射)。对于静态天空盒来讲,经过模型看到的反射只能看到天空盒自己,所以仍是显得不够真实。至于动态天空盒就仍是留到下一章再讲。
下图说明了反射是如何经过环境映射运做的。法向量n
对应的表面就像是一个镜面,摄像机在位置e
,观察点p
时能够看到通过反射获得的向量v
所指向的天空盒纹理的采样像素点:
首先在以前的Basic.hlsli
中加入TextureCube
:
// Basic.hlsli Texture2D g_DiffuseMap : register(t0); TextureCube g_TexCube : register(t1); SamplerState g_Sam : register(s0); // ...
而后只须要在Basic_PS.hlsl
添加以下内容:
float4 litColor = texColor * (ambient + diffuse) + spec; if (g_ReflectionEnabled) { float3 incident = -toEyeW; float3 reflectionVector = reflect(incident, pIn.NormalW); float4 reflectionColor = g_TexCube.Sample(g_Sam, reflectionVector); litColor += g_Material.Reflect * reflectionColor; } litColor.a = texColor.a * g_Material.Diffuse.a; return litColor;
而后在C++端,将采样器设置为各向异性过滤:
// 在RenderStates.h/.cpp能够看到 ComPtr<ID3D11SamplerState> 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_FLOAT32_MAX; HR(device->CreateSamplerState(&sampDesc, SSAnistropicWrap.GetAddressOf())); // 在BasicEffect.cpp能够看到 deviceContext->PSSetSamplers(0, 1, RenderStates::SSAnistropicWrap.GetAddressOf());
一般一个像素的颜色不彻底是反射后的颜色(只有镜面才是100%反射)。所以,咱们将原来的光照等式加上了材质反射的份量。当初Material
的Reflect
成员如今就派上了用场:
// 物体表面材质 struct Material { Material() { memset(this, 0, sizeof(Material)); } DirectX::XMFLOAT4 Ambient; DirectX::XMFLOAT4 Diffuse; DirectX::XMFLOAT4 Specular; // w = 镜面反射强度 DirectX::XMFLOAT4 Reflect; };
咱们能够指定该材质的反射颜色,若是该材质只反射完整的红光部分,则在C++指定Reflect = XMFLOAT4(1.0f, 0.0f, 0.0f, 0.0f)
。
使用带加法的反射容易引起一个问题:过分饱和。两个颜色的相加可能会存在RGB值超过1而变白,这会致使某些像素的颜色过于明亮。一般若是咱们添加反射份量的颜色,就必须减少材质自己的环境份量和漫反射份量来实现平衡。另外一种方式就是对反射份量和像素颜色s
进行插值处理:
\[\mathbf{f} = t\mathbf{c}_{R} + (1 - t)\mathbf{s} (0 <= t <= 1) \]
这样咱们就能够经过调整系数t
来控制反射程度,以达到本身想要的效果。
还有一个问题就是,在平面上进行环境映射并不会取得理想的效果。这是由于上面的HLSL代码关于反射的部分只使用了方向向量来进行采样,这会致使以相同的的倾斜角度看平面时,不一样的位置看到的反射效果倒是如出一辙的。正确的效果应该是:摄像机在跟随平面镜作平移运动时,平面镜的映象应该保持不动。下面用两张图来讲明这个问题:
这里给出龙书所提供相关论文,用以纠正环境映射出现的问题: Brennan02
本项目如今不考虑解决这个问题。
SkyRender类支持以前所述的3种天空盒的加载,因为在构造的同时还会建立球体,建议使用unique_ptr
来管理对象。
下面是SkyRender
的完整实现:
class SkyRender { public: template<class T> using ComPtr = Microsoft::WRL::ComPtr<T>; // 须要提供完整的天空盒贴图 或者 已经建立好的天空盒纹理.dds文件 SkyRender(ID3D11Device * device, ID3D11DeviceContext * deviceContext, const std::wstring& cubemapFilename, float skySphereRadius, // 天空球半径 bool generateMips = false); // 默认不为静态天空盒生成mipmaps // 须要提供天空盒的六张正方形贴图 SkyRender(ID3D11Device * device, ID3D11DeviceContext * deviceContext, const std::vector<std::wstring>& cubemapFilenames, float skySphereRadius, // 天空球半径 bool generateMips = false); // 默认不为静态天空盒生成mipmaps ID3D11ShaderResourceView * GetTextureCube(); void Draw(ID3D11DeviceContext * deviceContext, SkyEffect& skyEffect, const Camera& camera); // 设置调试对象名 void SetDebugObjectName(const std::string& name); private: void InitResource(ID3D11Device * device, float skySphereRadius); private: ComPtr<ID3D11Buffer> m_pVertexBuffer; ComPtr<ID3D11Buffer> m_pIndexBuffer; UINT m_IndexCount; ComPtr<ID3D11ShaderResourceView> m_pTextureCubeSRV; };
SkyRender::SkyRender( ID3D11Device * device, ID3D11DeviceContext * deviceContext, const std::wstring & cubemapFilename, float skySphereRadius, bool generateMips) : m_IndexCount() { // 天空盒纹理加载 if (cubemapFilename.substr(cubemapFilename.size() - 3) == L"dds") { HR(CreateDDSTextureFromFile( device, generateMips ? deviceContext : nullptr, cubemapFilename.c_str(), nullptr, m_pTextureCubeSRV.GetAddressOf() )); } else { HR(CreateWICTexture2DCubeFromFile( device, deviceContext, cubemapFilename, nullptr, m_pTextureCubeSRV.GetAddressOf(), generateMips )); } InitResource(device, skySphereRadius); } SkyRender::SkyRender(ID3D11Device * device, ID3D11DeviceContext * deviceContext, const std::vector<std::wstring>& cubemapFilenames, float skySphereRadius, bool generateMips) : m_IndexCount() { // 天空盒纹理加载 HR(CreateWICTexture2DCubeFromFile( device, deviceContext, cubemapFilenames, nullptr, m_pTextureCubeSRV.GetAddressOf(), generateMips )); InitResource(device, skySphereRadius); } ID3D11ShaderResourceView * SkyRender::GetTextureCube() { return m_pTextureCubeSRV.Get(); } void SkyRender::Draw(ID3D11DeviceContext * deviceContext, SkyEffect & skyEffect, const Camera & camera) { UINT strides[1] = { sizeof(XMFLOAT3) }; UINT offsets[1] = { 0 }; deviceContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), strides, offsets); deviceContext->IASetIndexBuffer(m_pIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0); XMFLOAT3 pos = camera.GetPosition(); skyEffect.SetWorldViewProjMatrix(XMMatrixTranslation(pos.x, pos.y, pos.z) * camera.GetViewProjXM()); skyEffect.SetTextureCube(m_pTextureCubeSRV.Get()); skyEffect.Apply(deviceContext); deviceContext->DrawIndexed(m_IndexCount, 0, 0); } void SkyRender::SetDebugObjectName(const std::string& name) { #if (defined(DEBUG) || defined(_DEBUG)) && (GRAPHICS_DEBUGGER_OBJECT_NAME) std::string texCubeName = name + ".CubeMapSRV"; std::string vbName = name + ".VertexBuffer"; std::string ibName = name + ".IndexBuffer"; m_pTextureCubeSRV->SetPrivateData(WKPDID_D3DDebugObjectName, static_cast<UINT>(texCubeName.length()), texCubeName.c_str()); m_pVertexBuffer->SetPrivateData(WKPDID_D3DDebugObjectName, static_cast<UINT>(vbName.length()), vbName.c_str()); m_pIndexBuffer->SetPrivateData(WKPDID_D3DDebugObjectName, static_cast<UINT>(ibName.length()), ibName.c_str()); #else UNREFERENCED_PARAMETER(name); #endif } void SkyRender::InitResource(ID3D11Device * device, float skySphereRadius) { auto sphere = Geometry::CreateSphere<VertexPos>(skySphereRadius); // 顶点缓冲区建立 D3D11_BUFFER_DESC vbd; vbd.Usage = D3D11_USAGE_IMMUTABLE; vbd.ByteWidth = sizeof(XMFLOAT3) * (UINT)sphere.vertexVec.size(); vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER; vbd.CPUAccessFlags = 0; vbd.MiscFlags = 0; vbd.StructureByteStride = 0; D3D11_SUBRESOURCE_DATA InitData; InitData.pSysMem = sphere.vertexVec.data(); HR(device->CreateBuffer(&vbd, &InitData, &m_pVertexBuffer)); // 索引缓冲区建立 m_IndexCount = (UINT)sphere.indexVec.size(); D3D11_BUFFER_DESC ibd; ibd.Usage = D3D11_USAGE_IMMUTABLE; ibd.ByteWidth = sizeof(WORD) * m_IndexCount; ibd.BindFlags = D3D11_BIND_INDEX_BUFFER; ibd.CPUAccessFlags = 0; ibd.StructureByteStride = 0; ibd.MiscFlags = 0; InitData.pSysMem = sphere.indexVec.data(); HR(device->CreateBuffer(&ibd, &InitData, &m_pIndexBuffer)); }
与其配套的SkyEffect
能够在源码中观察到。
说了那么多内容,是时候看一些动图了吧。
该项目加载了三种类型的天空盒,能够随时切换。
DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。