DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染

之所以需要多插槽(Slot 通道)上传数据到GPU(就是共享内存或显存),还有一方面原因,就是现代的大多数模型文件中其实数据都是按横向数组那样存储的,就是介绍原理时的那个图示中右下方的形式:
在这里插入图片描述

这样存储的好处很多,主要的就是如果想为模型顶点加上比如切线(tangent)这样的数据时非常的方便高效,不需要遍历顶点结构体挨个按位置进行数组元素插入,只需要追加一个数组到数据中即可,使用时按通道传输数据即可。

本例中,使用Assimp导入数据后,继续简单定义了如下结构体,来作为转换数据结构:

// 模型中子网格的顶点偏移等信息
struct ST\_GRS\_SUBMESH\_DATA
{
	UINT m_nNumIndices;
	UINT m_nBaseVertex;
	UINT m_nBaseIndex;
	UINT m_nMaterialIndex;
};

typedef CAtlArray<ST_GRS_SUBMESH_DATA> CGRSSubMesh;

const UINT			g_ncSlotCnt = 4;		// 用4个插槽上传顶点数据
struct ST\_GRS\_MESH\_DATA
{
	const aiScene\*		m_paiModel;
	CStringA			m_strFileName;
	XMMATRIX			m_mxModel;

	CGRSSubMesh			m_arSubMeshInfo;

	CGRSARPositions		m_arPositions;
	CGRSARNormals		m_arNormals;
	CGRSARTexCoords		m_arTexCoords;
	CGRSARVertexBones	m_arBoneIndices;
	CGRSARIndices		m_arIndices;

	CGRSMapString2UINT	m_mapTextrueName2Index;
	CGRSMapUINT2UINT	m_mapTextureIndex2HeapIndex;

	CGRSARBoneDatas		m_arBoneDatas;
	CGRSMapString2UINT	m_mapName2Bone;			//名称->骨骼的索引
	CGRSMapString2UINT	m_mapAnimName2Index;	//名称->动画的索引

	UINT				m_nCurrentAnimIndex;	// 当前播放的动画序列索引(当前动作)
};

这两个结构体定义的总体思路是将一个模型文件中所有的子网格顶点数据都分组连续存储在几个数组中(CGRSARPositions m_arPositions等5个数组),并且用ST_GRS_SUBMESH_DATA结构体数组来对应每个子网格数据在数组中的起始位置和长度信息(CGRSSubMesh m_arSubMeshInfo;中存储)。

另外用两个映射(map:CGRSMapString2UINT m_mapTextrueName2Index;CGRSMapUINT2UINT m_mapTextureIndex2HeapIndex;)来标识出每个子网格对应的纹理索引以及对应描述符索引。

有了结构体,下一步就是组织代码把数据稍作转换,变换到我们的结构体中,这正好就是一开始我们说的LoadMesh函数做的事情:

BOOL LoadMesh(LPCSTR pszFileName, ST_GRS_MESH_DATA& stMeshData)
{
	stMeshData.m_nCurrentAnimIndex = 0;
	stMeshData.m_paiModel = g_aiImporter.ReadFile(pszFileName, ASSIMP_LOAD_FLAGS);

	if (nullptr == stMeshData.m_paiModel)
	{
		ATLTRACE("无法解析文件(%s):%s (%d)\n", pszFileName, g_aiImporter.GetErrorString(), ::GetLastError());
		return FALSE;
	}

	// 获取根节点的变换矩阵,其实就是 Module->World 的变换矩阵
	stMeshData.m_mxModel = XMMatrixTranspose(MXEqual(stMeshData.m_mxModel, stMeshData.m_paiModel->mRootNode->mTransformation));

	// 获取网格数量
	UINT nMeshCnt = stMeshData.m_paiModel->mNumMeshes;
	if ( 0 == nMeshCnt )
	{
		ATLTRACE("文件(%s)中没有网格数据!\n", pszFileName);
		return FALSE;
	}

	const aiMesh\* paiSubMesh = nullptr;
	const aiVector3D	Zero3D(0.0f, 0.0f, 0.0f);
	UINT nNumBones = 0;
	UINT nNumVertices = 0;
	UINT nNumIndices = 0;

	stMeshData.m_arSubMeshInfo.SetCount(nMeshCnt);

	// 加载Vertex基本信息
	for (UINT i = 0; i < nMeshCnt; i++)
	{
		paiSubMesh = stMeshData.m_paiModel->mMeshes[i];

		stMeshData.m_arSubMeshInfo[i].m_nMaterialIndex = paiSubMesh->mMaterialIndex;
		stMeshData.m_arSubMeshInfo[i].m_nNumIndices = paiSubMesh->mNumFaces \* GRS_INDICES_PER_FACE;
		stMeshData.m_arSubMeshInfo[i].m_nBaseVertex = nNumVertices;
		stMeshData.m_arSubMeshInfo[i].m_nBaseIndex = nNumIndices;

		// 当前Mesh的顶点数量和索引数量累加后,就是下个Mesh顶点和索引在整体缓冲中的索引开始位置
		nNumVertices	+= stMeshData.m_paiModel->mMeshes[i]->mNumVertices;
		nNumIndices		+= stMeshData.m_arSubMeshInfo[i].m_nNumIndices;		

		// 加载顶点常规数据
		for (UINT j = 0; j < paiSubMesh->mNumVertices; j++)
		{
			stMeshData.m_arPositions.Add(XMFLOAT4(paiSubMesh->mVertices[j].x
				, paiSubMesh->mVertices[j].y
				, paiSubMesh->mVertices[j].z
				, 1.0f));

			stMeshData.m_arNormals.Add(XMFLOAT4(paiSubMesh->mNormals[j].x
				, paiSubMesh->mNormals[j].y
				, paiSubMesh->mNormals[j].z
				, 0.0f));

			// 注意这个地方只考虑一个纹理的情况,其实最多可以有八个,可以再做个循环进行加载
			const aiVector3D\* pTexCoord = paiSubMesh->HasTextureCoords(0)
				? &(paiSubMesh->mTextureCoords[0][j])
				: &Zero3D;

			stMeshData.m_arTexCoords.Add(XMFLOAT2(pTexCoord->x, pTexCoord->y));
		}

		// 加载索引数据
		for (UINT j = 0; j < paiSubMesh->mNumFaces; j++)
		{
			const aiFace& Face = paiSubMesh->mFaces[j];
			// 已经通过导入标志强制为三角形网格了,每个面就三个索引
			ATLASSERT(Face.mNumIndices == GRS_INDICES_PER_FACE);

			for (UINT k = 0; k < Face.mNumIndices; k++)
			{
				stMeshData.m_arIndices.Add(Face.mIndices[k]);
			}
		}
	}

	stMeshData.m_arBoneIndices.SetCount(nNumVertices);

	UINT		VertexID = 0;
	FLOAT		Weight = 0.0f;
	UINT		nBoneIndex = 0;
	CStringA	strBoneName;
	aiMatrix4x4 mxBoneOffset;
	aiBone\*		pBone = nullptr;

	// 加载骨骼数据
	for (UINT i = 0; i < nMeshCnt; i++)
	{
		paiSubMesh = stMeshData.m_paiModel->mMeshes[i];

		for (UINT j = 0; j < paiSubMesh->mNumBones; j++)
		{
			nBoneIndex = 0;
			pBone = paiSubMesh->mBones[j];
			strBoneName = pBone->mName.data;

			if ( nullptr == stMeshData.m_mapName2Bone.Lookup(strBoneName) )
			{
				// 新骨头索引
				nBoneIndex = nNumBones ++;
				stMeshData.m_arBoneDatas.SetCount(nNumBones);

				stMeshData.m_arBoneDatas[nBoneIndex].m_mxBoneOffset 
					= XMMatrixTranspose(MXEqual(stMeshData.m_arBoneDatas[nBoneIndex].m_mxBoneOffset, pBone->mOffsetMatrix));

				stMeshData.m_mapName2Bone.SetAt(strBoneName, nBoneIndex);
			}
			else
			{
				nBoneIndex = stMeshData.m_mapName2Bone[strBoneName];
			}

			for (UINT k = 0; k < pBone->mNumWeights; k++)
			{
				VertexID = stMeshData.m_arSubMeshInfo[i].m_nBaseVertex + pBone->mWeights[k].mVertexId;
				Weight = pBone->mWeights[k].mWeight;
				stMeshData.m_arBoneIndices[VertexID].AddBoneData(nBoneIndex, Weight);
			}
		}
	}

	// 获取材质数量
	UINT nMatCnt = stMeshData.m_paiModel->mNumMaterials;
	UINT nTextureIndex = 0;
	UINT nTmpIndex = 0;
	CStringA strTextureFileName;
	aiString aistrPath;
	for (UINT i = 0; i < stMeshData.m_paiModel->mNumMaterials; i++)
	{
		const aiMaterial\* pMaterial = stMeshData.m_paiModel->mMaterials[i];
		if (pMaterial->GetTextureCount(aiTextureType_DIFFUSE) > 0)
		{
			if ( pMaterial->GetTexture(aiTextureType_DIFFUSE
				, 0, &aistrPath, nullptr, nullptr, nullptr, nullptr, nullptr)
				== AI_SUCCESS )
			{
				strTextureFileName = aistrPath.C\_Str();
				nTmpIndex = 0;
				if ( !stMeshData.m_mapTextrueName2Index.Lookup( strTextureFileName , nTmpIndex ) )
				{
					stMeshData.m_mapTextrueName2Index.SetAt( strTextureFileName, nTextureIndex );
					nTmpIndex = nTextureIndex;
					++ nTextureIndex;
				}	
				stMeshData.m_mapTextureIndex2HeapIndex.SetAt( i, nTmpIndex );
			}
		}
	}

	return TRUE;
}

这个函数没什么复杂的逻辑,就不过多赘述了。

最后,这样的设计就是暗示大家可以将加载模型数据的整个过程设计成一个Builder模式。当然按照本系列教程的一贯风格,就不过多讨论封装设计的问题了,只是放个图,启发一下思路:
在这里插入图片描述

12.2、Layout的定义

其次,在D3D12中就不能再简单的通过定义与shader中对应的顶点结构的方式来组织和上传数据到GPU了,具体的只需要按照D3D12_INPUT_ELEMENT_DESC结构体的要求,结合数据自身的结构特点定义具体的数据上传布局。D3D12_INPUT_ELEMENT_DESC结构体的定义如下:

typedef struct D3D12\_INPUT\_ELEMENT\_DESC
 {
    LPCSTR SemanticName;
    UINT SemanticIndex;
    DXGI_FORMAT Format;
    UINT InputSlot;
    UINT AlignedByteOffset;
    D3D12_INPUT_CLASSIFICATION InputSlotClass;
    UINT InstanceDataStepRate;
} 	D3D12_INPUT_ELEMENT_DESC;

其定义中InputSlot即是指定当前记录使用哪个插槽(Slot)来传输数据(理解为通道更恰当一些)。

在本例中具体定义Layout如下:

// 定义传入管线的数据结构,这里使用了多Slot方式,注意Slot的用法
D3D12_INPUT_ELEMENT_DESC stIALayoutSphere[] =
{
    { "POSITION",         0, DXGI_FORMAT_R32G32B32A32_FLOAT,   0,    0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
    { "NORMAL",          0, DXGI_FORMAT_R32G32B32A32_FLOAT,   1,    0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
    { "TEXCOORD",       0, DXGI_FORMAT_R32G32_FLOAT,         	2,    0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
    { "BLENDINDICES", 0, DXGI_FORMAT_R32G32B32A32_UINT,     3,    0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
    { "BLENDWEIGHT", 0, DXGI_FORMAT_R32G32B32A32_FLOAT,   3,    16, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};

上面的代码片段中,可以看到整齐排列的第四列,按顺序指定了对应的Slot号,此时第五列的含义就是相对于当前Slot中的偏移量了,因此前4个偏移量就都是从头开始的,就是0了,也就是没有偏移的意思。而第五条记录中,因为与第四条记录使用了相同的插槽,所以偏移就是第四条记录表示的数据长度。

同时,第一列中除了常见的位置、法线、纹理坐标这三个语义外,还增加了BLENDINDICES、BLENDWEIGHT两个新的语义,其实就是代表顶点数据中的骨骼绑定需要的“骨骼索引”和“受影响权重”。对应的Shader中的Vertex Shader输入参数结构(也就是顶点数据结构)定义如下(注意语义和数据类型及向量大小):

struct VSInput
{
    float4 position : POSITION0;        //顶点位置
    float4 normal   : NORMAL0;          //法线
    float2 texuv    : TEXCOORD0;        //纹理坐标
    uint4  bonesID  : BLENDINDICES0;    //骨骼索引
    float4 fWeights : BLENDWEIGHT0;     //骨骼权重
};

12.3、缓冲区准备

准备完了合适的数据,并且定义完对应的Layout,接着就是创建数据的缓冲区。因为这次我们使用的是多Slot上传数据,所以在对应的顶点数据缓冲区的创建上也是有对应的特殊处理的,核心的思想就是不能再像以前那样按照一个结构体数组的形式创建一个Vertex Buffer和一个Index Buffer就完事了,多Slot情况下,就需要为每组数据分开创建缓冲区。在本例中是像下面这样创建了4个顶点数据缓冲区,以及对应的顶点缓冲描述符:

size_t szAlign = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT;
// 计算所有的缓冲大小
size_t szPositions   = g_stMeshData.m_arPositions.GetCount() \* sizeof( g_stMeshData.m_arPositions[0] );
size_t szNormals     = g_stMeshData.m_arNormals.GetCount() \* sizeof( g_stMeshData.m_arNormals[0] );
size_t szTexCoords   = g_stMeshData.m_arTexCoords.GetCount() \* sizeof( g_stMeshData.m_arTexCoords[0] );
size_t szBoneIndices = g_stMeshData.m_arBoneIndices.GetCount() \* sizeof( g_stMeshData.m_arBoneIndices[0] );
size_t szIndices     = g_stMeshData.m_arIndices.GetCount() \* sizeof( g_stMeshData.m_arIndices[0] );

// 需要的缓冲大小+64k-1 使得刚好是64k边界大小时,可以多分配64k出来,防止CreatePlacedResource报错
size_t szVBBuffer = GRS\_UPPER( szPositions, szAlign ) 
    + GRS\_UPPER( szNormals, szAlign )
    + GRS\_UPPER( szTexCoords, szAlign )
    + GRS\_UPPER( szBoneIndices, szAlign )
    + szAlign - 1;             

D3D12_HEAP_DESC stUploadHeapDesc = {  };
// 上传堆类型就是普通的缓冲,可以摆放任意数据
stUploadHeapDesc.Flags = D3D12_HEAP_FLAG_ALLOW_ONLY_BUFFERS;
// 实际数据大小的5\*64K边界对齐大小,因为有5个Buffer
stUploadHeapDesc.SizeInBytes = GRS\_UPPER( szVBBuffer, szAlign );
// 注意上传堆肯定是Buffer类型,可以不指定对齐方式,其默认是64k边界对齐
stUploadHeapDesc.Alignment = 0;
stUploadHeapDesc.Properties = stUploadHeapProps;

// 创建顶点数据的上传堆
GRS\_THROW\_IF\_FAILED( pID3D12Device4->CreateHeap( &stUploadHeapDesc, IID\_PPV\_ARGS( &pIUploadHeapModel ) ) );
GRS\_SET\_D3D12\_DEBUGNAME\_COMPTR( pIUploadHeapModel );

size_t szOffset = 0;
BYTE\* pData = nullptr;
// Positions Upload Buffer
stBufferResSesc.Width = GRS\_UPPER( szPositions, szAlign );
GRS\_THROW\_IF\_FAILED( pID3D12Device4->CreatePlacedResource(
    pIUploadHeapModel.Get()
    , szOffset
    , &stBufferResSesc
    , D3D12_RESOURCE_STATE_GENERIC_READ
    , nullptr
    , IID\_PPV\_ARGS( &pIVBPositionsUp ) ) );

GRS\_SET\_D3D12\_DEBUGNAME\_COMPTR( pIVBPositionsUp );

GRS\_THROW\_IF\_FAILED( pIVBPositionsUp->Map( 0, nullptr, reinterpret\_cast<void\*\*>( &pData ) ) );
// 第一次 Copy!CPU Memory -> Share Memory 实质上就是在内存中倒腾,只是物理地址不同
// 估计会有那么一天,这个Copy动作可能被简单替换为CPU物理地址映射的变更,传说Vulkan中已经大致是这样了
memcpy( pData, g_stMeshData.m_arPositions.GetData(), szPositions );
pIVBPositionsUp->Unmap( 0, nullptr );
pData = nullptr;

// Normals Upload Buffer
szOffset += stBufferResSesc.Width;
stBufferResSesc.Width = GRS\_UPPER( szNormals, szAlign );

GRS\_THROW\_IF\_FAILED( pID3D12Device4->CreatePlacedResource(
    pIUploadHeapModel.Get()
    , szOffset
    , &stBufferResSesc
    , D3D12_RESOURCE_STATE_GENERIC_READ
    , nullptr
    , IID\_PPV\_ARGS( &pIVBNormalsUp ) ) );

GRS\_SET\_D3D12\_DEBUGNAME\_COMPTR( pIVBNormalsUp );

GRS\_THROW\_IF\_FAILED( pIVBNormalsUp->Map( 0, nullptr, reinterpret\_cast<void\*\*>( &pData ) ) );
// 第一次 Copy!CPU Memory -> Share Memory 实质上就是在内存中倒腾,只是物理地址不同
// 估计会有那么一天,这个Copy动作可能被简单替换为CPU物理地址映射的变更,传说Vulkan中已经大致是这样了
memcpy( pData, g_stMeshData.m_arNormals.GetData(), szNormals );
pIVBNormalsUp->Unmap( 0, nullptr );
pData = nullptr;

// TexCoords Upload Buffer
szOffset += stBufferResSesc.Width;
stBufferResSesc.Width = GRS\_UPPER( szTexCoords, szAlign );

GRS\_THROW\_IF\_FAILED( pID3D12Device4->CreatePlacedResource(
    pIUploadHeapModel.Get()
    , szOffset
    , &stBufferResSesc
    , D3D12_RESOURCE_STATE_GENERIC_READ
    , nullptr
    , IID\_PPV\_ARGS( &pIVBTexCoordsUp ) ) );

GRS\_SET\_D3D12\_DEBUGNAME\_COMPTR( pIVBTexCoordsUp );

GRS\_THROW\_IF\_FAILED( pIVBTexCoordsUp->Map( 0, nullptr, reinterpret\_cast<void\*\*>( &pData ) ) );
// 第一次 Copy!CPU Memory -> Share Memory 实质上就是在内存中倒腾,只是物理地址不同
// 估计会有那么一天,这个Copy动作可能被简单替换为CPU物理地址映射的变更,传说Vulkan中已经大致是这样了
memcpy( pData, g_stMeshData.m_arTexCoords.GetData(), szTexCoords );
pIVBTexCoordsUp->Unmap( 0, nullptr );
pData = nullptr;

// Bone Indices Upload Buffer
szOffset += stBufferResSesc.Width;
stBufferResSesc.Width = GRS\_UPPER( szBoneIndices, szAlign );

GRS\_THROW\_IF\_FAILED( pID3D12Device4->CreatePlacedResource(
    pIUploadHeapModel.Get()
    , szOffset
    , &stBufferResSesc
    , D3D12_RESOURCE_STATE_GENERIC_READ
    , nullptr
    , IID\_PPV\_ARGS( &pIVBBoneIndicesUp ) ) );

GRS\_SET\_D3D12\_DEBUGNAME\_COMPTR( pIVBBoneIndicesUp );

GRS\_THROW\_IF\_FAILED( pIVBBoneIndicesUp->Map( 0, nullptr, reinterpret\_cast<void\*\*>( &pData ) ) );
// 第一次 Copy!CPU Memory -> Share Memory 实质上就是在内存中倒腾,只是物理地址不同
// 估计会有那么一天,这个Copy动作可能被简单替换为CPU物理地址映射的变更,传说Vulkan中已经大致是这样了
memcpy( pData, g_stMeshData.m_arBoneIndices.GetData(), szBoneIndices );
pIVBBoneIndicesUp->Unmap( 0, nullptr );
pData = nullptr;

// 创建默认堆(显存中的堆)
D3D12_HEAP_DESC stDefaultHeapDesc = {};
// 大小跟上传堆一样
stDefaultHeapDesc.SizeInBytes = stUploadHeapDesc.SizeInBytes;
stDefaultHeapDesc.Flags = D3D12_HEAP_FLAG_ALLOW_ONLY_BUFFERS;
// 指定堆的对齐方式,这里使用了默认的64K边界对齐,因为这里实际放的是顶点数据
stDefaultHeapDesc.Alignment = szAlign;
stDefaultHeapDesc.Properties = stDefautHeapProps;

// Vertex Data Default Heap
GRS\_THROW\_IF\_FAILED( pID3D12Device4->CreateHeap( &stDefaultHeapDesc, IID\_PPV\_ARGS( &pIDefaultHeapModel ) ) );
GRS\_SET\_D3D12\_DEBUGNAME\_COMPTR( pIDefaultHeapModel );

// Positions Default Buffer
szOffset = 0;
stBufferResSesc.Width = GRS\_UPPER( szPositions, szAlign );
GRS\_THROW\_IF\_FAILED( pID3D12Device4->CreatePlacedResource(
    pIDefaultHeapModel.Get()
    , szOffset
    , &stBufferResSesc
    , D3D12_RESOURCE_STATE_COPY_DEST
    , nullptr
    , IID\_PPV\_ARGS( &pIVBPositions ) ) );

GRS\_SET\_D3D12\_DEBUGNAME\_COMPTR( pIVBPositions );

// 第二次Copy!独显的时候:Share Memory -> Video Memory 
// 这里只是记录个复制命令,之后会在Copy Engine上Excute
pICopyCMDList->CopyBufferRegion( pIVBPositions.Get(), 0, pIVBPositionsUp.Get(), 0, szPositions );
// 然后加入个资源屏障,同步! 并确认复制操作完成 
stResStateTransBarrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST;
stResStateTransBarrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COMMON;
stResStateTransBarrier.Transition.pResource = pIVBPositions.Get();
pICopyCMDList->ResourceBarrier( 1, &stResStateTransBarrier );

// Normals Default Buffer
szOffset += stBufferResSesc.Width;
stBufferResSesc.Width = GRS\_UPPER( szNormals, szAlign );
GRS\_THROW\_IF\_FAILED( pID3D12Device4->CreatePlacedResource(
    pIDefaultHeapModel.Get()
    , szOffset
    , &stBufferResSesc
    , D3D12_RESOURCE_STATE_COPY_DEST
    , nullptr
    , IID\_PPV\_ARGS( &pIVBNormals ) ) );

GRS\_SET\_D3D12\_DEBUGNAME\_COMPTR( pIVBNormals );

// 第二次Copy!独显的时候:Share Memory -> Video Memory 
// 这里只是记录个复制命令,之后会在Copy Engine上Excute
pICopyCMDList->CopyBufferRegion( pIVBNormals.Get(), 0, pIVBNormalsUp.Get(), 0, szNormals );
// 然后加入个资源屏障,同步! 并确认复制操作完成
stResStateTransBarrier.Transition.pResource = pIVBNormals.Get();
pICopyCMDList->ResourceBarrier( 1, &stResStateTransBarrier );

// TexCoords Default Buffer
szOffset += stBufferResSesc.Width;
stBufferResSesc.Width = GRS\_UPPER( szTexCoords, szAlign );
GRS\_THROW\_IF\_FAILED( pID3D12Device4->CreatePlacedResource(
    pIDefaultHeapModel.Get()
    , szOffset
    , &stBufferResSesc
    , D3D12_RESOURCE_STATE_COPY_DEST
    , nullptr
    , IID\_PPV\_ARGS( &pIVBTexCoords ) ) );

GRS\_SET\_D3D12\_DEBUGNAME\_COMPTR( pIVBTexCoords );

// 第二次Copy!独显的时候:Share Memory -> Video Memory 
// 这里只是记录个复制命令,之后会在Copy Engine上Excute
pICopyCMDList->CopyBufferRegion( pIVBTexCoords.Get(), 0, pIVBTexCoordsUp.Get(), 0, szTexCoords );
// 然后加入个资源屏障,同步! 并确认复制操作完成
stResStateTransBarrier.Transition.pResource = pIVBTexCoords.Get();
pICopyCMDList->ResourceBarrier( 1, &stResStateTransBarrier );

// Bone Indices Default Buffer
szOffset += stBufferResSesc.Width;
stBufferResSesc.Width = GRS\_UPPER( szBoneIndices, szAlign );
GRS\_THROW\_IF\_FAILED( pID3D12Device4->CreatePlacedResource(
    pIDefaultHeapModel.Get()
    , szOffset
    , &stBufferResSesc
    , D3D12_RESOURCE_STATE_COPY_DEST
    , nullptr
    , IID\_PPV\_ARGS( &pIVBBoneIndices ) ) );

GRS\_SET\_D3D12\_DEBUGNAME\_COMPTR( pIVBBoneIndices );

// 第二次Copy!独显的时候:Share Memory -> Video Memory 
// 这里只是记录个复制命令,之后会在Copy Engine上Excute
pICopyCMDList->CopyBufferRegion( pIVBBoneIndices.Get(), 0, pIVBBoneIndicesUp.Get(), 0, szBoneIndices );
// 然后加入个资源屏障,同步! 并确认复制操作完成
stResStateTransBarrier.Transition.pResource = pIVBBoneIndices.Get();
pICopyCMDList->ResourceBarrier( 1, &stResStateTransBarrier );


// Positions Buffer View
staVBV[0].BufferLocation = pIVBPositions->GetGPUVirtualAddress();
staVBV[0].SizeInBytes = (UINT) szPositions;
staVBV[0].StrideInBytes = sizeof( g_stMeshData.m_arPositions[0] );

// Normals Buffer View
staVBV[1].BufferLocation = pIVBNormals->GetGPUVirtualAddress();
staVBV[1].SizeInBytes = (UINT) szNormals;
staVBV[1].StrideInBytes = sizeof( g_stMeshData.m_arNormals[0] );

// TexCoords Buffer View
staVBV[2].BufferLocation = pIVBTexCoords->GetGPUVirtualAddress();
staVBV[2].SizeInBytes = (UINT) szTexCoords;
staVBV[2].StrideInBytes = sizeof( g_stMeshData.m_arTexCoords[0] );

// BoneIndices Buffer View
staVBV[3].BufferLocation = pIVBBoneIndices->GetGPUVirtualAddress();
staVBV[3].SizeInBytes = (UINT) szBoneIndices;
staVBV[3].StrideInBytes = sizeof( g_stMeshData.m_arBoneIndices[0] );

代码中,首先创建了一个自定义上传堆(独立上传堆),接着用“定位”(CreatePlacedResource)方式创建了资源缓冲(共享内存中),紧接着将每组顶点数据从内存中Copy(memcpy)到上传堆中,又接着创建了对应的自定义默认堆(独立默认堆),同样在默认堆上创建对应的默认堆上的资源缓冲(显存中),然后使用第二次Copy(CopyBufferRegion)将数据从上传堆中复制到默认堆中。最后填充了资源缓冲视图数组(D3D12_VERTEX_BUFFER_VIEW staVBV[g_ncSlotCnt] = {};),即创建了资源缓冲视图。希望这个过程对你来说已经没有什么阅读和理解障碍了,这就是之前教程中已经反复讲过和用的代码了,只是因为没有封装,所以代码看起来有点长而已。

12.4、多Slot渲染

上面的步骤都结束后,所有的关于多Slot传输数据的准备工作就算完成了,剩下的就是渲染了。之前的教程示例代码中,因为模型的简单性,实质上只需要渲染一个网格的一个实例即可,而现在就需要考虑渲染多个缓冲视图组成的连续网格数据的一个实例(关于多实例渲染后续会讲到,敬请期待!),这时需要一次渲染中设置多个缓冲视图并做循环渲染:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值