实例化技术:常用于对场景中同一对象[可能不同的位置、朝向、缩放大小、材质以及纹理]反复绘制多次的情形;
视锥体剔除技术:通过简单的测试将位于视锥体外的整组三角形从后续的处理流程中剔除出去。
1.硬件实例化
对于同一个对象,可能只是不同的位置、朝向、缩放大小、材质以及纹理,使它们各自维护一套顶点数据与索引数据将极大地耗费系统资源。我们以存储一份相对于其局部空间的几何体副本(即顶点列表与索引列表)的方法来加以取代,在多次绘制调用时,给不同的世界矩阵与材质即可。
但是,为绘制每个对象而调用的API开销依然可观,因为我们要针对每一个对象,设置独有的材质和世界矩阵。Direct3D实例化API使我们可以通过一次绘制调用构造出一个对象的多个实例。再者,有了动态索引的辅助,实例化技术会比Direct3D 11时期更佳灵活。
①绘制实例数据:
cmdList->DrawIndexedInstanced(
ri->indexCount,
1, // 实例的数量
ri->StartIndexLocation,
ri->BaseVertexLocation,
0
);
我们之前一直在绘制实例,只是数量为1。如果将这个数量改为多个,就可以绘制多个完全一样的实例,但是如何给每个实例赋予其独有的数据(世界矩阵、纹理)呢?我们为每个实例对象指定它所独有的实例数据。
②实例数据:
在本书的前一版中,实例数据都是自输入装配阶段获取的。在创建输入布局(input layout)时,可以通过枚举项D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA代替D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA来指定输入的数据为逐实例(per-instance)数据流,而非逐顶点(per-vertex)数据流。随后,再将第二个顶点缓冲区与含有实例数据的输入流相绑定。Direct3D 12仍然支持这种向流水线传递实例数据的方式,但我们选择另一种更为现代化的方法。
这种现代化的方法是:为所有实例都创建一个存有其实例数据的结构化缓冲区。
例如,如果要将数据实例化100次,我们就创建一个具有100个实例数据元素的结构化缓冲区。接着把结构化缓冲区资源绑定到渲染流水线上,并根据要绘制的实例在VS中索引相应的数据。VS可以通过系统标识符SV_InstanceID来确定实例编号(从0开始~)。
示例:着色器程序
#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 "LightingUtil.hlsl"
struct InstanceData
{
float4x4 World;
float4x4 TexTransform;
uint MaterialIndex;
uint InstPad0;
uint InstPad1;
uint InstPad2;
};
struct MaterialData
{
float4 DiffuseAlbedo;
float3 FresnelR0;
float Roughness;
float4x4 MatTransform;
uint DiffuseMapIndex;
uint MatPad0;
uint MatPad1;
uint MatPad2;
};
Texture2D gDiffuseMap[7] : register(t0); // 占用t0~t6的space0空间
StructuredBuffer<InstanceData> gInstanceData : register(t0, space1);
StructuredBuffer<MaterialData> gMaterialData : register(t1, space1);
SamplerState gsamPointWrap : register(s0);
SamplerState gsamPointClamp : register(s1);
SamplerState gsamLinearWrap : register(s2);
SamplerState gsamLinearClamp : register(s3);
SamplerState gsamAnisotropicWrap : register(s4);
SamplerState gsamAnisotropicClamp : register(s5);
cbuffer cbPass : register(b0)
{
float4x4 gView;
float4x4 gInvView;
float4x4 gProj;
float4x4 gInvProj;
float4x4 gViewProj;
float4x4 gInvViewProj;
float3 gEyePosW;
float cbPerObjectPad1;
float2 gRenderTargetSize;
float2 gInvRenderTargetSize;
float gNearZ;
float gFarZ;
float gTotalTime;
float gDeltaTime;
float4 gAmbientLight;
// Indices [0, NUM_DIR_LIGHTS) are directional lights;
// indices [NUM_DIR_LIGHTS, NUM_DIR_LIGHTS+NUM_POINT_LIGHTS) are point lights;
// indices [NUM_DIR_LIGHTS+NUM_POINT_LIGHTS, NUM_DIR_LIGHTS+NUM_POINT_LIGHT+NUM_SPOT_LIGHTS)
// are spot lights for a maximum of MaxLights per object.
Light gLights[MaxLights];
};
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 TexC : TEXCOORD;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float2 TexC : TEXCOORD;
// 修饰符nointerpolation:不要在将顶点着色器的输出传递给像素着色器之前进行插值输出
nointerpolation uint MatIndex : MATINDEX;
};
VertexOut VS(VertexIn vin, uint instanceID : SV_InstanceID)
{
VertexOut vout = (VertexOut)0.0f;
// 获取实例数据
InstanceData instData = gInstanceData[instanceID];
float4x4 world = instData.World;
float4x4 texTransform = instData.TexTransform;
uint matIndex = instData.MaterialIndex;
vout.MatIndex = matIndex;
// 获取材质数据
MaterialData matData = gMaterialData[matIndex];
float4 posW = mul(float4(vin.PosL, 1.0f), world);
vout.PosW = posW.xyz;
// 向量变换:假设要执行的是等比缩放,否则就要使用世界矩阵的逆转值矩阵进行计算
vout.NormalW = mul(vin.NormalL, (float3x3)world);
vout.PosH = mul(posW, gViewProj);
float4 texC = mul(float4(vin.TexC, 0.0f, 1.0f), texTransform);
vout.TexC = mul(texC, matData.MatTransform).xy;
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
MaterialData matData = gMaterialData[pin.MatIndex];
float4 diffuseAlbedo = matData.DiffuseAlbedo;
float3 fresnelR0 = matData.FresnelR0;
float roughness = matData.Roughness;
uint diffuseTexIndex = matData.DiffuseMapIndex;
diffuseAlbedo *= gDiffuseMap[diffuseTexIndex].Sample(gsamLinearWrap, pin.TexC);
pin.NormalW = normalize(pin.NormalW);
float3 toEyeW = normalize(gEyePosW - pin.PosW);
float4 ambient = gAmbientLight*diffuseAlbedo;
const float shininess = 1.0f - roughness;
Material mat = { diffuseAlbedo, fresnelR0, shininess };
float3 shadowFactor = 1.0f;
float4 directLight = ComputeLighting(gLights, mat, pin.PosW,
pin.NormalW, toEyeW, shadowFactor);
float4 litColor = ambient + directLight;
// Common convention to take alpha from diffuse albedo.
litColor.a = diffuseAlbedo.a;
return litColor;
}
可以发现已经没有物体常量缓冲区的身影,每个物体的数据由实例缓冲区提供。
上述着色器程序对应的根签名代码如下:
void InstancingAndCullingApp::BuildRootSignature()
{
CD3DX12_DESCRIPTOR_RANGE texTable;
texTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 7, 0, 0); // 7个SRV--7张纹理
CD3DX12_ROOT_PARAMETER slotRootParameter[4];
// 对应两个结构化缓冲区
slotRootParameter[0].InitAsShaderResourceView(0, 1);
slotRootParameter[1].InitAsShaderResourceView(1, 1);
slotRootParameter[2].InitAsConstantBufferView(0);
slotRootParameter[3].InitAsDescriptorTable(1, &texTable, D3D12_SHADER_VISIBILITY_PIXEL);
auto staticSamplers = GetStaticSamplers();
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(4, slotRootParameter,
(UINT)staticSamplers.size(), staticSamplers.data(),
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
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())));
}
draw():
void InstancingAndCullingApp::Draw(const GameTimer& gt)
{
...
ID3D12DescriptorHeap* descriptorHeaps[] = { mSrvDescriptorHeap.Get() };
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
// 绑定此场景中所需的全部材质
// 对于结构化缓冲区而言,我们可以绕过描述符堆的使用而将其直接设置为根描述符
auto matBuffer = mCurrFrameResource->MaterialBuffer->Resource();
mCommandList->SetGraphicsRootShaderResourceView(1, matBuffer->GetGPUVirtualAddress());
auto passCB = mCurrFrameResource->PassCB->Resource();
mCommandList->SetGraphicsRootConstantBufferView(2, passCB->GetGPUVirtualAddress());
// 绑定此场景中所需的全部纹理
mCommandList->SetGraphicsRootDescriptorTable(3, mSrvDescriptorHeap->GetGPUDescriptorHandleForHeapStart());
DrawRenderItems(mCommandList.Get(), mOpaqueRitems);
...
}
DrawRenderItem:
void InstancingAndCullingApp::DrawRenderItems(ID3D12GraphicsCommandList* cmdList, const std::vector<RenderItem*>& ritems)
{
// For each render item...
for(size_t i = 0; i < ritems.size(); ++i)
{
auto ri = ritems[i];
cmdList->IASetVertexBuffers(0, 1, &ri->Geo->VertexBufferView());
cmdList->IASetIndexBuffer(&ri->Geo->IndexBufferView());
cmdList->IASetPrimitiveTopology(ri->PrimitiveType);
// 设置此渲染项要用到的实例缓冲区
auto instanceBuffer = mCurrFrameResource->InstanceBuffer->Resource();
mCommandList->SetGraphicsRootShaderResourceView(0, instanceBuffer->GetGPUVirtualAddress());
cmdList->DrawIndexedInstanced(ri->IndexCount, ri->InstanceCount, ri->StartIndexLocation, ri->BaseVertexLocation, 0);
}
}
③创建实例缓冲区:
在CPU端,实例数据结构:
struct InstanceData
{
DirectX::XMFLOAT4X4 World = MathHelper::Identity4x4();
DirectX::XMFLOAT4X4 TexTransform = MathHelper::Identity4x4();
UINT MaterialIndex;
UINT InstancePad0;
UINT InstancePad1;
UINT InstancePad2;
};
因为渲染项含有实例化次数的相关信息,所以位于系统内存中的实例数据也应算作渲染项结构体的组成部分:
struct RenderItem
{
...
UINT InstanceCount = 0;
...
};
为了使GPU可以访问到这些实例数据,还需要用InstanceData元素类型创建一个结构化缓冲区。由于该缓冲区是动态缓冲区(即上传缓冲区),所以就能在每一帧都对它进行更新。辅助类UploadBuffer创建动态缓冲区是极其方便的:
struct FrameResource
{
public:
FrameResource(ID3D12Device* device, UINT passCount, UINT maxInstanceCount, UINT materialCount);
FrameResource(const FrameResource& rhs) = delete;
FrameResource& operator=(const FrameResource& rhs) = delete;
~FrameResource();
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> CmdListAlloc;
std::unique_ptr<UploadBuffer<PassConstants>> PassCB = nullptr;
std::unique_ptr<UploadBuffer<MaterialData>> MaterialBuffer = nullptr;
// 注意:在演示程序中,实例只有一个渲染项,所以只用了一个结构化缓冲区来存储实例数据,但要使程序更具通用性,我们需要为每个渲染项都添加一个结构化缓冲区,并为每个缓冲区分配足够大的空间来容纳要绘制的最多实例个数
std::unique_ptr<UploadBuffer<InstanceData>> InstanceBuffer = nullptr;
UINT64 Fence = 0;
};
FrameResource::FrameResource(ID3D12Device* device, UINT passCount, UINT maxInstanceCount, UINT materialCount)
{
ThrowIfFailed(device->CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(CmdListAlloc.GetAddressOf())));
PassCB = std::make_unique<UploadBuffer<PassConstants>>(device, passCount, true);
MaterialBuffer = std::make_unique<UploadBuffer<MaterialData>>(device, materialCount, false);
InstanceBuffer = std::make_unique<UploadBuffer<InstanceData>>(device, maxInstanceCount, false);
}
2.包围体与视锥体
为了实现视锥体剔除(frnstum culling),我们要熟知视锥体与各种包围体(bounding volume)的数学描述。包围体只是与物体的形状近似,但是用数学表示起来比较简单,所以易于使用。
①DirectXMath碰撞检测库:
DirectXCollision.h工具库,它是DirectXMath库的一个组成部分。此库提供了一份常见几何图元相交测试的快速实现,例如:射线(光)与三角形(ray/triangle)相交检测、射(光)线与(包围)盒(ray/box)相交检测、盒盒(box/box)相交检测、盒与平面(box/plane)相交检测、盒与视锥体(box/fustum)相交检测以及球体与视锥体 (sphere/frustum )相交检测等。
②包围盒:
网格的轴对称包围盒(axis-aligned bounding box, AABB)是一种将目标网格紧密包围,且各面皆平行于坐标主轴的长方体。我们可以通过最小点和最大点来描述AABB。通过查找目标网格中所有顶点在x,y,z三个坐标轴上所取得的最小/大值,我们就能得到最小点和最大点的坐标。
或者以另一种方式来表示AABB:将盒的中心记作c、扩展(extents)向量记为e,后者存储的是由包围盒中心沿坐标轴到各盒面的距离。
DirectXMath碰撞检测库采用的是包围盒中心与扩展向量组合的表达方式:
struct BoundingBox
{
static const size_t CORNER_COUNT = 8;
XMFLOAT3 Center; // AABB中心
XMFLOAT3 Extents; // AABB中心到各盒面的距离
...
}
这两种方式进行转换也是很方便的:
代码示例:计算骷髅头网格包围盒的具体流程
XMFLOAT3 vMinf3(+MathHelper::Infinity, +MathHelper::Infinity, +MathHelper::Infinity);
XMFLOAT3 vMaxf3(-MathHelper::Infinity, -MathHelper::Infinity, -MathHelper::Infinity);
XMVECTOR vMin = XMLoadFloat3(&vMinf3);
XMVECTOR vMax = XMLoadFloat3(&vMaxf3);
std::vector<Vertex> vertices(vcount);
for(UINT i = 0; i < vcount; ++i)
{
fin >> vertices[i].Pos.x >> vertices[i].Pos.y >> vertices[i].Pos.z;
fin >> vertices[i].Normal.x >> vertices[i].Normal.y >> vertices[i].Normal.z;
XMVECTOR P = XMLoadFloat3(&vertices[i].Pos);
// 将点投射到单位球面上并生成球面纹理坐标
XMFLOAT3 spherePos;
XMStoreFloat3(&spherePos, XMVector3Normalize(P));
// 球坐标系的theta和phi
float theta = atan2f(spherePos.z, spherePos.x); // atan2f求两个向量的夹角
// Put in [0, 2pi].
if(theta < 0.0f)
theta += XM_2PI;
float phi = acosf(spherePos.y); // 反余弦值
float u = theta / (2.0f*XM_PI);
float v = phi / XM_PI;
vertices[i].TexC = { u, v };
vMin = XMVectorMin(vMin, P); // XMVectorMin:各分量取min
vMax = XMVectorMax(vMax, P);
}
BoundingBox bounds;
XMStoreFloat3(&bounds.Center, 0.5f*(vMin + vMax));
XMStoreFloat3(&bounds.Extents, 0.5f*(vMax - vMin));
轴对称包围盒及其旋转操作:
在局部空间中计算出的目标的AABB,可能放在世界空间中就不与坐标轴相平行,需要将它进行变换才能得到世界空间中的定向包围盒(oriented bounding box, OBB)。为了区别AABB这种特称的包围盒(任意朝向),在局部空间中的OBB称为OOBB(object-oriented bounding box)。但在实际工作中,我们通常总是先将网格变换到其局部空间里,再以局部空间内的轴对称包围盒进行碰撞检测。
如果我们在世界空间中重新计算AABB,可能会得到一个肥胖性的长方体,与实际物体存在偏差。
还有一种办法,即放弃轴对称对齐包围盒,仅采用定向包围盒。此时,我们只需保存好定向包围盒相对于世界空间的朝向即可。DirectX碰撞检测库提供下述结构体来表示定向包围盒:
struct BoundingOrientedBox
{
static const size_t CORNER_COUNT = 8;
XMFLOAT3 Center;
XMFLOAT3 Extends;
XMFLOAT4 Orientation; // 表示包围盒旋转(box->world)的单位四元数(unit quaternion)
...
}
四元数的概念会在第22章详细讲解,它可以像旋转矩阵那样来表示一种旋转动作。
通过一个给定的点集并借助DirectX碰撞检测库中的下列静态成员函数,我们就能构建出所需的AABB与OBB:
void BoundingBox::CreateFromPoints(
_Out_ BoundingBox& Out,
_In_ size_t Count,
_In_reads_bytes_(sizeof(XMFLOAT3)+Stride*(Count-1)) const XMFLOAT3* pPoints,
_In_ size_t Stride
);
void BoundingOrientedBox::CreateFromPoints(
_Out_ BoundingOrientedBox& Out,
_In_ size_t Count,
_In_reads_bytes_(sizeof(XMFLOAT3)+Stride*(Count-1)) const XMFLOAT3* pPoints,
_In_ size_t Stride
);
如果我们定义了顶点结构体如下:
struct Basic32{
XMFLOAT3 Posl;
XMFLOAT3 Normal;
XMFLOAT2 TexC;
};
并且,构成网络所用的顶点数组为:
std::vector<Vertex::Basic32> vertices;
那么我们就能按下面那样调用函数来生成包围盒:
BoundingBox box;
BoundingBox::CreateFromPoints(
box,
vertices.size(),
&vertices[0].Pos,
sizeof(Vertex::Basic32)
);
为了计算出目标网格的包围体,我们要在系统内存中准备一份可供使用的顶点列表副本,并存于如std::vector这样的类型中。这样做的原因是,CPU无法从以渲染为目的而创建的顶点缓冲区中读取数据。 -- 拾取(picking)与碰撞检测(collision detection)两种技术就是这样实现的。
③包围球:
包围球通过球心和半径来描述它。计算网格包围球的方法是先计算其AABB,然后再求取AABB的中心,以此作为包围球的中心,半径就是球心c到网格上任意顶点p之间的最大距离。
grid:网格
在世界变换中,由于缩放操作的存在,包围球可能不再紧绕于目标网格,所以我们需要对包围球进行缩放。我们也可以将所有的网格与游戏场景中相同的缩放比例进行建模,这也难怪就避免了后续的缩放变换。
DirectX碰撞检测库提供了下述结构体来表示包围球:
struct BoundingSphere
{
XMFLOAT3 Center;
float Radius;
...
}
还提供了下列静态成员函数,利用一组点集即可创建包围球:
void BoundingSphere::CreateFromPoints(
_Out_ BoundingSphere& Out,
_In_ size_t Count,
_In_reads_bytes_(sizeof(XMFLOAT3)+Stride*(Count-1)) const XMFLOAT3* pPoints,
_In_ size_t Stride
);
④视锥体:
视锥体(平截头体)有左、右、近、远、顶、底这6个相交的平面如👇:
1️⃣构建视锥体的众平面:
可以发现,左右平面都是通过原点的,且它们是对称的,顶底平面也是如此。这样,我们在表达观察空间中的视锥体时,我们就不必存储所有的平面方程,我们只需简单地记录顶底左右四个平面的平面斜率,以及近远平面在Z轴上到原点的距离即可。
DirectX碰撞检测库通过以下结构体来表示视锥体:
struct BoundingFrustum
{
static const size_t CORNER_COUNT = 8;
XMFLOAT3 Origin; // 视锥体(及其投影)的原点
XMFLOAT4 Orientation; // 表示旋转变化的四元数
float RightSlope; // X轴上的正斜率(X/Z),即右平面的斜率
float LeftSlope; // X轴上的负斜率,即左平面的斜率
float TopSlope; // Y轴上的正斜率(Y/Z),即顶平面的斜率
float BottomSlope; // Y轴上的负斜率,即底平面的斜率
float Near, Far; // 近远平面在Z轴上至原点的距离
...
}
在摄像机的观察空间中,Origin=0,Orientation表示恒等变换。我们也可以指定这两个成员以说明视锥体在世界空间中的朝向以及位置。
若缓存了摄像机视锥体的垂直视场角、纵横比、近平面以及远平面,再辅以一些简单的数学计算,便能确定出观察空间中的视锥体平面方程。当然,通过投影矩阵推导出观察空间中的视锥体平面方程也有多种方法。DirectXMath碰撞检测库采用下述方式来求取视锥体的平面方程:在NDC(规格化设备坐标)空间中,视锥体便被包围在方盒[-1,1]×[-1,1]×[0,1]之内。因此视锥体的8个角点(corner)可以简化表示为:
// 用齐次坐标来表示投影视锥体(projection frustum)中的各角点
static XMVECTORF32 HomogenousPoints[6] =
{
{1.f, 0.f, 1.f, 1.f}, // 计算右平面斜率所用的点(远平面右边中点)
{-1.f, 0.f, 1.f, 1.f}, // 计算左平面斜率所用的点(远平面左边中点)
{0.f, 1.f, 1.f, 1.f}, // 计算上平面斜率所用的点(远平面上边中点)
{0.f, -1.f, 1.f, 1.f}, // 计算下平面斜率所用的点(远平面下边中点)
{0.f, 0.f, 0.f, 1.f}, // 计算近平面到原点距离所用的点(近平面中心点)
{0.f, 0.f, 1.f, 1.f} // 计算远平面到原点距离所用的点(远平面中心点)
};
局部空间 ---model---> 世界空间 ---view---> 观察空间 ---projection---> 齐次裁剪空间 ---透视除法---> NDC(规格化设备空间:[-1,1]×[-1,1]×[0,1])
我们通过计算投影矩阵的逆矩阵(以及齐次除法的逆运算),可以将6?个角点变换回观察空间。通过观察空间中视锥体的6个角点,我们就能通过简单的运算计算出各平面方程。
在DirectX碰撞检测库中,根据投影矩阵计算观察空间中视锥体的代码如下:
// 根据投影矩阵来构造视锥体,输入的矩阵中只能含有一个投影,若有旋转平移缩放变换,则会构造不同的视锥体
_Use_decl_annotations_
inline void XM_CALLCONV BoundingFrustum::CreateFromMatrix(
BoundingFrustum& Out,
FXMMATRIX Projection
)
{
// 齐次裁剪空间中的角点
static XMVECTORF32 HomogenousPoints[6] = {
// 用齐次坐标来表示投影视锥体中的各角点
{1.f, 0.f, 1.f, 1.f}, // 计算右平面斜率所用的点(远平面右边中点)
{-1.f, 0.f, 1.f, 1.f}, // 计算左平面斜率所用的点(远平面左边中点)
{0.f, 1.f, 1.f, 1.f}, // 计算上平面斜率所用的点(远平面上边中点)
{0.f, -1.f, 1.f, 1.f}, // 计算下平面斜率所用的点(远平面下边中点)
{0.f, 0.f, 0.f, 1.f}, // 计算近平面到原点距离所用的点(近平面中心点)
{0.f, 0.f, 1.f, 1.f} // 计算远平面到原点距离所用的点(远平面中心点)
}
XMVECTOR Deteminant; // 行列式
XMMATRIX matInverse = XMMatrixInverse(&Determinant, Projection); // 逆矩阵
// 计算位于世界空间中的视锥体诸角点
XMVECTOR Points[6];
for(size_t i = 0; i < 6; ++i)
{
// 齐次裁剪空间->摄像机空间:projection逆运算
Points[i] = XMVector4Transform(HomogenousPoints[i], matInverse);
}
Out.Origin = XMFLOAT3{ 0.f, 0.f, 0.f };
Out.Orientation = XMFLOAT4(0.f, 0.f, 0.f, 1.f);
// 计算各右左顶底四个平面的斜率 -- XMVectorReciprocal计算每个分量的倒数、
// 除以Z以计算四个平面的斜率
Points[0] = Points[0]*XMVectorReciprocal(XMVectorSplatZ(Points[0]));
Points[1] = Points[1]*XMVectorReciprocal(XMVectorSplatZ(Points[1]));
Points[2] = Points[2]*XMVectorReciprocal(XMVectorSplatZ(Points[2]));
Points[3] = Points[3]*XMVectorReciprocal(XMVectorSplatZ(Points[3]));
Out.RightSlope = XMVectorGetX(Points[0]);
Out.LeftSlope = XMVectorGetX(Points[1]);
Out.TopSlope = XMVectorGetY(Points[2]);
Out.BottomSlope = XMVectorGetY(Points[3]);
// 计算近平面与远平面在z轴上到原点的距离
Points[4] = Points[4]*XMVectorReciprocal(XMVectorSplatW(Points[4]));
Points[5] = Points[5]*XMVectorReciprocal(XMVectorSplatW(Points[5]));
Out.Near = XMVectorGetZ(Points[4]);
Out.Far = XMVectorGetZ(Points[5]);
};
2️⃣视锥体与球体的相交检测:
我们可以将视锥体看作6个内向的平面所构成的空间范围,所以它与球体相交的测试过程如👇:如果存在一视锥体平面L,且球体位于L的负半空间之内,则球体在视锥体之外,否则,球体与视锥体相交(包括了球体在视锥体内的情况)。
因此,视锥体与球体的相交检测变成了球体和6个平面的相交检测。如何判定球体是在平面的某一侧?球心到平面的有向距离为,其中k为有向距离,n为平面法线,c为球心坐标。如果,则相交,如果,则球体在平面的前侧,如果,则球体在平面的后侧。
其实就是,如果>0,则点在平面前侧;如果<0,则在后侧。
所以刚好就是点到平面的距离公式:
BoundingFrustum类提供了下列成员函数来测试球体与视锥体是否相交:为了检测结构具有意义,球体与视锥体必须位于同一坐标系内
enum ContainmentType
{
DISJOINT = 0, // 对象完全在视锥体外
INTERSECTS = 1, // 对象与视锥体的边界相交
CONTAINS = 2, // 对象完全在视锥体内
}
ContainmentType BoundingFrustum::Contains(
_In_ const BoundingSphere& sphere
) const;
// BoundingSphere也存在contains成员函数:
ContainmentType BoundingSphere::Contains(
_In_ const BoundingFrustum& fr
) const;
3️⃣视锥体与轴对称包围盒(AABB)的相交检测:
AABB与视锥体的相交检测与球体所采取的策略相同,也就是判断AABB在6个平面的内部(前侧)。具体做法:找到一条穿过包围盒体中心且方向与平面法线n最为接近的包围盒体对角线向量如下图👇。
根据P和Q关于平面的位置关系,可以判定AABB与平面的相交情况。
通过下述代码求出方向最接近于平面法向量n的PQ:
// 针对每个坐标轴x,y,z...
for(int j=0; j < 3; ++j)
{
// 找寻在坐标轴上使PQ与平面法线具有相同方向的点:
if(planeNormal[j] >= 0.f)
{
P[j] = box.minPt[j];
Q[j] = box.maxPt[j];
}
else
{
P[j] = box.maxPt[j];
Q[j] = box.minPt[j];
}
}
BoundingFrustum类提供了下列成员函数来测试AABB与视锥体是否相交:
ContainmentType BoundingFrustum::Contains(
_In_ const BoundingBox& box
) const;
// BoundingBox类中含有与之对应的成员函数:
ContaimentType BoundingBox::Contains(
_In_ const BoundingFrustum& fr
) const;
3.视锥体剔除
回顾之前第5章的内容,硬件会在裁剪阶段自动丢弃位于视锥体外的三角形。但是,这些三角形在被丢弃之前仍然会被送往渲染流水线,并传至VS、曲面细分阶段、GS,直到这些三角形进入裁剪环节才能被丢弃。此流程的效率是极其低下的。
视锥体剔除的思路是:在高于以三角形为基本单位的层级中,按组剔除三角形。比如,为一个物体(包含许多个三角形)创建一个包围盒,如果包围盒完全在视锥体外,则包围盒内的所有三角形都不用绘制。如果包围盒完全在视锥体内,则这些三角形需要全部绘制。如果视锥体和包围盒部分相交,那么需要对包围盒内的三角形进行裁剪操作,通过硬件裁剪丢弃掉在视锥体外的三角形。
效果是十分显著的,因为视锥体空间只占整个世界空间的极小一部分,就能节约巨多三角形计算。
演示程序中,我们绘制多个骷髅头实例,我们对每一个实例进行视锥体裁剪操作,如果实例与视锥体相交,我们就将它加入到存有实例数据的结构化缓冲区的下一个空槽中。另外,为了完成相交检验,我们可以将视锥体变换到每个实例的局部空间中。或者,也可以将AABB与视锥体都一同变换到世界空间中。具体代码如下:
// 观察矩阵的逆矩阵: 摄像机空间 -> 世界空间
XMMATRIX view = mCamera.GetView();
XMMATRIX invView = XMMatrixInverse(&XMMatrixDeterminant(view), view);
auto currInstanceBuffer = mCurrFrameResource->InstanceBuffer.Get();
for(auto& e : mAllRitems)
{
const auto& instanceData = e->Instances;
int visibleInstanceCount = 0;
for(UINT i=0; i<(UINT)instanceData.size(); ++i)
{
XMMATRIX world = XMLoadFloat4x4(&instanceData[i].World);
XMMATRIX texTransform = XMLoadFloat4x4(&instanceData[i].TexTransform);
XMMATRIX invWorld = XMMatrixInverse(&XMMatrixDeterminant(world), world);
// 从 观察空间 -> 物体局部空间 的变换矩阵 (world*view)^-1
XMMATRIX viewToLocal = XMMatrixMultiply(invView, invWorld);
// 将摄像机视锥体从观察空间变换到物体局部空间
BoundingFrustum localSpaceFrustum;
mCamFrustum.Transform(localSpaceFrustum, viewToLocal);
// 从局部空间中执行相交检测
if(localSpaceFrustum.Contains(e->Bounds) != DirectX::DISJOIN)
{
InstanceData data;
XMStoreFloat4x4(&data.World, XMMatrixTranspose(world));
XMStoreFloat4x4(&data.TexTransform, XMMatrixTranspose(texTransform));
data.MaterialIndex = instanceData[i].MaterialIndex;
currInstanceBuffer->CopyData(visibleInstanceCount++, data);
}
}
e->InstanceCount = visibleInstanceCount;
std::wostringstream outs;
outs.precision(6);
outs << L"Instancing and Culling Demo" <<
L" " << e->InstanceCount <<
L" objects visible out of " << e->Instances.size();
mMainWndCaption = outs.str();
}
我们尽管会为结构化缓冲区预留每个实例的空间,但是我们其实只绘制与视锥体相交的实例:
cmdList->DrawIndexedInstanced(ri->IndexCount,
ri->InstanceCount,
ri->StartIndexLocation,
ri->BaseVertexLocation,
0);
效果十分显著,演示程序的帧率提高了一倍。
总结:
实例化技术:就是给定一套固定的顶点和索引缓冲区,多次绘制相同物体的技术。如何区分当前绘制的是哪一个实例,通过着色器中的SV_InstanceID语义来判定,根据这个语义,我们可以使用动态索引的方式找到对应的材质、纹理、渲染过程常量等相关数据,对不同实例进行区别化处理。