2024年DirectX12(D3D12)基础教程(十七)—(4)

		}
	}
}

return TRUE;

}


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


  最后,这样的设计就是暗示大家可以将加载模型数据的整个过程设计成一个Builder模式。当然按照本系列教程的一贯风格,就不过多讨论封装设计的问题了,只是放个图,启发一下思路:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/2021071812310712.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQwMzgxNDM=,size_16,color_FFFFFF,t_70)


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



// Multi Input Slot 第一种方式 IASetVertexBuffers
pIMainCMDList->IASetVertexBuffers( 0, g_ncSlotCnt, staVBV );
// 或者
// Multi Input Slot 第二种方式 IASetVertexBuffers
for (UINT i = 0; i < g_ncSlotCnt; i++)
{
pIMainCMDList->IASetVertexBuffers(i, 1, &staVBV[i]);
}

pIMainCMDList->IASetIndexBuffer( &stIBV );

// …
// Draw Call
for ( UINT i = 0; i < g_stMeshData.m_arSubMeshInfo.GetCount(); i++ )
{
UINT nHeapIndex = 0;
D3D12_GPU_DESCRIPTOR_HANDLE stHSRV = stGPUSRVHandle;
if ( g_stMeshData.m_mapTextureIndex2HeapIndex.Lookup( g_stMeshData.m_arSubMeshInfo[i].m_nMaterialIndex, nHeapIndex ) )
{
stHSRV.ptr += ((size_t) nHeapIndex * nCBVSRVDescriptorSize );
}

pIMainCMDList->SetGraphicsRootDescriptorTable( 1, stHSRV );

pIMainCMDList->DrawIndexedInstanced( g_stMeshData.m_arSubMeshInfo[i].m_nNumIndices
    , 1
    , g_stMeshData.m_arSubMeshInfo[i].m_nBaseIndex
    , g_stMeshData.m_arSubMeshInfo[i].m_nBaseVertex
    , 0 );

}


  代码中已经描述清楚了,我们有两种方法来设置这多个Slot对应的缓冲视图,然后循环来调用DrawCall。在祖传的D3D11之前的版本中,这样的代码就会引起极大的性能问题,就是之前教程中讲过的“同步DrawCall”问题,而现在D3D12中这已经不是什么问题了,希望你也已经彻底明白了D3D12中“异步”的全部含义了,之前的教程中也已经详细深入的介绍过相关内容,这里只是帮你回忆一下。


  最后重点强调一下多Slot加载顶点数据,也是多实例渲染的一个基本功能,所以请务必牢固掌握这一技巧!


## 13、动画动作状态机


  至此主要的动画渲染过程就介绍完毕了,最后探讨一个稍微高级的点的话题,就是相对于“3D骨骼动画渲染”的“上层建筑”的内容。在本章之前的介绍中,已经说过,对于一个aiScene对象来说,其aiAnimation成员变量其实是一个数组,而我们的过程看上去好像只用一个元素,就可以渲染完一组动画了,这是什么意思呢?


  其实aiAnimation数组中,每一个成员就代表一个“完整的动作”,比如,对于人体模型来说,走动这个动作的完整过程(也就是“左-右-左”的一个完整周期,剩下的重复播放就可以了),就存储为一个aiAnimation数组元素,接着可能还有“跑动”、“站立”、“攻击”、“大招”等等完整的动作,每一个都作为一个数组元素。在本章示例配送的hero.x模型中,就有大概6个动作,大家可以运行示例旋转加载该模型,然后按空格键切换查看。另一个模型lxq.x中则只有一个动作。


  那么接着你可能就会简单的以为:哇!太酷了!这样按数组索引就可以控制播放不同的动作,人物的动画状态就可以控制了!


  但是请等等,不要太过兴奋,思考这样一个问题,所有的动作间是不是真的可以随便切换?既然从头到尾,其实我们说的整个动画渲染的过程,包括网格、纹理等等都不过是一个“模拟”的过程,模拟的目的无非就是要“逼真”。那么真实的分解“动作”之间是否真的可以随便切换呢?现实中这是不可能的,比如,一个跑动中的人,不可能是轻易就可以放出大招的,往往需要一些中间动作,比如:从跑动先停下来变成站立预备动作,然后放出致命的大招攻击敌人。


  另外从控制的角度,其实就是比如我们用鼠标键盘控制角色的动作时,一样也要考虑动作过渡的过程,比如人物角色正在匍匐前进,这是要进入跑动射击状态,那么就需要首先经过从匍匐姿势到站立姿势再到跑动并混合射击的过程,那么同样如何正确的在程序中表达这一控制过程呢?进一步的,思考如果某个角色是AI控制的NPC角色时?动作状态又如何优雅自然的控制过渡呢?


  最终像这样的“动作切换”过程,也就是动作控制的过程,就可以用一个“动作状态机”(FSM)来描述,比如刚才描述的情况就可以像下面这样来描述:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210718123211715.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTQwMzgxNDM=,size_16,color_FFFFFF,t_70)


  这个图看上去有点复杂,并且因为绘图工具的问题,只好用双向箭头来表示两条不同向的边,另外边上的条件也没有描述。先不用纠结这些细节,来看一下图中如果我们要从跑步的动作到攻击的动作,那就必须要经过站立的动作作为过渡,即停下来开始攻击,这样最终动作的过渡无非就是计算一条从某个当前状态至目标状态的一条路径。


  而图中边上的条件其实就是“控制指令”(即状态图上的输入,但不是说输入就非得来自于鼠标或键盘等输入设备),它可以来自于鼠标键盘手柄等输入设备,也可以来自于AI。


  最终这样的状态图就可以完美的解决动画动作的过渡,并天然的抽象了对动作过渡的控制,形成一个“动画控制系统”,因为状态转换的指令可以来自输入设备,也可以来自AI的控制指令,还可以是从网络传输过来的别的玩家的指令(对网络传输来说,只传输动作指令和时间点基本上就可以同步多玩家之间的动画了,而不用传输“动画调试般”,这是巨大的优势!)。


## 14、后记


  最后需要提到的是,在本章示例中,其实我们并没有考虑诸如动作之间的插值过渡、动作混合、碰撞反馈等等更高级一点的话题,当然这些都是现代完整的动画系统中必不可少的基础功能。但本章的基本目的是让大家对骨骼动画有个基础性的了解和掌握,有了基础剩下的内容我相信各位再通过进一步学习,并掌握也就不是什么难事了。


  本章内容历时将近一个多月才完成,而代码早就提交到了Github,内容也接近3万字了,期间实在是因为最近实在是太忙了,有很多事情,也就是工作、生活、学习等等方面的事情都需要去处理,因此过程有点拖沓了,请各位谅解!并恳请各位看过后不忘点赞、收藏、评论三连支持一下,算是鼓励下我,继续把该系列教程完成下去。  


![img](https://img-blog.csdnimg.cn/img_convert/a8c570aaff520802852232f55473d843.png)
![img](https://img-blog.csdnimg.cn/img_convert/8fdea869b2eb4ca435231f8b8351d978.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

、生活、学习等等方面的事情都需要去处理,因此过程有点拖沓了,请各位谅解!并恳请各位看过后不忘点赞、收藏、评论三连支持一下,算是鼓励下我,继续把该系列教程完成下去。  


[外链图片转存中...(img-ZS0jhx2h-1714629626535)]
[外链图片转存中...(img-SZpUtbLt-1714629626536)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

不错的dx11入门教程 Tutorial 1: Setting up DirectX 11 with Visual Studio Tutorial 2: Creating a Framework and Window Tutorial 3: Initializing DirectX 11 Tutorial 4: Buffers, Shaders, and HLSL Tutorial 5: Texturing Tutorial 6: Diffuse Lighting Tutorial 7: 3D Model Rendering Tutorial 8: Loading Maya 2011 Models Tutorial 9: Ambient Lighting Tutorial 10: Specular Lighting Tutorial 11: 2D Rendering Tutorial 12: Font Engine Tutorial 13: Direct Input Tutorial 14: Direct Sound Tutorial 15: FPS, CPU Usage, and Timers Tutorial 16: Frustum Culling Tutorial 17: Multitexturing and Texture Arrays Tutorial 18: Light Maps Tutorial 19: Alpha Mapping Tutorial 20: Bump Mapping Tutorial 21: Specular Mapping Tutorial 22: Render to Texture Tutorial 23: Fog Tutorial 24: Clipping Planes Tutorial 25: Texture Translation Tutorial 26: Transparency Tutorial 27: Reflection Tutorial 28: Screen Fades Tutorial 29: Water Tutorial 30: Multiple Point Lights Tutorial 31: 3D Sound Tutorial 32: Glass and Ice Tutorial 33: Fire Tutorial 34: Billboarding Tutorial 35: Depth Buffer Tutorial 36: Blur Tutorial 37: Coming Soon... DirectX 10 Tutorials: Tutorial 1: Setting up DirectX 10 with Visual Studio Tutorial 2: Creating a Framework and Window Tutorial 3: Initializing DirectX 10 Tutorial 4: Buffers, Shaders, and HLSL Tutorial 5: Texturing Tutorial 6: Diffuse Lighting Tutorial 7: 3D Model Rendering Tutorial 8: Loading Maya 2011 Models Tutorial 9: Ambient Lighting Tutorial 10: Specular Lighting Tutorial 11: 2D Rendering Tutorial 12: Font Engine Tutorial 13: Direct Input Tutorial 14: Direct Sound Tutorial 15: FPS, CPU Usage, and Timers Tutorial 16: Frustum Culling Tutorial 17: Multitexturing and Texture Arrays Tutorial 18: Light Maps Tutorial 19: Alpha Mapping Tutorial 20: Bump Mapping Tutorial 21: Specular Mapping Tutorial 22: Render to Texture Tutorial 23: Fog Tutorial 24: Clipping Planes Tutorial 25: Texture Translation Tutorial 26: Transparency Tutorial 27: Reflection Tutorial 28: Screen Fades Tutorial 29: Water Tutorial 30: Multiple Point Lights Tutorial 31: 3D Sound Tutorial 32: Glass and Ice Tutorial 33: Fire Tutorial 34: Billboarding Tutorial 35: Depth Buffer Tutorial 36: Blur Tutorial 37: Coming Soon... DirectX 10 Terrain Tutorials: Tutorial 1: Grid and Camera Movement Tutorial 2: Height Maps Tutorial 3: Terrain Lighting Tutorial 4: Terrain Texturing Tutorial 5: Color Mapped Terrain Tutorial 6: Quad Trees Tutorial 7: Coming Soon... 。。。。。。。。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值