DirectX11学习笔记二 渲染一个立方体

上一节学习笔记地址
  上一节画三角形只有三个顶点,所以直接按照三个顶点顺序画即可。不过那里漏过了一个细节,那就是你的三角形的顶点顺序,这里要引入一个叫“背面消除(backface culling)”的概念。因为三角形有两个面,假如你的正面是背对着摄像机,那摄像机就不会渲染你三角形的正面的图像(被剔除掉),复习一下上节所传入的三角形顶点。

	// 设置三角形顶点
	VertexPosColor vertices[] =
	{
		{ XMFLOAT3(0.0f, 0.5f, 0), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
		{ XMFLOAT3(0.5f, -0.5f, 0), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) },
		{ XMFLOAT3(-0.5f, -0.5f, 0), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) }
	};

在这里插入图片描述
  DX使用左手坐标系,并规定顶点顺序按照顺时针排序的为正面,那另一面就是反面。
  可以修改这种剔除方法Device->SetRenderState(D3DRS_CULLMODE,Value);
  可以传入三种参数

参数描述
D3DCULL_NONE表示不采用背面消除,那么此时,不过正面指向哪里都将不会被剔除掉
D3DCULL_CW表示跟我们刚才说的相反,按右手顺时针方向指向的的方向指向我们的眼睛的话,那么剔除掉
D3DCULL_CCW默认值,顺时针为正面

  所以,如果你把第一个顶点和第三个顶点调换,就显示不出东西了,因为你看到的是背面。
  如果你要渲染一个四边形,可以在三角形的基础上再加三个点,,这样就变成了两个三角形,6个顶点,表示一个四边形,但是实际上有两个顶点是重复的,在顶点比较多的时候,会引起巨大的浪费,比如渲染一个立方体,甚至一个顶点会重复更多次,所以为了节省资源,DX引入了索引这个概念。
  另外,由于我们现在渲染的是3维空间,必然要引入MVP变换,这个到着色器里再说。

顶点缓冲区

// ******************
	// 设置立方体顶点
	//    5________ 6
	//    /|      /|
	//   /_|_____/ |
	//  1|4|_ _ 2|_|7
	//   | /     | /
	//   |/______|/
	//  0       3
	VertexPosColor vertices[] =
	{
		{ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f) },
		{ XMFLOAT3(-1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) },
		{ XMFLOAT3(1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f) },
		{ XMFLOAT3(1.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
		{ XMFLOAT3(-1.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) },
		{ XMFLOAT3(-1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 0.0f, 1.0f, 1.0f) },
		{ XMFLOAT3(1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
		{ XMFLOAT3(1.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 1.0f, 1.0f, 1.0f) }
	};
	// 设置顶点缓冲区描述
	D3D11_BUFFER_DESC vbd;
	ZeroMemory(&vbd, sizeof(vbd));
	vbd.Usage = D3D11_USAGE_IMMUTABLE;
	vbd.ByteWidth = sizeof vertices;
	vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
	vbd.CPUAccessFlags = 0;
	// 新建顶点缓冲区
	D3D11_SUBRESOURCE_DATA InitData;
	ZeroMemory(&InitData, sizeof(InitData));
	InitData.pSysMem = vertices;
	HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffer.GetAddressOf()));
	m_pd3dImmediateContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), &stride, &offset);

  方法与上一节的笔记一样,只不过变成了8个顶点,三维空间坐标。

索引缓冲区

  索引会引导DX选择渲染次序。

// ******************
	// 索引数组
	//
	WORD indices[] = {
		// 正面
		0, 1, 2,
		2, 3, 0,
		// 左面
		4, 5, 1,
		1, 0, 4,
		// 顶面
		1, 5, 6,
		6, 2, 1,
		// 背面
		7, 6, 5,
		5, 4, 7,
		// 右面
		3, 2, 6,
		6, 7, 3,
		// 底面
		4, 0, 3,
		3, 7, 4
	};
	// 设置索引缓冲区描述
	D3D11_BUFFER_DESC ibd;
	ZeroMemory(&ibd, sizeof(ibd));
	ibd.Usage = D3D11_USAGE_IMMUTABLE;
	ibd.ByteWidth = sizeof indices;
	ibd.BindFlags = D3D11_BIND_INDEX_BUFFER;
	ibd.CPUAccessFlags = 0;
	// 新建索引缓冲区
	InitData.pSysMem = indices;
	HR(m_pd3dDevice->CreateBuffer(&ibd, &InitData, m_pIndexBuffer.GetAddressOf()));
	// 输入装配阶段的索引缓冲区设置
	m_pd3dImmediateContext->IASetIndexBuffer(m_pIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);

  方法同上文的顶点缓冲区类似,简单比较一下就能看懂了。

void ID3D11DeviceContext::IASetIndexBuffer( 
    ID3D11Buffer *pIndexBuffer,     // [In]索引缓冲区
    DXGI_FORMAT Format,             // [In]数据格式
    UINT Offset);                   // [In]字节偏移量

  在装配的时候你需要指定每个索引所占的字节数:’

Format参数字节数索引范围
DXGI_FORMAT_R8_UINT10-255
DXGI_FORMAT_R16_UINT20-65535
DXGI_FORMAT_R32_UINT40-2147483647

  使用16位的索引有利于节省空间,现阶段绝大多数模型的顶点数目都少于65535个。

数学基础

  补习一下向量和线代的知识

向量(vector)
  向量由模(magnitude)和方向(direction)组成,与标量(scalar)区分。
标量与向量积
在这里插入图片描述
向量加法
在这里插入图片描述
二维向量加法的几何意义
在这里插入图片描述
向量的模
在这里插入图片描述
向量单位化
在这里插入图片描述
向量点积(dot product)
在这里插入图片描述
在这里插入图片描述
  (ka)b=a(kb)=k(ab)
  a(b+c)=ab+ac
  vv=|v|^2
向量点积的几何意义
在这里插入图片描述
向量叉积(cross product)
在这里插入图片描述
  axb!=bxa
  axb=-(bxa)
  (axb)xc!=ax(bxc)
   叉积的几何意义: 得到垂直于a和b所在平面的向量。通过将a的头和b的尾相接,并检查从a到b是顺时针还是逆时针,能够确定a叉乘b的方向。左手坐标系中,如果a与b呈顺时针,则叉乘方向指向你,否则远离,右手坐标系相反。
矩阵(matrix)
   常数与矩阵乘法
在这里插入图片描述
矩阵与矩阵乘法
在这里插入图片描述
  rxn和nxc矩阵相乘,得到rxc矩阵
  矩阵的性质:
  AB!=BA
  (AB)C=A(BC)
方阵(square matrix )
  即nxn矩阵,如果一个方阵除了对角元素(diagonal elements)外都是0,则称为对角矩阵(diagonal matrix)
单位矩阵(identity matrix)
  对角元素全为1的对角矩阵
转置矩阵(transposed matrix)
在这里插入图片描述
转置矩阵性质
在这里插入图片描述
在这里插入图片描述
逆矩阵(inverse matrix)
矩阵的行列式(determinant)不为0则可逆(invertible)
逆矩阵性质
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
正交矩阵(orthogonal matrix)
MxMT=E,则称M正交,所以M的逆等于M转置
变换(Transform)
在这里插入图片描述
以下按照行主续计算,顶点的齐次矩阵是1x4。
平移变换
在这里插入图片描述
缩放变换
在这里插入图片描述
旋转变换
X轴
在这里插入图片描述
Y轴
在这里插入图片描述
Z轴
在这里插入图片描述
三维坐标系的旋转是要具体细分是按照哪个轴旋转的,选定某个轴之后,剩下的部分按二维坐标系旋转来判断。旋转的本质是二维坐标向量的分解。
目前我知道有两种有关不同坐标系的转换,一个是向量的变换(就是上面的旋转矩阵),一个是坐标的变换

左乘和右乘

  finalvertex.pos = vertex.pos * worldMatrix * viewMatrix * projectionMatrix;这是MVP变换左乘的写法,很好理解,模型坐标到世界空间就乘相应的矩阵,而且dx里也可以这么写。如果是右乘,那写法就要倒过来,变成PVWv。
  左乘跟右乘,出现这个概念的一大原因就是坐标的表示:用行矩阵or列矩阵?
  假设我们要将(1,2,3)平移(4,5,6),即到(5,7,9),那么平移矩阵写法:
在这里插入图片描述

空间类型

  上一节提到了

  综上所述,要获得从对象(又称局部)空间到投影空间的3d模型,我们将每个顶点乘以世界,然后是视图,然后是投影。它看起来像这样:
finalvertex.pos = vertex.pos * worldMatrix * viewMatrix * projectionMatrix;
  在我们的顶点着色器中,我们实际上将使用世界/视图/投影矩阵(包含所有三个空间的单个矩阵)将每个顶点移入投影空间,如下所示:
  output.pos = mul(input.pos, wvpMat); wvp代表后三个矩阵的缩写。

  在计算机图形学中通常使用下列空间:模型空间(model space),世界空间(world space),视空间(view space),投影空间(projection space)和屏幕空间(screen space)。

模型空间(Model Space)
  注意立方体的中心在原点。对象空间又称为对象空间(object space),表示创建3D模型的艺术家使用的空间。通常,艺术家在创建模型时将它的中点放置在原点上,这样做可以很容易地进行诸如旋转之类的变换操作,立方体的8个顶点坐标如下:
(-1, 1, -1)
( 1, 1, -1)
(-1, -1, -1)
( 1, -1, -1)
(-1, 1, 1)
( 1, 1, 1)
(-1, -1, 1)
( 1, -1, 1)
  因为模型空间是艺术家在设计和创建模型时使用的,所以存储在磁盘上的模型也处于模型空间。应用程序可以创建一个顶点缓存代表这个模型并使用模型数据进行缓存的初始化。因此,顶点缓存中的顶点通常也是处于模型空间中的,这样意味着顶点着色器接收的是模型空间中的顶点数据。

世界空间(World Space)
  世界空间被场景中的所有对象共享,它用来定义对象的空间联系。要理解世界空间,你可以想象自己站在一个长方体房间的西南角面朝北方。我们定义自己的脚站在原点(0, 0, 0)。X轴在我们右方,Y轴向上,Z轴向前,即我们面朝的方向。这样,房间中的每个位置都可以定义为一组XYZ坐标。例如,有一个椅子在我们前方5英尺,右方2英尺,椅子上方8英尺高处的天花板上有一盏灯,我们可以将椅子的位置定为(2, 0, 5),灯的位置定为(2, 8, 5)。如你所见,世界空间如其名所示,它说明了物体在世界中的位置联系。

视图空间(View Space)
  视图空间,有时也称为相机空间,但是,视空间中的原点是观察者或者相机。观察方向(观察者视线方向)定义了Z轴,由程序定义的“up”方向成为Y轴,如下图所示。
在这里插入图片描述
      同一个物体在世界空间(左)和时空间(右)
  左图表示一个由人形对象和观察者(相机)组成的场景。用于世界空间的原点和轴为红色。右图表示对应世界空间的视空间。视空间轴用蓝色表示。为了能说明得更清楚,视空间与世界空间的朝向不同。注意在视空间中,相机朝向Z轴方向。
  这么讲可能还有些模糊,再举个例子,世界坐标中物体的移动在摄像机坐标系中是反方向,因此需要将世界坐标系中的物体放到摄像机坐标系中来处理。再比如假如摄像机在世界中是倾斜的,那世界坐标系中物体的水平移动在摄像机看来必然不可能还是水平的移动。这就要求将世界坐标系中的顶点变换到摄像机坐标系来计算。
投影空间(Projection Space)
  投影空间表示在视空间上施加了投影变换的空间。在这个空间中,可视内容的X、Y轴坐标范围从-1到1,而Z轴坐标的范围从0到1。
  投影空间的目标是能够方便地对渲染图元进行裁剪,完全位于这块空间内部的图元将会被保留,完全位于这块空间外部的图元将会被剔除,而与这块空间边界相交的图元就会被裁剪。那么,这块空间是如何决定的呢?答案就是由视锥体(view frustum)来决定。
  视锥体指的是空间中的一块区域,这块区域决定了摄像机可以看到的空间。视锥体由六个平面包围而成,这些平面也被称为裁剪平面(clip planes)。视锥体有两种类型,这涉及两种投影雷影:一种是正交投影(orthographic projection),一种是透视投影(perspective projection)。
  在视锥体的6块菜间平面中,有两块裁剪平面比较特殊,他们分别被称为近裁剪平面(near clip plane)和远裁剪平面(far clip plane)。他们决定了摄像机可以看到的深度范围。正交投影和透视投影的视锥体如图所示。
在这里插入图片描述
对于透视投影
在这里插入图片描述
  视锥体的意义在于定义了场景中的一块三维空间。所有位于这块空间内的物体将会被渲染,否则就会被剔除或裁剪。那这六个裁剪平面是如何定义的呢?
  FOV属性来改变视锥体竖直方向的张开角度,而剪裁平面中的near和far距离可以控制视锥体的近剪裁平面和远剪裁平面距离摄像机的远近。
  由图我们可以求出视锥体near和far平面的高度。但是我们还缺乏横向的信息。这可以通过摄像机的横纵比得到,也可以理解为分辨率的横纵比,800/600 etc.
屏幕空间(Screen Space)
  屏幕空间常常用来表示在帧缓存中的位置。因为帧缓存通常是一张2D纹理,所以屏幕空间是一个2D空间。左上角为原点坐标为(0, 0)。X轴正方向指向右方,Y轴正方向指向下方。一个宽w像素高h像素的缓存右下角的坐标为(w - 1, h - 1)。

DX和OPENGL数学上的区别

https://www.cnblogs.com/graphics/archive/2012/08/02/2616017.html

MVP变换

  WVP或者叫MVP,model->view->projection,(tranform your model into world space, then into camera space and then into projection space),上节还提到了齐次坐标,实际上齐次坐标的矩阵形式非常适合这里的变换,
  都是矩阵乘法,难点在于理解变换流程和原理,理解这三个矩阵都是什么东西,先看一下着色器的代码。

cbuffer ConstantBuffer : register(b0)
{
    matrix g_World; // matrix可以用float4x4替代。不加row_major的情况下,矩阵默认为列主矩阵,
    matrix g_View;  // 可以在前面添加row_major表示行主矩阵
    matrix g_Proj;  // 该教程往后将使用默认的列主矩阵,但需要在C++代码端预先将矩阵进行转置。
}


struct VertexIn
{
	float3 posL : POSITION;
	float4 color : COLOR;
};

struct VertexOut
{
	float4 posH : SV_POSITION;
	float4 color : COLOR;
};

VertexOut VS(VertexIn vIn)
{
    VertexOut vOut;
    vOut.posH = mul(float4(vIn.posL, 1.0f), g_World);  // mul 才是矩阵乘法, 运算符*要求操作对象为
    vOut.posH = mul(vOut.posH, g_View);               // 行列数相等的两个矩阵,结果为
    vOut.posH = mul(vOut.posH, g_Proj);               // Cij = Aij * Bij*/
    vOut.color = vIn.color;                         // 这里alpha通道的值默认为1.0
    return vOut;
}

// 像素着色器
float4 PS(VertexOut pIn) : SV_Target
{
    return pIn.color;
}

  再次看一下定义正方体顶点的代码

// ******************
	// 设置立方体顶点
	//    5________ 6
	//    /|      /|
	//   /_|_____/ |
	//  1|4|_ _ 2|_|7
	//   | /     | /
	//   |/______|/
	//  0       3
	VertexPosColor vertices[] =
	{
		{ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f) },
		{ XMFLOAT3(-1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) },
		{ XMFLOAT3(1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f) },
		{ XMFLOAT3(1.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
		{ XMFLOAT3(-1.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) },
		{ XMFLOAT3(-1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 0.0f, 1.0f, 1.0f) },
		{ XMFLOAT3(1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
		{ XMFLOAT3(1.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 1.0f, 1.0f, 1.0f) }
	};

  定义MVP的代码

// 初始化常量缓冲区的值
	m_CBuffer.world = XMMatrixIdentity();	// 单位矩阵的转置是它本身
	m_CBuffer.view = XMMatrixTranspose(XMMatrixLookAtLH(
		XMVectorSet(0.0f, 0.0f, -5.0f, 0.0f),
		XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f),
		XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f)
	));
	m_CBuffer.proj = XMMatrixTranspose(XMMatrixPerspectiveFovLH(XM_PIDIV2, AspectRatio(), 1.0f, 1000.0f));

  我看完之后第一时间感觉头大,为什么要转置???
  首先顶点模型空间齐次矩阵是行矩阵,这是可以肯定的。
  原来是hlsl着色器会在默认情况下帮我自动转置传入的其它齐次矩阵,参考着色器矩阵序列,可以看到是column-major by default,而且column-major的表现就是矩阵转置后的样子。我们可以通过在hlsl头文件写上#pragmapack_matrix(row_major或者column_major)来决定时哪种序列方式。
  本节用到的代码是在着色器外部已经转置过的,所以进去着色器自动转置的时候相当于又转置了回去。如果你选择row主序,亲测那在外面就不用手动转置了。(微软为什么要这么做???)

  • model to world矩阵是四阶单位矩阵的逆矩阵
	m_CBuffer.world = XMMatrixIdentity();	// 单位矩阵的转置是它本身

  几何管线的世界变换阶段,以一个位于其自身局部坐标系统的物体为输入,将该物体变换到世界坐标系统中。世界坐标系统是一个包含 3D 世界中的所有物体的位置和方向的坐标系统。该坐标系统有一个单一固定的原点,所有变换到该系统的模型都是以该原点为参考。
  从模型空间变换到世界空间的过程,通常发生在顶点着色器之内并且当它渲染几何图形时执行该过程。你也可以在着色器外部变换几何图形,但是要求重新注入(绑定)顶点缓存的动态信息,这并不是一种好的实践方式(特别是对于静态几何图形来说)。
  几何管线的下一个阶段是视图变换。因为通过世界变换后,此时所有的物体都是以单一固定的世界原点为参考,你只能从该点处来观察它们。为了允许从任意点观察场景,这些物体还必须通过视图变换
  其实这个矩阵可以处理很多操作,比如平移,旋转,缩放。
①平移矩阵
  XMMatrixTranslation
②旋转矩阵
  XMMatrixRotation 顺序Z X Y
③缩放矩阵
  XMMatrixScaling
变换顺序:缩放->旋转->平移 具体原因可以参考世界坐标变换要先缩放、后旋转、再平移的原因
  因为模型坐标的中心肯定是(0,0,0),那要被安放在世界空间,自然要进行一个变换。这个矩阵说白了就是让模型空间里的顶点找到在世界空间中的坐标。

  • world to view矩阵通过XMMatrixLookAtLH方法创建
      视图变换是将坐标从世界空间变换到视图空间。视图空间所涉及的坐标系统以一个虚拟照相机的位置为参考点(原点),换句话说,就是在游戏世界中视图变换模拟照相机观察世界空间中的物体,除了图形用户界面(例如:用于血条,时间统计,弹药数量的屏幕元素,等)。当你选择一个点用于放置你的虚拟照相机观察时,世界空间的坐标需要再次变换来适应照相机的观察。例如,照相机放置在原点,而此时世界空间本身需要移动到照相机的视图中。
      变换时,你的照相机的角度和视图将应用于场景,之后就可以准备屏幕的显示了。
m_CBuffer.view = XMMatrixTranspose(XMMatrixLookAtLH(
		XMVectorSet(0.0f, 0.0f, -5.0f, 0.0f),
		XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f),
		XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f)
	));
inline XMMATRIX XM_CALLCONV XMMatrixLookAtLH  //创建视图矩阵
(
    FXMVECTOR EyePosition,   //摄像机位置
    FXMVECTOR FocusPosition,  //焦点位置,是个位置!或者可以理解为要观察的物体的坐标
    FXMVECTOR UpDirection  //规定上的方向,一般为0.0f, 1.0f, 0.0f
)
{
    XMVECTOR EyeDirection = XMVectorSubtract(FocusPosition, EyePosition); //焦点位置减去摄像机位置,得到摄像机朝向向量
    return XMMatrixLookToLH(EyePosition, EyeDirection, UpDirection);  //通过XMMatrixLookToLH函数创建视图矩阵
}
inline XMMATRIX XM_CALLCONV XMMatrixLookToLH
(
    FXMVECTOR EyePosition, //摄像机位置
    FXMVECTOR EyeDirection, //摄像机朝向
    FXMVECTOR UpDirection  //up方向
)
{
    assert(!XMVector3Equal(EyeDirection, XMVectorZero()));
    assert(!XMVector3IsInfinite(EyeDirection));
    assert(!XMVector3Equal(UpDirection, XMVectorZero()));
    assert(!XMVector3IsInfinite(UpDirection));

    XMVECTOR R2 = XMVector3Normalize(EyeDirection);  

    XMVECTOR R0 = XMVector3Cross(UpDirection, R2);  
    R0 = XMVector3Normalize(R0);  

    XMVECTOR R1 = XMVector3Cross(R2, R0);  

    XMVECTOR NegEyePosition = XMVectorNegate(EyePosition);  

    XMVECTOR D0 = XMVector3Dot(R0, NegEyePosition);  
    XMVECTOR D1 = XMVector3Dot(R1, NegEyePosition);  
    XMVECTOR D2 = XMVector3Dot(R2, NegEyePosition);  

    XMMATRIX M;  
    M.r[0] = XMVectorSelect(D0, R0, g_XMSelect1110.v);  
    M.r[1] = XMVectorSelect(D1, R1, g_XMSelect1110.v);
    M.r[2] = XMVectorSelect(D2, R2, g_XMSelect1110.v);
    M.r[3] = g_XMIdentityR3.v;

    M = XMMatrixTranspose(M);

    return M;
}

  XMMatrixLookToLH()创建视图矩阵,如何推导的?
  我们要知道并不存在真正的摄像机,只不过是在世界坐标系里面选择一个点,作为摄像机的位置。然后根据一些参数,在这个点构建一个坐标系。然后通过视图矩阵将世界坐标系的坐标变换到摄像机坐标系下。
  我们先简单说一下我们的目标,在世界坐标系中选取一点作为观察点,并以观察点建立一个坐标系以观察点建立的坐标系就是我们需要的摄像机坐标系,在建立此坐标系后,我们做的就是通过矩阵将世界坐标系下点的坐标变换到摄像机坐标系下。
  用我自己的理解,就是摄像机的三个轴是一组基向量,世界坐标中心是一组基向量,我们要做的就是用摄像机的三个轴表示世界坐标中的坐标。这里牵扯到坐标转换变换。可以分解为平移+旋转,平移非常简单,就是将摄像机坐标系沿着摄像机在世界坐标系中的坐标反向移动,而旋转,就需要求出摄像机坐标系的三个基向量。
在这里插入图片描述
  这里其实有一条细节没说,我后面看懂了才回来改的,实际上,视图矩阵的作用是将摄像机移动到世界原点,然后让摄像机的xyz轴与世界轴重合,镜头方向指向+Z,这样就可以方便的用z来判断物体的深度来计算遮挡关系。
那么接下来重新考虑这个矩阵。
还是分为两步1.平移,平移跟前面的一样,就考虑让摄像机移动到世界原点的平移矩阵即可,摄像机移动其实就是所有物体同向移动,别忘了其实根本不存在所谓的“摄像机”。
2.接下来的操作虽然可以叫做“旋转”,但实际上是一种坐标系转换,将所有定点从xyz坐标系变换到相机的坐标系中,公式就是(x,y,z)分别点积相机的三个轴向量(两两正交)。这样理解就简单多了。

  • view to projection
      视图变换之后,几何管线的下一个步骤是投影变换。投影变换是管线应用(处理)深度的阶段。当你需要将靠近照相机的物体显示得比远离的物体大一些,就需要创建深度错觉。这种投影就是透视投影,用于 3D 游戏。另一种投影将会保持所有渲染的物体在它们的不同距离(相对于照相机)上看起来一样大。这种投影就是正交投影。还有一种投影是透视投影,用于以 2D 技术绘制显示 3D 物体,这种投影是对于 3D 显示的一种模拟而不是在图形 API 中实际进行 3D 处理。我们通常所使用的投影就是透视投影和正交投影。
      最后,顶点可以在视口中测量(定位)并且投影在 2D 空间。其结果就是在显示器上显示拥有 3D 场景错觉的 2D图像。
      投影变换矩阵由XMMatrixPerspectiveFovLH方法创建
inline XMMATRIX XM_CALLCONV XMMatrixPerspectiveFovLH
(
    float FovAngleY,  //FOV,比如XM_PIDIV2(即π/2)
    float AspectRatio,   //视图矩阵XY横纵比,比如800/600
    float NearZ,   //与近处剪裁平面的距离,必须比0大
    float FarZ  //与远处剪裁平面的距离,必须比0大
)
{
    assert(NearZ > 0.f && FarZ > 0.f);
    assert(!XMScalarNearEqual(FovAngleY, 0.0f, 0.00001f * 2.0f));
    assert(!XMScalarNearEqual(AspectRatio, 0.0f, 0.00001f));
    assert(!XMScalarNearEqual(FarZ, NearZ, 0.00001f));

#if defined(_XM_NO_INTRINSICS_)

    float    SinFov;
    float    CosFov;
    XMScalarSinCos(&SinFov, &CosFov, 0.5f * FovAngleY);

    float Height = CosFov / SinFov;
    float Width = Height / AspectRatio;
    float fRange = FarZ / (FarZ-NearZ);

    XMMATRIX M;
    M.m[0][0] = Width;
    M.m[0][1] = 0.0f;
    M.m[0][2] = 0.0f;
    M.m[0][3] = 0.0f;

    M.m[1][0] = 0.0f;
    M.m[1][1] = Height;
    M.m[1][2] = 0.0f;
    M.m[1][3] = 0.0f;

    M.m[2][0] = 0.0f;
    M.m[2][1] = 0.0f;
    M.m[2][2] = fRange;
    M.m[2][3] = 1.0f;

    M.m[3][0] = 0.0f;
    M.m[3][1] = 0.0f;
    M.m[3][2] = -fRange * NearZ;
    M.m[3][3] = 0.0f;
    return M;

#elif defined(_XM_ARM_NEON_INTRINSICS_)
    float    SinFov;
    float    CosFov;
    XMScalarSinCos(&SinFov, &CosFov, 0.5f * FovAngleY);

    float fRange = FarZ / (FarZ-NearZ);
    float Height = CosFov / SinFov;
    float Width = Height / AspectRatio;
    const XMVECTOR Zero = vdupq_n_f32(0);

    XMMATRIX M;
    M.r[0] = vsetq_lane_f32( Width, Zero, 0 );
    M.r[1] = vsetq_lane_f32( Height, Zero, 1 );
    M.r[2] = vsetq_lane_f32( fRange, g_XMIdentityR3.v, 2 );
    M.r[3] = vsetq_lane_f32( -fRange * NearZ, Zero, 2 );
    return M;
#elif defined(_XM_SSE_INTRINSICS_)
    float    SinFov;
    float    CosFov;
    XMScalarSinCos(&SinFov, &CosFov, 0.5f * FovAngleY);

    float fRange = FarZ / (FarZ-NearZ);
    // Note: This is recorded on the stack
    float Height = CosFov / SinFov;
    XMVECTOR rMem = {
        Height / AspectRatio,
        Height,
        fRange,
        -fRange * NearZ
    };
    // Copy from memory to SSE register
    XMVECTOR vValues = rMem;
    XMVECTOR vTemp = _mm_setzero_ps(); 
    // Copy x only
    vTemp = _mm_move_ss(vTemp,vValues);
    // CosFov / SinFov,0,0,0
    XMMATRIX M;
    M.r[0] = vTemp;
    // 0,Height / AspectRatio,0,0
    vTemp = vValues;
    vTemp = _mm_and_ps(vTemp,g_XMMaskY);
    M.r[1] = vTemp;
    // x=fRange,y=-fRange * NearZ,0,1.0f
    vTemp = _mm_setzero_ps();
    vValues = _mm_shuffle_ps(vValues,g_XMIdentityR3,_MM_SHUFFLE(3,2,3,2));
    // 0,0,fRange,1.0f
    vTemp = _mm_shuffle_ps(vTemp,vValues,_MM_SHUFFLE(3,0,0,0));
    M.r[2] = vTemp;
    // 0,0,-fRange * NearZ,0.0f
    vTemp = _mm_shuffle_ps(vTemp,vValues,_MM_SHUFFLE(2,1,0,0));
    M.r[3] = vTemp;
    return M;
#endif
}

推导过程
  MVP创建绑定传值之后,只需要在顶点着色器里做乘法就好了。

常量缓冲区

  在HLSL中,常量缓冲区的变量类似于C++这边的全局常量,供着色器代码使用。但是这个“常量”并不是指不能修改,实际上在c++这里完全是可以动态修改的。下面是一个HLSL常量缓冲区示例:

cbuffer ConstantBuffer : register(b0)  //指的是该常量缓冲区位于寄存器索引为0的缓冲区
{
    matrix g_World; // matrix可以用float4x4替代。不加row_major的情况下,矩阵默认为列主矩阵,
    matrix g_View;  // 可以在前面添加row_major表示行主矩阵
    matrix g_Proj;  // 该教程往后将使用默认的列主矩阵,但需要在C++代码端预先将矩阵进行转置。
}

  如何创建呢?
  首先定义要传入的结构体

struct ConstantBuffer
{
	DirectX::XMMATRIX world;
	DirectX::XMMATRIX view;
	DirectX::XMMATRIX proj;
};

  然后定义和初始化这个结构体的m_CBuffer对象
  m_CBuffer()
  初始化之后,我们再创建常量缓冲区

// ******************
	// 设置常量缓冲区描述
	//
	D3D11_BUFFER_DESC cbd;
	ZeroMemory(&cbd, sizeof(cbd));
	cbd.Usage = D3D11_USAGE_DYNAMIC;  //表示程序运行之后还可以动态修改
	cbd.ByteWidth = sizeof(ConstantBuffer);
	cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
	cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;  //支持CPU写操作
	// 新建常量缓冲区,不使用初始数据
	HR(m_pd3dDevice->CreateBuffer(&cbd, nullptr, m_pConstantBuffer.GetAddressOf()));

  注意,这个是否常量结构体对象还没有传入常量缓冲区,要将常量传入常量缓冲区,可以按照创建其他缓冲区需要使用的D3D11_SUBRESOURCE_DATA对象,或者使用D3D11_MAPPED_SUBRESOURCE,下面旋转效果中会讲。
  接下来初始化常量结构体的值

// 初始化常量的值
	m_CBuffer.world = XMMatrixIdentity();	// 单位矩阵的转置是它本身
	m_CBuffer.view = XMMatrixTranspose(XMMatrixLookAtLH(
		XMVectorSet(0.0f, 0.0f, -5.0f, 0.0f),
		XMVectorSet(0.0f, 0.0f, 0.0f, 0.0f),
		XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f)
	));
	m_CBuffer.proj = XMMatrixTranspose(XMMatrixPerspectiveFovLH(XM_PIDIV2, AspectRatio(), 1.0f, 1000.0f));

  实际上流程跟创建其它缓冲区没什么区别。
  最后别忘了绑定到渲染管线
  绑定常量缓冲区的操作通常只需要调用一次即可:

void ID3D11DeviceContext::VSSetConstantBuffers( 
    UINT StartSlot,     // [In]放入缓冲区的起始索引,例如上面指定了b0,则这里应为0
    UINT NumBuffers,    // [In]设置的缓冲区数目
    ID3D11Buffer *const *ppConstantBuffers);    // [In]用于设置的缓冲区数组
// 将更新好的常量缓冲区绑定到顶点着色器
	//m_pd3dImmediateContext->VSSetConstantBuffers(0, 1, m_pConstantBuffer.GetAddressOf());

  再次注意,目前常量缓冲区里还没值(在下节传入)。如果要在初始化的时候传值,可以用D3D11_SUBRESOURCE_DATA对象传值,仿照其它缓冲区的初始化。

旋转效果

  通过main()-(消息循环)>D3DApp::Run()->UpdateScene()方法调用。

void GameApp::UpdateScene(float dt)
{
	
	static float phi = 0.0f, theta = 0.0f;
	phi += 0.0001f, theta += 0.00015f;
	m_CBuffer.world = XMMatrixTranspose(XMMatrixRotationX(phi) * XMMatrixRotationY(theta));
	// 更新常量缓冲区,让立方体转起来(将常量传入(更新)缓冲区)
	D3D11_MAPPED_SUBRESOURCE mappedData;
	HR(m_pd3dImmediateContext->Map(m_pConstantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
	memcpy_s(mappedData.pData, sizeof(m_CBuffer), &m_CBuffer, sizeof(m_CBuffer));
	m_pd3dImmediateContext->Unmap(m_pConstantBuffer.Get(), 0);
}

  结果很明显,定义一个D3D11_MAPPED_SUBRESOURCE对象。D3D11_MAPPED_SUBRESOURCE用来提供对子资源的访问。
  接着调用Map方法。

Gets a pointer to the data contained in a subresource, and denies the GPU access to that subresource.

HRESULT ID3D11DeviceContext::Map(
    ID3D11Resource           *pResource,          // [In]包含ID3D11Resource接口的资源对象
    UINT                     Subresource,         // [In]缓冲区资源填0
    D3D11_MAP                MapType,             // [In]D3D11_MAP枚举值,指定读写相关操作
    UINT                     MapFlags,            // [In]填0,CPU需要等待GPU使用完毕当前缓冲区
    D3D11_MAPPED_SUBRESOURCE *pMappedResource     // [Out]获取到的已经映射到缓冲区的内存
);

D3D11_MAP枚举值类型的成员如下:

D3D11_MAP成员含义
D3D11_MAP_READ映射到内存的资源用于读取。该资源在创建的时候必须绑定了D3D11_CPU_ACCESS_READ标签
D3D11_MAP_WRITE映射到内存的资源用于写入。该资源在创建的时候必须绑定了D3D11_CPU_ACCESS_WRITE标签
D3D11_MAP_READ_WRITE映射到内存的资源用于读写。该资源在创建的时候必须绑定了D3D11_CPU_ACCESS_READ和D3D11_CPU_ACCESS_WRITE标签
D3D11_MAP_WRITE_DISCARD映射到内存的资源用于写入,之前的资源数据将会被抛弃。该资源在创建的时候必须绑定了D3D11_CPU_ACCESS_WRITE和D3D11_USAGE_DYNAMIC标签
D3D11_MAP_WRITE_NO_OVERWRITE映射到内存的资源用于写入,但不能复写已经存在的资源。该枚举值只能用于顶点/索引缓冲区。该资源在创建的时候需要有D3D11_CPU_ACCESS_WRITE标签,在Direct3D 11不能用于设置了D3D11_BIND_CONSTANT_BUFFER标签的资源,但在11.1后可以。具体可以查阅MSDN文档

  得到相应的指针后,调用memcpy_s(mappedData.pData, sizeof(m_CBuffer), &m_CBuffer, sizeof(m_CBuffer));将m_CBuffer中修改过的数据赋值给该指针,然后调用Unmap方法让指向资源的指针无效并重新启用GPU对该资源的访问权限。

void ID3D11DeviceContext::Unmap(
    ID3D11Resource *pResource,      // [In]包含ID3D11Resource接口的资源对象
    UINT           Subresource      // [In]缓冲区资源填0
);
//m_pd3dImmediateContext->Unmap(m_pConstantBuffer.Get(), 0);

调用渲染

void GameApp::DrawScene()
{
	assert(m_pd3dImmediateContext);
	assert(m_pSwapChain);

	static float black[4] = { 0.0f, 0.0f, 0.0f, 1.0f };	// RGBA = (0,0,0,255)
	m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), reinterpret_cast<const float*>(&black));
	m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

	// 绘制立方体
	m_pd3dImmediateContext->DrawIndexed(36, 0, 0);
	HR(m_pSwapChain->Present(0, 0));
}

  由于我们使用了索引缓冲区,所以为了配合索引使用,调用

void ID3D11DeviceContext::DrawIndexed( 
    UINT IndexCount,            // 索引数目
    UINT StartIndexLocation,    // 起始索引位置
    INT BaseVertexLocation);    // 起始顶点位置

思考

  能否在正方体的旁边再创建一个四棱锥?
  首先假设正方体和四棱锥共用同一个顶点缓冲区

// ******************
	// 设置立方体顶点
	//    5________ 6
	//    /|  8   /|
	//   /_|_____/ |
	//  1|4|_ _ 2|_|7
	//   | /     | /
	//   |/______|/
	//  0       3
	VertexPosColor vertices[] =
	{
		{ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f) },
		{ XMFLOAT3(-1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) },
		{ XMFLOAT3(1.0f, 1.0f, -1.0f), XMFLOAT4(1.0f, 1.0f, 0.0f, 1.0f) },
		{ XMFLOAT3(1.0f, -1.0f, -1.0f), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
		{ XMFLOAT3(-1.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) },
		{ XMFLOAT3(-1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 0.0f, 1.0f, 1.0f) },
		{ XMFLOAT3(1.0f, 1.0f, 1.0f), XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f) },
		{ XMFLOAT3(1.0f, -1.0f, 1.0f), XMFLOAT4(0.0f, 1.0f, 1.0f, 1.0f) },
		{ XMFLOAT3(0.0f, 1.0f, 0.0f), XMFLOAT4(0.0f, 1.0f, 1.0f, 1.0f) }//8号点
	};

//四棱锥的索引
	WORD indices2[] = {
		0, 8, 3,
		4, 8, 0,
		7, 8, 4,
		3, 8, 7,
		7, 4, 0,
		0, 3, 7
	};

  然后创建第二个索引缓冲区

	ComPtr<ID3D11Buffer> m_pIndexBuffer2;			// 索引缓冲区2
	......
	InitData.pSysMem = indices2;
	HR(m_pd3dDevice->CreateBuffer(&ibd, &InitData, m_pIndexBuffer2.GetAddressOf()));

  把将索引缓冲区绑定到渲染管线的代码放到执行渲染的代码中,渲染完立方体之后,再绑定四棱锥,重新渲染,修改后的渲染方法如下

void GameApp::DrawScene()
{
	assert(m_pd3dImmediateContext);
	assert(m_pSwapChain);

	static float black[4] = { 0.0f, 0.0f, 0.0f, 1.0f };	// RGBA = (0,0,0,255)
	m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), reinterpret_cast<const float*>(&black));
	m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
	// 输入装配阶段的索引缓冲区设置
	m_pd3dImmediateContext->IASetIndexBuffer(m_pIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);
	// 绘制立方体
	m_pd3dImmediateContext->DrawIndexed(36, 0, 0);

	//绘制四棱锥
	static float phi2 = 0.0f, theta2 = 0.0f;
	phi2 += 0.0001f, theta2 += 0.00015f;
	m_CBuffer.world = XMMatrixTranspose(XMMatrixRotationX(phi2) * XMMatrixRotationY(theta2) * XMMatrixTranslation(3, 0, 0));
	// 更新常量缓冲区,让四棱锥转起来
	D3D11_MAPPED_SUBRESOURCE mappedData;
	HR(m_pd3dImmediateContext->Map(m_pConstantBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
	memcpy_s(mappedData.pData, sizeof(m_CBuffer), &m_CBuffer, sizeof(m_CBuffer));
	m_pd3dImmediateContext->Unmap(m_pConstantBuffer.Get(), 0);
	m_pd3dImmediateContext->IASetIndexBuffer(m_pIndexBuffer2.Get(), DXGI_FORMAT_R16_UINT, 0);
	// 绘制四棱锥
	m_pd3dImmediateContext->DrawIndexed(18, 0, 0);

	
	HR(m_pSwapChain->Present(0, 0));
}

在这里插入图片描述
  写完之后感觉自己有些蠢啊!在unity里那么多三角形,要是画个图形都要写好几行代码,那图形多了是不是要爆炸。。。目前对游戏引擎的渲染底层代码一无所知,我最多就接触过虚幻2引擎底层的一些逻辑代码,还是要深入学习啊。后面好像有抽象出GameObject的demo,希望能快一点学习到那里。

  MVP变换在unity里好像被简化了好多。我翻了DX9龙书,OpenGL红宝书和3D数学基础这三本书,发现他们对视图变换的公式推导全部一笔带过。。。投影变换我也不打算看懂了,反正会用函数就行。
在这里插入图片描述

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值