Game Programming with DirectX -- 02[并非3D的3D]
第二集 并非3D的3D
我们开始展示3D的世界了, 好好复习一下几何吧。
2.1 从来都没有3D的游戏
2.1.1 3D pipeline
3D pipeline, 应该翻译成3D流水线比较能让大家清楚, 它和汽车制造厂的流水线是有共性的. 在我们编写演示3D的例子前, 我们简单的了解一下3D pipeline, 如图2.1.
图2.1
要成像的数据经过3D pipeline之后, 我们在屏幕上看到了3D的世界, 但实质上并不是3D的, 最主要的原因就是没有三维显示器, 这个过程很像我们平时的照相, 3D pipeline在一个虚拟的3D空间完成3D的物体构造, 然后拿个照相机, 在虚拟的3D空间中选定要显示的物体, 调整好和这物体的距离及方向, 然后按下快门, 是的, 这个物体的形状就在一张2维的照片上显示出来了, 这就是我们看到的3D.
这是3D的吗? 你拿张照片看看是不是就很清楚了. 确切的讲, 现在的3D游戏都是2.5D的游戏.
2.1.2 Direct3D Graphics Pipeline
我们再来看看Direct3D Graphics Pipeline的组成, 如图2.2.
图2.2
在Direct3D Graphics Pipeline里, 我们要了解Geometry Processing 中的几何数学模型. 下面的章节都讨论这个.
2.1.3 三维坐标系统
有左手坐标系和右手坐标系, 如图2.3.
图2.3
左手手臂向你的右边, 手心朝上, 四指向上与手臂垂直, 大拇指就朝外与手臂成垂直, 你看到以手臂为X轴, 四指为Y轴, 大拇指为Z轴的左手坐标系.
右手手臂向你的右边, 手心朝上, 四指向上与手臂垂直, 大拇指就朝内与手臂成垂直, 你看到以手臂为X轴, 四指为Y轴, 大拇指为Z轴的右手坐标系.
一般DirectX Graphics默认的为左手坐标系表示的三维空间坐标系.
2.1.4 虚拟摄象机模型
我们上面讲的在虚拟的3D空间中拍照的照相机称为虚拟摄象机模型.在计算机动画中, 计算机生成一帧图形需执行四个步骤, 场景造型, 取景变换, 透视投影, 隐藏面消除, 场景中可见面的颜色计算.
2.1.4.1 场景坐标系
物体的造型总在某个参考坐标系中进行的, 我们称场景中物体造型所取的坐标系为场景坐标系. 场景坐标系包括场景的局部坐标系和世界坐标系.
在造型时, 我们都取描述物体模型最方便的局部坐标系; 然后将所有的物体模型放入世界坐标系, 我们才能反映出物体模型彼此间的关系, 这一过程称为模型转换, 将物体模型从它的局部坐标系变换到世界坐标系.
2.1.4.2 摄影机坐标系
场景坐标系中的物体都是三维的, 而屏幕上显示的画面只是某一角度和某一距离下的三维物体在垂直与视线方向的成像平面上的投影. 我们将物体的三维坐标系变换到屏幕上的像素坐标系, 经过的一系列坐标变换称为取景变换.
这里我们会用到另一坐标系 — 摄象机坐标系, 它的原点为视点, 沿Z轴正方向是视线方向, 经过视点且垂直于视线方向的平面为视平面.
一般DirectX Graphics中选视平面中垂直向上的方向为Y轴方向, 而且摄象机坐标系是可以随意移动的, 那么如何有效的用世界坐标系来表示摄象机坐标系的几何模型呢?
一般有两种参数化的形式, 如图2.4.
(1) (E, T, G), E为视点, T为摄影机目标观察点, G为用来明确摄影机朝向的点, 与E和T不共线.
(2)(E, T, roll), E为视点, T为摄影机目标观察点, roll为确摄影机朝向与世界坐标系Y轴的夹角.
图2.4
两种形式其实是一样的, 以X轴单位矢量为 u , Y轴矢量为 v ,Z轴单位矢量为 w , 我们以第一种方法为例, 已知E, T, G, 我们可算出 w, v ’ , 则,
u = w X v ’
v = u X w
这样我们就建立了一个左手摄影机坐标系, 在这个坐标系, E(Ex, Ey, Ez), u (Ux, Uy, Uz), v (Vx, Vy, Vz), w (Wx, Wy, Wz), 则两坐标系变换齐次矩阵为,
(Xe, Ye, Ze, 1) = (Xw, Yw, Zw, 1)M
| Ux Vx Wx 0 |
| Uy Vy Wy 0 |
M = | Uz Vz Wz 0 |
| - E . u - E . v - E . w 1 |
2.1.4.3 屏幕坐标系
在摄影机坐标系, 场景种所有物体的几何数据都被通过透视变换到屏幕坐标系。 屏幕就是上节中的一个视平面,假设屏幕到视点的距离为D,摄象机的水平广角为r, 画面宽高比为c, 则屏幕宽为2Dh, 高为2Dh/c, h = tan(r/2), 视域四棱锥的四侧面方程,
Xe = +/- h*Ze Ye = +/- h*Ze / c
假设与视点相距足够远的物体不显示, 就得到视域四棱锥的近裁截面和远裁截面,
Zmin = D Zmax = F
这样, 视域四棱锥转化为一棱台, 如图2.5.
图2.5
从图上, 我们发现EP线上的Q点在屏幕上投影也是P ’ , 但实际和P是不同点,为反映P和Q离视点E的距离(深度), 引入第三维坐标分量Zs, 则(Xs, Ys, Zs)为空间点的屏幕坐标系坐标.
确定Zs需要满足三个条件,
a. 我们发现Xs和Ys可以由摄影机坐标系表示,
Xs = D*Xe / Ze, Ys = D*Ye / Ze
所以Zs要求也可以用摄影机坐标系来表示.
b. 在屏幕坐标系里任意两点相对视点的前后顺序与他们在摄影机坐标系中的保持一致.
c. 摄影机坐标系中的直线和平面转换到屏幕坐标系还是直线和平面.
由上得, Zs = A + B / Ze (A和B是常数, B < 0), 当P点远离屏幕Ze增大, Zs也增大, 保持一致. 将Zs归一化, 即 Ze 从[D, F]映射到[0, 1]后, 上面Xs, Ys, Zs分别为,
Xs = Xe / h * Ze Ys = c * Ye / h * Ze Zs = F(1 – D/Ze) / (F - D)
这是非线性变换, 我们把它转化成线性部分和非线性部分, 就需第四维坐标w, 用w对其余三坐标分量作透视除法, 得到如下,
1. 线性部分
X = Xe Y = cYe Z = (hFZe/(F - D)) – (hDF/(F - D)) w = hZe
2. 非线性部分
Xs = X / w Ys = Y / w Zs = Z / w
称(X, Y, Z, w)为齐次坐标, 第四个坐标w可看成图象的透视参数, 我们前一例子的transformed vertex的四个坐标值的意义就在此了.
从摄影机坐标系变换到屏幕坐标系的透视变换矩阵P, 则
[Xs, Ys, Zs, w] = [Xe, Ye, Ze, 1]P
| 1 0 0 0 |
| 0 c 0 0 |
P = | 0 0 hF/(F-D) h |
| 0 0 - hDF/(F-D) 0 |
问题 : 由于Zs和Ze之间是非线性的关系, 我们会发现当Ze越远离屏幕, Zs接近1的速度越快, 这导致在屏幕坐标系中, 位于视域四棱锥后面的物体将发生挤压和变形, 在屏幕坐标系计算物体表面的法向量会产生错误.
Ze --> 增大
| - | - | - | - | - | - | - | - | - | - | - | - | - | - |
视点 --- >
| - | - | - | - | - | - | - | | | | |||||||||||
Zs --> 增大
虽然有上面的问题, 但它非常适合隐藏面的剔除. 经过透视后, 投影中心移至Zs轴的负无穷远, 所有穿过视点的光线都平行于Zs轴, 在屏幕上相同(Xs, Ys)点的遮挡判断简化为对点的Zs的大小的比较, 后面的深度测试也使用到了, 如图2.6.
图2.6
2.1.4.4 视域四棱锥裁剪
在摄影机坐标系, 一物体与视域四棱锥的关系分为3类,
a. 物体完全在视域四棱锥外, 直接剔除.
b. 物体完全在视域四棱锥内, 转换成屏幕坐标系再绘制.
c. 物体部分在视域四棱锥内, 对物体裁剪, 在视域四棱锥内的部分转换成屏幕坐标系再绘制.
我们来看看画面绘制的全过程,
1. 局部坐标系 --> [对物体的组成部分进行造型和定位] -- > 在局部坐标系定位物体完成, 把物体变换到场景的世界坐标系 --> 到2
2. 场景世界坐标系 --> [指定物体的表面属性和光照属性] --> 指定摄影机位置和视线方向 --> 到3
3. 摄影机坐标系 --> [背面剔除, 视域四棱锥裁剪] --> 指定屏幕的参数, 进行透视 --> 到4
4. 屏幕坐标系 --> [绘制 : 隐藏面剔除, 光栅化, 光亮度计算]
裁剪变换的变换矩阵C, (Xc, Yc, Zc, 1)为经过上面讲的裁剪变换后的裁剪空间的齐次坐标, 则
(Xc, Yc, Zc, 1) = (Xe, Ye, Ze, 1)C
| D/h 0 0 0 |
| 0 D/h 0 0 |
C = | 0 0 1 0 |
| 0 0 0 1 |
在裁剪空间中, 视域四棱锥为,
- Zc <= Xc <= Zc
-Zc <= Yc <= Zc
D <= Zc <= F
裁剪操作完成后, 就可用上面的透视除法得到屏幕坐标了.
2.1.4.5 背面剔除
DirectX Graphics Culling Mode.
1. 对于多边形, 默认是逆时针的三角形被裁剪, 如图2.7.
图2.7
要使你的多边形正面朝视点, 组成多边形的三角形三顶点要顺时针的顺序定义.
2. 对于三角形带, 个顶点被如下图2.8方式渲染一保持顺时针.
图2.8
你传给DirectX Graphics的顶点顺序是V1, V2 ... V7, DirectX Graphics按V1, V2, V3第一个三角形, V2, V4, V3第二个三角形渲染, 余下同例.
3. 对于三角扇形, 个顶点被如下图2.9方式渲染一保持顺时针.
图2.9
你传给DirectX Graphics的顶点顺序是V1, V2, V3, V4, DirectX Graphics按V1, V2, V3第一个三角形, V1, V3, V4第二个三角形渲染.
4. 表面剔除, 面剔除先计算面的法向量和视线的向量, 视线向量是从面到视点的向量, 然后计算视线向量和面法向量的夹角, 大于90度夹角说明面的背面朝视点, 要被剔除.
图2.10为表面剔除.
图2.10
2.1.4.6 光栅化
光栅化过程和二维的一样.
2.2 3D物体的例子
2.2.1 代码更新
我们来看看game1的主要更新的代码(下载game1 project),
---------------------------------------------------------------
#define D3DFVF_MYVERTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE)
struct MYVERTEX
{
FLOAT x, y, z;
DWORD colour;
};
HRESULT Init3D(HWND hWnd)
{
g_pD3D = Direct3DCreate9(D3D_SDK_VERSION);
if (g_pD3D == NULL)
{
return E_FAIL;
}
D3DDISPLAYMODE d3ddm;
FillMemory(&d3ddm, sizeof(D3DDISPLAYMODE), 0);
if (FAILED(g_pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm)))
{
return E_FAIL;
}
g_nWidth = d3ddm.Width - 32;
g_nHeight = d3ddm.Height - 32;
D3DPRESENT_PARAMETERS d3dpm;
FillMemory(&d3dpm, sizeof(D3DPRESENT_PARAMETERS), 0);
d3dpm.BackBufferWidth = d3ddm.Width;
d3dpm.BackBufferHeight = d3ddm.Height;
d3dpm.BackBufferFormat = d3ddm.Format;
d3dpm.BackBufferCount = 0;
d3dpm.MultiSampleType = D3DMULTISAMPLE_NONE;
//d3dpm.MultiSampleQuality = 0;
d3dpm.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpm.hDeviceWindow = hWnd;
d3dpm.Windowed = FALSE;
d3dpm.Flags = D3DPRESENTFLAG_LOCKABLE_BACKBUFFER;
d3dpm.FullScreen_RefreshRateInHz = d3ddm.RefreshRate;
d3dpm.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
if (g_pD3D->CheckDeviceFormat(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
d3ddm.Format, D3DUSAGE_DEPTHSTENCIL,
D3DRTYPE_SURFACE, D3DFMT_D32) == D3D_OK)
{
d3dpm.EnableAutoDepthStencil = TRUE;
d3dpm.AutoDepthStencilFormat = D3DFMT_D32;
}
else if (g_pD3D->CheckDeviceFormat(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
d3ddm.Format, D3DUSAGE_DEPTHSTENCIL,
D3DRTYPE_SURFACE, D3DFMT_D16) == D3D_OK)
{
d3dpm.EnableAutoDepthStencil = TRUE;
d3dpm.AutoDepthStencilFormat = D3DFMT_D16;
}
else
{
d3dpm.EnableAutoDepthStencil = FALSE;
}
if (FAILED(g_pD3D->CreateDevice(D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpm,
&g_pD3DDevice)))
{
return E_FAIL;
}
if (g_pD3DDevice == NULL)
{
return E_FAIL;
}
//g_pD3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);
g_pD3DDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
return S_OK;
}
HRESULT InitVertexBuffer()
{
LPVOID pV = NULL;
MYVERTEX aVertex[] =
{
{ 0.0f, 8.0f, 0.0f, D3DCOLOR_XRGB(255, 0 , 0 ) },
{ 5.66f, 0.0f, -5.66f, D3DCOLOR_XRGB(0 , 255, 0 ) },
{-5.66f, 0.0f, -5.66f, D3DCOLOR_XRGB(0 , 0 , 255) },
{-5.66f, 0.0f, 5.66f, D3DCOLOR_XRGB(0 , 255, 255) },
{ 0.0f, 8.0f, 0.0f, D3DCOLOR_XRGB(255, 0 , 0 ) },
{ 5.66f, 0.0f, 5.66f, D3DCOLOR_XRGB(255, 0 , 255) },
{ 5.66f, 0.0f, -5.66f, D3DCOLOR_XRGB(0 , 255, 0 ) },
{-5.66f, 0.0f, 5.66f, D3DCOLOR_XRGB(0 , 255, 255) }
};
UINT nSize = 8 * sizeof(MYVERTEX);
if(FAILED(g_pD3DDevice->CreateVertexBuffer(nSize,
D3DUSAGE_SOFTWAREPROCESSING,
D3DFVF_MYVERTEX,
D3DPOOL_DEFAULT,
&g_pD3DVBuffer,
NULL)))
{
return E_FAIL;
}
if(FAILED(g_pD3DVBuffer->Lock(0, nSize, &pV, 0)))
{
return E_FAIL;
}
MoveMemory(pV, aVertex, nSize);
g_pD3DVBuffer->Unlock();
return S_OK;
}
VOID Render()
{
g_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
D3DCOLOR_XRGB(0, 128, 0), 1.0f, 0);
g_pD3DDevice->BeginScene();
g_pD3DDevice->SetStreamSource(0, g_pD3DVBuffer, 0, sizeof(MYVERTEX));
g_pD3DDevice->SetFVF(D3DFVF_MYVERTEX);
g_pD3DDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 6);
g_pD3DDevice->EndScene();
g_pD3DDevice->Present(NULL, NULL, NULL, NULL);
}
---------------------------------------------------------------
2.2.2 说明
实际的代码包含有世界坐标系显示, 物体的旋转和视点的移动; 还有为和2维的作比较, 我在表面上放了几个精灵.
言归正传, 首先我们的顶点特征变成为在三维空间中的坐标表示, 物体还有漫反射颜色, 我们创建了一个金字塔型的多边形, 还有, 我们以后的代码都在全屏下演示, 注意, 我没有加窗口切换资源丢失的代码, 所以运行时别切换窗口. 我们创建了14个顶点来画6个三角形, 我们使用三角形带来画.
可以结合这集的内容来仔细分析一下例子.
第二集 小结
这一集我们学习了要进行DirectX Graphics 3D编程的一部分几何知识, 我们用一个例子来演示.