利用Direct3D绘制几何体(续)

学习目标

  1. 学会一种无须每帧都要刷新命令队列的渲染流程,以此来优化性能
  2. 了解另外两种根签名参数类型:根常量和根描述符
  3. 探索如何在程序中生成和绘制常见的几何体:如栅格、圆台和球体
  4. 研究怎样通过动态顶点缓冲区来更新CPU中的顶点数据,并且向GPU上传顶点的新的位置信息

7.1 帧资源

首先先回顾一下CPU和GPU并行工作的情形,CPU构建并提交命令列表,同时还需要执行一些必要的工作,而GPU则负责处理命令队列中的各种命令。我们的目标则是使CPU和GPU持续工作,从而充分利用系统当中的可用硬件资源。

在上一章的演示程序中,我们在绘制每一帧时都使CPU和GPU进行一次同步,这样做的主要原因有两个:

1、在GPU未执行完命令列表分配器中的所有命令之前,我们都不能将命令列表分配器重置。如果不对CPU和GPU进行同步,那么GPU中可能会有一些未执行的命令被清除

2、在GPU未完成与常量缓冲区相关的绘制命令之前,不能让CPU更新这些常量缓冲区,如果不对CPU和GPU进行同步,那么在GPU在绘制第n帧画面时,常量缓冲区存储的可能是绘制第n + 1帧画面所需的数据

所以,在上一章的演示程序中,我们在每一帧绘制的结尾都会调用D3DApp::FlushCommandQueue函数,以确保GPU中的命令可以被正确执行,这种办法虽然有效,但效率却很低:

1、在每一帧的起始阶段,GPU不会执行任何命令,因为CPU还没有向GPU提交命令

2、在每一帧的收尾阶段,CPU会等待GPU完成命令的处理

所以,GPU和CPU在每一帧都存在一些空闲的时间被浪费。解决此问题的一种方法是:以CPU每一帧都需要更新的资源作为基本元素,创建一个环形数组(circular array),我们称这些资源为帧资源(frame resource),而这种循环数组一般都是由3个帧资源元素构成的。该方法的思路是在处理第n帧的时候,CPU将从环形数组(帧资源数组)中获取下一个可用的帧资源(即没有正在被GPU使用的帧资源),趁着GPU在处理第n-1帧的画面时,CPU将为第n帧的绘制准备资源。下面我们将创建一个仅含有常量缓冲区的帧资源类

struct FrameResource
{
public:
FrameResource(ID3D12Device * device, UINT passCount, UINT objectCount);
FrameResource(const FrameResource& rhs) = delete;
FrameResource& operator=(const FrameResource& rhs) = delete;
~FrameResource(); //GPU处理完与命令分配器相关的命令之前,不能对命令分配器进行重置操作
//所以要每一帧都要有属于它自己的命令分配器
ComPtr<ID3D12CommandAllocator> CmdListAlloc; //在GPU执行与常量缓冲区相关的命令之前,不能对常量缓冲区进行重置
//所以每一帧都要有属于它自己的常量缓冲区
std::unique_ptr<UploadBuffer<PassConstants>> passCB = nullptr;
std::unique_ptr<UploadBuffer<ObjectConstants>> ObjectCB = nullptr; //通过围栏值将命令标记到此围栏点,这可以使我们检测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()
{ }

7.2 渲染项

绘制一个物体需要设置多种参数,例如绑定顶点缓冲区和索引缓冲区、绑定和物体有关的常量数据、设定图元类型以及指定DrawIndexedInstanced方法的参数。随着场景中物体数量的不断增加,我们需要创建一个轻量级结构体来存储绘制物体所需要的数据。我们把单次绘制调用过程中需要向渲染流水线提交的数据集称为渲染项。(由于每一个物体的特征不同,绘制过程中需要的参数也不一样,因此该结构体中的数据也会因具体程序而异)。

下面我们将展示本章演示程序的渲染项结构体(RenderItem):

struct RenderItem
{
RenderItem() = default; //描述物体局部空间相对于世界空间的世界矩阵
//该世界矩阵定义了物体位于世界空间的位置、朝向以及大小
XMFLOAT4X4 World = MathHelper::Identity4x4(); //用于更新标志(dirty flag)来表示物体的相关数据已经发生改变,这意味着
//我们需要更新常量缓冲区,由于每个FrameResource都有一个物体的常量缓冲
//区,所以我们需要对每一个FrameResource进行更新。所以我们要将NumFramesDirty
//的值设为gNumFrameResource,从而使每一个帧资源都得到更新
int NumFramesDirty = gNumFrameResources; //指向当前GPU常量缓冲区对应的物体常量缓冲区
UINT ObjectIndex = -1; //此渲染项参与绘制的几何体(绘制一个几何体可能需要多个渲染项)
MeshGeometry* Geo = nullptr; //图元拓扑
D3D12_PRIMITIVE_TOPOLOGY PrimitiveType = D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST; //DrawIndexedInstanced方法的参数
UINT IndexCount = 0;
UINT StartIndexLoaction = 0;
int BaseVertexLocation = 0;
};

我们的应用程序会根据各渲染项的绘制目的,根据不同PSO(流水线状态对象)所需要的渲染项,将它们划分到不同的向量中

//存有所有渲染项的向量
std::vector<std::unique_ptr<RenderItem>> mAllRitems; //根据PSO来划分渲染项
std::vector<RenderItem*> mOpaqueRitems;
std::vector<RednerItem*> mTransparentRitems;

7.3 渲染过程中所用到的常量数据

从7.1节可以看出,我们在自己实现的FrameResource类中加入了新的常量缓冲区,

std::unique_ptr<UploadBuffer<PassConstant>> PassCB = nullptr;

随着演示代码复杂度的不断提升,该缓冲区中存储的内容会会根据特定的渲染过程(render pass)而确定下来,它们是着色器程序中要访问的极有用的数据,虽然外面不会用到PassCB中的全部数据,但留着它们并不是一件坏事,因为迟早会用到:

struct ConstantBufferPass
{
float4x4 gView; //观察矩阵
float4x4 gInvView; //观察矩阵的逆矩阵
float4x4 gProj; //投影矩阵
float4x4 gInvProj; //投影矩阵的逆矩阵
float3 gEyePosW; //观察点
float cbPerObjectPad1;
float2 gRenderTargetSize;
float2 gInvRenderTargetSize;
float gNearZ; //观察点到近平面的距离
float gFarZ; //观察点到远平面的距离
float gTotalTime; //游戏总时间
float gDeltaTime; //上一帧与本帧的时间间隔
}; ConstantBuffer<ConstantBufferPass> cbPass : register(b1);

此时外面也修改了物体常量缓冲区,使它只存储世界矩阵

struct ObjectConstant
{
float4x4 gWorld;
}; ConstantBuffer<ObjectConstant> cbPerObject : register(b0);

我们做出上述调整的主要原因是:基于资源的更新频率对常量数据进行分组。在每次渲染过程(render pass)中,我们只需要把本次所用的常量(cbPass)更新一次即可,在物体的世界矩阵发生改变时,更新物体的cbPerObject即可。

在本章的演示程序中,将通过下列方法来更新渲染过程常量缓冲区和物体常量缓冲区。在绘制每一帧的画面时,这两个方法都会被调用一次:

物体常量缓冲区的更新

void ShapesApp::UpdateObjectCBs(const GameTimer & gt)
{
//获取当前帧资源中的常量缓冲区
auto currObjectCB = mCurrFrameResource->ObjectCB.get(); //遍历所有渲染项
for (auto& e : mAllRitems)
{
//如果常量发生了改变就更新所有帧资源的常量缓冲区
if (e->NumFramesDirty > 0)
{
XMMATRIX world = XMLoadFloat4x4(&e->World); ObjectConstants objectConstant;
XMStoreFloat4x4(&objectConstant.world, world);
currObjectCB->CopyData(e->ObjectIndex, objectConstant); //对下一个帧资源进行更新
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));
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);
}

随着常量缓冲区结构体的变化,我们也要相应的更新顶点着色器:

VertexOut VS(VertexIn vIn)
{
VertexOut vOut; //将局部空间的顶点坐标变换到齐次裁剪空间(局部-世界-观察-投影和齐次裁剪空间)
float4 PosW = mul(float4(vIn.PosL,1.0f),cbPerObject.gWorld);
vOut.PosH = mul(PosW, cbPass.gViewProj); //直接向像素着色器输入顶点的颜色数据
vOut.Color = vIn.Color; return vOut;
}

现在,着色器所期待的输入资源已经发生了改变,所以我们也需要相应的调整根签名来使着色器获取所需的描述符表:

//创建两个常量缓冲区描述符表
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);

7.4 不同形状的几何体

在本节中,我们将展示如何创建不同形状的几何体(球体、柱体、椭圆体等等),这些几何体对绘制天空穹顶(sky dome),图形程序调试、碰撞检测以及延迟渲染(deferred rendering)是极其有用的。比如在调试检测中,我们可以把正在制作中的游戏角色简化成球体。

我们将程序性几何体(procedural geometry,根据用户提供的参数生成对应的几何体)的代码放到GeometryGenerator文件中,GeometryGenerator是一个工具类,用于生成一些简单的几何体,该工具类将数据生成在系统内存中,而我们必须把这些数据复制到顶点缓冲区和索引缓冲区中。MeshData是一个嵌套在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;
}; 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;
};
……
};

7.4.1 生成柱体网格

在定义一个柱体之前,我们要先指定其顶和底面的半径、高度,切片数量(slice count即将横截面分割的块数)和堆叠层数(stack count)。在程序中,我们会把柱体分为侧面几何体、顶面几何体和底面几何体三部分。

7.4.2 柱体的侧面几何体

下面我们将展示创建侧面几何体的所有顶点数据代码(具体的推导过程暂时不详细解释,基本思路是遍历每一个环,并生成环上的各个顶点):

//该圆台是一个中心(高度1/2出截面的中心点)位于原点,且旋转轴平行于y轴的圆台

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.5*height + i * stackHeight;
float r = bottomRadius + radiusStep * i; //环上的各个顶点
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 / sliceCount; //切线的单位长度(顶点方程的导数)
vertex.TangentU = XMFLOAT3(-s, 0.0f, c); float dr = bottomRadius - topRadius;
XMFLOAT3 bitangent(dr*c, -height, dr*s); XMVECTOR T = XMLoadFloat3(&vertex.TangentU);
XMVECTOR B = XMLoadFloat3(&bitangent);
//返回T和B的规范化后的外积(叉积、向量积)
XMVECTOR N = XMVector3Normalize(XMVector3Cross(T, B)); XMStoreFloat3(&vertex.Normal, N); meshData.Vertices.push_back(vertex);
}
}
}

注意点:在上面的代码可以看出,每个环上的第一个顶点和最后一个顶点在位置上是相互重合的,但是二者的纹理坐标不相同,这有这样才能保证在圆台上绘制出正确的纹理。

生成顶点数据之后,我们需要生成侧面几何体的索引数据,下面是生成索引数据的示例代码(此代码接上面的CreateCylinder方法):

//+1是想多创建一个和第一个顶点重合的顶点,具体原因看上面的注意点
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);
meshData.indices32.push_back(i*ringVertexCount + j + 1);
}
} BuildCylinderTopCap(bottomRadius, topRadius, height, sliceCount, stackCount, sliceCount);
BuildCylinderBottomCap(bottomRadius, topRadius, height, sliceCount, stackCount, sliceCount); return meshData;

7.4.1.2 柱体的端面几何体

下面是生成圆台顶点几何体的方法:

void GeometryGenerator::BuildCylinderTopCap(float bottomRaidus, float topRadius, float height,
uint32 sliceCount, uint32 stackCount, GeometryGenerator::MeshData& meshData)
{
uint32 baseIndex = (uint32)meshData.Vertices.size(); float y = 0.5f*height;
float dTheta = 2.0f*XM_PI / sliceCount; //生成底面环的顶点坐标
for (uint32 i = 0; i < sliceCount; i++)
{
float x = topRadius * cosf(i*dTheta);
float z = topRadius * sinf(i*dTheta); //根据圆台的高度使顶面纹理坐标的范围按比例缩小
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));
} //顶面的中心顶点的顶点数据
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)); //中心顶点的索引值
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);
}
}

生成圆台底面几何体的方法类似于顶面几何体(所以跳过)

7.4.2 生成球体网格

欲定义一个球体,就要指定其半径、切片数量以及堆叠层数,除了每一个环上的半径是依三角函数非线性变化之外,生成球体的算法和生成圆台的算法十分接近。最后说一点,若采用不等比缩放世界矩阵,即可把球体转换为椭球体

7.4.3 生成几何球体网格

为了生成几何球体,我们会以一个正二十面体作为基础,细分上面的三角形,再根据给定的半径向球面投影新生成的顶点。反复这个过程,便可以提高几何球体的曲面细分程度了。下面是相关的代码:

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 < meshData.Vertices.size(); ++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); //根据球面坐标推导出纹理坐标(计算z和x的反正切值)
float theta = atan2f(meshData.Vertices[i].Position.z, meshData.Vertices[i].Position.x); //将theta的值限制在0到2pi
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;
}

DirectX12 3D 游戏开发与实战第七章内容(上)的更多相关文章

  1. DirectX12 3D 游戏开发与实战第七章内容(下)

    利用Direct3D绘制几何体(续) 学习目标 学会一种无须每帧都要刷新命令队列的渲染流程,由此来优化程序的性能 了解另外两种跟签名参数类型:根描述符和根常量 探索如何在程序中生成和绘制常见的几何体, ...

  2. DirectX12 3D 游戏开发与实战第五章内容

    渲染流水线 学习目标: 了解用于在2D图像中表现出场景立体感和空间深度感等真实效果的关键因素 探索如何用Direct3D表示3D对象 学习如何建立虚拟摄像机 理解渲染流水线,根据给定的3D场景的几何描 ...

  3. DirectX12 3D 游戏开发与实战第六章内容

    利用Direct3D绘制几何体 学习目标 探索用于定义.存储和绘制几何体数据的Direct接口和方法 学习编写简单的顶点着色器和像素着色器 了解如何用渲染流水线状态对象来配置渲染流水线 理解怎样创建常 ...

  4. DirectX12 3D 游戏开发与实战第四章内容(上)

    Direct3D的初始化(上) 学习目标 了解Direct3D在3D编程中相对于硬件所扮演的角色 理解组件对象模型COM在Direct3D中的作用 掌握基础的图像学概念,例如2D图像的存储方式,页面翻 ...

  5. DirectX12 3D 游戏开发与实战第四章内容(下)

    Direct3D的初始化(下) 学习目标 了解Direct3D在3D编程中相对于硬件所扮演的角色 理解组件对象模型COM在Direct3D中的作用 掌握基础的图像学概念,例如2D图像的存储方式,页面翻 ...

  6. DirectX12 3D 游戏开发与实战第三章内容

    变换 学习目标 理解如何使用矩阵表示线性变换和仿射变换 学习对几何体进行缩放.旋转和平移的坐标变换 根据矩阵之间的乘法运算性质,将多个变换矩阵合并为一个单独的净变换矩阵 找寻不同坐标系之间的坐标转换方 ...

  7. DirectX12 3D 游戏开发与实战第十一章内容

    仅供个人学习使用,请勿转载.谢谢! 11.模板 模板缓冲区(stencil buffer)是一种"离屏"(off-screen)缓冲区,我们可以利用它来实现一些效果.模板缓冲区.后 ...

  8. DirectX12 3D 游戏开发与实战第八章内容(下)

    DirectX12 3D 游戏开发与实战第八章内容(下) 8.9.材质的实现 下面是材质结构体的部分代码: // 简单的结构体来表示我们所演示的材料 struct Material { // 材质唯一 ...

  9. DirectX12 3D 游戏开发与实战第八章内容(上)

    8.光照 学习目标 对光照和材质的交互有基本的了解 了解局部光照和全局光照的区别 探究如何用数学来描述位于物体表面上某一点的"朝向",以此来确定入射光照射到表面的角度 学习如何正确 ...

随机推荐

  1. Golang通脉之接口

    接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节. 接口类型 在Go语言中接口(interface)是一种类型,一种抽象的类型. interface是 ...

  2. Sequence Model-week3编程题2-Trigger Word Detection

    1. Trigger Word Detection 我们的触发词将是 "Activate.".每当它听到你说 "Activate.",它就会发出 "c ...

  3. find&正则表达式

    标准的正则表示式格式 常用元字符 代码 说明 . 匹配除换行符以外的任意字符 \w 匹配字母或数字或下划线 \s 匹配任意的空白符 \d 匹配数字 \b 匹配单词的开始或结束 ^ 匹配字符串的开始 $ ...

  4. DDD领域驱动设计-案例建模设计-Ⅲ

    1. 背景 参考<DDD领域驱动设计-案例需求文档>,本文将构建实体,聚合根详述领域驱动中的建模设计.构建实体,聚合根的一些原则或方法,将在后续文章中说明. 2. 建模设计 2.1. 实体 ...

  5. Spring Cloud Alibaba 介绍及工程准备

    简介 SpringCloud Alibaba是阿里巴巴集团开源的一套微服务架构解决方案. 微服务架构是为了更好的分布式系统开发,将一个应用拆分成多个子应用,每一个服务都是可以独立运行的子工程.其中涵盖 ...

  6. 六个好习惯让你的PCB设计更优

    PCB layout工程师每天对着板子成千上万条走线,各种各样的封装,重复着拉线的工作,也许很多人会觉得是很枯燥无聊的工作内容.看似软件操作搬运工,其实设计人员在过程中要在各种设计规则之间做取舍,兼顾 ...

  7. 极速上手 VUE 3—v-model 的使用变化

    本篇文章主要介绍 v-model 在 Vue2 和 Vue3 中使用变化. 一.Vue2 中 v-model 的使用 v-model 是语法糖,本质还是父子组件间的通信.父子组件通信时有两种方式: 父 ...

  8. Windows 2008 R2 NTP 时钟同步配置

    一.配置 本地组策略 a.windows+R 或  "开始菜单"  | "运行"  ,打开运行窗口. 输入gpedit.msc打开本地组策略 b.在 本地组策略 ...

  9. Vue3.x 关于组件的那些变化(新手必看篇)

    一.组件内的 data 为什么总是函数形式? 我们试着先做一个计数器案例,把 data 的返回形式修改成一个对象.具体的代码如下: <template> <div> <b ...

  10. Solon & Solon Cloud 1.5.62 发布,轻量级 Java 基础开发框架

    Solon 已有120个生态扩展插件,此次更新主要为细节打磨,且对k8s和docker-compose更友好: 1.插件 solon.coud ,事件总线增加支持本地同主题多订阅模式(以支持同服务内, ...