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

前言

从现在开始可以说算是要进入到高级主题部分了。这一章我们主要学习由6个纹理所构成的立方体映射,以及用它来实现一个静态天空盒。

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

立方体映射(Cube Mapping)

一个立方体(通常是正方体)包含六个面,对于立方体映射来说,它的六个面对应的是六张纹理贴图,然后以该立方体建系,中心为原点,且三个座标轴是轴对齐的。我们可以使用方向向量(±X,±Y,±Z),从原点开始,发射一条射线(取方向向量的方向)来与某个面产生交点,取得该纹理交点对应的颜色。

注意:

  1. 方向向量的大小并不重要,只要方向一致,那么不管长度是多少,最终选择的纹理和取样的像素都是一致的。
  2. 使用方向向量时要确保所处的座标系和立方体映射所处的座标系一致,如方向向量和立方体映射同时处在世界座标系中。

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表面;

使用立方体映射意味着我们需要使用3D纹理座标进行寻址。

在HLSL中,立方体纹理用TextureCube来表示。

环境映射(Environment Maps)

关于立方体映射,应用最广泛的就是环境映射了。为了获取一份环境映射,我们可以将摄像机绑定到一个物体的中心(或者摄像机本身视为一个物体),然后使用90°的垂直FOV和水平FOV(即宽高比1:1),再让摄像机朝着±X轴、±Y轴、±Z轴共6个轴的方向各拍摄一张不包括物体本身的场景照片。因为FOV的角度为90°,这六张图片已经包含了以物体中心进行的透视投影,所记录的完整的周遭环境。接下来就是将这六张图片保存在立方体纹理中,以构成环境映射。综上所述,环境映射就是在立方体表面的纹理中存储了周围环境的图像。

由于环境映射仅捕获了远景的信息,这样附近的许多物体都可以共用同一个环境映射。这种做法称之为静态立方体映射,它的优点是仅需要六张纹理就可以轻松实现,但缺陷是该环境映射并不会记录临近物体信息,在绘制反射时就看不到周围的物体了。

注意到环境映射所使用的六张图片不一定非得是从Direct3D程序中捕获的。因为立方体映射仅存储纹理数据,它们的内容通常可以是美术师预先生成的,或者是自己找到的。

一般来说,我们能找到的天空盒有如下三种:

  1. 已经创建好的.dds文件,可以直接通过DDSTextureLoader读取使用
  2. 6张天空盒的正方形贴图,格式不限。(暂不考虑只有5张的)
  3. 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个步骤:

  1. 读取天空盒的贴图
  2. 创建包含6个纹理的数组
  3. 选取原天空盒纹理的6个子正方形区域,拷贝到该数组中
  4. 创建立方体纹理的SRV

而将六张天空盒的正方形贴图转换成立方体需要经历这4个步骤:

  1. 读取这六张正方形贴图
  2. 创建包含6个纹理的数组
  3. 将这六张贴图完整地拷贝到该数组中
  4. 创建立方体纹理的SRV

可以看到这两种类型的天空盒资源在处理上有很多相似的地方。

在d3dUtil.h中,提供了CreateWICTextureCubeFromFile的重载函数,原型如下:

//
// 纹理立方体相关函数
//

// 根据给定的一张包含立方体六个面的纹理,创建纹理立方体
// 要求纹理宽高比为4:3,且按下面形式布局:
// .  +Y .  .
// -X +Z +X -Z 
// .  -Y .  .
// 该函数默认不生成mipmap(即等级仅为1),若需要则设置generateMips为true
Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> CreateWICTextureCubeFromFile(
    Microsoft::WRL::ComPtr<ID3D11Device> device,
    Microsoft::WRL::ComPtr<ID3D11DeviceContext> deviceContext,
    std::wstring cubemapFileName,
    bool generateMips = false);

// 根据按D3D11_TEXTURECUBE_FACE索引顺序给定的六张纹理,创建纹理立方体
// 要求纹理是同样大小的正方形
// 该函数默认不生成mipmap(即等级仅为1),若需要则设置generateMips为true
Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> CreateWICTextureCubeFromFile(
    Microsoft::WRL::ComPtr<ID3D11Device> device,
    Microsoft::WRL::ComPtr<ID3D11DeviceContext> deviceContext,
    std::vector<std::wstring> cubemapFileNames,
    bool generateMips = false);

1.读取天空盒纹理

CreateWICTextureFromFileEx函数--使用更多的参数,从文件中读取WIC纹理

HRESULT __cdecl 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_DEFAULTcpuAccessFlags设为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_TARGETD3D11_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<ID3D11Texture2D> srcTex;
ComPtr<ID3D11ShaderResourceView> 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());
}

注意srcTexsrcTexSRV都指向同一份资源。

至于读取六张正方形贴图的操作也是一样的,这里就不赘述了。

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<ID3D11Texture2D> texCube;
HR(device->CreateTexture2D(&texCubeDesc, nullptr, texCube.GetAddressOf()));

D3D11_BIND_SHADER_RESOURCED3D11_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纹理(体积纹理)可以看做一系列纹理的堆叠,因此frontback可以用来选定哪些纹理需要节选。

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;
}

至于天空盒的六张正方形贴图的话,我们不需要对原贴图进行裁剪,但还是需要将子资源逐个转移到纹理数组中。为了拷贝整个纹理子资源,需要指定pSrcBoxnullptr:

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<ID3D11ShaderResourceView> texCubeSRV;
HR(device->CreateShaderResourceView(texCube.Get(), &viewDesc, texCubeSRV.GetAddressOf()));

绘制天空盒

尽管天空盒是一个立方体,但是实际上渲染的是一个很大的"球体"(由大量的三角形逼近)表面。使用方向向量来映射到立方体纹理对应的像素颜色,同时它也指向当前绘制的"球"面上对应点。另外,为了保证绘制的天空盒永远处在摄像机能看到的最远处,通常会将该球体的中心设置在摄像机所处的位置。这样无论摄像机如何移动,天空盒也跟随摄像机移动,用户将永远到不了天空盒的一端。可以说这和公告板一样,都是一种欺骗人眼的小技巧。如果不让天空盒跟随摄像机移动,这种假象立马就会被打破。

天空球体和纹理立方体的中心一致,不需要管它们的大小关系。

实际绘制的天空球体

绘制天空盒需要以下准备工作:

  1. 将天空盒载入HLSL的TextureCube中
  2. 在光栅化阶段关闭背面消隐
  3. 在输出合并阶段的深度/模板状态,设置深度比较函数为小于等于,以允许深度值为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.hlslSky_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<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%反射)。因此,我们将原来的光照等式加上了材质反射的分量。当初MaterialReflect成员现在就派上了用场:

// 物体表面材质
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类

SkyRender类支持之前所述的3种天空盒的加载,由于在构造的同时还会创建球体,建议使用unique_ptr来管理对象。

下面是SkyRender的完整实现:

class SkyRender
{
public:
    template<class T>
    using ComPtr = Microsoft::WRL::ComPtr<T>;


    // 需要提供完整的天空盒贴图 或者 已经创建好的天空盒纹理.dds文件
    SkyRender(ComPtr<ID3D11Device> device, 
        ComPtr<ID3D11DeviceContext> deviceContext, 
        const std::wstring& cubemapFilename, 
        float skySphereRadius,
        bool generateMips = false);


    // 需要提供天空盒的六张正方形贴图
    SkyRender(ComPtr<ID3D11Device> device, 
        ComPtr<ID3D11DeviceContext> deviceContext, 
        const std::vector<std::wstring>& cubemapFilenames, 
        float skySphereRadius,
        bool generateMips = false);


    ComPtr<ID3D11ShaderResourceView> GetTextureCube();

    void Draw(ComPtr<ID3D11DeviceContext> deviceContext, SkyEffect& skyEffect, const Camera& camera);

private:
    void InitResource(ComPtr<ID3D11Device> device, float skySphereRadius);

private:
    ComPtr<ID3D11Buffer> mVertexBuffer;
    ComPtr<ID3D11Buffer> mIndexBuffer;

    UINT mIndexCount;

    ComPtr<ID3D11ShaderResourceView> mTextureCubeSRV;
};
SkyRender::SkyRender(
    ComPtr<ID3D11Device> device, 
    ComPtr<ID3D11DeviceContext> deviceContext, 
    const std::wstring & cubemapFilename, 
    float skySphereRadius,
    bool generateMips)
{
    // 天空盒纹理加载
    if (cubemapFilename.substr(cubemapFilename.size() - 3) == L"dds")
    {
        HR(CreateDDSTextureFromFile(
            device.Get(),
            cubemapFilename.c_str(),
            nullptr,
            mTextureCubeSRV.GetAddressOf()
        ));
    }
    else
    {
        mTextureCubeSRV = CreateWICTextureCubeFromFile(
            device,
            deviceContext,
            cubemapFilename,
            generateMips
        );
    }

    InitResource(device, skySphereRadius);
}

SkyRender::SkyRender(ComPtr<ID3D11Device> device, 
    ComPtr<ID3D11DeviceContext> deviceContext, 
    const std::vector<std::wstring>& cubemapFilenames, 
    float skySphereRadius,
    bool generateMips)
{
    // 天空盒纹理加载

    mTextureCubeSRV = CreateWICTextureCubeFromFile(
        device,
        deviceContext,
        cubemapFilenames,
        generateMips
    );

    InitResource(device, skySphereRadius);
}

ComPtr<ID3D11ShaderResourceView> SkyRender::GetTextureCube()
{
    return mTextureCubeSRV;
}

void SkyRender::Draw(ComPtr<ID3D11DeviceContext> deviceContext, SkyEffect & skyEffect, const Camera & camera)
{
    UINT strides[1] = { sizeof(XMFLOAT3) };
    UINT offsets[1] = { 0 };
    deviceContext->IASetVertexBuffers(0, 1, mVertexBuffer.GetAddressOf(), strides, offsets);
    deviceContext->IASetIndexBuffer(mIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);

    XMFLOAT3 pos = camera.GetPosition();
    skyEffect.SetWorldViewProjMatrix(XMMatrixTranslation(pos.x, pos.y, pos.z) * camera.GetViewProjXM());
    skyEffect.SetTextureCube(mTextureCubeSRV);
    skyEffect.Apply(deviceContext);
    deviceContext->DrawIndexed(mIndexCount, 0, 0);
}

void SkyRender::InitResource(ComPtr<ID3D11Device> device, float skySphereRadius)
{
    Geometry::MeshData sphere = Geometry::CreateSphere(skySphereRadius);
    size_t size = sphere.vertexVec.size();
    std::vector<XMFLOAT3> vertices(size);
    for (size_t i = 0; i < size; ++i)
    {
        vertices[i] = sphere.vertexVec[i].pos;
    }

    // 顶点缓冲区创建
    D3D11_BUFFER_DESC vbd;
    vbd.Usage = D3D11_USAGE_IMMUTABLE;
    vbd.ByteWidth = sizeof(XMFLOAT3) * (UINT)vertices.size();
    vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
    vbd.CPUAccessFlags = 0;
    vbd.MiscFlags = 0;
    vbd.StructureByteStride = 0;

    D3D11_SUBRESOURCE_DATA InitData;
    InitData.pSysMem = vertices.data();

    HR(device->CreateBuffer(&vbd, &InitData, &mVertexBuffer));

    // 索引缓冲区创建
    mIndexCount = (UINT)sphere.indexVec.size();

    D3D11_BUFFER_DESC ibd;
    ibd.Usage = D3D11_USAGE_IMMUTABLE;
    ibd.ByteWidth = sizeof(WORD) * mIndexCount;
    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, &mIndexBuffer));

}

与其配套的SkyEffect可以在源码中观察到。

项目演示

说了那么多内容,是时候看一些动图了吧。

该项目加载了三种类型的天空盒,可以随时切换。

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章