本篇是创建3D图形引擎(1)的续篇,3D图形引擎的代码以创建游戏内核中编写的代码为基础进行开发。
下载源码和工程
视锥的介绍
视锥(viewing frustum)是6个平面的集合,它从观察点向外扩展,以确定某个多边形是否能够被观察到。
首先,可以将视锥看作是一个以观察者开始扩展的金字塔,如下图所示:
这个金字塔代表了观察范围(field of view,FOV),位于观察范围内的物体是可见的,而位于观察范围外的物体则是不可见的。
三维图形引擎的每个物体都是由3D点(称为顶点)构成的,每个视锥包含6个面(前、后、左、右、上、下)。通过一些数学计算,可以确定哪些顶点位于视锥内,哪些顶点位于视锥外。位于视锥内的顶点被渲染,而位于视锥外的顶点则不被渲染。同样,当渲染多边形时,只有那些在视锥内的顶点才被渲染。
平面与裁剪
视锥的6个侧面被称为剪切平面(clipping plane),Direct3D使用了一个名为D3DXPLANE的特定对象去包含平面数据,D3DXPLANE包含了4个变量:a、b、c、d,它们都是浮点型数据。
定义好一个平面后,为它指定一个特定的方向,并将它从原点移到相应的位置。事实上,一个平面就是由一条法线以及它与原点的距离进行定义的,如下图所示,它演示了一个平面在三维空间中的方位。
不需要指定X、Y、Z的具体数值,你只要使用变量A、B、C,同时还有一个数值D来确定平面离原点的距离。为了定义一个平面,将A、B、C设置为法向量的值,这样就可以使用一个平面去检测指定的点位于平面的前面或者后面。
为了计算视锥的6个平面,可以将当前的观察变换矩阵和投影矩阵组合起来,然后使用组合矩阵直接计算每个平面的A、B、C、D。
平面可见性检测
为了检测一个顶点位于平面的前方或后方,可以利用点积来计算,通过调用D3DXPlaneCoord函数来实现。
Computes the dot product of a plane and a 3D vector. The w parameter of the
vector is assumed to be 1.
FLOAT D3DXPlaneDotCoord(
CONST D3DXPLANE * pP,
CONST D3DXVECTOR3 * pV
);
Parameters
- pP
- [in] Pointer to a source D3DXPLANE structure.
- pV
- [in] Pointer to a source D3DXVECTOR3 structure.
Return Values
The dot product of the plane and 3D vector.
Remarks
Given a plane (a, b, c, d) and a 3D vector (x, y, z) the return value of this
function is a*x + b*y + c*z + d*1. The D3DXPlaneDotCoord function is
useful for determining the plane's relationship with a coordinate in 3D space.
完整视锥的检测
对于立方体和长方体,需检测所有的拐角顶点。如果所有的顶点都位于任一平面之后,那么立方体或长方体就位于视锥之外(因而也在视野之外)。如果有任一顶点位于视锥之内,或者说位于任一平面前(也就是说不是所有的顶点都位于任一平面后),那就意味着立方体或者长方体是可见的。至于球体,只要它与每个平面的距离
等于或大于球形的平面,那么它就是可见的。
FRUSTUM类
因为每次使用视锥所涉及的数学运算都是一样的,所以完全可以创建一个类,让它处理好数学方面的问题,包括创建视锥以及使用视锥去检测一个物体是否可见等。
//==============================================================================
// This class encapsulate for frustum, judge whether other object is in frustum.
//==============================================================================
typedef class FRUSTUM
{
public:
// Construct the six planes from current view and projection.
// Can override the default depth value.
BOOL construct(GRAPHICS_PTR graphics, float z_distance = 0.0f);
// The following functions check a single point, cube, rectangle, and sphere if
// contained in the frustum. A return value of TRUE means visible, FALSE not visible.
// When checking cubes or rectangles, you can supply a BOOL variable that determines
// if all the points are in the frustum.
BOOL check_point(float x_pos, float y_pos, float z_pos);
BOOL check_cube(float x_center, float y_center, float z_center,
float radius,
BOOL* completely_contained = NULL);
BOOL check_rectangle(float x_center, float y_center, float z_center,
float x_radius, float y_radius, float z_radius,
BOOL* completely_contained = NULL);
BOOL check_sphere(float x_center, float y_center, float z_center,
float radius);
private:
D3DXPLANE _planes[6]; // the frustum planes
} *FRUSTUM_PTR;
每当观察或投影矩阵发生变化时,请调用
construct去构造6个测试平面,如果仅希望最接近的物体能被看到,则可以为远端剪切平面指定一个新的距离值。
//----------------------------------------------------------------------------
// Construct frustum.
//----------------------------------------------------------------------------
BOOL FRUSTUM::construct(GRAPHICS_PTR graphics, float z_distance)
{
D3DXMATRIX matrix, mat_view, mat_proj;
// error checking
if(graphics == NULL)
return FALSE;
// calculate FOV data
graphics->get_device_com()->GetTransform(D3DTS_PROJECTION, &mat_proj);
if(! float_equal(z_distance, 0.0f))
{
// Calculate new projection matrix based on distance provided.
//
// projection matrix is:
//
// | xScale 0 0 0 |
// | 0 yScale 0 0 |
// | 0 0 zf/(zf-zn) 1 |
// | 0 0 -zn*zf/(zf-zn) 0 |
//
// where:
// yScale = cot(fovY/2)
// xScale = yScale / aspect ratio
float z_min = -mat_proj._43 / mat_proj._33;
float q = z_distance / (z_distance - z_min);
mat_proj._33 = q;
mat_proj._43 = -q * z_min;
}
graphics->get_device_com()->GetTransform(D3DTS_VIEW, &mat_view);
D3DXMatrixMultiply(&matrix, &mat_view, &mat_proj);
// calculate the planes
_planes[0].a = matrix._14 + matrix._13; // Near
_planes[0].b = matrix._24 + matrix._23;
_planes[0].c = matrix._34 + matrix._33;
_planes[0].d = matrix._44 + matrix._43;
D3DXPlaneNormalize(&_planes[0], &_planes[0]);
_planes[1].a = matrix._14 - matrix._13; // Far
_planes[1].b = matrix._24 - matrix._23;
_planes[1].c = matrix._34 - matrix._33;
_planes[1].d = matrix._44 - matrix._43;
D3DXPlaneNormalize(&_planes[1], &_planes[1]);
_planes[2].a = matrix._14 + matrix._11; // Left
_planes[2].b = matrix._24 + matrix._21;
_planes[2].c = matrix._34 + matrix._31;
_planes[2].d = matrix._44 + matrix._41;
D3DXPlaneNormalize(&_planes[2], &_planes[2]);
_planes[3].a = matrix._14 - matrix._11; // Right
_planes[3].b = matrix._24 - matrix._21;
_planes[3].c = matrix._34 - matrix._31;
_planes[3].d = matrix._44 - matrix._41;
D3DXPlaneNormalize(&_planes[3], &_planes[3]);
_planes[4].a = matrix._14 - matrix._12; // Top
_planes[4].b = matrix._24 - matrix._22;
_planes[4].c = matrix._34 - matrix._32;
_planes[4].d = matrix._44 - matrix._42;
D3DXPlaneNormalize(&_planes[4], &_planes[4]);
_planes[5].a = matrix._14 + matrix._12; // Bottom
_planes[5].b = matrix._24 + matrix._22;
_planes[5].c = matrix._34 + matrix._32;
_planes[5].d = matrix._44 + matrix._42;
D3DXPlaneNormalize(&_planes[5], &_planes[5]);
return TRUE;
}
使用
check系列函数判断物体在视锥内是否可见。
//----------------------------------------------------------------------------
// Check one point whether in frustum.
//----------------------------------------------------------------------------
BOOL FRUSTUM::check_point(float x_pos, float y_pos, float z_pos)
{
// make sure point is in frustum
for(short i = 0; i < 6; i++)
{
if(D3DXPlaneDotCoord(&_planes[i], &D3DXVECTOR3(x_pos, y_pos, z_pos)) < 0.0f)
return FALSE;
}
return TRUE;
}
//----------------------------------------------------------------------------
// Check whether a cube in frustum, if total cube in frustum then
// completely_contained will be set TRUE.
//----------------------------------------------------------------------------
BOOL FRUSTUM::check_cube(float x_center, float y_center, float z_center,
float radius,
BOOL* completely_contained)
{
DWORD num_points_in_frustum = 0;
// count the number of points inside the frustum
for(short i = 0; i < 6; i++)
{
DWORD count = 8;
BOOL in_all_planes = TRUE;
// test all eight points against plane
if(D3DXPlaneDotCoord(&_planes[i], &D3DXVECTOR3(x_center - radius, y_center - radius, z_center - radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
if(D3DXPlaneDotCoord(&_planes[i], &D3DXVECTOR3(x_center + radius, y_center - radius, z_center - radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
if(D3DXPlaneDotCoord(&_planes[i], &D3DXVECTOR3(x_center - radius, y_center + radius, z_center - radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
if(D3DXPlaneDotCoord(&_planes[i], &D3DXVECTOR3(x_center + radius, y_center + radius, z_center - radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
if(D3DXPlaneDotCoord(&_planes[i], &D3DXVECTOR3(x_center - radius, y_center - radius, z_center + radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
if(D3DXPlaneDotCoord(&_planes[i], &D3DXVECTOR3(x_center + radius, y_center - radius, z_center + radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
if(D3DXPlaneDotCoord(&_planes[i], &D3DXVECTOR3(x_center - radius, y_center + radius, z_center + radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
if(D3DXPlaneDotCoord(&_planes[i], &D3DXVECTOR3(x_center + radius, y_center + radius, z_center + radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
// if none contained, return FALSE.
if(count == 0)
return FALSE;
// update counter if they were all in front of plane.
if(in_all_planes)
++num_points_in_frustum;
}
// store BOOL flag if completely contained
if(completely_contained)
*completely_contained = (num_points_in_frustum == 6);
return TRUE;
}
//----------------------------------------------------------------------------
// Check whether a rectangle is in frustum, if total in then completely_contained
// will be set TRUE.
//----------------------------------------------------------------------------
BOOL FRUSTUM::check_rectangle(float x_center, float y_center, float z_center,
float x_radius, float y_radius, float z_radius,
BOOL* completely_contained)
{
DWORD num_points_in_frustum = 0;
// count the number of points inside the frustum
for(short i = 0; i < 6; i++)
{
DWORD count = 8;
BOOL in_all_planes = TRUE;
// Test all eight points against plane
if(D3DXPlaneDotCoord(&_planes[i],
&D3DXVECTOR3(x_center - x_radius, y_center - y_radius, z_center - z_radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
if(D3DXPlaneDotCoord(&_planes[i],
&D3DXVECTOR3(x_center + x_radius, y_center - y_radius, z_center - z_radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
if(D3DXPlaneDotCoord(&_planes[i],
&D3DXVECTOR3(x_center - x_radius, y_center + y_radius, z_center - z_radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
if(D3DXPlaneDotCoord(&_planes[i],
&D3DXVECTOR3(x_center + x_radius, y_center + y_radius, z_center - z_radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
if(D3DXPlaneDotCoord(&_planes[i],
&D3DXVECTOR3(x_center - x_radius, y_center - y_radius, z_center + z_radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
if(D3DXPlaneDotCoord(&_planes[i],
&D3DXVECTOR3(x_center + x_radius, y_center - y_radius, z_center + z_radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
if(D3DXPlaneDotCoord(&_planes[i],
&D3DXVECTOR3(x_center - x_radius, y_center + y_radius, z_center + z_radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
if(D3DXPlaneDotCoord(&_planes[i],
&D3DXVECTOR3(x_center + x_radius, y_center + y_radius, z_center + z_radius)) < 0.0f)
{
in_all_planes = FALSE;
count--;
}
// If none contained, return FALSE
if(count == 0)
return FALSE;
// Update counter if they were all in front of plane
if(in_all_planes)
++num_points_in_frustum;
}
// Store BOOL flag if completely contained
if(completely_contained)
*completely_contained = (num_points_in_frustum == 6);
return TRUE;
}
//----------------------------------------------------------------------------
// Check whether a sphere is in frustum.
//----------------------------------------------------------------------------
BOOL FRUSTUM::check_sphere(float x_center, float y_center, float z_center,
float radius)
{
// make sure radius is in frustum
for(short i = 0; i < 6; i++)
{
if(D3DXPlaneDotCoord(&_planes[i], &D3DXVECTOR3(x_center, y_center, z_center)) < -radius)
return FALSE;
}
return TRUE;
}
测试代码:
/************************************************************************************
PURPOSE:
frustum test.
************************************************************************************/
#include "core_global.h"
#include "frustum.h"
#define MAX_OBJECTS 256
class APP : public APPLICATION
{
public:
APP()
{
_width = 640;
_height = 480;
APPLICATION::_x_pos = (get_screen_width() - _width) / 2;
APPLICATION::_y_pos = (get_screen_height() - _height) / 4;
_style = WS_BORDER | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU;
strcpy(_class_name, "object clipping class");
strcpy(_caption, "object clipping demo");
}
BOOL init()
{
// initialize graphics and set display mode
_graphics.init();
_graphics.set_mode(get_hwnd(), TRUE, TRUE);
_graphics.set_perspective(D3DX_PI / 4, 1.3333f, 1.0f, 10000.0f);
// create a font
_font.create(&_graphics, "Arial", 16, TRUE, FALSE);
// initialize input and input device
_input.init(get_hwnd(), get_inst());
_mouse.create(&_input, MOUSE, TRUE);
// load mesh
if(! _mesh.load(&_graphics, "..\\Data\\Yodan.x", "..\\Data\\"))
return FALSE;
for(short i = 0; i < MAX_OBJECTS; i++)
{
_objects[i].create(&_graphics, &_mesh);
_objects[i].move((float) (rand() % 4000) - 2000.0f, 0.0f, (float) (rand() % 4000) - 2000.0f);
}
return TRUE;
}
BOOL frame()
{
// read mouse data
_mouse.read();
// position camera and rotate based on mouse position
_camera.move(0.0f, 100.0f, 0.0f);
// _mouse.get_y_delta():
// get mouse's relative x movement coordinate.
//
// _mouse.get_x_delta():
// get mouse's relative y movement coordinate.
_camera.rotate_rel(_mouse.get_y_delta() / 200.0f, _mouse.get_x_delta() / 200.0f, 0.0f);
// set camera
_graphics.set_camera(&_camera);
// render everything
_graphics.clear(D3DCOLOR_RGBA(0, 64, 128, 255));
// begin render now
if(_graphics.begin_scene())
{
FRUSTUM frustum;
frustum.construct(&_graphics);
long num_drawn = 0;
// render each object in frustums
for(short i = 0; i < MAX_OBJECTS; i++)
{
float radius;
_objects[i].get_bounds(NULL, NULL, NULL, NULL, NULL, NULL, &radius);
if(frustum.check_sphere(_objects[i].get_x_pos(), _objects[i].get_y_pos(), _objects[i].get_z_pos(),
radius))
{
_objects[i].render();
num_drawn++;
}
}
char stats[128];
// display statistics
sprintf(stats, "%lu of 256 objects drawn.", num_drawn);
_font.print(stats, 0, 0, 400, 100);
_graphics.end_scene();
}
_graphics.display();
return TRUE;
}
BOOL shutdown()
{
return TRUE;
}
private:
GRAPHICS _graphics;
CAMERA _camera;
FONT _font;
INPUT _input;
INPUT_DEVICE _mouse;
MESH _mesh;
OBJECT _objects[MAX_OBJECTS];
};
int WINAPI WinMain(HINSTANCE inst, HINSTANCE, LPSTR cmd_line, int cmd_show)
{
APP app;
return app.run();
}
截图: