本篇是2D和3D圖形引擎的混合(1)的續篇。
在游戲如最終幻想和寄生前夜(兩者都是由Square公司出品)中,可以在欣賞三維模型的同時,享受到非常精美的預先渲染好的背景圖像。將二維和三維圖形混合在一起,一直以來都是游戲公司高度保密的技術,現在可以
揭開謎底一看究竟。
如何從一個二維圖像中獲取三維的深度信息呢?有幾種方法可以實現三維物體在二維圖像中的背景幕效果。
(1)在一個三維建模工具中創建預先進行渲染的背景幕,例如Caligari公司的gamespace
light或discreet公司的3D
Studio Max,并將圖像與包含每個像素z值的深度緩沖區一起保存。對于游戲中的每一幀,將圖像的深度緩沖區拷貝到背景深度緩沖區中,并繼續繪制三維物體。
(2)在層次中創建背景幕,從底層開始,一個接一個地繪制每個圖像,并將三維角色繪制到適當的層次上,這樣就可以使用后面的層次覆蓋較低層次的部分內容(以及三維物體)。
(3)使用一個非常詳盡的預先渲染好的背景幕,以及一個在三維建模軟件中渲染場景的簡化的網格模型。使用網格模型以呈現z數值并進行碰撞檢測,三維物體可以使用z緩沖區負責繪制正確的深度。
我們采用第三種方法來繪制。
二維背景幕的處理
用一個三維建模軟件開發二維的背景幕,例如gamespace light(而不是使用一個繪圖程序,因為需要從建模軟件中獲取多邊形的數據),下圖顯示了一個簡單的網格模型以及最終的渲染效果:

一旦場景被渲染好,就需要將它作為一個位圖保存到磁盤上,那個位圖文件需要被切分成較小的紋理,如下圖所示,這個背景幕被切割成多個Direct3D可以處理的紋理,比如背景幕圖像為640
x 480,所以紋理將為256 x 256(對于塊1、2、4、5),以及128
x 256(對于塊3和6)。

場景網格模型的處理
詳盡的層次看起來非常不錯,現在想要包含一些三維物體到它里面。首先,需要構造一個簡化的場景,可使用兩種方法,包括填充每一幀的深度緩沖區,以便三維物體能夠正確地與二維的背景幕進行混合;作為運動物體的碰撞網格模型。
因為網格模型必須在每幀中被渲染出來,以便創建場景中的z緩存,使用越少的多邊形當然越好,然而必須使用足夠的多邊形以確保三維物體能夠被正確地混合,如下圖所示,它顯示了最終渲染好的圖像,實際的場景網格模型,以及簡化的場景網格模型。

當處理一個簡化的網格模型時,僅使用了兩種材質(沒有紋理)。第一種材質代表了實際繪制到背景幕上的多邊形區域,而第二種材質隱藏了在交集測試中所使用的多邊形,對于第二種材質,使用的alpha的數值為0.0(意味著它是不可見的,不會被實際渲染)。
應該使用正確的多邊形數量去渲染場景。如果有太多的多邊形,引擎將變得非常緩慢。如果多邊形太少,將會在玩游戲時得到貼圖錯誤的信息。請這樣思考一下:一個使用了500個多邊形的球形網格模型,很明顯比一個簡化的網格模型復雜許多,在一個簡化的網格模型里,僅需要足夠的多邊形去表示球體,并確保它在進行渲染時覆蓋相同的屏幕區域。
下圖演示了在創建簡化網格模型時常出現的一個錯誤,那就是使用了太少的多邊形。

為了簡化網格模型中多邊形的數量,切割掉那些看不到的表面或者在交集測試中所使用的表面,同時僅繪制那些實際覆蓋三維物體的多邊形
。舉個例子,如果在背景幕中有一個盒子,而玩家角色從不會接近它,那么在簡化的網格模型中就不用繪制它。
對于本例中的背景幕,僅需要繪制如下的簡化網格模型:

場景的渲染
現在將完成最后一步,以確保背景幕圖像能夠包含深度信息(通過簡化的網格模型)。如果加載了背景幕圖像和簡化的網格模型,就可以很容易地渲染游戲中的每幀,通過使用如下步驟:
(1)將z緩沖區清除為1.0(并確保z緩沖區被啟動)。
(2)渲染簡化的網格模型(因而填充場景的z緩沖區),跳過那些數值為0.0的多邊形(它們是不可見的)。
(3)禁用z緩沖區。
(4)使用ID3DXSprite位塊傳送背景幕紋理。
(5)啟動z緩沖區。
絕大多數新近的顯卡都允許處理1024 x 1024像素大小的紋理,這意味著可以將整個背景幕圖像加載到內存中,而不需要將它切割成6個小紋理。
在繪制好背景幕后,剩下的就是將三維物體(網格模型)繪制到場景中,因為Z緩沖區包含了所有與每個像素相關的深度數值。請不要猶豫,隨心所欲繪制角色、物體、甚至是增強背景的圖像。
下載源碼和工程
代碼:
/************************************************************************************
PURPOSE:
3D in 2D test.
************************************************************************************/
#include "core_common.h"
#include "core_framework.h"
#include "core_graphics.h"
#include "core_input.h"
#define ANIM_NONE -1
#define ANIM_WALK 1
#define ANIM_IDLE 2
const float g_angles[13] = {
0.0f, 4.71f, 0.0f, 5.495f, 1.57f, 0.0f,
0.785f, 0.0f, 3.14f, 3.925f, 0.0f, 0.0f, 2.355f
};
class APP : public FRAMEWORK
{
private:
CAMERA m_camera;
INPUT m_input;
INPUT_DEVICE m_keyboard;
TEXTURE m_background[6];
// the simplified scene mesh and object
MESH m_scene_mesh;
OBJECT m_scene;
// 3D meshes and objects
MESH m_monster_mesh;
OBJECT m_monster;
ANIMATION m_monster_anim;
static const float m_above_floor;
public:
BOOL init()
{
if(! create_display(g_hwnd, get_client_width(g_hwnd), get_client_height(g_hwnd), 16, TRUE, TRUE))
return FALSE;
set_perspective(0.6021124f, 1.3333f, 1.0f, 10000.0f);
ShowCursor(TRUE);
// initialize input and input device
m_input.create(g_hwnd, get_window_inst());
m_keyboard.create_keyboard(&m_input);
// load the backdrop textures
for(short i = 0; i < 6; i++)
{
char filename[81];
sprintf(filename, "..\\data\\scene%u.bmp", i+1);
if(! m_background[i].load(filename, 0, D3DFMT_UNKNOWN))
return FALSE;
}
// load the scene mesh and configure object
if(! m_scene_mesh.load("..\\Data\\Scene.x", ".\\"))
return FALSE;
m_scene.create(&m_scene_mesh);
// load the monster mesh and setup monster object
if(! m_monster_mesh.load("..\\data\\yodan.x", "..\\data\\"))
return FALSE;
m_monster_anim.load("..\\data\\yodan.x", &m_monster_mesh);
m_monster_anim.set_loop(TRUE, "Idle");
m_monster_anim.set_loop(TRUE, "Walk");
m_monster.create(&m_monster_mesh);
// position the camera for the scene
m_camera.move(0.0f, 200.0f, -650.0f);
m_camera.rotate(0.348888f, 0.0f, 0.0f);
g_d3d_device->SetTransform(D3DTS_VIEW, m_camera.get_view_matrix());
return TRUE;
}
BOOL frame()
{
static DWORD time_begin = timeGetTime();
// calculate elapsed time (plus speed boost)
DWORD time_end = timeGetTime();
ulong time_elapsed = time_end - time_begin;
time_begin = time_end;
// read keyboard data
m_keyboard.read();
// process input and update everything, ESC quits program.
if(m_keyboard.get_key_state(KEY_ESC))
return FALSE;
// process movement
long dir = 0;
if(m_keyboard.get_key_state(KEY_UP))
dir |= 1;
if(m_keyboard.get_key_state(KEY_RIGHT))
dir |= 2;
if(m_keyboard.get_key_state(KEY_DOWN))
dir |= 4;
if(m_keyboard.get_key_state(KEY_LEFT))
dir |= 8;
float x_move, z_move;
x_move = z_move = 0.0f;
if(dir)
{
m_monster.rotate(0.0f, g_angles[dir], 0.0f);
x_move = cos(g_angles[dir]) * (time_elapsed * 0.25f);
z_move = -sin(g_angles[dir]) * (time_elapsed * 0.25f);
}
float x_pos, y_pos, z_pos;
// get monster coordinates in local variables (make it easier)
x_pos = m_monster.get_x_pos();
y_pos = m_monster.get_y_pos();
z_pos = m_monster.get_z_pos();
D3DXMESH_PTR d3d_mesh = m_scene_mesh.get_root_mesh_info()->m_d3d_mesh;
// check for collision in movement (4 points).
//
// I hard-coded the bounding size of the object (25 radius) and added ability to climb up at mose 32 units.
for(long i = 0; i < 4; i++)
{
float x_add[4] = { 0.0f, 25.0f, 0.0f, -25.0f };
float z_add[4] = { 25.0f, 0.0f, -25.0f, 0.0f };
float dist;
if(is_ray_intersect_mesh(d3d_mesh,
x_pos + x_add[i], y_pos + m_above_floor, z_pos + z_add[i],
x_pos + x_add[i] + x_move, y_pos + m_above_floor, z_pos + z_add[i] + z_move,
&dist))
{
// clear out movement and break
x_move = z_move = 0.0f;
break;
}
}
// fix height of monster
y_pos = closest_height_below_object(d3d_mesh, x_pos, y_pos + m_above_floor, z_pos);
// move monster and set new animations as needed
m_monster.move(x_pos + x_move, y_pos, z_pos + z_move);
static short last_anim = ANIM_NONE;
if(!float_equal(x_move, 0.0f) || !float_equal(z_move, 0.0f))
{
if(last_anim != ANIM_WALK)
{
last_anim = ANIM_WALK;
m_monster.set_anim_info_set(&m_monster_anim, "Walk", time_end / 20);
}
}
else
{
if(last_anim != ANIM_IDLE)
{
last_anim = ANIM_IDLE;
m_monster.set_anim_info_set(&m_monster_anim, "Idle", time_end / 20);
}
}
// update monster animations
m_monster.update_anim_info_set(time_end / 20, TRUE);
// render everything
clear_display_zbuffer(1.0f);
// begin render now
if(SUCCEEDED(g_d3d_device->BeginScene()))
{
// render the level mesh
g_d3d_device->SetRenderState(D3DRS_ZENABLE, D3DZB_TRUE);
m_scene.render();
// draw the backdrop (composed of six textures)
g_d3d_device->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
g_d3d_sprite->Begin(0);
for(int row = 0; row < 2; row++)
{
for(int col = 0; col < 3; col++)
m_background[row * 3 + col].draw(col * 256, row * 256, 0, 0, 0, 0, 1.0f, 1.0f, 0xFFFFFFFF);
}
g_d3d_sprite->End();
// draw the 3D monster
g_d3d_device->SetRenderState(D3DRS_ZENABLE, D3DZB_TRUE);
m_monster.render();
g_d3d_device->EndScene();
}
present_display();
return TRUE;
}
BOOL shutdown()
{
destroy_display();
return TRUE;
}
};
const float APP::m_above_floor = 32.0f;
int WINAPI WinMain(HINSTANCE inst, HINSTANCE, LPSTR cmd_line, int cmd_show)
{
DWORD client_width = 640;
DWORD client_height = 480;
DWORD x_pos = (get_screen_width() - client_width) / 2;
DWORD y_pos = (get_screen_height() - client_height) / 4;
DWORD window_style = WS_BORDER | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU;
if(! build_window(inst, "3d_in_2d_class", "3D in 2D Test", window_style,
x_pos, y_pos, client_width, client_height))
{
return -1;
}
APP app;
app.run();
return 0;
}
截圖:
