代码工程地址:
https://github.com/jiabaodan/Direct12BookReadingNotes
学习目标
- 学习什么是立方体贴图,并且如何在HLSL中对它们采样;
- 如何使用DX的纹理工具创建立方体贴图;
- 学习如何用立方体贴图来模仿反射;
- 学习如何使用立方体贴图对球体采样来模拟一个天空和远处的山。
1 立方体纹理映射
在Direct3D中,立方体纹理是使用一组具有6个元素的纹理数组:
1、索引0代表指向+X面;
2、索引1代表指向-X面;
3、索引2代表指向+Y面;
4、索引3代表指向-Y面;
5、索引4代表指向+Z面;
6、索引5代表指向-Z面;
相对于2D纹理,我们不能再用2D纹理坐标来采样,需要使用3D纹理坐标(代表看向的方向)来采样。在第九章中介绍的纹理滤波器对于立方体纹理依然适用。
对于立方体纹理采样,查找向量的长度是不重要的,只有方向重要;如果两个具有相同方向但是不同长度的向量,采样出来的结果是一致的。
在HLSL中立方体纹理是TextureCube类型,下面的代码段展示了如何采样:
TextureCube gCubeMap;
SamplerState gsamLinearWrap : register(s2);
…
// in pixel shader
float3 v = float3(x,y,z); // some lookup vector
查找向量应该和立方体纹理关联的坐标系是一致的,否则采样结果会不正确。
2 环境贴图
对于立方体贴图最主要的应用就是环境纹理映射(environment mapping)。它的思路就是将相机固定在一个位置,然后朝6个方向分别拍摄图片,然后组成立方体纹理来模拟环境:
根据上面的描述,我们需要对场景中的每个物体创建用以环境纹理采样的环境贴图,这样会更准确,但是也需要更多的纹理内存。有一种折中的办法是,在场景中几个重要的点上创建环境纹理,然后物体从最近的环境纹理上进行采样;这个方法在应用中效果不错,因为对于曲面物体,不正确的采样很难让玩家察觉。还有一种简化的方法是省略场景中一些特定的物品:比如只拍摄远处的山和天空,近处的物体直接省略掉。如果要拍摄近处的物体,我们就需要使用Direct3D来渲染6个图片,这个在本章第五节中讲解。
如果相机向下拍摄的图片是是在世界坐标的轴,那么这个环境贴图我们就说是与世界坐标系相关。
因为立方体贴图只是保存了纹理数据,所以它可以让艺术家提前制作,而不需要在D3D中实时渲染。对于户外环境,可以使用软件Terragen(http://www.planetside.co.uk/)来生成。
如果你尝试使用Terragen,你需要到摄像机设置中,设置缩放因子为1.0来达到一个90度的视野。同时设置输出图片的维度要相等,这样水平和竖直方向的视野就都是相等的90度。
(https://developer.valvesoftware.com/wiki/Skybox_(2D)_with_Terragen)中有一个很好用的Terragen脚本,会使用当前摄像机位置,渲染6个立方体贴图使用的纹理。
DDS纹理贴图格式可以支持立方体贴图,并且我们可以使用texassemble工具通过6个图像来创建立方体贴图。下面是使用texassemble创建的一个例子:
texassemble -cube -w 256 -h 256 -o cubemap.dds
lobbyxposjpg lobbyxneg.jpg lobbyypos.jpg
lobbyyneg.jpg lobbyzpos.jpg lobbyzneg.jpg
NVIDIA提供了一个PS的保存.DDS格式立方体贴图的插件:https://developer.nvidia.com/nvidia-texture-tools-adobe-photoshop 。
2.1 在D3D中加载和使用立方体贴图
我们的DDS纹理加载代码(DDSTextureLoader.h/.cpp)已经支持的对立方体贴图的加载。加载代码会检测出包含的立方体贴图,然后创建纹理数组并加载它们:
auto skyTex = std::make_unique<Texture>();
skyTex->Name = "skyTex";
skyTex->Filename = L"Textures/grasscube1024.dds";
ThrowIfFailed(DirectX::CreateDDSTextureFromFile12(md3dDevice.mCommandList.Get(), skyTex->Filename.c_str(),
skyTex->Resource, skyTex->UploadHeap));
在SRV中使用D3D12_SRV_DIMENSION_TEXTURECUBE维度和TextureCube属性:
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Shader4ComponentMapping =
D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE;
srvDesc.TextureCube.MostDetailedMip = 0;
srvDesc.TextureCube.MipLevels = skyTex->GetDesc().MipLevels;
srvDesc.TextureCube.ResourceMinLODClamp = 0.0f;
srvDesc.Format = skyTex->GetDesc().Format;
md3dDevice->CreateShaderResourceView(skyTex.Get(), &srvDesc, hDescriptor);
3 纹理映射一个天空
创建一个大的球体然后映射一个环境贴图:
我们假设天空球是无限远的,并且把它在世界坐标系下的位置设置为何摄像机一致。
着色器文件代码如下:
//*********************************************************************
// Sky.hlsl by Frank Luna (C) 2015 All Rights Reserved.
//*********************************************************************
// Include common HLSL code.
#include "Common.hlsl"
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 TexC : TEXCOORD;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosL : POSITION;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Use local vertex position as cubemap lookup vector.
vout.PosL = vin.PosL;
// Transform to world space.
float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
// Always center sky about camera.
posW.xyz += gEyePosW;
// Set z = w so that z/w = 1 (i.e., skydome always on far plane).
vout.PosH = mul(posW, gViewProj).xyww;
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
return gCubeMap.Sample(gsamLinearWrap, pin.PosL);
}
绘制天空的着色器程序明显和我们绘制物体的着色器程序不同,但是他们分享了相同的根签名,所以我们不需要切换根签名。下面的代码在Default.hlsl和Sky.hlsl是一样的,所以直接移到了Common.hlsl里面,来防止代码重复:
//****************************************************************************
// Common.hlsl by Frank Luna (C) 2015 All Rights Reserved.
//****************************************************************************
// Defaults for number of lights.
#ifndef NUM_DIR_LIGHTS
#define NUM_DIR_LIGHTS 3
#endif
#ifndef NUM_POINT_LIGHTS
#define NUM_POINT_LIGHTS 0
#endif
#ifndef NUM_SPOT_LIGHTS
#define NUM_SPOT_LIGHTS 0
#endif
// Include structures and functions for lighting.
#include "LightingUtil.hlsl"
struct MaterialData
{
float4 DiffuseAlbedo;
float3 FresnelR0;
float Roughness;
float4x4 MatTransform;
uint DiffuseMapIndex;
uint MatPad0;
uint MatPad1;
uint MatPad2;
};
TextureCube gCubeMap : register(t0);
// An array of textures, which is only supported in shader model 5.1+. Unlike
// Texture2DArray, the textures in this array can be different sizes and
// formats, making it more flexible than texture arrays.
Texture2D gDiffuseMap[4] : register(t1);
// Put in space1, so the texture array does not overlap wit