Game Programming with DirectX -- 03[谁把我挡住了]
第三集 谁把我挡住了
现实中拍的照片中, 刚好按快门时, 有人从摄象机前走过, 结果照片中只能看到部分想照下来的。
3D世界中, 也存在A物体被物体B遮挡的时候, 那怎么知道的呢?
这主要是基于局部光照模型的几种简单画面绘制算法 : Z缓冲器算法, 扫描线算法, 多边形明暗过渡及阴影生成算法. 其中光照模型我们在灯光例子中讲解.
3.1 深度缓冲器
3.1.1 Z缓冲器算法
Z缓冲器算法是最简单的隐藏面剔除算法, 得到硬件的支持, 所以是目前应用最广泛的. Z缓冲器其实就是另一种帧缓冲器, 不同的是帧缓冲器存的是画面上每个像素的光亮度, 而Z缓冲器存的是画面上每个像素内可见表面采样点的深度.
Z缓冲器工作机制依赖上一集的屏幕坐标系, 过程如下,
a. 把帧缓冲器各像素初始化为背景颜色, Z缓冲器各像素初始化为最大深度值.
b. 将要测试的物体表面上的采样点(x, y)变换到屏幕坐标系(假设是左手坐标系), 得Xs, Ys, Zs
’
, 采样点的深度值Zs
’
= Z
’
(x, y).
c. 然后根据采样点的Xs, Ys找到Z缓冲器中存储相应的像素的原可见点的Zs = Z(x, y),进行大小比较.
(1)如果Zs
’
< Zs, 说明采样点在原可见点前, 更新Z缓冲器相应的像素的深度值为新的Zs
’
, 再更新帧缓冲器相应的像素为采样点的光亮度;
(2)如果Zs
’
>= Zs, 不更新两缓冲器.
现实处理时还常加上背面剔除来提高效率, 设Zsection为远截面的深度值, 则更新两缓冲器的充分条件为 : Zs
’
< Zs && Zs
’
> Zsection.
优点 : 算法简单, 计算复杂度O(N), N为场景中物体表面采样点数.
缺点 : 由于可见点是任意顺序写入帧缓冲器和Z缓冲器, 在实现反走样, 透明或半透明等效果时就很困难, 如处理透明或半透明, 因为可见点是任意顺序写入的, 不清楚同一点先后的值, 导致错误.
另一就是内存消耗严重.
为避免上面的缺点, 扫描线Z缓冲器算法就产生了.
3.1.2 扫描线Z缓冲器算法
避免内存消耗严重问题, 可将屏幕划分为4, 16或更多的带壮区域, 扫描线Z缓冲器算法就是其中最少消耗内存的, 它以一水平扫描线为带壮区域, 为充分利用物体表面沿相邻扫描线和相邻像素的连贯性, 可采用y桶分类, 活化多边形表和活化多边形来提高效率.
因为它是优化的Z缓冲器算法, 本质是一样的, 不在描述了, 有兴趣的可查找相关的文献.
3.2 快速明暗处理
分为三种, Flat, Gouraud和Phong. 其中Phong明暗处理现在的DirectX Graphics还不支持.
3.2.1 Flat(平面明暗处理)
由三角形构成的多边形或表面的颜色和镜面反射效果(不包括alpha通道)如下,
a. 对于用三角形列构成的, 第
n
个三角形的颜色和镜面反射效果 = 第
n * 3
顶点的颜色和镜面反射效果.
b. 对于用三角形带构成的, 第
n
个三角形的颜色和镜面反射效果 = 第
n
顶点的颜色和镜面反射效果.
a. 对于用三角扇形构成的, 第
n
个三角形的颜色和镜面反射效果 = 第
n + 1
顶点的颜色和镜面反射效果.
3.2.2 Gouraud(光滑明暗处理)
Gouraud明暗处理是将曲面表面的光亮度取为近似表示该曲面的各多边形顶点亮度的加权平均.Direct3D默认使用此设置.
假设已知曲面各多边形的法向, 则每一顶点的法向可取共享该顶点的各多边形法向的平均值, 然后把该点法向代入光照明模型. 顶点的法向的计算如图3.1, 顶点A处的曲面的法向可取共享该顶点的各多边形单位法向
N
1
,
N
2
N
3
N
4
的平均值,
N
A
= (
N
1
+
N
2
+
N
3
+
N
4
) / 4, 然后对
N
A
单位化.
图3.1
当用上面讲的扫描线算法对多边形进行绘制时, 扫描线与多边形边界的交点的光亮度是线性插值多边形边界顶点的光亮度得到的, 扫描线与多边形平面内的相交线上的点的光亮度由扫描线与多边形边界的交点的光亮度线性插值得到.
图3.2中, 扫描线与多边形边界的交点为A和B, P是扫描线与多边形平面内的相交线上的任意点, 设三顶点V
1
, V
2
, V
3
的光亮度分别为I
1
, I
2
, I
3
, 则A, B点的光亮度I
A
, I
B
分别由V
1
, V
2
和V
1
, V
3
线性插值得到; B点的光亮度I
P
由IA, IB线性插值得到.
图3.2
I
A
= uI
1
+ (1
–
u)*I
2
, u = | AV
2
| / | V
1
V
2
|
I
B
= vI
1
+ (1
–
v)*I
3
, v = | BV
3
| / | V
1
V
3
|
I
P
= wI
A
+ (1
–
w)*I
B
, w = | PB | / | AB |
(u, v, w为插值参数)
实际上, 以每像素的扫描, 则从A到B的线性插值的增量是恒等的, 设相邻两像素的插值参数的增量为t, 在P点后的P
’
的光亮度为,
Ip
’
= I
P
+ (I
A
- I
B
)*t = I
P
+ i (i为同一扫描线上常数增量)
这样, 每一扫描线只需计算一次两交点及常数增量, 其余的仅需简单的加法.
错误 : 1. 图3.3, N
1
= N
2
= N
3
= N
4
, 各顶点的光亮度相同, 则面a内各点的光亮度都相同. 解决的方法是把a面及多边形分成更小的面和多边形.
图3.3
2. 还有Gouraud明暗处理不能正确地模拟高光.
3. Gouraud明暗处理在绘制面时会有马赫带效应, Gouraud明暗处理虽然能正确处理面多边形内的光亮度的连续变化, 但在多边形的交界处, 由于光亮度的一阶倒数不连续, 会出现亮带或黑带 -- 马赫带效应.
为解决高光和马赫带效应问题, 就有了Phong明暗处理.
3.2.3 Phong明暗处理(法向量插值明暗处理)
Phong的算法和Gouraud相似, 用顶点的法向量N代替上面的光亮度I就是Phong的计算方法.
N
A
= uN
1
+ (1
–
u)*N
2
, u = | AV
2
| / | V
1
V
2
|
N
B
= vN
1
+ (1
–
v)*N
3
, v = | BV
3
| / | V
1
V
3
|
N
P
= wN
A
+ (1
–
w)*N
B
, w = | PB | / | AB |
(u, v, w为插值参数)
线性插值的增量同样恒等,
Np
’
= N
P
+ (N
A
- N
B
)*t = N
P
+ i (i为同一扫描线上常数增量)
我们只是求出了每点的法向量, 要计算点的光亮度, 都需要使用光照模型, 所以计算量增大.实际物体的表面都由三角形构成, 所以对上面的公式可以简化.
如图3.4, O(x, y)是屏幕坐标系, P1(X1, Y1), P2(X2, Y2), P3(X3, Y3)是三角形三顶点的屏幕坐标, 设各点光亮度和法向量为I
1
, I
2
, I
3
; N
1
, N
2
, N
3
. 我们以P1(0, 0)为中心建立仿射坐标系(m, n), 其中P2(1, 0), P3(0, 1), m, n用屏幕坐标系表示为,
m = A
1
*X + B
1
*Y + C
1
n = A
2
*X + B
2
*Y + C
2
图3.4
三角形内任一点P的光亮度I(x, y)的插值公式可表示为,
I(x, y) = (1
–
m
–
n)I
1
+ mI
2
+ nI
3
= Ax + By + C
其中
A = A
1
*I
2
+ A
2
*I
3
–
(A
1
+ A
2
)I
1
B = B
1
*I
2
+ B
2
*I
3
–
(B
1
+ B
2
)I
1
C = C
1
*I
2
+ C
2
*I
3
–
(C
1
+ C
2
)I
1
+ I
1
同样法向量插值公式为,
N
(x, y) = (1
–
m
–
n)N
1
+ mN
2
+ nN
3
= Ax + By + C
其中
A
= A
1
*N
2
+ A
2
*N
3
–
(A
1
+ A
2
)N
1
B
= B
1
*N
2
+ B
2
*N
3
–
(B
1
+ B
2
)N
1
C
= C
1
*N
2
+ C
2
*N
3
–
(C
1
+ C
2
)N
1
+ N
1
3.3 阴影生成算法
这里我们加入这节的目的是为和Z缓冲器算法结合在一起.
阴影效果能增强画面的真实感, 如图3.5, A中我们无法确定球和地面的相对高度, B中加入了阴影, 为球和地面的相对高度提供了信息.
图3.5
我们这里简单讲解阴影缓存器算法, 由于阴影效果要在有光照条件下测试, 所以阴影效果的例子我们放到以后给出.
3.3.1 阴影缓存器算法
阴影缓存器算法的效率相对是最好最简单的, 但它还存在很多问题, 如阴影走样.
由于阴影是世界坐标系中光源无法直接照射的地方, 它和光源之间被其他不透明物体表面所遮挡而形成, 所以阴影是特定光源的隐藏面. 这样, 阴影生成可以类似以光源为视点对世界中物体进行剔除的过程.
如图3.6, 阴影生成过程为,
a. 以光源为视点, 光线照射方向为视线对世界中物体进行Z缓冲器计算, 那么阴影缓存器(即Z缓冲器)中存放的是离光源各最近点的深度Z1, 此时不对帧缓冲器存的画面上每个像素的光亮度进行计算.
b. 回到实际视点和视线, 把此时物体表面可见点的坐标转换到先前的光源坐标系, 进行Z缓冲器计算, 现在比较的是可见点深度值Z2(xl, yl)和阴影缓存器(即Z缓冲器)对应点深度值Z1(xl, yl)的比较,
若Z2(xl, yl) > Z1(xl, yl), 该可见点是在光源的不可见处, 即在光源的阴影中; 反之, 该可见点不在光源的阴影中.
优点 : 同Z缓冲器一样.
缺点 : 同Z缓冲器一样, 内存消耗大; 当光源离视线方向过远时, 图象在阴影区域附近会产生走样(aliasing).
图
3.6
3.4 Z缓冲器算法的例子
3.4.1 代码更新
我们在这例子里会创建6各四边形, 我们把物体和环境代码封装在不同的class中进行管理.
在Direct3D中
d3dpm.EnableAutoDepthStencil = TRUE;
可以理解为使用
阴影缓存器,
m_pD3DDev->SetRenderState(D3DRS_ZENABLE, D3DZB_TRUE)
开Z缓存器.
要注意, 阴影缓存器和Z缓存器理论上是同一个, 所以使用阴影缓存器就要开Z缓存器, 否则物体无法正确表示.
你可以开/关阴影缓存器(Z缓存器), 看看例子中夹在中间的立方体的显示情况.
我们来看看game2的主要更新的代码(下载game2 project),
// direct9.cpp
HRESULT CD9Game::InitD3D(HWND hWnd)
{
m_pD3D = Direct3DCreate9(D3D_SDK_VERSION);
if (m_pD3D == NULL)
{
return E_FAIL;
}
// here we suppose only have the default adapter
D3DDISPLAYMODE d3ddm;
FillMemory(&d3ddm, sizeof(D3DDISPLAYMODE), 0);
if (FAILED(m_pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm)))
{
return E_FAIL;
}
D3DPRESENT_PARAMETERS d3dpm;
FillMemory(&d3dpm, sizeof(D3DPRESENT_PARAMETERS), 0);
d3dpm.BackBufferWidth = d3ddm.Width;
d3dpm.BackBufferHeight = d3ddm.Height;
d3dpm.BackBufferFormat = d3ddm.Format;
d3dpm.BackBufferCount = 1;
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_ONE;
if (m_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 (m_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 = TRUE时,Z缓存器自动设成TRUE
else
{
d3dpm.EnableAutoDepthStencil = FALSE;
}
if (FAILED(m_pD3D->CreateDevice(D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpm,
&m_pD3DDev)))
{
return E_FAIL;
}
if (m_pD3DDev == NULL)
{
return E_FAIL;
}
m_pD3DDev->SetRenderState(D3DRS_LIGHTING, FALSE);
//
这儿注解关Z缓存器
m_pD3DDev->SetRenderState(D3DRS_ZENABLE, D3DZB_TRUE);
m_pD3DDev->GetTransform(D3DTS_WORLD, &g_mWorld);
SetRotation();
SetCamera();
SetPerspective();
return S_OK;
}
//
创建物体
HRESULT CD9Game::InitObject()
{
m_paObject[0] = new CD9Object(m_pD3DDev);
if ( m_paObject[0] == NULL )
{
return E_FAIL;
}
m_paObject[0]->SetPos(0.0, 0.0, 0.0, 8.0, 8.0, 8.0);
m_paObject[1] = new CD9Object(m_pD3DDev);
if ( m_paObject[1] == NULL )
{
return E_FAIL;
}
m_paObject[1]->SetPos(16.0, 0.0, 0.0, 8.0, 8.0, 8.0);
m_paObject[2] = new CD9Object(m_pD3DDev);
if ( m_paObject[2] == NULL )
{
return E_FAIL;
}
m_paObject[2]->SetPos(-16.0, 0.0, 0.0, 8.0, 8.0, 8.0);
m_paObject[3] = new CD9Object(m_pD3DDev);
if ( m_paObject[3] == NULL )
{
return E_FAIL;
}
m_paObject[3]->SetPos(0.0, 0.0, 16.0, 32.0, 8.0, 8.0);
m_paObject[4] = new CD9Object(m_pD3DDev);
if ( m_paObject[4] == NULL )
{
return E_FAIL;
}
m_paObject[4]->SetPos(-8.0, 8.0, -8.0, 16.0, 8.0, 8.0);
m_paObject[5] = new CD9Object(m_pD3DDev);
if ( m_paObject[5] == NULL )
{
return E_FAIL;
}
m_paObject[5]->SetPos(8.0, 0.0, -16.0, 16.0, 8.0, 8.0);
return S_OK;
}
VOID CD9Game::Render()
{
//
这儿注解关Z缓存器和阴影缓存器用
// m_pD3DDev->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 0.0f, 0);
//
来清楚表面
m_pD3DDev->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
m_pD3DDev->BeginScene();
SetRotation();
m_paObject[0]->Render();
m_paObject[1]->Render();
m_paObject[2]->Render();
m_paObject[3]->Render();
m_paObject[4]->Render();
m_paObject[5]->Render();
m_pD3DDev->EndScene();
m_pD3DDev->Present(NULL, NULL, NULL, NULL);
}
//
在d9object.cpp中填写四边形顶点坐标值和各顶点的漫反射颜色
HRESULT CD9Object::InitD3DVertex(LPDIRECT3DDEVICE9 pD3DDev)
{
if(FAILED(pD3DDev->CreateVertexBuffer(18 * sizeof(MYVERTEX),
D3DUSAGE_SOFTWAREPROCESSING,
D3DFVF_MYVERTEX,
D3DPOOL_DEFAULT,
&m_pD3DVBuffer,
NULL)))
{
return E_FAIL;
}
m_bInit = TRUE;
return S_OK;
}
HRESULT CD9Object::UpdateD3DVertex()
{
LPVOID pV = NULL;
UINT nSize = 18 * sizeof(MYVERTEX);
MYVERTEX aVertex[] =
{
{m_fx - m_fW, m_fy + m_fH, m_fz - m_fD, D3DCOLOR_XRGB(0, 0, 255) },
{m_fx - m_fW, m_fy + m_fH, m_fz + m_fD, D3DCOLOR_XRGB(255, 0, 0) },
{m_fx + m_fW, m_fy + m_fH, m_fz - m_fD, D3DCOLOR_XRGB(255, 0, 0) },
{m_fx + m_fW, m_fy + m_fH, m_fz + m_fD, D3DCOLOR_XRGB(0, 255, 0) },
{m_fx - m_fW, m_fy - m_fH, m_fz - m_fD, D3DCOLOR_XRGB(255, 0, 0) },
{m_fx - m_fW, m_fy + m_fH, m_fz - m_fD, D3DCOLOR_XRGB(0, 0, 255) },
{m_fx + m_fW, m_fy - m_fH, m_fz - m_fD, D3DCOLOR_XRGB(0, 255, 0) },
{m_fx + m_fW, m_fy + m_fH, m_fz - m_fD, D3DCOLOR_XRGB(255, 0, 0) },
{m_fx + m_fW, m_fy - m_fH, m_fz + m_fD, D3DCOLOR_XRGB(0, 0, 255) },
{m_fx + m_fW, m_fy + m_fH, m_fz + m_fD, D3DCOLOR_XRGB(0, 255, 0) },
{m_fx - m_fW, m_fy - m_fH, m_fz + m_fD, D3DCOLOR_XRGB(0, 255, 0) },
{m_fx - m_fW, m_fy + m_fH, m_fz + m_fD, D3DCOLOR_XRGB(255, 0, 0) },
{m_fx - m_fW, m_fy - m_fH, m_fz - m_fD, D3DCOLOR_XRGB(255, 0, 0) },
{m_fx - m_fW, m_fy + m_fH, m_fz - m_fD, D3DCOLOR_XRGB(0, 0, 255) },
{m_fx + m_fW, m_fy - m_fH, m_fz - m_fD, D3DCOLOR_XRGB(0, 255, 0) },
{m_fx + m_fW, m_fy - m_fH, m_fz + m_fD, D3DCOLOR_XRGB(0, 0, 255) },
{m_fx - m_fW, m_fy - m_fH, m_fz - m_fD, D3DCOLOR_XRGB(255, 0, 0) },
{m_fx - m_fW, m_fy - m_fH, m_fz + m_fD, D3DCOLOR_XRGB(0, 255, 0) }
};
if(FAILED(m_pD3DVBuffer->Lock(0, nSize, &pV, 0)))
{
return E_FAIL;
}
MoveMemory(pV, aVertex, nSize);
m_pD3DVBuffer->Unlock();
return S_OK;
}
---------------------------------------------------------------
3.4.2 说明
上下键调整Y值大小, 左右键调整X值大小, Enter显示世界坐标系, 空格键物体自动旋转, ESC退出.
可以结合这集的内容来仔细分析一下例子.
第三集 小结
这一集我们学习了要进行DirectX Graphics 3D编程中的Z缓存器的用法, 同时了解了明暗处理的几种方法, 最后结合Z缓存器讲解了阴影缓存器, 同样我们用一个例子来演示.