首页 > 其他分享 >DX12 绘制几何体和优化渲染循环

DX12 绘制几何体和优化渲染循环

时间:2023-02-19 21:34:52浏览次数:74  
标签:渲染 0.0 float 几何体 DX12 缓冲区 GPU DirectX meshData

几何图形辅助结构体

​ 随着项目越来越复杂,顶点和索引也会愈来愈多,因此我们可以选择创建一个结构体专门来管理所有的几何体

  • 几何图形辅助结构有何好处?

    1. 管理几何体方便明了
    2. 将顶点和索引数据置于系统内存,供CPU读取使用。因为碰撞检测等需要CPU访问几何体数据
    3. 缓存顶点和索引的属性(每个顶点占用的字节数等),且提供返回缓冲区视图的方法
  • MeshGeometry的实现

    //利用SubmeshGeometry定义MeshGeometry中的单个几何体
    struct SubmeshGeometry
    {
    	UINT IndexCount = 0;
    	UINT StartIndexLocation = 0;
    	INT BaseVertexLocation = 0;
    
        //当前几何体的包围盒
    	DirectX::BoundingBox Bounds;
    };
    
    //适用于多个几何体存于同一个顶点缓冲区和同一个索引缓冲区
    struct MeshGeometry
    {
        // 几何体集合的名称
        std::string Name;
    
        //顶点/索引格式不定,因此用Blob类型存储顶点和索引。待使用时再转换为适当类型
        Microsoft::WRL::ComPtr<ID3DBlob> VertexBufferCPU = nullptr;
        Microsoft::WRL::ComPtr<ID3DBlob> IndexBufferCPU  = nullptr;
    
        Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferGPU = nullptr;
        Microsoft::WRL::ComPtr<ID3D12Resource> IndexBufferGPU = nullptr;
    
        Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferUploader = nullptr;
        Microsoft::WRL::ComPtr<ID3D12Resource> IndexBufferUploader = nullptr;
    
        //与缓冲区有关的数据
        UINT VertexByteStride = 0;
        UINT VertexBufferByteSize = 0;
        DXGI_FORMAT IndexFormat = DXGI_FORMAT_R16_UINT;
        UINT IndexBufferByteSize = 0;
    
        //借此存储多个几何体
        std::unordered_map<std::string, SubmeshGeometry> DrawArgs;
    
        D3D12_VERTEX_BUFFER_VIEW VertexBufferView()const
        {
            D3D12_VERTEX_BUFFER_VIEW vbv;
            vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();
            vbv.StrideInBytes = VertexByteStride;
            vbv.SizeInBytes = VertexBufferByteSize;
    
            return vbv;
        }
    
        D3D12_INDEX_BUFFER_VIEW IndexBufferView()const
        {
            D3D12_INDEX_BUFFER_VIEW ibv;
            ibv.BufferLocation = IndexBufferGPU->GetGPUVirtualAddress();
            ibv.Format = IndexFormat;
            ibv.SizeInBytes = IndexBufferByteSize;
    
            return ibv;
        }
    
        //数据上传至GPU后进行释放
        void DisposeUploaders()
        {
            VertexBufferUploader = nullptr;
            IndexBufferUploader = nullptr;
        }
    };
    

优化渲染循环

之前学习的CPU和GPU工作有何不妥?

image-20230112170421646

​ 上图中描述的便是我们之前学习的并行工作方式——在绘制每一帧时都对CPU和GPU进行一次同步。为什么需要这么做?有以下两点

  1. GPU未执行完命令分配器中的所有命令前,不能将其重置

    ​ 若GPU还未完成第n帧的工作,CPU在每一帧中都会重置命令分配器,这会导致在新的一帧中清除掉GPU还未执行的命令

  2. GPU未执行完和常量缓冲区相关的绘制命令前,CPU不可更新与之相关的常量缓冲区

​ 可以看到这种方式确实有效但效率不高,CPU和GPU都有段时间无事可做,这并不是我们期望的。原因有以下两点

  1. 在每一帧刚开始时,命令队列是空的,因此GPU无事可做
  2. 每帧要结束时,CPU需要等待GPU完成命令的处理
  3. 每次更新缓冲区,需要复制数据两次:第一次是复制到CPU可访问的内存;第二次是复制到常量缓冲区,且每次在绘制调用前必须执行读写状态的转换,这样的转换都会刷新GPU管道,这样的结果便是导致序列化所有绘制命令,如此对GPU的性能有明显的影响
    image-20230219133035345

如何优化?

​ 解决之道便是采样环形缓冲区实现动态资源:基于帧资源创建一个环形缓冲区,通常环形缓冲区由三个帧资源构成。处理每一帧时,CPU都会从环形缓冲区中下一个未被GPU使用且需要的资源。假设GPU正在处理第n帧,CPU将提前为第n+1帧更新资源并构建和提交命令列表,当GPU处理完第n帧去执行下一帧时.当GPU使用完一个帧时,系统便会自动回收所有动态资源占用的内存

具体流程如下图所示:

image-20230208211612825

实现环形缓冲区

​ 我们需要实现的第一个部分是环形缓冲区class

​ 需要注意的是因为在GPU执行完命令分配器、常量缓冲区里的命令前,我们依旧不可对其进行更新,所以每一帧都需要有它们子集的常量缓冲区

struct ObjectConstants
{
    DirectX::XMFLOAT4X4 World = MathHelper::Identity4x4();
};

struct PassConstants
{
    DirectX::XMFLOAT4X4 View = MathHelper::Identity4x4();
    DirectX::XMFLOAT4X4 InvView = MathHelper::Identity4x4();
    DirectX::XMFLOAT4X4 Proj = MathHelper::Identity4x4();
    DirectX::XMFLOAT4X4 InvProj = MathHelper::Identity4x4();
    DirectX::XMFLOAT4X4 ViewProj = MathHelper::Identity4x4();
    DirectX::XMFLOAT4X4 InvViewProj = MathHelper::Identity4x4();
    DirectX::XMFLOAT3 EyePosW = { 0.0f, 0.0f, 0.0f };
    float cbPerObjectPad1 = 0.0f;
    DirectX::XMFLOAT2 RenderTargetSize = { 0.0f, 0.0f };
    DirectX::XMFLOAT2 InvRenderTargetSize = { 0.0f, 0.0f };
    float NearZ = 0.0f;
    float FarZ = 0.0f;
    float TotalTime = 0.0f;
    float DeltaTime = 0.0f;
};

struct Vertex
{
    DirectX::XMFLOAT3 Pos;
    DirectX::XMFLOAT4 Color;
};
 
struct FrameResource
{
public:
    
    FrameResource(ID3D12Device* device, UINT passCount, UINT objectCount);
    FrameResource(const FrameResource& rhs) = delete;
    FrameResource& operator=(const FrameResource& rhs) = delete;
    ~FrameResource();

    // We cannot reset the allocator until the GPU is done processing the commands.
    // So each frame needs their own allocator.
    Microsoft::WRL::ComPtr<ID3D12CommandAllocator> CmdListAlloc;

    // We cannot update a cbuffer until the GPU is done processing the commands
    // that reference it.  So each frame needs their own cbuffers.
    std::unique_ptr<UploadBuffer<PassConstants>> PassCB = nullptr;
    std::unique_ptr<UploadBuffer<ObjectConstants>> ObjectCB = nullptr;

    // Fence value to mark commands up to this fence point.  This lets us
    // check if these frame resources are still in use by the GPU.
    UINT64 Fence = 0;
};

FrameResource::FrameResource(ID3D12Device* device, UINT passCount, UINT objectCount)
{
    ThrowIfFailed(device->CreateCommandAllocator(
        D3D12_COMMAND_LIST_TYPE_DIRECT,
		IID_PPV_ARGS(CmdListAlloc.GetAddressOf())));

    PassCB = std::make_unique<UploadBuffer<PassConstants>>(device, passCount, true);
    ObjectCB = std::make_unique<UploadBuffer<ObjectConstants>>(device, objectCount, true);
}

FrameResource::~FrameResource()
{

}

CPU处理第n帧算法也需改变:

void ShapesApp::Update(const GameTimer& gt)
{
    //循环不断地获取Frame Resource数组中的元素 
    mCurrFrameResourceIndex = (mCurrFrameResourceIndex + 1) % gNumFrameResources;
    mCurrFrameResource = mFrameResources[mCurrFrameResourceIndex].get();

    //如果GPU没有执行完处理当前frame resource的命令,则令CPU等待,直到GPU完成并抵达这个围栏点
    if(mCurrFrameResource->Fence != 0 && mFence->GetCompletedValue() < mCurrFrameResource->Fence)
    {
        HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
        ThrowIfFailed(mFence->SetEventOnCompletion(mCurrFrameResource->Fence, eventHandle));
        WaitForSingleObject(eventHandle, INFINITE);
        CloseHandle(eventHandle);
    }
    
   ······
}
  • CreateEvent():创建一个Event同步对象,如果CreateEvent调用成功的话,会返回新生成的对象的句柄,否则返回NULL

    HANDLE CreateEventA(
      [in, optional] LPSECURITY_ATTRIBUTES lpEventAttributes,
      [in]           BOOL                  bManualReset,
      [in]           BOOL                  bInitialState,
      [in, optional] LPCSTR                lpName
    );
    
    • lpEventAttributes:指向SECURITY_ATTRIBUTES结构体,此结构体决定函数的返回句柄是否可以让子进程继承。如果此参数为NULL,此句柄不能继承的
    • bManualReset:指定将创建的EVENT是自动复位还是手动复位。如果为TRUE,一旦该EVENT被设置成有信号,则它会一直等到ResetEvent调用时才为无信号状态。如果为FALSE,当一个有信号的等待线程被释放后,系统会自动复位状态为无信号状态
    • bInitialState:指定事件对象的初始状态。如果为TRUE,有信号,否则无信号
    • lpName:事件对象的名称
  • SetEventOnCompletion():指定一个事件,当围栏达到某个值时,就会引发该事件

    HRESULT SetEventOnCompletion(
      UINT64 Value,
      HANDLE hEvent
    );
    
  • WaitForSingleObject():等待Object被标为有信号才返回

    DWORD WaitForSingleObject(
      [in] HANDLE hHandle,
      [in] DWORD  dwMilliseconds
    );
    
    • dwMilliseconds:超时间隔(以毫秒为单位)。 如果指定了非零值,该函数将等待对象发出信号或间隔。 如果 dwMilliseconds 为零,则如果对象未发出信号,则函数不会输入等待状态;它始终会立即返回。 如果 dwMillisecondsINFINITE,则仅当发出对象信号时,该函数才会返回
    • 可以等待的object:Event,Mutex,Semaphore,Process,Thread
    • 返回类型:
      • WAIT_OBJECT_0, 表示等待的对象有信号(对线程来说,表示执行结束);
      • WAIT_TIMEOUT, 表示等待指定时间内,对象一直没有信号(线程没执行完);
      • WAIT_ABANDONED 表示对象有信号,但还是不能执行 一般是因为未获取到锁或其他原因
  • CloseHandle():关闭线程句柄对象,表示不再使用该句柄。并没有结束线程

    • 线程和线程句柄(Handle)不是一个东西,线程句柄是一个内核对象。我们可以通过句柄来操作线程,但是线程的生命周期和线程句柄的生命周期不一样的。线程的生命周期是线程函数从开始执行到return,线程句柄的生命周期是从CreateThread返回到CloseHandle()
    • 所有的内核对象(包括线程Handle)都是系统资源,用了要还,否则句柄资源会用完
    • 调用closehandle(HANDLE)表示创建者放弃对该内核对象的操作。如果该对象的引用对象记数为0就撤消该对象
  • void ShapesApp:Draw(const GameTimer& gt)
    {
        ······
        // 围栏值增加
        mCurrFrameResource->Fence = ++mCurrentFence;
        
        //向命令队列添加指令来设置新的围栏点
        //在GPU处理完signal()之前的所有命令前,不会设置新的围栏点
        mCommandQueue->Signal(mFence.Get(), mCurrentFence);
    }
    

render item

何为render item?

​ render item(渲染项)意为向渲染流水线提交的数据集合,是一个结构体,根据不同用途存储绘制物体所需数据

​ 它存储以下内容

  1. 参与绘制的MeshGeometry
  2. 图元拓扑结构
  3. DrawIndexedInstanced()的有关参数
  4. 世界矩阵
  5. 一个索引,该索引指向的GPU常量缓冲区对应于当前渲染项中的物体常量缓冲区
  6. 一个更新标志

实现

//存储绘制图形所需参数
struct RenderItem
{
	RenderItem() = default;

    //世界矩阵
    XMFLOAT4X4 World = MathHelper::Identity4x4();

	//已更新标志(dirty flag)表示物体的数据已经改变,此时我们需要更新常量缓冲区
    //每个frame resource含有一个物体常量缓冲区,需对每个frame resource更新
	int NumFramesDirty = gNumFrameResources;	//如此可使每个frame resource都更新

	//此索引指向的GPU常量缓冲区对应于当前render item中的物体常量缓冲区
	UINT ObjCBIndex = -1;

    //绘制用到的几何体
	MeshGeometry* Geo = nullptr;

    // 图元拓扑
    D3D12_PRIMITIVE_TOPOLOGY PrimitiveType = D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST;

    // DrawIndexedInstanced parameters
    UINT IndexCount = 0;
    UINT StartIndexLocation = 0;
    int BaseVertexLocation = 0;
};
  • 根据不同PSO所需的render items,将其划分到不同的向量中

    std::vector<std::unique_ptr<RenderItem>> mAllRitems;	//存储所有render item
    
    //根据PSO划分
    std::vector<RenderItem*> mOpaqueRitems;
    std::vector<RenderItem*> mTransparentRitems;
    

渲染项存储的常量数据

  • 对frame resource中常量缓冲区再次进行分类,基于资源的更新频率对常量数据进行分组

    //某物体世界矩阵发生变化时更新
    struct ObjectConstants
    {
        DirectX::XMFLOAT4X4 World = MathHelper::Identity4x4();
    };
    
    //每次render pass更新
    struct PassConstants
    {
        DirectX::XMFLOAT4X4 View = MathHelper::Identity4x4();
        DirectX::XMFLOAT4X4 InvView = MathHelper::Identity4x4();
        DirectX::XMFLOAT4X4 Proj = MathHelper::Identity4x4();
        DirectX::XMFLOAT4X4 InvProj = MathHelper::Identity4x4();
        DirectX::XMFLOAT4X4 ViewProj = MathHelper::Identity4x4();
        DirectX::XMFLOAT4X4 InvViewProj = MathHelper::Identity4x4();
        DirectX::XMFLOAT3 EyePosW = { 0.0f, 0.0f, 0.0f };
        float cbPerObjectPad1 = 0.0f;
        DirectX::XMFLOAT2 RenderTargetSize = { 0.0f, 0.0f };
        DirectX::XMFLOAT2 InvRenderTargetSize = { 0.0f, 0.0f };
        float NearZ = 0.0f;
        float FarZ = 0.0f;
        float TotalTime = 0.0f;
        float DeltaTime = 0.0f;
    };
    
  • 绘制每一帧画面时,Updata()将调用一次以下两函数

    void ShapesApp::UpdateObjectCBs(const GameTimer& gt)
    {
    	auto currObjectCB = mCurrFrameResource->ObjectCB.get();
    	for(auto& e : mAllRitems)
    	{
    		//只要常量改变就需要更新所有frame resource
    		if(e->NumFramesDirty > 0)
    		{
    			XMMATRIX world = XMLoadFloat4x4(&e->World);
    
    			ObjectConstants objConstants;
    			XMStoreFloat4x4(&objConstants.World, XMMatrixTranspose(world));
    
    			currObjectCB->CopyData(e->ObjCBIndex, objConstants);
    
    			//更新下一个frame resource
    			e->NumFramesDirty--;
    		}
    	}
    }
    
    void ShapesApp::UpdateMainPassCB(const GameTimer& gt)
    {
    	XMMATRIX view = XMLoadFloat4x4(&mView);
    	XMMATRIX proj = XMLoadFloat4x4(&mProj);
    
    	XMMATRIX viewProj = XMMatrixMultiply(view, proj);
    	XMMATRIX invView = XMMatrixInverse(&XMMatrixDeterminant(view), view);
    	XMMATRIX invProj = XMMatrixInverse(&XMMatrixDeterminant(proj), proj);
    	XMMATRIX invViewProj = XMMatrixInverse(&XMMatrixDeterminant(viewProj), viewProj);
    
    	XMStoreFloat4x4(&mMainPassCB.View, XMMatrixTranspose(view));
    	XMStoreFloat4x4(&mMainPassCB.InvView, XMMatrixTranspose(invView));
    	XMStoreFloat4x4(&mMainPassCB.Proj, XMMatrixTranspose(proj));
    	XMStoreFloat4x4(&mMainPassCB.InvProj, XMMatrixTranspose(invProj));
    	XMStoreFloat4x4(&mMainPassCB.ViewProj, XMMatrixTranspose(viewProj));
    	XMStoreFloat4x4(&mMainPassCB.InvViewProj, XMMatrixTranspose(invViewProj));
    	mMainPassCB.EyePosW = mEyePos;
    	mMainPassCB.RenderTargetSize = XMFLOAT2((float)mClientWidth, (float)mClientHeight);
    	mMainPassCB.InvRenderTargetSize = XMFLOAT2(1.0f / mClientWidth, 1.0f / mClientHeight);
    	mMainPassCB.NearZ = 1.0f;
    	mMainPassCB.FarZ = 1000.0f;
    	mMainPassCB.TotalTime = gt.TotalTime();
    	mMainPassCB.DeltaTime = gt.DeltaTime();
    
    	auto currPassCB = mCurrFrameResource->PassCB.get();
    	currPassCB->CopyData(0, mMainPassCB);
    }
    
  • 常量缓冲区改变,VS亦需要更新

    VertexOut VS(VertexIn vin)
    {
        VertexOut vout;
        
        float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);		//从局部空间到世界空间	
        vout.posH = mul(posW, gViewProj);		//从世界到齐次裁剪
        
        //传递给PS
        vout.Color = vin.Color;
        
        return vout;
    }
    
  • 着色器的输入资源改变,需要调整根签名获取所需的两个描述符表(因为我们用的render pass CBV 和 object CBV更新频率不同)

    CD3DX12_DESCRIPTOR_RANGE cbvTable0;
    cbvTable0.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);
    
    CD3DX12_DESCRIPTOR_RANGE cbvTable1;
    cbvTable1.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 1);
    
    CD3DX12_ROOT_PARAMETER slotRootParameter[2];
    
    slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable0);
    slotRootParameter[1].InitAsDescriptorTable(1, &cbvTable1);
    
    CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(2, slotRootParameter, 0, nullptr,			
                                            D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
    

不同形状的几何体

​ 在这之前我们只是绘制简单的立方体,此篇将学习到如何创建不同形状的几何体,诸如椭圆体、球体、柱体对于绘制sky dome(描述玩家头顶的天空部分。译为天空穹顶,天空穹,天穹)、图形程序调试、碰撞检测的可视化及延迟渲染很有益处

程序化几何体

​ 程序化几何体(procedural geometry。意为根据用户提供的参数让程序自动生成对应的几何体)的生成代码放入GeometryGenerator类。此类是一个工具类,用于生成栅格、球体、柱体以及长方体,将数据生成在系统内存中,因此我们需要将这些数据复制到顶点缓冲区和索引缓冲区

class GeometryGenerator
{
public:

    using uint16 = std::uint16_t;
    using uint32 = std::uint32_t;

	struct Vertex
	{
		Vertex(){}
        Vertex(
            const DirectX::XMFLOAT3& p, 
            const DirectX::XMFLOAT3& n, 
            const DirectX::XMFLOAT3& t, 
            const DirectX::XMFLOAT2& uv) :
            Position(p), 
            Normal(n), 
            TangentU(t), 
            TexC(uv){}
		Vertex(
			float px, float py, float pz, 
			float nx, float ny, float nz,
			float tx, float ty, float tz,
			float u, float v) : 
            Position(px,py,pz), 
            Normal(nx,ny,nz),
			TangentU(tx, ty, tz), 
            TexC(u,v){}

        DirectX::XMFLOAT3 Position;
        DirectX::XMFLOAT3 Normal;
        DirectX::XMFLOAT3 TangentU;
        DirectX::XMFLOAT2 TexC;
	};

    //MeshData用于存储顶点列表和索引列表
	struct MeshData
	{
		std::vector<Vertex> Vertices;
        std::vector<uint32> Indices32;

        std::vector<uint16>& GetIndices16()
        {
			if(mIndices16.empty())
			{
				mIndices16.resize(Indices32.size());
				for(size_t i = 0; i < Indices32.size(); ++i)
					mIndices16[i] = static_cast<uint16>(Indices32[i]);
			}

			return mIndices16;
        }

	private:
		std::vector<uint16> mIndices16;
	};
    ······
}

生成柱体网格

  • 定义柱体,我们需要先指定柱体的顶、底面半径、高度、切片数量(slice count,截面分割块数)、堆叠层数(stack count,横向切割的层数)

image-20221011215018211

柱体侧面几何体

  • 要生成的圆台中心(1/2高度处截面中心点)位于原点,且旋转轴平行于y轴
    • 圆台所有顶点位于每个环的侧面
    • 共有stack count + 1个环,且每个环上有slice count个顶点
    • 相邻环半径差:(topRadius - bottomRadius) / stackCount
    • 第i环高度值:-h/2 + i * Δh
  • 生成圆台的思路:遍历每个环,并生成位于环上的各个顶点
  • 实现:
GeometryGenerator::MeshData GeometryGenerator::CreateCylinder(float bottomRadius, float topRadius, float height, uint32 sliceCount, uint32 stackCount)
{
    MeshData meshData;

	//
	// 构建堆叠层
	// 

    
	float stackHeight = height / stackCount;	//每层高度

	//相邻环的半径差
	float radiusStep = (topRadius - bottomRadius) / stackCount;

    //环数
	uint32 ringCount = stackCount+1;

	// 从下到上计算每层环上的顶点坐标
	for(uint32 i = 0; i < ringCount; ++i)
	{
		float y = -0.5f*height + i*stackHeight;
		float r = bottomRadius + i*radiusStep;

		// 环上每个顶点
		float dTheta = 2.0f*XM_PI/sliceCount;	//顶部和底部三角形的弧度值
		for(uint32 j = 0; j <= sliceCount; ++j)
		{
			Vertex vertex;

			float c = cosf(j*dTheta);
			float s = sinf(j*dTheta);

			vertex.Position = XMFLOAT3(r*c, y, r*s);

             //纹理坐标
			vertex.TexC.x = (float)j/sliceCount;
			vertex.TexC.y = 1.0f - (float)i/stackCount;

			// 切线单位向量
			vertex.TangentU = XMFLOAT3(-s, 0.0f, c);

			float dr = bottomRadius-topRadius;
             //副切线
			XMFLOAT3 bitangent(dr*c, -height, dr*s);

            //TBN矩阵向量
			XMVECTOR T = XMLoadFloat3(&vertex.TangentU);
			XMVECTOR B = XMLoadFloat3(&bitangent);
			XMVECTOR N = XMVector3Normalize(XMVector3Cross(T, B));	//法线
			XMStoreFloat3(&vertex.Normal, N);

			meshData.Vertices.push_back(vertex);
		}
	}
  • 什么是切线空间(tangent space)?

    • 模型顶点的纹理坐标定义在切线空间。2维纹理坐标包含U、V两项,U坐标增长的方向, 即切线空间中的tangent轴,V坐标增加的方向,为切线空间中的bitangent轴。模型中不同的三角形,都有对应的切线空间,其tangent轴和bitangent轴分别位于三角形所在平面上,结合三角形面对应的法线,我们称tangant轴(T)、bitangent轴(B)及法线轴(N)所组成的坐标系,即切线空间(TBN)

    • TBN矩阵是由Tangent切线、bitangent副切线、Normal法线组成的3x3矩阵

  • 为什么需要切线空间?

    • 因为法线贴图需要的是切向空间的顶点法线信息,而我们实际获得的是世界空间的法线信息,为此我们需要一个桥梁来将切向空间的法线信息转化会世界空间的法线信息,这个桥梁就是TBN矩阵
    • 优点:
      • 切线空间存储的是相对法线信息,因此换个网格应用该纹理,也能得到合理的结果
      • 可以进行uv动画,通过移动该纹理的uv坐标实现凹凸移动的效果
      • 可以重用法线纹理,比如,一个砖块,我们仅使用一张法线纹理就可以用到所有的6个面
      • 可以压缩。因为切线空间的法线z方向总是正方向,因此可以仅存储xy方向,从而推到z方向(normal是单位向量,用勾股定理由xy得出z,取z为正的一个即可)。而模型空间的法线纹理方向各异,无法压缩
  • 根据下图可以看出,分割出的侧面块是由两个三角形组成的四边形,其中第i层第i块切片的侧面块的两个三角形索引分别为:

    • \[\bigtriangleup ABC = ( i * n + j, (i+1) * n + j, (i+1) * n + j + 1 ) \\ \bigtriangleup ACD = ( i * n + j, (i+1) * n + j + 1, i * n + j + 1 ) \\ n为每个环上的顶点数量 \]

  • 求圆台侧面块上所有三角形索引的主要思路:遍历每个堆叠层和每个切片,并运用上述公式计算

image-20221012222444862

  • 实现:
	//这里加一是因为让每个环的第一个顶点和最后一个顶点重合,毕竟他们的纹理坐标不同
	uint32 ringVertexCount = sliceCount + 1;

	// 计算每个侧面块的三角形索引
	for(uint32 i = 0; i < stackCount; ++i)
	{
		for(uint32 j = 0; j < sliceCount; ++j)
		{
			meshData.Indices32.push_back(i*ringVertexCount + j);
			meshData.Indices32.push_back((i+1)*ringVertexCount + j);
			meshData.Indices32.push_back((i+1)*ringVertexCount + j+1);

			meshData.Indices32.push_back(i*ringVertexCount + j);
			meshData.Indices32.push_back((i+1)*ringVertexCount + j+1);
			meshData.Indices32.push_back(i*ringVertexCount + j+1);
		}
	}
	
	//生成柱体顶部和底部
	BuildCylinderTopCap(bottomRadius, topRadius, height, sliceCount, stackCount, meshData);
	BuildCylinderBottomCap(bottomRadius, topRadius, height, sliceCount, stackCount, meshData);

    return meshData;
}

柱体的底部和顶部几何体

  • 生成柱体的端面,我们只需切割出足够多的三角形,让其近似为圆形
  • 实现:
void GeometryGenerator::BuildCylinderTopCap(float bottomRadius, float topRadius, float height,
											uint32 sliceCount, uint32 stackCount, MeshData& meshData)
{
	uint32 baseIndex = (uint32)meshData.Vertices.size();

	float y = 0.5f*height;
	float dTheta = 2.0f*XM_PI/sliceCount;

	// Duplicate cap ring vertices because the texture coordinates and normals differ.
	for(uint32 i = 0; i <= sliceCount; ++i)
	{
		float x = topRadius*cosf(i*dTheta);
		float z = topRadius*sinf(i*dTheta);

		// Scale down by the height to try and make top cap texture coord area
		// proportional to base.
		float u = x/height + 0.5f;
		float v = z/height + 0.5f;

		meshData.Vertices.push_back( Vertex(x, y, z, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, u, v) );
	}

	// Cap center vertex.
	meshData.Vertices.push_back( Vertex(0.0f, y, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.5f) );

	// Index of center vertex.
	uint32 centerIndex = (uint32)meshData.Vertices.size()-1;

	for(uint32 i = 0; i < sliceCount; ++i)
	{
		meshData.Indices32.push_back(centerIndex);
		meshData.Indices32.push_back(baseIndex + i+1);
		meshData.Indices32.push_back(baseIndex + i);
	}
}

生成球体网格

  • 定义一个球体,我们需要指定其半径、切片数量及堆叠层数
  • 若采用不等比缩放世界变换,可以将球体转换为椭圆体
  • 球体的三角形面积并不相同

image-20221013221550010

GeometryGenerator::MeshData GeometryGenerator::CreateSphere(float radius, uint32 sliceCount, uint32 stackCount)
{
    MeshData meshData;

	// 从上到下计算每个顶点

    // 由于将矩形纹理映射到球体上时,纹理图上没有一个唯一的点来分配给顶点,因此会出现纹理坐标失真。
    //依次对应Position、Normal、TangentU、TexC
	Vertex topVertex(0.0f, +radius, 0.0f, 0.0f, +1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f);
	Vertex bottomVertex(0.0f, -radius, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f);

	meshData.Vertices.push_back( topVertex );

	float phiStep   = XM_PI/stackCount;
	float thetaStep = 2.0f*XM_PI/sliceCount;

	// 计算每个环上的顶点
	for(uint32 i = 1; i <= stackCount-1; ++i)
	{
		float phi = i*phiStep;

		// Vertices of ring.
        for(uint32 j = 0; j <= sliceCount; ++j)
		{
			float theta = j*thetaStep;

			Vertex v;

			// 从球坐标系转换直角坐标系
			v.Position.x = radius*sinf(phi)*cosf(theta);
			v.Position.y = radius*cosf(phi);
			v.Position.z = radius*sinf(phi)*sinf(theta);

			// P 相对于 θ 的偏导数
			v.TangentU.x = -radius*sinf(phi)*sinf(theta);
			v.TangentU.y = 0.0f;
			v.TangentU.z = +radius*sinf(phi)*cosf(theta);

			XMVECTOR T = XMLoadFloat3(&v.TangentU);
			XMStoreFloat3(&v.TangentU, XMVector3Normalize(T));

			XMVECTOR p = XMLoadFloat3(&v.Position);
			XMStoreFloat3(&v.Normal, XMVector3Normalize(p));

			v.TexC.x = theta / XM_2PI;
			v.TexC.y = phi / XM_PI;

			meshData.Vertices.push_back( v );
		}
	}

	meshData.Vertices.push_back( bottomVertex );

	// 计算top stack的索引。top stack首先被写入顶点缓冲区并将顶点与第一环相连。

    for(uint32 i = 1; i <= sliceCount; ++i)
	{
		meshData.Indices32.push_back(0);
		meshData.Indices32.push_back(i+1);
		meshData.Indices32.push_back(i);
	}
	
	//
	// 为 inner stacks 计算索引(not connected to poles).
	//

	// 将索引偏移到第一个环中的第一个顶点的索引
	// 跳过top pole vertex
    uint32 baseIndex = 1;
    uint32 ringVertexCount = sliceCount + 1;
	for(uint32 i = 0; i < stackCount-2; ++i)
	{
		for(uint32 j = 0; j < sliceCount; ++j)
		{
			meshData.Indices32.push_back(baseIndex + i*ringVertexCount + j);
			meshData.Indices32.push_back(baseIndex + i*ringVertexCount + j+1);
			meshData.Indices32.push_back(baseIndex + (i+1)*ringVertexCount + j);

			meshData.Indices32.push_back(baseIndex + (i+1)*ringVertexCount + j);
			meshData.Indices32.push_back(baseIndex + i*ringVertexCount + j+1);
			meshData.Indices32.push_back(baseIndex + (i+1)*ringVertexCount + j+1);
		}
	}

	// 为 bottom stack计算索引.bottom stack 最后被写入顶点缓冲区,并将最下面的顶点和最下面的环连接

	// 底部的顶点最后添加
	uint32 southPoleIndex = (uint32)meshData.Vertices.size()-1;

	// Offset the indices to the index of the first vertex in the last ring.
    //将索引偏移到最后一个环中第一个顶点的索引
	baseIndex = southPoleIndex - ringVertexCount;
	
	for(uint32 i = 0; i < sliceCount; ++i)
	{
		meshData.Indices32.push_back(southPoleIndex);
		meshData.Indices32.push_back(baseIndex+i);
		meshData.Indices32.push_back(baseIndex+i+1);
	}

    return meshData;
}

生成几何球体网格

  • 由于球体网格中三角形面积并不相同,因此我们利用足够多的面积相同且边长相等的三角形逼近球体
  • 我们以一个正二十面体为基础,在其上细分三角形,再根据给定的radius向球面投影新生成的顶点,如此反复
  • 下图中,三角形细分为四个小三角形,随后先将顶点投影到单位球面上,再用v' = r * v/||v||(比如求m0,设球心为圆点o,v = op1 + op0),便可把新顶点也投影到球体上

image-20221014232907073

  • 实现如下:

    GeometryGenerator::MeshData GeometryGenerator::CreateGeosphere(float radius, uint32 numSubdivisions)
    {
        MeshData meshData;
    
    	// 确定细分次数
        numSubdivisions = std::min<uint32>(numSubdivisions, 6u);
    
    	// 对一个正二十面体细分逼近球体
    
    	const float X = 0.525731f; 
    	const float Z = 0.850651f;
    
    	XMFLOAT3 pos[12] = 
    	{
    		XMFLOAT3(-X, 0.0f, Z),  XMFLOAT3(X, 0.0f, Z),  
    		XMFLOAT3(-X, 0.0f, -Z), XMFLOAT3(X, 0.0f, -Z),    
    		XMFLOAT3(0.0f, Z, X),   XMFLOAT3(0.0f, Z, -X), 
    		XMFLOAT3(0.0f, -Z, X),  XMFLOAT3(0.0f, -Z, -X),    
    		XMFLOAT3(Z, X, 0.0f),   XMFLOAT3(-Z, X, 0.0f), 
    		XMFLOAT3(Z, -X, 0.0f),  XMFLOAT3(-Z, -X, 0.0f)
    	};
    
        uint32 k[60] =
    	{
    		1,4,0,  4,9,0,  4,5,9,  8,5,4,  1,8,4,    
    		1,10,8, 10,3,8, 8,3,5,  3,2,5,  3,7,2,    
    		3,10,7, 10,6,7, 6,11,7, 6,0,11, 6,1,0, 
    		10,1,6, 11,0,9, 2,11,9, 5,2,9,  11,2,7 
    	};
    
        meshData.Vertices.resize(12);
        meshData.Indices32.assign(&k[0], &k[60]);
    
    	for(uint32 i = 0; i < 12; ++i)
    		meshData.Vertices[i].Position = pos[i];
    
    	for(uint32 i = 0; i < numSubdivisions; ++i)
    		Subdivide(meshData);
    
    	// 将每个顶点都投影到球面,并推导其对应的纹理坐标
    	for(uint32 i = 0; i < meshData.Vertices.size(); ++i)
    	{
    		// 投影到单位球面
    		XMVECTOR n = XMVector3Normalize(XMLoadFloat3(&meshData.Vertices[i].Position));
    
    		// 投射到球面
    		XMVECTOR p = radius*n;
    
    		XMStoreFloat3(&meshData.Vertices[i].Position, p);
    		XMStoreFloat3(&meshData.Vertices[i].Normal, n);
    
    		// 根据球面坐标推导出纹理坐标
            float theta = atan2f(meshData.Vertices[i].Position.z, meshData.Vertices[i].Position.x);
    
            // 将theta限制在[0,2Π]
            if(theta < 0.0f)
                theta += XM_2PI;
    
    		float phi = acosf(meshData.Vertices[i].Position.y / radius);
    
    		meshData.Vertices[i].TexC.x = theta/XM_2PI;
    		meshData.Vertices[i].TexC.y = phi/XM_PI;
    
    		// 求出P关于theta的偏导数
    		meshData.Vertices[i].TangentU.x = -radius*sinf(phi)*sinf(theta);
    		meshData.Vertices[i].TangentU.y = 0.0f;
    		meshData.Vertices[i].TangentU.z = +radius*sinf(phi)*cosf(theta);
    
    		XMVECTOR T = XMLoadFloat3(&meshData.Vertices[i].TangentU);
    		XMStoreFloat3(&meshData.Vertices[i].TangentU, XMVector3Normalize(T));
    	}
    
        return meshData;
    }
    

总结

​ 通过实现环形缓冲区,我们可以使得CPU提前处理下一帧中GPU未被GPU使用且需要的资源。GeometryGenerator结构体用于程序化生成各式各样的几何体,这些几何体由MeshGeometry结构体管理,最终MeshGeometry被添加进渲染项进行渲染

reference

Implementing Dynamic Resources with Direct3D12 – Diligent Graphics

基于围栏的资源管理 - Win32 apps | Microsoft Learn

Directx12 3D 游戏开发实战

标签:渲染,0.0,float,几何体,DX12,缓冲区,GPU,DirectX,meshData
From: https://www.cnblogs.com/chenglixue/p/17135645.html

相关文章

  • 使用 Angular Universal 进行服务器端渲染避免 window is not defined 的错误消息
    尽管Universal项目的目标是能够在服务器上无缝呈现Angular应用程序,但开发人员还是应该考虑一些注意事项。首先,服务器和浏览器环境之间存在明显差异。在服务器上呈现时......
  • 使用 Angular Universal 进行服务器端渲染的防御性编程思路
    如果无法从Angular平台注入所需的正确全局值,则可以避免调用浏览器代码,只要不需要在服务器上访问该代码即可。例如,全局窗口元素的调用通常是为了获取窗口大小或其他一些......
  • 使用 Angular Universal 进行服务器端渲染避免 window is not defined 的错误消息
    尽管Universal项目的目标是能够在服务器上无缝呈现Angular应用程序,但开发人员还是应该考虑一些注意事项。首先,服务器和浏览器环境之间存在明显差异。在服务器上呈现......
  • 关于服务器端渲染的 Web 应用的 504 错误问题
    除非客户在SSR中添加了用于显式发送504的自定义逻辑,否则504不会来自SSR。在默认的Spartacus/SSR中,没有显式发送504的逻辑。默认情况下它只发送200或500(仅......
  • Cesium体渲染之复刻ThreeJS案例
    体渲染体渲染具体是什么这个知乎上可以搜到,具体是什么这里就不过多赘述了,这里主要是讲讲如何在cesium中实现体渲染。Cesium的体渲染在Cesium最新的开发者版本中(102版本)......
  • 浏览器渲染机制
    1.浏览器如何渲染网页概述:浏览器渲染一共有五步处理HTML并构建DOM树。处理CSS构建CSSOM树。将DOM与CSSOM合并成一个渲染树。根据渲染树来布局,计算每个......
  • vue2 - 条件渲染,列表熏染
    1.条件熏染v-if(1).v-if="表达式"(2).v-else-if="表达式"(3).v-else="表达式"适用于:切换频率较低的场景特点:不展示的DOM元素直接被移除注意:v-if可以和:v-else-if,v-e......
  • 指令语法-class和style-条件渲染-列表渲染
    目录指令语法-class和style-条件渲染-列表渲染今日内容概要今日内容详细1插值语法mvvm演示2文本指令3属性指令4事件指令5class和style6条件渲染7列表渲染指令语法-......
  • 大规模即时云渲染技术,追求体验与成本的最佳均衡
     现实世界映射其中,传统文化沉浸其境,旧时记忆交互其间。仲升|技术作者IMMENSE|内容编辑 在刚刚过温的春节,云之上,带来了一场「数字文化」新体验。 游花车、舞狮子、......
  • Cesium渲染模块之概述
    1.引言Cesium是一款三维地球和地图可视化开源JavaScript库,使用WebGL来进行硬件加速图形,使用时不需要任何插件支持,基于Apache2.0许可的开源程序,可以免费用于商业和非商业......