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

前言

这一章咱们主要学习由6个纹理所构成的立方体映射,以及用它来实现一个静态天空盒。html

可是在此以前先要消除两个误区:git

  1. 认为这一章的天空盒就是简单的在一个超大立方体的六个面内部贴上天空盒纹理;
  2. 认为天空盒的顶点都是固定的,距离起始点的位置特别远。

我提出这两个误区,是由于看到有些人的做品直接贴了六个立方体,就说本身用到了天空盒技术,可是当你真正学这一章的话会发现此天空盒非彼天空盒,并且该篇教程除了天空盒的技术实现外,还有其他的一些干货值得学习,建议认真研读。github

在此以前还须要回顾一下里面有关纹理子资源的部分:数组

章节回顾
深刻理解与使用2D纹理资源(重点了解纹理子资源、纹理数组和纹理天空盒)

DirectX11 With Windows SDK完整目录网络

Github项目源码app

欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。ide

立方体映射(Cube Mapping)

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

注意:函数

  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填写正方形纹理的像素宽度和高度

若是你须要自动生成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个步骤:

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

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

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

能够看到这两种类型的天空盒资源在处理上有不少类似的地方。

有关天空盒读取的代码实现若是你想了解,须要回到开头了解"深刻理解与使用2D纹理资源"这章

绘制天空盒

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

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

实际绘制的天空球体

绘制天空盒须要如下准备工做:

  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 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%反射)。所以,咱们将原来的光照等式加上了材质反射的份量。当初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(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完整目录

Github项目源码

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

相关文章
相关标签/搜索