- 注意!本文是在下几年前入门期间所写(young and naive),其中许多表述可能不正确,为防止误导,请各位读者仔细鉴别。
Cube Mapping
Cube Mapping简介
Cube Map是用6张贴图存一个正方体上的贴图,存的时候是跟坐标轴对齐的,index的0~5分别对应+X,-X,+Y,-Y,+Z,-Z,然后采样不再用uv,而使用一个三维矢量v,v的长度无所谓,v无限延长的射线与正方体的交点就是采样位置,注意v如果要采样的话需要变换到和cube map同一个空间里来,因为cube map是和坐标轴对齐的,要得到正确采样结果的话v也要在这个坐标空间下才行。
Cube Map的采样一般是用6个横竖FOV都是90度的摄像头。
cube map经常用来实现环境贴图。环境贴图我们希望是离我们无限远的,一个很简单的做法就是以摄像头为中心创建一个球,然后这个球总是跟着摄像头一起动,也就是说摄像头无论怎么动背景都不会动,这也就形成了无穷远的错觉。
接下来讨论一下Cube Map怎么样实现反射环境的效果。首先如下图所示
E是我们眼睛的位置,I是入射光,n是法线,可以看出我们应该用矢量r=reflect(-v,n)来采样。代码如下
const float shininess = 1.0f - roughness;
// Add in specular reflections.
float3 r = reflect(-toEyeW, pin.NormalW);
float4 reflectionColor = gCubeMap.Sample(gsamLinearWrap, r);
float3 fresnelFactor = SchlickFresnel(fresnelR0, pin.NormalW, r);
litColor.rgb += shininess * fresnelFactor * reflectionColor.rgb;
然而这种方法并不适合于比较大的平面,对曲面状的物体而言这种方法一般不会穿帮,但是对一个很大的平面来说,这种方法没有把位置考虑进去,就会出现问题,如下图所示。
图中两个位置的采样结果是一样的,这是一种错误,因此real-time rendering这本书的第三版提出了一种解决方法。如下图
这种方法是把反射点的位置也考虑进去了,最后用的采样矢量是p+t0r,代码如下
float3 BoxCubeMapLookup(float3 rayOrigin, float3 unitRayDir,
float3 boxCenter, float3 boxExtents)
{
// Based on slab method as described in Real-Time Rendering
// 16.7.1 (3rd edition).
// Make relative to the box center.
float3 p = rayOrigin - boxCenter;
// The ith slab ray/plane intersection formulas for AABB are:
//
// t1 = (-dot(n_i, p) + h_i)/dot(n_i, d) = (-p_i + h_i)/d_i
// t2 = (-dot(n_i, p) - h_i)/dot(n_i, d) = (-p_i - h_i)/d_i
// Vectorize and do ray/plane formulas for every slab together.
float3 t1 = (-p+boxExtents)/unitRayDir;
float3 t2 = (-p-boxExtents)/unitRayDir;
// Find max for each coordinate. Because we assume the ray is inside
// the box, we only want the max intersection parameter.
float3 tmax = max(t1, t2);
// Take minimum of all the tmax components:
float t = min(min(tmax.x, tmax.y), tmax.z);
// This is relative to the box center so it can be used as a
// cube map lookup vector.
return p + t*unitRayDir;
}
这里t1和t2都是float3,共包含6个浮点数,对应的是6个t,分别是aabb的六个面和直线交点的t,然后把t1和t2的三个分量分别取max,使用因为反射的射线的t都是大于0的,小于0的那三个是和反射方向相反的方向,应该直接舍弃掉,然后剩下的这三个t里面t最小的就是和aabb的交点,其他两个都是扩展平面上的。
天空球与环境反射demo
接下来用上面提到的内容实现一个天空球和一些反射天空的小球,并列出关键部分的代码。
首先Cube Map的载入方法和普通贴图是一样的,dds支持cube map,所以和以前一样读取即可,这里不再列出代码。
然后创建Root Signature的时候单独用一个SRV来存天空球的cube map
void CubeMapApp::BuildRootSignature()
{
CD3DX12_DESCRIPTOR_RANGE texTable0;
texTable0.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0, 0);
CD3DX12_DESCRIPTOR_RANGE texTable1;
texTable1.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 5, 1, 0);
// Root parameter can be a table, root descriptor or root constants.
CD3DX12_ROOT_PARAMETER slotRootParameter[5];
// Perfomance TIP: Order from most frequent to least frequent.
slotRootParameter[0].InitAsConstantBufferView(0);
slotRootParameter[1].InitAsConstantBufferView(1);
slotRootParameter[2].InitAsShaderResourceView(0, 1);
slotRootParameter[3].InitAsDescriptorTable(1, &texTable0, D3D12_SHADER_VISIBILITY_PIXEL);
slotRootParameter[4].InitAsDescriptorTable(1, &texTable1, D3D12_SHADER_VISIBILITY_PIXEL);
auto staticSamplers = GetStaticSamplers();
// A root signature is an array of root parameters.
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(5, slotRootParameter,
(UINT)staticSamplers.size(), staticSamplers.data(),
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
// create a root signature with a single slot which points to a descriptor range consisting of a single constant buffer
ComPtr<ID3DBlob> serializedRootSig = nullptr;
ComPtr<ID3DBlob> errorBlob = nullptr;
HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc, D3D_ROOT_SIGNATURE_VERSION_1,
serializedRootSig.GetAddressOf(), errorBlob.GetAddressOf());
if(errorBlob != nullptr)
{
::OutputDebugStringA((char*)errorBlob->GetBufferPointer());
}
ThrowIfFailed(hr);
ThrowIfFailed(md3dDevice->CreateRootSignature(
0,
serializedRootSig->GetBufferPointer(),
serializedRootSig->GetBufferSize(),
IID_PPV_ARGS(mRootSignature.GetAddressOf())));
}
创建descriptor heap的时候,天空球贴图的srv要设置格式成D3D12_SRV_DIMENSION_TEXTURECUBE
void CubeMapApp::BuildDescriptorHeaps()
{
···
// next descriptor
hDescriptor.Offset(1, mCbvSrvDescriptorSize);
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);
mSkyTexHeapIndex = 3;
}
创建材质的时候天空球和反射很强的材质如下,反射强就用一个很大的菲涅尔系数就可以了,具体见后面的shader代码
auto sky = std::make_unique<Material>();
sky->Name = "sky";
sky->MatCBIndex = 4;
sky->DiffuseSrvHeapIndex = 3;
sky->DiffuseAlbedo = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
sky->FresnelR0 = XMFLOAT3(0.1f, 0.1f, 0.1f);
sky->Roughness = 1.0f;
auto mirror0 = std::make_unique<Material>();
mirror0->Name = "mirror0";
mirror0->MatCBIndex = 2;
mirror0->DiffuseSrvHeapIndex = 2;
mirror0->DiffuseAlbedo = XMFLOAT4(0.0f, 0.0f, 0.1f, 1.0f);
mirror0->FresnelR0 = XMFLOAT3(0.98f, 0.97f, 0.95f);
mirror0->Roughness = 0.1f;
创建render item的时候天空单独作为一个层,然后建立一个很大的球体
auto skyRitem = std::make_unique<RenderItem>();
XMStoreFloat4x4(&skyRitem->World, XMMatrixScaling(5000.0f, 5000.0f, 5000.0f));
skyRitem->TexTransform = MathHelper::Identity4x4();
skyRitem->ObjCBIndex = 0;
skyRitem->Mat = mMaterials["sky"].get();
skyRitem->Geo = mGeometries[