本篇是创建3D图形引擎(3)的续篇,3D图形引擎的代码以创建游戏内核中编写的代码为基础进行开发。
下载源码和工程
将三维物体添加到世界中
盲目地绘制成千的物体而没有执行任何的剪切,是导致在图形-渲染通道中时间延迟的一个主要原因。需要再次使用视锥以快速确定哪些物体位于视野中,为了确定哪些物体是可见的,可将每个三维物体包围到一个可见的球体中,称之为框界球体(bounding
sphere)。
下图演示了框界球体和视锥的运用,它展示了一个场景,其中有三个物体和一个视锥,每个网格模型都有一个可见的框界球体围绕着它。当一个球体位于构成视锥的6个平面前时,它就被认为是可见的。
可以看出仅两个位于视锥中的物体是可见的,而另一个物体完全位于视锥外,绘制时能完全跳过那个位于视锥之外的物体。为了实现这一点,必须计算每个物体的框界球体,然后检测球体是否位于视锥之内。
网格模型的碰撞
为了检测一个多边形是否堵塞了节点之间的路径,可以在两个节点发射一条想象中的射线,以检测它们是否与一个平面相交。如下图所示:
有一个功能函数负责执行交集的测试,以确保射线与平面的相交点位于多边形内,并给出那个相交点的确切坐标,同时显示从射线的开始点到相交点的距离长度,它是一个非常有用的功能函数。
Determines if a ray intersects with a mesh.
HRESULT D3DXIntersect(
LPD3DXBASEMESH pMesh,
CONST D3DXVECTOR3 * pRayPos,
CONST D3DXVECTOR3 * pRayDir,
BOOL * pHit,
DWORD * pFaceIndex,
FLOAT * pU,
FLOAT * pV,
FLOAT * pDist,
LPD3DXBUFFER * ppAllHits,
DWORD * pCountOfHits
);
Parameters
- pMesh
- [in] Pointer to an ID3DXBaseMesh interface, representing the mesh to be
tested.
- pRayPos
- [in] Pointer to a D3DXVECTOR3 structure, specifying the point where the
ray begins.
- pRayDir
- [in] Pointer to a D3DXVECTOR3 structure, specifying the direction
of the ray.
- pHit
- [out] Pointer to a BOOL. If the ray intersects a triangular face on the
mesh, this value will be set to TRUE. Otherwise, this value is set to FALSE.
- pFaceIndex
- [out] Pointer to an index value of the face closest to the ray origin,
if pHit is TRUE.
- pU
- [out] Pointer to a barycentric hit coordinate, U.
- pV
- [out] Pointer to a barycentric hit coordinate, V.
- pDist
- [out] Pointer to a ray intersection parameter distance.
- ppAllHits
- [out] Pointer to an ID3DXBuffer object, containing an array of
D3DXINTERSECTINFO structures.
- pCountOfHits
- [out] Pointer to a DWORD that contains the number of entries in the
ppAllHits array.
Return Values
If the function succeeds, the return value is D3D_OK. If the function fails,
the return value can be: E_OUTOFMEMORY.
Remarks
The D3DXIntersect function provides a way to understand points in and
around a triangle, independent of where the triangle is actually located. This
function returns the resulting point by using the following equation: V1 + U(V2
- V1) + V(V3 - V1).
Any point in the plane V1V2V3 can be represented by the barycentric
coordinate (U,V). The parameter U controls how much V2 gets weighted into the
result, and the parameter V controls how much V3 gets weighted into the result.
Lastly, the value of [1 - (U + V)] controls how much V1 gets weighted into the
result.
Barycentric coordinates are a form of general coordinates. In this context,
using barycentric coordinates represents a change in coordinate systems. What
holds true for Cartesian coordinates holds true for barycentric coordinates.
Barycentric coordinates define a point inside a triangle in terms of the
triangle's vertices. For a more in-depth description of barycentric coordinates,
see Mathworld's Barycentric Coordinates Description.
我们编写一个函数来检测一条线段是否与一个网格中的三角形面相交。
//------------------------------------------------------------------------------
// Check if a polygon blocks path from start to end.
//------------------------------------------------------------------------------
BOOL is_ray_intersect_mesh(LPD3DXBASEMESH mesh,
float x_start, float y_start, float z_start,
float x_end, float y_end, float z_end,
float* distance)
{
float x_diff, y_diff, z_diff;
x_diff = x_end - x_start;
y_diff = y_end - y_start;
z_diff = z_end - z_start;
D3DXVECTOR3 ray_pos(x_start, y_start, z_start);
D3DXVECTOR3 ray_dir;
D3DXVec3Normalize(&ray_dir, &D3DXVECTOR3(x_diff, y_diff, z_diff));
BOOL is_hit;
float u, v, dist;
DWORD face_index;
// determins if a ray intersects with a mesh
D3DXIntersect(mesh, &ray_pos, &ray_dir, &is_hit, &face_index, &u, &v, &dist, NULL, NULL);
if(is_hit)
{
float ray_length = (float) sqrt(x_diff * x_diff + y_diff * y_diff + z_diff * z_diff);
if(dist > ray_length)
return FALSE;
else
{
if(distance != NULL)
*distance = dist;
}
}
return TRUE;
}
在一个多边形层次中运用碰撞检测的另一个好处就是可以使物体随着其下多边形变化的高度而变化。换句话说,可以使物体永远在多边形上移动,这样就能在一个三维建模软件里绘制层次,而不用定义哪些区域是物体可以“行走”的,因为多边形就是那些区域,这使得四叉树和八叉树网格模型的处理更加容易。
可通过下面这三个函数来执行物体与多边形的相交测试:
//------------------------------------------------------------------------------
// Get closest height above or below point.
//------------------------------------------------------------------------------
float get_closest_height(LPD3DXBASEMESH mesh, float x_pos, float y_pos, float z_pos)
{
float y_above, y_below;
y_above = get_closest_height_above(mesh, x_pos, y_pos, z_pos);
y_below = get_closest_height_below(mesh, x_pos, y_pos, z_pos);
if(fabs(y_above - y_pos) < fabs(y_below - y_pos))
return y_above;
return y_below;
}
//------------------------------------------------------------------------------
// Get closet height below point.
//------------------------------------------------------------------------------
float get_closest_height_below(LPD3DXBASEMESH mesh, float x_pos, float y_pos, float z_pos)
{
BOOL is_hit;
float u, v, dist;
DWORD face_index;
D3DXVECTOR3 ray_pos(x_pos, y_pos, z_pos);
D3DXVECTOR3 ray_dir(0.0f, -1.0f, 0.0f);
D3DXIntersect(mesh, &ray_pos, &ray_dir, &is_hit, &face_index, &u, &v, &dist, NULL, NULL);
if(is_hit)
return y_pos - dist;
return y_pos;
}
//------------------------------------------------------------------------------
// Get closet height above point.
//------------------------------------------------------------------------------
float get_closest_height_above(LPD3DXBASEMESH mesh, float x_pos, float y_pos, float z_pos)
{
BOOL is_hit;
float u, v, dist;
DWORD face_index;
D3DXVECTOR3 ray_pos(x_pos, y_pos, z_pos);
D3DXVECTOR3 ray_dir(0.0f, 1.0f, 0.0f);
D3DXIntersect(mesh, &ray_pos, &ray_dir, &is_hit, &face_index, &u, &v, &dist, NULL, NULL);
if(is_hit)
return y_pos + dist;
return y_pos;
}
为了方便,在
NODE_TREE_MESH类中继续封装这些函数:
//------------------------------------------------------------------------------
// Get closest height above or below point.
//------------------------------------------------------------------------------
float NODE_TREE_MESH::get_closest_height(float x_pos, float y_pos, float z_pos)
{
return ::get_closest_height(m_root_mesh->m_mesh, x_pos, y_pos, z_pos);
}
//------------------------------------------------------------------------------
// Get closest height below point.
//------------------------------------------------------------------------------
float NODE_TREE_MESH::get_closest_height_below(float x_pos, float y_pos, float z_pos)
{
return ::get_closest_height_below(m_root_mesh->m_mesh, x_pos, y_pos, z_pos);
}
//------------------------------------------------------------------------------
// Get closest height above point.
//------------------------------------------------------------------------------
float NODE_TREE_MESH::get_closest_height_above(float x_pos, float y_pos, float z_pos)
{
return ::get_closest_height_above(m_root_mesh->m_mesh, x_pos, y_pos, z_pos);
}
//------------------------------------------------------------------------------
// Check if a polygon blocks path from start to end.
//------------------------------------------------------------------------------
BOOL NODE_TREE_MESH::is_ray_intersect_mesh(float x_start, float y_start, float z_start,
float x_end, float y_end, float z_end,
float* distance)
{
return ::is_ray_intersect_mesh(m_root_mesh->m_mesh,
x_start, y_start, z_start,
x_end, y_end, z_end,
distance);
}
当网格模型发生碰撞时
除了检测物体网格模型与构造世界的网格模型之间的碰撞,还需要知道较小网格模型碰撞的情况。举个例子,如果不希望角色穿透其他的角色,就需要结合物体和物体之间(object-to-object)的碰撞检测。进行操作前需要了解一些事情,包括框界球体使用的知识。如下图所示,其显示了两个怪物,它们都有长长的尾巴,那些尾巴影响了网格模型的框界球体的整体尺寸,为了包围住整个网格模型(包括尾巴),球体将会很大。如果将这两个怪物移动到下图所示的位置,会看到这两个框界球体相交,但怪物并没有那么靠近。
尽管有许多办法解决大型框界球体问题,但有一种快速执行的方法。取代使用网格模型的框界球体,而是为每个网格模型计算自己的框界球体半径,通过计算框界球体的半径,可以快速调节它以覆盖网格模型确实需要的空间。
我们编写一个函数来检测两个框界球体是否相交:
//------------------------------------------------------------------------------
// Check if two spheres intersect.
//------------------------------------------------------------------------------
BOOL is_tow_sphere_intersect(float x_center_1, float y_center_1, float z_center_1,
float radius1,
float x_center_2, float y_center_2, float z_center_2,
float radius2)
{
float x_diff, y_diff, z_diff, dist;
// calculate distance between two sphere center]
x_diff = fabs(x_center_2 - x_center_1);
y_diff = fabs(y_center_2 - y_center_1);
z_diff = fabs(z_center_2 - z_center_1);
dist = (float) sqrt(x_diff * x_diff + y_diff * y_diff + z_diff * z_diff);
// if two spheres intersect, retuen TRUE.
if(dist <= radius1 + radius2)
return TRUE;
return FALSE;
}
天框的使用
天框(sky box)是一种围绕观察者进行纹理映射的三维立方体的图形技术,渲染一个天框时,观察点总是定于中心位置,以便用户始终能够看到方框内部纹理映射的表面,这种技术能够制造出一种世界围绕用户的效果,如下图所示:
天框是非常容易是实现,只需一个立方体网格模型(其表面朝向里面)即可,通过一个顶点缓冲区存储立方体网格模型会非常不错。至于纹理,可以使用多达6个的纹理,每个表面一个纹理。网格模型并不需要很大,仅20.0单元大小的立方体就行了。纹理大小应该为256
x 256或者更高,较小的纹理将出现拉伸而且不生动。
创建一个天框类
SKY_BOX负责控制天框的每个方面,从创建渲染盒子所使用的顶点缓冲区,到渲染时所使用的纹理。
定义:
enum SKY_BOX_SIDES { TOP = 0, BOTTOM, LEFT, RIGHT, FRONT, BACK };
//=====================================================================================
// This calss encapsulate how to make sky box.
//=====================================================================================
typedef class SKY_BOX
{
private:
typedef struct SKY_BOX_VERTEX
{
float x, y, z;
float u, v;
} *SKY_BOX_VERTEX_PTR;
#define SKY_BOX_FVF (D3DFVF_XYZ | D3DFVF_TEX1)
private:
GRAPHICS_PTR m_graphics; // parent GRAPHICS object
TEXTURE m_textures[6]; // face texture (0 - 5)
VERTEX_BUFFER m_vertex_buffer; // mesh vertex buffer
WORLD_POSITION m_pos; // sky box position
public:
SKY_BOX();
~SKY_BOX();
BOOL create(GRAPHICS_PTR graphics);
void free();
BOOL load_texture(short side, pcstr filename, D3DCOLOR transparent = 0, D3DFORMAT format = D3DFMT_UNKNOWN);
void rotate(float x_rot, float y_rot, float z_rot);
void rotate_rel(float x_rot, float y_rot, float z_rot);
BOOL render(CAMERA_PTR camera, BOOL alpha_blend = FALSE);
} *SKY_BOX_PTR;
实现:
//----------------------------------------------------------------------------------
// Constructor, initialize member data.
//----------------------------------------------------------------------------------
SKY_BOX::SKY_BOX()
{
m_graphics = NULL;
}
//----------------------------------------------------------------------------------
// Destructor, release allocated resource.
//----------------------------------------------------------------------------------
SKY_BOX::~SKY_BOX()
{
free();
}
//----------------------------------------------------------------------------------
// Release allocated resource.
//----------------------------------------------------------------------------------
void SKY_BOX::free()
{
m_graphics = NULL;
for(short i = 0; i < 6; i++)
m_textures[i].free();
m_vertex_buffer.free();
}
//----------------------------------------------------------------------------------
// Create a sky box class object.
//----------------------------------------------------------------------------------
BOOL SKY_BOX::create(GRAPHICS_PTR graphics)
{
SKY_BOX_VERTEX verts[24] = {
{ -10.0f, 10.0f, -10.0f, 0.0f, 0.0f }, // Top
{ 10.0f, 10.0f, -10.0f, 1.0f, 0.0f },
{ -10.0f, 10.0f, 10.0f, 0.0f, 1.0f },
{ 10.0f, 10.0f, 10.0f, 1.0f, 1.0f },
{ -10.0f, -10.0f, 10.0f, 0.0f, 0.0f }, // Bottom
{ 10.0f, -10.0f, 10.0f, 1.0f, 0.0f },
{ -10.0f, -10.0f, -10.0f, 0.0f, 1.0f },
{ 10.0f, -10.0f, -10.0f, 1.0f, 1.0f },
{ -10.0f, 10.0f, -10.0f, 0.0f, 0.0f }, // Left
{ -10.0f, 10.0f, 10.0f, 1.0f, 0.0f },
{ -10.0f, -10.0f, -10.0f, 0.0f, 1.0f },
{ -10.0f, -10.0f, 10.0f, 1.0f, 1.0f },
{ 10.0f, 10.0f, 10.0f, 0.0f, 0.0f }, // Right
{ 10.0f, 10.0f, -10.0f, 1.0f, 0.0f },
{ 10.0f, -10.0f, 10.0f, 0.0f, 1.0f },
{ 10.0f, -10.0f, -10.0f, 1.0f, 1.0f },
{ -10.0f, 10.0f, 10.0f, 0.0f, 0.0f }, // Front
{ 10.0f, 10.0f, 10.0f, 1.0f, 0.0f },
{ -10.0f, -10.0f, 10.0f, 0.0f, 1.0f },
{ 10.0f, -10.0f, 10.0f, 1.0f, 1.0f },
{ 10.0f, 10.0f, -10.0f, 0.0f, 0.0f }, // Back
{ -10.0f, 10.0f, -10.0f, 1.0f, 0.0f },
{ 10.0f, -10.0f, -10.0f, 0.0f, 1.0f },
{ -10.0f, -10.0f, -10.0f, 1.0f, 1.0f },
};
free(); // free a prior sky box
// error checking
if((m_graphics = graphics) == NULL)
return FALSE;
// create the vertex buffer (and copy over sky box vertices)
if(m_vertex_buffer.create(m_graphics, 24, sizeof(SKY_BOX_VERTEX), SKY_BOX_FVF))
m_vertex_buffer.set(0, 24, (void*)verts);
// rotate the sky box into default orientation
rotate(0.0f, 0.0f, 0.0f);
return TRUE;
}
//----------------------------------------------------------------------------------
// Set a specific side's texture map, allow for transparent and storage format changes.
//----------------------------------------------------------------------------------
BOOL SKY_BOX::load_texture(short side, pcstr filename, D3DCOLOR transparent, D3DFORMAT format)
{
// error checking
if(m_graphics == NULL || side < 0 || side > 5)
return FALSE;
m_textures[side].free(); // free prior texture
return m_textures[side].load(m_graphics, filename, transparent, format);
}
//----------------------------------------------------------------------------------
// Rotate box to an absolute rotation.
//----------------------------------------------------------------------------------
void SKY_BOX::rotate(float x_rot, float y_rot, float z_rot)
{
m_pos.rotate(x_rot, y_rot, z_rot);
}
//----------------------------------------------------------------------------------
// Rotate box to an relative rotation.
//----------------------------------------------------------------------------------
void SKY_BOX::rotate_rel(float x_rot, float y_rot, float z_rot)
{
m_pos.rotate_rel(x_rot, y_rot, z_rot);
}
//----------------------------------------------------------------------------------
// Render the sky box (using optional alpha-blending) and using current view
// transformation from Camera.
//----------------------------------------------------------------------------------
BOOL SKY_BOX::render(CAMERA_PTR camera, BOOL alpha_blend)
{
// error checking
if(m_graphics == NULL || camera == NULL)
return FALSE;
// position sky box around viewer
m_pos.move(camera->get_x_pos(), camera->get_y_pos(), camera->get_z_pos());
m_graphics->set_world_position(&m_pos);
// enable alpha testing and alpha blending
m_graphics->enable_alpha_blending(TRUE);
if(alpha_blend)
m_graphics->enable_alpha_blending(TRUE, D3DBLEND_SRCCOLOR, D3DBLEND_DESTCOLOR);
// draw each layer
for(short i = 0; i < 6; i++)
{
if(m_textures[i].is_loaded())
{
m_graphics->set_texture(0, &m_textures[i]);
m_vertex_buffer.render(i * 4, 2, D3DPT_TRIANGLESTRIP);
}
}
// disable alpha testing and alpha blending
m_graphics->enable_alpha_blending(FALSE);
if(alpha_blend)
m_graphics->enable_alpha_blending(FALSE);
return TRUE;
}
测试代码:
/************************************************************************************
PURPOSE:
node tree mesh test.
************************************************************************************/
#include "core_global.h"
#include "frustum.h"
#include "node_tree.h"
#include "sky_box.h"
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, "node tree class");
strcpy(_caption, "node tree 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);
show_mouse(TRUE);
// enable lighting and setup light
_graphics.enable_lighting(TRUE);
_graphics.set_ambient_light(24, 24, 24);
_graphics.enable_light(0, TRUE);
_light.set_attenuation_0(0.4f);
_light.set_range(1000.0f);
// initialize input and input device
_input.init(get_hwnd(), get_inst());
_keyboard.create(&_input, KEYBOARD);
_mouse.create(&_input, MOUSE, TRUE);
// load the mesh and create a nodetree mesh from it
if(! _mesh.load(&_graphics, "..\\Data\\Level.x", "..\\Data\\"))
return FALSE;
_node_tree_mesh.create(&_graphics, &_mesh, QUADTREE);
// position view at origin
_x_pos = _y_pos = _z_pos = 0.0f;
// setup sky box
_sky_box.create(&_graphics);
for(short i = 0; i < 6; i++)
_sky_box.load_texture(i, "..\\data\\stars.bmp");
// initialize the sound system to play with
_sound.init(get_hwnd());
_sound_data.load_wav("..\\data\\cricket.wav");
for(short i = 0; i < 3; i++)
_sound_channel[i].create(&_sound, &_sound_data);
return TRUE;
}
BOOL frame()
{
static DWORD time_now = timeGetTime();
// play a random cricket sound
for(short i = 0; i< 3; i++)
{
if(!_sound_channel[i].is_playing() && rand()%256 < 16)
_sound_channel[i].play(&_sound_data, 10);
}
// calculate elapsed time (plus speed boost)
ulong time_elapsed = timeGetTime() - time_now;
time_now = timeGetTime();
// read keyboard and mouse data
_keyboard.read();
_mouse.read();
// process input and update everything, ESC quits program.
if(_keyboard.get_key_state(KEY_ESC))
return FALSE;
float x_move, z_move;
// process movement
x_move = z_move = 0.0f;
if(_keyboard.get_key_state(KEY_UP) || _keyboard.get_key_state(KEY_W))
{
x_move = (float) sin(_camera.get_y_rotation()) * time_elapsed;
z_move = (float) cos(_camera.get_y_rotation()) * time_elapsed;
}
if(_keyboard.get_key_state(KEY_DOWN) || _keyboard.get_key_state(KEY_S))
{
x_move = (float) -sin(_camera.get_y_rotation()) * time_elapsed;
z_move = (float) -cos(_camera.get_y_rotation()) * time_elapsed;
}
if(_keyboard.get_key_state(KEY_LEFT) || _keyboard.get_key_state(KEY_A))
{
x_move = (float) sin(_camera.get_y_rotation() - 1.57f) * time_elapsed;
z_move = (float) cos(_camera.get_y_rotation() - 1.57f) * time_elapsed;
}
if(_keyboard.get_key_state(KEY_RIGHT) || _keyboard.get_key_state(KEY_D))
{
x_move = (float) sin(_camera.get_y_rotation() + 1.57f) * time_elapsed;
z_move = (float) cos(_camera.get_y_rotation() + 1.57f) * time_elapsed;
}
// check for height changes (can step up to 64 units)
float height = _node_tree_mesh.get_closest_height_below(_x_pos, _y_pos + _above_floor, _z_pos);
if(_y_pos > height)
{
// dropping
if((_y_pos -= (float)time_elapsed) < height)
_y_pos = height;
else
x_move = z_move = 0.0f;
}
else
{
// climbing
_y_pos = height;
}
float dist;
// check for movement collision - can not walk past anything blocking path.
if(_node_tree_mesh.is_ray_intersect_mesh(_x_pos, _y_pos + _above_floor, _z_pos,
_x_pos + x_move, _y_pos + _above_floor, _z_pos + z_move,
&dist))
{
// adjust coordinates to be exactly 2.5 units away from target
float diff = dist - 2.5f;
D3DXVECTOR2 dir;
D3DXVec2Normalize(&dir, &D3DXVECTOR2(x_move, z_move));
dir *= diff;
x_move = dir.x;
z_move = dir.y;
}
// update view coordinats
_x_pos += x_move;
_z_pos += z_move;
// position camera and rotate based on mouse position
_camera.move(_x_pos, _y_pos + 50.0f, _z_pos);
// _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((float) _mouse.get_y_delta() / 200.0f, (float) _mouse.get_x_delta() / 200.0f, 0.0f);
// position
_light.move(_x_pos, _y_pos + 60.0f, _z_pos);
_graphics.set_light(0, &_light);
FRUSTUM frustum;
// set camera and calculate frustum
_graphics.set_camera(&_camera);
frustum.construct(&_graphics);
// render everything
_graphics.clear_zbuffer(1.0f);
// begin render now
if(_graphics.begin_scene())
{
_graphics.enable_zbuffer(FALSE);
_graphics.enable_lighting(FALSE);
_sky_box.render(&_camera);
_graphics.enable_zbuffer(TRUE);
_graphics.enable_lighting(TRUE);
_node_tree_mesh.render(&frustum);
_graphics.end_scene();
}
_graphics.display();
return TRUE;
}
BOOL shutdown()
{
return TRUE;
}
private:
GRAPHICS _graphics;
CAMERA _camera;
LIGHT _light;
SOUND _sound;
SOUND_DATA _sound_data;
SOUND_CHANNEL _sound_channel[3];
SKY_BOX _sky_box;
INPUT _input;
INPUT_DEVICE _keyboard;
INPUT_DEVICE _mouse;
MESH _mesh;
NODE_TREE_MESH _node_tree_mesh;
float _x_pos, _y_pos, _z_pos;
static const float _above_floor;
};
const float APP::_above_floor = 64.0f;
int WINAPI WinMain(HINSTANCE inst, HINSTANCE, LPSTR cmd_line, int cmd_show)
{
APP app;
return app.run();
}
截图: