> > 使用Direct3D渲染2D图素 2001-09-22 中国游戏开发者.CN
图片及源代码请登陆下面网站:
合作翻译:
中国游戏开发者.CN – mays
http://mays.6to23.com
游戏制作天地 – wonyee(rocks_lee)
http://wonyee.top263.net/index.html
译文版本:1.0 Beta
原文信息:http://www.gamedev.net
《Using Direct3D For 2D Tile Rendering》
Author:bracket@unforgettable.com
版权声明:中文版权属“中国游戏开发者.CN”、“游戏制作天地”共同所有。因为在我把文章翻译到后半部的时候,wonyee已经把这篇文章翻译好了。所以我就参考了他的部分翻译内容。
--------------------------------------------------------------------------------
目录
1 引言
2 传统的方法
3 如何获得3D架构的帮助
4 一些假设
5 第一步:设置DirectX
6 DXEngine的结构
7 设置DirectDraw
8 设置Direct3D
9 渲染一个Isometric视角的网格线
10 加入纹理因素
11 在引擎中加入光照
12 联合起来:带有高度映射及光照效果的图素引擎
13 将来的工作
--------------------------------------------------------------------------------
1、引言
Introduction
基于Isometric及Tile引擎的游戏已经出现很多年了。在此其间这类游戏的制作技术已经愈来愈成熟,但图素渲染方式还是没有多大的变化。当人们在争论3D技术是否会导致2D渲染技术(伪3D)被废弃的时候,我相信这个替代过程一定会发生,但可能需要一个好的理由 - 由时间去证明吧。有件事情是,在以前要制作一个动态3D引擎,对实力较小的游戏开发组来说是比较困难的。而且在3D图形硬件成熟之前,兼容性也是一个比较严重的问题。另外,直到所有3D加速卡都可以处理海量多边形的今天,让它与一个基于图素但组织良好的游戏相比,3D图形并不被看好。不管怎么说:3D加速卡有高速处理多边形、纹理填充的能力,它可以使不带加速渲染的程序的性能得到提高。如:Alpha 混合、纹理映射、高得洛着色及其它一些特效在2D程序中也可以实现,但3D加速卡可以为它们提供更多加速潜力。这篇文章讲述了如何使用 Direct3D 来提高 2D 图形性能,并且提出了每个人都希望掌握的基于Tile引擎的光照。关于“菱形及方形图素”的问题,在论坛上已经有很多年的争论了,每方都有各自的道理。这篇文章将集中讨论菱形图素的使用,因为大部读者都比较偏爱它(可给人立体视角)。当然,将此文的渲染技术转换到方型图素引擎上,应该是相当容易的。Jim Adams的《Isometric 引擎介绍》可给你对此类菱形图素渲染引擎有个总体的认识。
Isometric and tile-based games have been around for a very long time. Rendering techniques for this type of game have grown considerably in sophistication since then, but the style itself remains popular. While some have argued (vocally!) that the arrival of 3D graphics will render 2D (and "psuedo-3D ") rendering techniques obsolete, there is good reason to believe that if this is the case - it will not be true for some time. For one thing, writing a dynamic 3D engine is difficult - hard enough to be prohibitive for small development groups. Until 3D graphics hardware matures, compatibility remains an issue. Likewise, until the average 3D card can handle a significantly higher polygon throughput than today, 3D graphics will not always look significantly better than a well-planned tile-based game. 3D-renderers do, however, offer 2D graphics a significant boost in performance. After all, all a "3D card " does is spit out textured polygons at high speed - something that can seriously slow down a non-accelerated rendering routine. Alpha blending, texture mapping, gouraud shading and other features have all been implemented in 2D routines - but a 3D card offers the potential to make them fast enough for regular use. This article describes the use of Direct3D to enhance 2D graphics - and suggests that nicely lit tile-based engines are within everybody 's grasp. Tile-based and Isometric Views A significant volume of material has been written on both tile-based and isometric rendering. The "diamond tiles versus square tiles " argument has raged across many forums for years, with both sides having advantages. This article will focus on diamond-shaped tiles, largely because of author preference (they look nicer!) It should be relatively easy to switch to a square tile rendering engine using these techniques. Jim Adams ' Guide to Isometric Engines gives a great overview of this kind of isometric rendering:
2、传统的方法
Traditional Methods
惯例上,处理菱形图素是一件令人比较头痛的事情。图素首先要设计成菱形,其次要定义透明背景关键色。接着要建立菱形图素的无缝拼接。因此,程序员就得面对相当大的程序开销:在位块传送过程中,必须明度背景关键色不被绘制到屏幕上。虽然现在有很多的位块传送优化方案及其它技巧,可事实总是无法改变的,你的游戏必须负担额外的程序开销。
Traditionally, diamond tiles have been something of a pain. Tiles needed to be designed as diamond-shaped bitmaps, usually with a color-keyed background. It could be extremely different to make tile boundaries appear seamless. Additionally, the programmer was faced with a fair degree of overhead: the color keyed area of each bitmap had to be ignored during the blitting process. Many innovative ways of reducing this overhead exist (and many artists have proven innovative in tile design), but the fact remained: isometric rendering was tricky.
3、如何获得3D架构的帮助
How 3D Architectures Can Help
3D加速卡可以用很方法完美地表现基于菱形图素的isometric引擎。作为3D引擎中的一个主要功能,三角形纹理够实现将位图应用到各种形状奇怪的区域上。而菱形可以被简单的分解成两个三角形(as do any other quad),因此对3D加速卡来说,在处理矩型及菱形图素的位块传送时,不会有速度上的差别。美工的生活也为此轻松许多:在3D 引擎中纹理一般用矩形表示,并且通过映射将它们贴到相应的多边形上。这样,美工就需要设计方块形状的纹理(which tend to tile much more nicely),剩下的菱形图素无缝拼接工作则由3D加速卡去完成。另外,其它一些原本很复杂的问题,比如高度映射等也由此迎刃而解了。
(CGD.CN注:架构是一种规格说明。它决定系统如何构成,确定功能模块以及允许模块间进行通信和协同的协议与接口)。
3D cards are in many ways ideally suited to rendering diamond-tile based isometric views. Textured triangles, the staple of 3D engines, require the application of bitmaps to odd-shaped regions. Diamonds break down simply into two triangles (as do any other quad), so there is no difference in raw blitting overhead between square and diamond tiles. The artist 's life is made easier, too: textures in 3D engines are typically rectangular, and are mapped to fit polygons. Thus, artists are able to design square textures (which tend to tile much more nicely), and let the 3D hardware worry about making it look good when stretched over a diamond-shaped tile. Additionally, height-mapping and other concerns become significantly easier to implement because a textured quad, unlike a static bitmap, may be readily warped.
Note: Most 3D accelerators are really 2D accelerators. With the exception of some new ones that help with transformation & lighting, all these cards can do is pump out textured triangles - onto a 2D surface. Thus, there is absolutely nothing unnatural about using a 3D card to speed up 2D rendering - in fact, its just doing what the card always does, without some heavy matrix math!
4、一些假设
Some Assumptions
这篇文章使用到Isometric/Tile引擎的应用知识。假如你对此不慎了解,你应该去看看参考书目,以获得更多的基础信息。一些关于isometric的基本内容将会在实现引擎时给出,但文章不会把主要时间花在基本概念的介绍上。对不起,这篇文章同样也不会教给你DirectDraw(not Direct3D)及Windows程序设计的方法。因为在因特网有很多此类的教程。Andr LaMothe编写的《Tricks of Windows Game Programming Gurus 》就是初学这些主题的比较好的书(当然还有更多此类的书)。
This article assumes a working knowledge of isometric/tile-based graphics. If you are unsure of how these work, you are advised to check out the bibliography for more information. While plenty of information is given on implementing an isometric engine, this article doesn 't take the time to introduce the basic concepts. Sorry! This article also assumes a basic knowledge of DirectDraw (not Direct3D) and Windows programming. There are many tutorials on these on the Internet. Andr?LaMothe 's book Tricks of the Windows Game Programming Gurus makes an excellent starting point for learning these subjects (and many others).
5、第一步:设置DirectX
First Steps: Setting Up DirectX
在学习如何利用“Enhanced 2D”时,需要创建一个最简的Win32应用程序。这你可以看看文章附带例程的WinMain.cpp,它便是一个极简单的Win32应用程序。对于这个程序,须注意以下的代码项目:
The first stage in learning to use "Enhanced 2D " requires the creation of a minimal Win32 application. The sources that come with this article include WinMain.cpp (for each example), which is about as minimal as you can get if you still want your program to be called "Win32 "! The only significant items to note in this code are the following:
含一个全局变量CEngine *Engine。CEngine是我实现图素引擎的基类。
在WinMain主消息循环之前,注意调用Engine-> GameInit()。
在主循环内重复地调用Engine-> GameMain() 当程序结束时,调用Engine-> GameDone()。
The inclusion of CEngine *Engine as a global variable.CEngine is my basic tile engine class.
Just before the main message loop in WinMain, note the call to Engine-> GameInit().
The main loop itself repeatedly calls Engine-> GameMain(). When closing down, Engine-> GameDone() is called.
6、DXEngine的结构
The DXEngine Framework
我是一个喜爱C++的顽固分子。因为爱C++,所以我在设计DXEngine结构及例程的时候,全采用了面向对象的编程方法。DXEngine本身就是游戏底层的基类,也是一个抽象类(虚类)- 这意味你不可以实例化这个类的对象。InitDirectX()已经包含隐藏了初始化DirectDraw及Direct3D的必要代码。假如你要在你的程序中使用这些函数,那你只需继承这个类,并且改写一下GameInit (),GameMain ()和GameDone ()函数即可。那不是很好吗?
I 'm a diehard C++ fan. I admit it. Classes, Object Oriented Programming and similar concepts appeal to me in ways that probably can 't be described to minors?or at least intellectually, anyway. ;-) Because of my love for C++, I 've designed the framework used for all the demonstrations in an object oriented manner. DXEngine is the base class for the game itself. Its an abstract class - meaning you can 't instantiate it. It includes all of the code necessary to initialize DirectDraw and Direct3D, and hides it away in a single call to InitDirectX(). All a programmer wanting this functionality in his/her program needs to do is inherit this class and write GameInit(), GameMain() and GameDone(). Is that not nifty?
7、设置DirectDraw
Setting up DirectDraw
DXEngine做相当很多的DirectDraw 处理工作。这个程序的代码大部分来自Lamothe 编写的引擎中(although the BOB engine itself was trashed very quickly - talk about slow)。InitDirectDraw ()负责完成所有DirectDraw的设置工作(没有什么令人兴奋的)。它所做的与你经常做的步骤没有什么显著地区别 - 只不过在建立窗口及全屏模式上花了些时间。总体说,它定义了一个DirectDraw 接口,设置协作层,创建屏幕模式,创建主表面及缓冲区,然后建立一个裁剪器。没有革命性的东西。
DXEngine does quite a lot of work to bring DirectDraw to life. Much of this code originally came from LaMothe 's work (although the BOB engine itself was trashed very quickly - talk about slow). The function that does all the work for setting up DirectDraw is InitDirectDraw() (no surprises there!). It doesn 't differ significantly from the steps that you would need in general - although it does take the time to work in both windowed and fullscreen modes. Basically, it gets an interface to DirectDraw, sets the cooperation level, creates the screen mode, creates a primary and secondary surface and then sets up a clipper. Nothing too revolutionary.
可在Direct3D部分的内容中,有几点必须要指明:
A few points in it need to be highlighted, however, in the context of Direct3D:
我们建立协作层的时候,我设置了DDSCL_FPUSETUP标识。这是告诉DirectDraw,Direct3D将会使用到浮点指针单元。你不一定要这样做,但是使用了标识可以提高性能。其次,你应该避免在你的程序中使用到double类型。
当我们设置表面特征(ddscaps)时,我使用了DDSCAPS_3DDEVICE标识。你可以推测一下它是做什么用的,有没有奖金?它告诉Windows你要创建的表面应该支持3D设备渲染。如果没有包含这个标识,那么你在渲染的时候就得到意想不到的结果。
When setting up the Cooperation Level, I included the keyword DDSCL_FPUSETUP. This tells DirectDraw that Direct3D is likely to be using the floating point unit. Its not compulsory, but it can improve performance. You should avoid putting doubles in your code if you include this statement.
When setting the surface description (ddscaps), I included the keyword DDSCAPS_3DDEVICE. No prizes for guessing what this does?it tells Windows that the surface you want should be compatible with 3D device rendering. Not including this can result in nothing happening when you start rendering.
8、设置Direct3D
Setting Up Direct3D
这些工作都由InitDirect3D()来处理。相信许多人都是刚接触此类东西,幸好我已经详细的注解了整个程序初始化过程的,你应该可以很好的理解它。Direct3D 7 实际上包含了一些辅助函数,它使得初始化过程变得容易 - 但是还有许多难懂的方法需要我们去学习。(具体的做法参看 DirectX 7 SDK)
InitDirect3D() handles most of this work. Since this is all new material to most people, I 'll go through the initialization process in detail. Fortunately, its not as complicated as it could be. Direct3D 7 actually includes some helper functions to make this even easier - but we 'll learn more doing it the hard way! (See the DirectX 7 SDK for details of the easy way to do this).
第一步 初始化Direct3D,用QueryInterface()用询问DirectDraw接口是否可用(ReportError是我DXEngine类的一个部分,用来显示错误信息):
The first step in initializing Direct3D is to query DirectDraw for an interface (ReportError is part of my DXEngine class - it displays a message box):
LPDIRECT3D3 Direct3D;
if (FAILED(DirectDraw-> QueryInterface(IID_IDirect3D3,(LPVOID *)&Direct3D))){
ReportError( "Direct3D Query Failed ");
};
第二步 枚举3D设备。这项工作可以用FindDevice命令来完成(Direct3D的接口部分)。 查找支持硬件加速的Direct3D设备的代码:
The second step is to enumerate 3D devices. This is done with the FindDevice command (part of the Direct3D interface). The following code looks for hardware accelerated Direct3D devices:
D3DFINDDEVICESEARCH search;
D3DFINDDEVICERESULT result;
memset(&search,0,sizeof(search));
search.dwSize = sizeof(search);
search.bHardware = 1;
memset(&result,0,sizeof(result));
result.dwSize = sizeof(result);
if (FAILED(Direct3D-> FindDevice(&search,&result))) {
ReportError( "3D Hardware Not Found! ");
exit(10);
};
假设发现一个设备,下一步就是要创建它。这你可以用CreateDevice来完成调用(Direct3D 的接口部分)。下面就是创建硬件加速设备的代码:
Assuming that this found a device, the next stage is to go ahead and create it. This can be as simple as calling CreateDevice (again, part of the Direct3D interface). The following line of code searches for a hardware accelerated device:
LPDIRECT3DDEVICE3 D3DDevice;
Direct3D-> CreateDevice(IID_IDirect3DHALDevice,BBuffer,&D3DDevice,NULL);
在我的程序中,我扩展了一些代码:如果没有发现HAL设备,DXEngine将查找MMX设备,然后使用常规的RGB(软件仿真) 设备。不过,这并不是必要的。在Direct3D 设置的最后一个部分是创建视口(viewport)。视口会指令Direct3D 如何渲染世界。结构的另一部分还将建立 3D 裁剪功能 - 这个仅当你正在使用 Direct3D 变换函数的时候才有用。但在这篇文章中,我们可以忽略它。比较重要的部分如下:
In my code, I actually expand on this somewhat; if a HAL device isn 't found, DXEngine will look for an MMX device, then a regular RGB (software emulated) device. This isn 't strictly necessary, though. The final stage in starting up Direct3D involves the creation of a viewport. Viewports tell Direct3D how to render the world. Part of this structure sets up 3D clipping - and is only really of use if you are using Direct3D 's transformation functions. It 's of no use to this article, so we can ignore it. The important part is this:
LPDIRECT3DVIEWPORT3 Viewport;
D3DVIEWPORT2 Viewdata;
memset(&Viewdata,0,sizeof(Viewdata));
Viewdata.dwSize = sizeof(Viewdata);
Viewdata.dwWidth = ScreenWidth;
Viewdata.dwHeight = ScreenHeight;
// Create the viewport
if (FAILED(Direct3D-> CreateViewport(&Viewport,NULL))) {
ReportError( "Failed to create a viewport ");
};
if (FAILED(D3DDevice-> AddViewport(Viewport))) {
ReportError( "Failed to add a viewport ");
};
if (FAILED(Viewport-> SetViewport2(&Viewdata))) {
ReportError( "Failed to set Viewport data ");
};
D3DDevice-> SetCurrentViewport(Viewport);
这样做,是为建立一个与屏幕大小的视口(viewport)结构(如果你想要渲染一个更小的窗口,你可以改变这些设定值)。接着创建视口,把它加载到设备上,增加视口自身的数据,并且告诉设备使用它。真是太复杂了,但是我们已经完成设置。当你完成Direct3D后,你还应该记得释放你申请的变量:
What this does is it sets up a viewport structure with the screen size (if you wanted to render a smaller window, you can change these values). Then it creates the viewport, adds it to the device, adds the data to the viewport itself, and tells the device to use it. Overly complicated, if you ask me, but it gets the job done. When you are finished with Direct3D, it 's a good idea to release the variables you 've allocated:
if (Viewport) Viewport-> Release();
if (Direct3D) Direct3D-> Release();
假如不这么做,你的内存有多大。你也无法再运行其它的Direct3D程序(直到重新启动电脑)?不错的想法 - 如果你希望自己是一个顽固者,你可以这么做。
If you don 't do this, memory leaks are pretty likely. You may also make it impossible to run other Direct3D programs (until you reboot)?not a good idea - especially if you want to keep any fans you might have!
9、渲染一个Isometric视角的网格线
Rendering a Wireframe Isometric View
DEMO1(参看附带的源代码)绘制了一个滚轴画面,显示isometric类的网格线(Wireframe)- 由一些相对三角形组成的轮廓。这是一个isometric图素引擎交错贴图的好例证,然而它看起来相当的无聊?但是,它却是学习Direct3D的一个很好的开始。与其它演示一样,Demo 1是基于CEngine类的。Cengine继承了DXEngine底层部分的初始化及释放代码。
DEMO1 (see the accompanying source code) draws a scrolling, wireframe isometric display - little more than a bunch of outlined triangles adjacent to one another. This is a good illustration of how isometric tile engines stagger tiles (and of how quads may be broken down into triangles), and is pretty boring?but it 's a great start to learning Direct3D. Demo 1, like all of my other demos, is based around a class CEngine. CEngine inherits all of its low-level DirectX initialisation/destruction code from DXEngine.
GameInit,基础的游戏初始化函数,就这个例程来说是再简单不过了。整个过程包含了2个方法。一个是初始化DirectDraw/Direct3D,另一个是将滚动位置计数器清零。
GameInit, the basic game initialisation, couldn 't be simpler for this example. The entire method includes just 2 calls, 1 to initialise DirectDraw/Direct3D and the other to zero my scrolling position counter:
void CEngine :: GameInit()
{
InitDirectX();
ScrollX = 0;
};
GameMain,每帧都要调用的函数,这非常简单:
GameMain, the function that gets called for each frame, is also pretty simple:
void CEngine :: GameMain() {
ScrollX++;
if (ScrollX > 64) ScrollX = 0;
FillSurface(BBuffer,0,NULL);
Demo1Render(ScrollX, 0);
Flip();
// check of user is trying to exit
if (KEY_DOWN(VK_ESCAPE)) {
PostMessage(MainWindow, WM_DESTROY,0,0);
} // end if
};
滚屏计数值的大小将递增,直到超过一块图素的宽度时再被清零。FillSurface是我创建的应用程序段,用于设定某种纯色来填充一个DirectDraw表面;假如我不是每帧都花些时间来清除后缓冲区(back buffer),那么屏幕很快就会变得一塌糊涂。下面详细描述了Demo1 Render - 这个函数是此例程中真正的Direct3D渲染程序段。最后,后缓冲被翻转,同时程序检查用户是否按下了Esc键退出。
The scroller location is incremented and zeroed again if it exceeds the width of a tile. FillSurface is a utility routine I created that simply sets a DirectDraw surface to a solid color; if I didn 't take the time to clear the back buffer each frame, things quickly become pretty ugly. Demo1Render is described in detail below - it 's the actual Direct3D rendering routine for this example. Finally, the back buffer is flipped, and the program checks to see if the user has pressed ESC to quit.
GameDone 这么简单吗?它在这个程序中什么也不做!( 保证Direct3D/DirectDraw对象被完全释放)。
GameDone is really simple?it does nothing in this example! (The underlying class makes sure that Direct3D/DirectDraw are released properly).
例程中重要部分是渲染代码(我打赌,你认为自己从来没有使用过它)。例程本身附带了详细的注释,不过在这里我们还是一步一步学习它是如何工作的。
The real meat of this example is the rendering code (I bet you thought I 'd never get to it!). The example itself is heavily commented, but here is a step-by-step breakdown of how it works:
首先,我定义了一些变量。用ScreenX及ScreenY里存储渲染时的屏幕坐标。WhereX及Where用来保存世界坐标。(假如你对这些术语存在些疑惑,那么可以参见图素渲染教程?一般来说,世界坐标是相对整块大地图中某一图素的位置而言的,而屏幕坐标以象素位置而言的)。最重要的变量则是以下这个:
First of all, I declare some variables. ScreenX and ScreenY are used to store screen coordinates for rendering. WhereX and WhereY are used to store world coordinates. (If you are confused by these terms, check out one of the tile rendering tutorials?basically, world coordinates work in terms of whole tiles on a larger map, screen coordinates work in terms of pixel locations). The most important variable, however, is the following:
D3DTLVERTEX Vertex[4];
D3DTLVERTEX 结构对使用Direct3D来改进2D性能是中心所在。它是Direct3d“可塑顶点格式”系统的一部分。其它预先规定了的顶点格式包括 D3DVERTEX 及D3DLVERTEX。每个定义都有一个不同数据集合,并且它们告诉 Direct3D 管道如何工作。
The D3DTLVERTEX structure is central to using Direct3D to improve 2D performance. It is part of Direct3D 's "flexible vertex format " system. Other predefined vertex formats include D3DVERTEX and D3DLVERTEX. Each defines a different set of data, and tells the Direct3D pipeline what it needs to do.
3DVERTEX需要使用到变换(transformed)及照明(lit)顶点数据。
D3DLVERTEX已经包含了光照信息数据,但是还需要对其进行变换。
D3DTLVERTEX已经有屏幕坐标系及光照信息的数据。它是在提高2D环境(此文)中使用得较多的数据结构。
D3DVERTEX data needs to be both transformed and lit.
D3DLVERTEX data already has lighting information, but needs to be transformed.
D3DTLVERTEX data already has screen coordinates and lighting information included. As such, it 's of the most use in an Enhanced 2D context.
下一步是一个普通的3D渲染系统。每一帧3D图形的由BenginScene()调用而完成:
The next step is common to most 3D rendering systems. Every frame of 3D graphics has to be preceded by a call to BeginScene():
D3DDevice-> BeginScene();
接着,告诉Direct3D,不使用光照效果。虽然没有使用到光照,但是我依然指定了D3DTLVERTEX结构,Direct3D 有想要使用它自己系统的一个习惯吗?因此需要显式的告诉它不使用光照。虽然这并不需要每一帧都说明,但是为了清楚起见,我还是把它放在了渲染过程中。
Next, I inform Direct3D that I have no intention of using its lighting routines. Even though I 'm specifying D3DTLVERTEX structures, Direct3D has a habit of wanting to use its own system?so this tells it not to. This doesn 't really need to be called every frame, but I kept it in the rendering routine for clarity.
D3DDevice-> SetLightState(D3DLIGHTSTATE_MATERIAL,NULL);
再下一步,仅在这个演示中,我通知Direct3D我只想在网格线(Wireframe)模式下渲染。例子中使用了SetRenderState命令,它是Direct3D的概念之一。D3D会保存一张变量列表,表中的变量可以通过SetRenderState命令改变 - 包括雾化设置,过滤,透视修正等。详情请参考DirectX SDK手册。
Next, and only in this demo, I inform Direct3D that I 'd like to render in wireframe mode. This illustrates the SetRenderState command, one of Direct3D 's most powerful concepts. D3D maintains a list of variables that may be changed with this command - including fog settings, filtering, perspective correction, and more. Check the SDK for more information.
D3DDevice-> SetRenderState(D3DRENDERSTATE_FILLMODE,D3DFILL_WIREFRAME);
由于我们正在使网格线(Wireframe)作为渲染演示,并且我们希望线是白色的。这是很容易做到。每个网格的4个顶点上色彩属性都设置为白色的。Direct3D包含了一个宏定义D3DRGB。它可以接收三个浮点数(大小从0.0f 到1.0f)并将它们转换成D3DCOLOR格式:
Because we are rendering a wireframe demo, we want the lines to be white. This is nice and easy to achieve. For each of the 4 vertices (points at the edges of the square), the color property can be set to white. Direct3D includes a macro, D3DRGB that takes 3 floats (from 0.0f to 1.0f) and converts them into its own D3DCOLOR format:
Vertex[0].color = D3DRGB(1.0f,1.0f,1.0f);
Vertex[1].color = D3DRGB(1.0f,1.0f,1.0f);
Vertex[2].color = D3DRGB(1.0f,1.0f,1.0f);
Vertex[3].color = D3DRGB(1.0f,1.0f,1.0f);
最后的工作就是初始化渲染定位,WhereY及WhereX设置为0。ScreenY设置为-16,这样可以保证即使滚屏偏移量很大也不会留下空隙。
Finishing up the initialisation phase of rendering, WhereY, and WhereX are set to 0. ScreenY is set to -16, ensuring that even if a large scroll offset is in use it will not leave any gaps.
实际的isometric渲染循环与其它大部分文章介绍的没有什么不同。它可以总结成如下的伪代码(伪代码 - 实际代码参见压缩包):
The actual isometric rendering loop is pretty much the same as that described in numerous other articles. It may be summarized as (in pseudocode - see the example for actual code):
While (ScreenY < ScreenHeight) {
If (WhereY MOD 2) = 1 then ScreenX = -64 else ScreenX = -96
While (ScreenX < ScreenWidth) {
Setup Vertex Information
Render The Triangles
ScreenX = ScreenX + 64 (tile width)
WhereX = WhereX + 1
}
WhereY = WhereY + 1
WhereX = 0
ScreenY = ScreenY + 16 (half the tile height)
}
为什么我会在这里使用伪代码?因为它相当标准,而且可以保持文章简短些,我希望用更长的篇幅去描述渲染的代码部分。就此,伪代码是一个不错的选择。我用斜体字注明了与Direct3D相关部分,在后面将会详述它们。
Why did I leave that in pseudocode? Because its pretty standard stuff, and to keep this article short I 'd rather focus on the actual rendering code. Besides, pseudocode is good practice. ;-) I 've italicized the parts of this loop that concern Direct3D and will be expanded upon.
为了渲染一个方块建立起顶点信息并不是很难的事。尽管可以更简单,但Direct3D不支持四方行(Quads)作为基本类型(OpenGL支持)。幸运的是将一个菱形切分为两个三角形并不是很麻烦的事情。图中白色的数字代表了每4个顶点的位置:
Setting up the vertex information for rendering a square isn 't too hard?although it could be easier. Direct3D doesn 't support Quads as a primitive type (OpenGL does). Fortunately, its not all that hard to break a diamond into two triangles. The yellow numbers represent the location of each of the 4 vertices:
在这里存在一个显然问题?为什么2是底部数而不最右边的顶点数呢?答案是由于使用了一点TRIANGLESTRIP优化。假如能将大量三角形分批处理并一起传送到管道(pipeline),那么Direct3D的性能将发挥得更出色。不幸的是一起传送的三角形必须使用同一纹理。由于我们很可能希望邻接的图块看起来不一样,因此我们只能把相邻的两个三角形作为一组。多个三角形必须以顺时针顺序传送给Direct3D-否则它们们就无法被绘制!由此,我们从左面的三角形开始:
An obvious question at this point is?why is 2 the bottom and not the rightmost vertex? The answer is a little optimization known as the TRIANGLESTRIP. Direct3D performs much better if you can batch triangles and send them through the pipeline together. Unfortunately, the triangles you send together have to be using the same texture. Since we will probably want adjacent tiles to look different, I 've just grouped two triangles together. Triangles have to be sent to Direct3D in clockwise order - or they don 't draw at all! Because of this, I start with the left most triangle:
然后再加上一个顶点,形成第二个三角形:
and then add one more vertex to get a second one:
对顶点的排列处理使得我可以仅仅加上一个1顶点,而不需要使用三角形列表来详细列出两个三角形的顶点。不错!
The vertex arrangement has allowed me to just add 1 vertex rather than using a triangle list and listing both triangles in detail. Neat!
(mays注:三角带一个三角带是一些相互连接的三角形。因为这些三角形相互连接,所以程序不必再重复说明每个三角形中的三个顶点。)
SDK包含了一些图片举例进一步说明三角带(triangle strips)。不过有一个问题,在渲染一个三角带时,你只能使用同一个纹理;有时候你可能希望将一个大纹理贴到多个图素上,但是一般来说由于存在以上的限制,这是无法实现的。
The SDK includes some nice pictures illustrating how much farther triangle strips may be taken. Its a problem that you can 't change texture during the rendering of a texture strip; sometimes, you might want to glue together a big texture for multiple tiles, but in general their utility is greatly diminished because of this.
不管怎样,以下的代码示例如何被填充D3DTLVERTEX结构:
Anyway, in terms of this example, the following code fills up the D3DTLVERTEX structures
Vertex[0].sx = ScreenX + OffsetX;
Vertex[0].sy = ScreenY+16 + OffsetY;
Vertex[1].sx = ScreenX+32 + OffsetX;
Vertex[1].sy = ScreenY + OffsetY;
Vertex[2].sx = ScreenX+32 + OffsetX;
Vertex[2].sy = ScreenY+32 + OffsetY;
Vertex[3].sx = ScreenX+64 + OffsetX;
Vertex[3].sy = ScreenY+16 + OffsetY;
以上的分配方式有很大的优化余地-不过我希望我的代码保持清晰。OffsetX和OffsetY是平滑滚屏时的关键值-它们是一个象素大小的偏移,也就是说每次移动屏幕时只移动一个象素(所谓象素级移动)。每个顶点的sx和sy定义了其屏幕位置。Vertex[0]是菱形的左顶点,Vertex[1]是上顶点,Vertex[2]是下顶点,Vertex[3]在右边。这里没有什么革命性的东西。
There is plenty of room to optimize these allocations - but I wanted the code to remain clear. OffsetX and OffsetY are the key to smooth pixel scrolling - they are simply a pixel offset by which the entire image is shunted. sx and sy in each vertex define screen positions. Vertex[0] is the left of the diamond, Vertex[1] if the top of the diamond, Vertex[2] is the bottom, and Vertex[3] is the right side. Nothing too revolutionary here!
最后,在内层循环中,quad被传送以渲染:
Finally for the inner loop, the quad is sent off to be rendered:
D3DDevice-> DrawPrimitive(D3DPT_TRIANGLESTRIP,D3DFVF_TLVERTEX,Vertex,4,D3DDP_WAIT);
DrawPrimitive是目前Direct3D渲染的基础。它所有要做的如下:
DrawPrimitive is the basis of modern Direct3D rendering. All this does is:
1) 告诉DrawPrimitive顶点包含的是三角带(triangle strip)(而不是需要使用6个顶点的三角形列表(triangle list)) 。
Tell DrawPrimitive that Vertex contains a triangle strip (as opposed to a triangle list - which would need 6 vertices)
2) 说明顶点已经变换并且包含了光照值(D3DFVF_TLVERTEX)。
Explains that the vertices are already transformed and lit (D3DFVF_TLVERTEX)
3) 指明D3D在那里可以找到顶点信息。
Indicates where D3D may find the vertex information.
4) 通知D3D有4个顶点 [0-3] 。
Notes that there are 4 vertices [0-3]
5) 告知DrawPrimitive在渲染之前等待前一步工作。你也可以把这个参数设为0,它仍然可以正常工作
Tells DrawPrimitive to wait if it has to before rendering. You can use 0 for this parameter and it should still work.
渲染过程的最后部分,你必须调用EndScene:D3DDevice-> EndScene();
Lastly for the render routine, you have to call EndScene: D3DDevice-> EndScene();
总结,这个例程向你展示了如何建立Direct3D,渲染网格线,以及如何执行简单的isometric滚屏。在引擎中我使用了大小为64X32的图素,不过你可以很简单的改为其它的任何尺寸。对于一个仅47k的可执行文件来说,不错了!
To recap, this example has shown you how to fire up Direct3D, render wireframe quads, and perform basic isometric scrolling. The tile engine assumes 64x32 tiles, but could be easily adapted for almost any other size. Not bad for a 47 k executable!
10、加入纹理因素
Adding Textures to the Equation
DEMO2 (以及接下来的其它演示)扩展了上述的程序。与DEMO1相比,代码并没有很大的改动,但是已经能够实现对渲染的图素贴上纹理。
DEMO2 (and all subsequent demos) extends the code described above. Not an awful lot changes - but the code gains the ability to texture the tiles it renders, as opposed to the demonstrative (but painfully retro) wireframe graphics of DEMO1.
DEMO2中最大部分的改动就是加入了一个新的类,CTexture。CTexture封装了一段代码,用于将位图加载到内存中,然后把控制权交给Direct3D的纹理管理接口,最后在你完成纹理的使用后销毁它。代码令人吃惊的简单,所有的处理问题都考虑了吗?实际上,LoadTexture函数的代码可以完全使用DirectX 7 的同名工具函数代替。但我编写自己版本的函数是为了在加载位图的时候可以控制位深(bit depths)- 这个问题好象与我们要讨论的问题无关。因此,我想你既可以拷贝我的代码也可以使用SDK里的例程。
The biggest addition to the mix in DEMO2 is a new class, CTexture. CTexture encapsulates code required to load a bitmap into memory, hand control of it over to Direct3D 's texture management interface, and finally destroy the texture when you are done with it. The code itself is surprisingly simple, all things considered?although the bitmap loading routine can be simplified considerably if you so wish. In fact, the code for LoadTexture can be entirely replaced with the DirectX 7 utility function of the same name! The only reason I 've included my own version is that I like to be able to tweak bit depths during loading - something that isn 't too relevant for this article. So, I suggest that you either copy my code or use the SDK example!
同这里讨论有关系的代码部分已经修改包含在新版本的CEngine类中:
The changes to the code most relevant to this article are contained within the new version of CEngine:
GameInit()现在包含了初始化SampleTexture的代码(一个CTexture类的实例)。
GameMain()除了调用Demo1Render改成了调用Demo2Render外,其它没有改变。
GameDone()包含了对SampleTexture的删除。
GameInit() now includes code to instantiate SampleTexture (a CTexture object).
GameMain() is unchanged, except that Demo2Render is called instead of Demo1Render.
GameDone() now deletes SampleTexture.
重要的代码组织在Demo2Render中。变量声明后,第一部分是处理纹理对齐(texture alignment)。Direct3D及OpenGL都使用浮点数来表示纹理的位置(相对于顶点位置)。值为0表示坐标轴的起点,值为1.0 表示纹理的结束。因此,就这点来说纹理的尺寸大小都是没有关系的。由此程序员和美工可以很好的把精力放在对模型贴图的控制上而无需担心象素位置的精度影响。它同时使得将纹理应用于Quad的过程变得简单容易了。我决定顶点0 是左上角的顶点,顶点1 是右上角的,顶点2 是左下角的,而顶点3是右下角的。一般习惯在讨论材质时使用u,v而不是x,y(估计是为了不至于混淆)。如果使用D3DTLVERTEX,那么这些坐标将保存在tu和tv中。因此,DEMO2的材质对齐的建立过程如下:
The meat of the texturing code is found in Demo2Render. Once variables have been declared, the first new section deals with texture alignment. Direct3D (and OpenGL for that matter) both use floats to indicate texture location relative to a vertex. A value of 0 indicates the beginning of an axis, a value of 1.0 indicates the end of a texture. Thus, the size of a texture is irrelevant at this point. This lets a programmer/artist have pretty fine control over model skinning without needing to worry about precise pixel coordinates. It also makes it nice and easy to apply any texture you want to a quad. I decided that vertex 0 would be the top left, vertex 1 would be the top right, vertex 2 would be bottom left and vertex 3 would be bottom right. Its traditional when talking about textures to use u and v instead of x and y (presumably for the sake of clarity). Within a D3DTLVERTEX, these coordinates are stored as tu and tv. Thus, texture alignment for DEMO2 is setup as follows:
Vertex[0].tu = 0.0f;
Vertex[0].tv = 0.0f;
Vertex[1].tu = 1.0f;
Vertex[1].tv = 0.0f;
Vertex[2].tu = 0.0f;
Vertex[2].tv = 1.0f;
Vertex[3].tu = 1.0f;
Vertex[3].tv = 1.0f;
无论你的坐标是如何选择的,纹理都会变形以符合多边形的形状。这也使得旋转、缩放和平移纹理变得快捷而容易!
Its worth noting that whatever coordinates you choose, the texture will be warped to fit your polygon. This gives a very quick and easy way of rotating, zooming and panning textures!
使用Direct3D贴图,下一步需要考虑的是D3DTLVERTEX的RHW组件。RHW在2D的环境下一般是不使用的。“often 1 divided by the distance from the origin to the object along the z-axis”(引自 SDK)。由于不需要使用z轴,我们就不会遇到透视校正,因此我们只要把RHW的值设成1.0f 就可以了:(mays注:RHW是指齐次W的倒数,reciprocal homogenous W)
The next consideration when texturing in Direct3D is the rhw component of D3DTLVERTEX. rhw generally isn 't used in a 2D context, since its (to quote the SDK) "often 1 divided by the distance from the origin to the object along the z-axis. " We don 't have a z axis, so perspective correction isn 't going to happen... so we 'll just set this to 1.0f:
Vertex[0].rhw = 1.0f;
Vertex[1].rhw = 1.0f;
Vertex[2].rhw = 1.0f;
Vertex[3].rhw = 1.0f;
最后,由于我们只使用一个纹理,因此我通知Direct3D在接下来场景中所有的多边形上都应用这个纹理:
Finally, since I 'm only using one texture, I tell Direct3D to use it to texture all subsequent polygons for the scene:
D3DDevice-> SetTexture(0,SampleTexture-> Texture);
SetTexture是一个有趣的命令。参数中的0代表纹理stage(texture平台)。有时候可能在使用一个渲染命令的时候设置多个纹理。如果硬件支持多重纹理(multitexturing),那么在处理器性能损耗不大的情况下,您可以得到相当漂亮的效果。当您要使用凹凸贴图, 光线映射,覆盖等效果时就需要使用多重纹理。如果使用了透明处理,甚至还能在一个渲染调用中实现多层图素渲染!就此而言,SetTexture可能是你最有用的函数了。不过也需要注意几点:它运行有点慢。你只有在必须的时候才可以调用它。每个图块调用一次就可以了,但是假如你能找到方法确保相同的图块无需重复调用,那么你能提高每秒帧数。
SetTexture is an interesting command. The 0 represents the texture stage. It is possible (and a good idea, sometimes!) to set several textures to apply to the same render command. With hardware multitexturing, this can be inexpensive in terms of processor performance, and you can create some really neat effects. This is where you would specify bump maps, lightmaps, overlays, etc. With transparency, its even possible to perform multilayer tile rendering this way with only one render call! SetTexture may be your best friend in this respect, but be warned: it is a little slow. You really only want to call it when you have to. Once per tile will work, but if you can find a way to make sure that runs of identical tiles don 't require a call to it, then you 'll get a frame rate boost.
渲染过程的内层循环保持不变!在我测试DEMO用的机器上,使用了纹理的与仅使用网格线的渲染程序在速度上没有多大区别,不过这可能和使用的硬件有很大关系。
The renderer 's inner loop remains unchanged! On the machines I tested the demo on, there wasn 't much speed difference between texturing the polygons and just rendering them in wireframe, although this may vary depending upon hardware..
在我看来,这种渲染中最重要的特征很可能就是纹理处理引擎了。纹理是矩形的位图,不需要预先扭曲(不用处理透明关键色)。这使得美工的工作负担大大减轻了。
For me, probably the best feature of this type of rendering is the texturing engine. Textures are rectangular bitmaps, and don 't need to be pre-distorted (with wasted space for color keying). This makes life a lot easier for your artist!
重要提示:引擎的尺寸必须为2的整次幂(mays注:在数学中,一个量自身相乘的次数),才能在Direct3D中被正确的渲染。那么,64x64, 128x32 和 256x16 等都可以吗?但是 15x15 就不行了。不过这通常不是个问题。另外一个问题是,很多3D卡不能使用大纹理。超过 256x256 的纹理很可能在相当一部分显卡上(比如到现在为止所有3DFX出品的显卡)都无法使用。大纹理也非常耗费材质内存,所以最好还是保持纹理的小尺寸-尤其对一个图素引擎而言。
Important Note: Texture sizes must be powers of 2 to render properly in Direct3D. Thus, 64x64, 128x32 and 256x16 (etc) are all fine?but 15x15 isn 't. This generally isn 't a problem, though. Many 3D cards, however, choke on larger textures. Anything above 256x256 probably won 't work on a large portion of cards out there (such as everything 3DFX have released when I wrote this article). Larger textures also use up a lot of texture memory, so its probably a good idea to keep textures small - particularly for a tiled engine.
11、在引擎中加入光照
Adding Lighting to the Engine
DEMO3在渲染的时候加入了随机的彩色光(colored lights)。在每一帧,彩色光被加到每一块图素的每一个顶点上。得到的杂乱效果很容易让我们想起Disco舞厅。除此以外,这个例程很好的表现了大多数支持Direct3D的显卡在应用Gouraud明暗处理模式时能达到的速度。我记得曾经很费劲的使用通用的2D来实现这个效果,仅仅使用DirectDraw - 得到的速度是差不多为5 fps。而Direct3D使得实现这种灯光效果的过程变得如此的简单,以至于我非常后悔当初不先尝试使用Direct3D来实现它。
DEMO3 adds random colored lights to the renderer. A random color light is applied to each vertex of every tile, on every frame. The result is decidedly trippy, and has a nasty habit of making me think of disco. Despite this, it is a good example of the speed at which Direct3D can apply Gouraud shading on most cards. I remember working hard to do this in regular 2D, with just DirectDraw - and arriving with framerates around 5 fps. Direct3D makes it so ridiculously easy to use this form of lighting that I 've been kicking myself ever since for not trying it earlier!
一种更高级的灯光处理是使用光照图(lightmaps)。他们主要上是一张灰度纹理,使用SetTexture(1,texture)作为第二纹理被应用于对象,同时通知渲染过程第二纹理以alpha混合方式进行。Direct3D运行起来有着不可思议的速度。此外,光照映射(lightmapping)这个主题大得足够专门写一篇文章了 - 所以在这里我只是点一下题 (基本上比我见过的所有DirectDraw的实现方案要快而且效率很高)。
A more advanced form of lighting uses lightmaps. These are basically a grayscale texture, applied as the second texture with SetTexture(1,texture) and a call to ensure that the renderer knows that it should alpha-blend the second texture. Direct3D makes doing this incredibly fast. However, lightmapping is a sufficiently large topic to warrant its own article - so I 'm only going to say that it can be done (and quickly, substantially more quickly than any DirectDraw solution I 've seen thus far). Vertex lighting should be more than enough to get you started on the Direct3D road.
DEMO3的GameInit及GameDone 同DEMO2的一样,没有什么变化。GameMain调用的是Demo3Render而不是Demo2Render,但是这已是非渲染代码部分的所有的改动了。
DEMO3 's GameInit and GameDone are unchanged from DEMO2. GameMain calls Demo3Render instead of Demo2Render, but that 's really all that changes in the non-rendering code.
Demo3Render本身与Demo2Render也并没有什么实质的区别。唯一的不同在于,在渲染的内层循环中,在DrawPrimitive被调用之前,我将每个顶点的颜色值设置成一个随机的浮点数。D3DRGB将处于0到1之间的rgb浮点值转化为Direct3D在D3DCOLOR中实际使用的颜色格式。全部的彩色光(colored lights)的代码如下(看起来有点不雅,我仅仅是想举例说明颜色属性的威力罢了)。
Demo3Render itself isn 't substantially different from Demo2Render, either. The only difference is that inside the inner loop, before DrawPrimitive is called, I set the color value of each vertex to a random float. D3DRGB meshes red, green and blue floating point values between 0 and 1 into whatever color format Direct3D happens to be using within D3DCOLOR. The entirety of the random lighting code is this (it 's a little inelegant, I just wanted to demonstrate the color property 's power:
Vertex[0].color = D3DRGB(rand()/5000,rand()/5000,rand()/5000);
Vertex[1].color = D3DRGB(rand()/5000,rand()/5000,rand()/5000);
Vertex[2].color = D3DRGB(rand()/5000,rand()/5000,rand()/5000);
Vertex[3].color = D3DRGB(rand()/5000,rand()/5000,rand()/5000);
12、联合起来:带有高度映射及光照效果的图素引擎
Pulling This Together: A Height-mapped, Lit Tile Engine
DEMO4被设计用于显示前面所做的东西的实际用处 - 它把前面大部分的工作合到一个引擎中。要使它成为一个真正意义上的游戏还需要做很多的工作,但是这是一个很好的开始。这个DEMO中我加入了一个map结构,储存了每个顶点的高度及光照度的级别。这在一个通常意义的图素引擎中是极难实现的 - 制作垂直元素通常需要很厉害的美工技巧才能实现,而处理斜坡效果也是美工极头疼的事情。
DEMO4 is designed to demonstrate the advantages to having made it this far - it pulls most of the work from earlier sections into one, reasonably coherent engine. A lot would need to be done to make this into a working game, but it 's a pretty good start. What this demo does is add a map structure, storing a height and lighting level for each vertex. This is something that would be extremely hard to do in a regular tile engine - the vertical element usually had to be added with clever artist tricks, and ramps had been known to give artists serious headaches.
第一个改变是在DEMO4中定义了tMapNode类。整个类定义如下
The first major change incorporated in DEMO4 is the tMapNode class. This is simply a storage class (I 'd have used a struct, except that in C++ structs are stored as classes anyway so there really isn 't a lot of point!). The entire class definition is as follows:
class tMapNode
{
public:
int VertexHeight[4];
D3DCOLOR VertexColor[4];
int Texture;
};
基本上,这个类存储了VertexHeight(相对于普通象素位置的偏移量),一个颜色值,以及一个用来指定图素所用纹理的索引值。在一个真正的游戏中,这个类应该有更多的东西 - 比如一个指向图素集(所有位于此图素之上的图素)的指针,让你觉得视图中有无限多的图层。
Basically, this class stores VertexHeight (an offset from the normal pixel location), a color for each vertex, and an index number designed to identify that tile 's texture. In a real game, there would be plenty more - possibly including a pointer to any tiles that live above this one, giving you the option of infinite layering (something I 'm fond of in this age of super-fast rendering!).
在CEngine中,我扩展了滚屏的代码,使得它可以处理包含有高度图素的情况。ScrollX现在表示的是将被渲染的左上方的图素。ScrollXOffset则表示的是用于调整渲染位置的象素偏移量。另外我还加入了Direction标识,使得滚屏可以先从左到右,然后从右到左。我没有加入垂直滚屏,因为它的实现原理基本是相同的。GameInit的代码段初始化了这些变量。GameMain也调用了一个新的函数,MakeDemoMap。MakeDemoMap是一个相对简单的函数,用来初始化图素的随机高度。它包含了一些相邻图素要素,这是直接从《TANSTAAFL 's Isometric Tutorial》中提取出来的。它还分配了基于高度的光照度 - 高的图素比较亮。对于一个简单的地形渲染来说,这已经是不错的了;在实时游戏中,你也许需要更高级的东西。光照系统是另一主题了,因此我仅仅告诉你得到这些效果的基本点 - 并且让你体验一下。没准什么时候我会另写一篇关于光照的教程!
Within CEngine, I 've expanded the scrolling code to cope with having a map defined, rather than just static tiles. ScrollX now indicates the upper-left tile to be rendered. ScrollXOffset indicates the pixel offset by which to adjust the rendering pass. Direction has been added so that the scroller will bounce left, then right, then left, etc. I didn 't add vertical scrolling, even though it would have been easy to do so - exactly the same principles apply. GameInit has gained code to initialize these variables. GameMain now also calls a new method, MakeDemoMap. MakeDemoMap is a relatively simple routine to initialize the map with random heights. It includes some tile adjacency stuff taken straight from TANSTAAFL 's isometric tutorial (q.v.). It also assigns a lighting level based on the height of a vertex - higher equals brighter. This isn 't a bad lighting system for a simple landscape render; in a real game, you would probably want something a little more sophisticated. Lighting systems is another topic that could easily fill another tutorial, so for now I 'll give you the basics of how to apply the results - and let you experiment. Who knows, I may be talked into writing a lighting tutorial someday!
GameMain 包含了一些基础滚屏的逻辑,如下:
GameMain includes some basic scrolling logic, now:
if (Direction == 0) {
ScrollXOffset--;
if (ScrollXOffset < 1) {
ScrollX++;
ScrollXOffset = 63;
if (ScrollX > 20) Direction = 1;
};
}
else {
ScrollXOffset++;
if (ScrollXOffset > 63) {
ScrollX--;
ScrollXOffset = 0;
if (ScrollX < 1) Direction = 0;
};
};
这些东西太基础了。如果Direction 为 0, 那么从ScrollXOffset减去1。如果ScrollXOffset小于1,那么移动图素。如果到了地图的最边缘,那么就反向滚屏。当Direction等于1时,同样的过程,不过方向相反。
This is pretty basic. If Direction is 0, it subtracts 1 from ScrollXOffset. If ScrollXOffset is less than 1, it moves tile. If it has run out of map, it reverses direction. When Direction is equal to 1, it does the same thing but in the other direction!
GameMain同样调用了Demo4Render。这个函数现在多了几个参数。WorldX和WorldY目前也作为参数传给它了,他们指定了要渲染的图素视图的左上角的坐标位置。
GameMain also includes a call to Demo4Render, which has gained some extra parameters. WorldX and WorldY are now passed to it, and these specify the coordinates (in tilespace) of the top-left tile to be rendered.
GameDone还没有涉及到。其实并不需要这个函数!
GameDone hasn 't been touched. It didn 't really need to be!
Demo4Render和前面几个阶段的基本上还是一样的,不过为了实现map结构和高度图的渲染加入了一些东西。一个看起来最不重要(其实非常重要)的变化在于WhereX和WhereY被重置为世界坐标,而不是0。如果不这么做,那么每一帧渲染的就是地图中的同一部分了。几乎Demo4中所有的改变都在渲染过程的内层循环中:
Demo4Render is largely the same as previous incarnations, but some changes have been made to incorporate both the map structure and heightmapped rendering. The most trivial (but important) change is that WhereX and WhereY are now reset to World coordinates rather than zero. If I didn 't do this, I 'd be rendering the same portion of map every frame - which is boring! Almost all of the changes in Demo4 come within the renderer 's inner loop:
顶点sx和sy的赋值部分改变了。每个顶点的高度值现在需要减去它的sy坐标值;我选择减法,这样正数的高度值看起来就像小山,而负数的高度值看起来就像山谷了。这是符合逻辑的。现在的顶点位置赋值过程如下:
The Vertex sx and sy assignment section has changed. The height for each vertex is now subtracted from its sy coordinate; I chose subtraction so that positive height numbers would give the appearance of hills, while negative would give valleys?it just seemed more logical to me, that way around. The vertex position assignments now look like this:
Vertex[0].sx = ScreenX + OffsetX;
Vertex[0].sy = ScreenY+16 + OffsetY - Map[WhereX][WhereY].VertexHeight[0];
Vertex[1].sx = ScreenX+32 + OffsetX;
Vertex[1].sy = ScreenY + OffsetY - Map[WhereX][WhereY].VertexHeight[1];
Vertex[2].sx = ScreenX+32 + OffsetX;
Vertex[2].sy = ScreenY+32 + OffsetY - Map[WhereX][WhereY].VertexHeight[2];
Vertex[3].sx = ScreenX+64 + OffsetX;
Vertex[3].sy = ScreenY+16 + OffsetY - Map[WhereX][WhereY].VertexHeight[3];
这些并不是很难呀!最幸的是,这些小变化导致了整个纹理系统的巨大变化。而渲染/位块传送的代码却丝毫没有改变!难道这不是很令人吃惊的吗?我想到这里我应该去要一杯味道甜美的冷饮。记得以前我试着用很复杂的方法去实现(2D),现在不快乐吗?
That wasn 't too difficult! The neat thing is, this tiny change causes all of the textures to warp as appropriate with NO change to the rendering/blitting code! Isn 't that wonderful? I like this so much that I should probably go and take a nice, long, cold drink. I remember trying to do this the hard way (2D!)?not pleasant. Enough to give me gray hair, anyway!
颜色部分的代码也改变了,现在使用事先存储好的颜色值而不是刚才的随机颜色。具体做法如下:
The color code has also changed, so that it uses the stored colors rather than making you wonder if I 'm on drugs. ;-) This is another pretty simple change:
Vertex[0].color = Map[WhereX][WhereY].VertexColor[0];
Vertex[1].color = Map[WhereX][WhereY].VertexColor[1];
Vertex[2].color = Map[WhereX][WhereY].VertexColor[2];
Vertex[3].color = Map[WhereX][WhereY].VertexColor[3];
不管你信不信,这便是生成这样一个漂亮的阴影,带有高度映射并实现光照效果的引擎的所有代码了!您可以进一步使用更多优化的技巧,也还有很多改进的空间,这仅仅是一个初级的引擎。
Believe it or not, that 's all that it took to produce a neatly shaded, height-mapped tiling engine. There are plenty of optimizations that could be applied, lots of room for additional innovation, but that 's the basics. With luck, lots of vertex-shaded tile games will appear, now! (If you write something cool, and think this article helped, I 'd love to get email from you!)
13、将来的工作(或我没有写到的)
Where To Go From Here (Or What I didn 't cover!)
有些方面我在这个教程中没有提及到,因为我不想把这篇文章变成一本教科书。其中的一部分如下,但是如果你要得到进一步的信息,那么就去看关于它们的专门教程。如下:
A number of topics could have been in this tutorial, but were omitted so that it didn 't turn into a book. The basics of each is here, but a more advanced treatment of any of these would require its own tutorial. Some of these things are:
精灵。我没有生成一个通常的Quad-Drawing系统,虽然我在文中提到的已经足以让你轻松的编写出一个。但我还是要提示一下:将精灵作为纹理加载(使用透明色,如果你没有使用任何纹理过滤)-这和2D的处理道理是一样的,不过要记得至少要使用一次SetRenderState(D3DRENDERSTATE_COLORKEYENABLE,TRUE)。
复杂光照。顶点光照虽然酷,但是如果使用动态光照或者更先进的光照生成系统将会更酷。光照图(Lightmaps)也很酷,虽然他们很快就会吃掉大量的资源。
层。我有意的避免文章涉及到它;网上有大量的文章谈到层(图像的遮挡)问题 - 况且3D中的层和2D中的没有任何区别。
Sprites. I didn 't produce a general quad-drawing system, although I gave out enough information that it should be really easy to do. Hint: load the sprite as a texture (using color keying if you aren 't using any form of texture filter - it works the same was as in 2D, just remember to use SetRenderState(D3DRENDERSTATE_COLORKEYENABLE, TRUE) at least once.
Complex Lighting. Vertex lighting is cool, and can be made a lot cooler if you apply dynamic lighting routines, more sophisticated light generation systems in the first place, etc. Lightmaps can also be cool, although they can eat up a lot of resources pretty quickly.
Layering. I deliberately didn 't go into this; there are a lot of articles out there covering layering in tile based engines - and layering in Direct3D is no different from layering in 2D.
当然,我愿意接受任何提议。作为利用Direct3D 提高图素渲染的初级文章,希望它对您有帮助。你可以就任何问题,评价,询问或是抱怨给我发电子邮件。
bracket@unforgettable.com。 I 'm open to any other suggestions, of course. This tutorial was intended as a primer in using Direct3D to enhance one 's tile rendering experience, and I hope it has helped. You can contact me at bracket@unforgettable.com with any questions, comments, queries or complaints!
14、源代码
Source Code
你在想从那里开始学习吗?如果是我,那么一定从这里开始!点击下载源代码。
Did you start here? I know I would have! Here is the accompanying source code.
15、关于作者
About The Author
作者Herbert Wolverson,大家通常叫他做 Bracket,是一个24岁的电脑顾问。白天编写数据库(类似世俗的任务),晚上钻研游戏制作技术。他也得抽时间去陪蒂娜,他的女朋友,以及他的2只宠物鼠、一条宠物蛇、一把吉他和参加每周都要召开的角色扮演会议。Bracket真的需要更多的时间去睡眠,还应该少喝点咖啡!
Herbert Wolverson, commonly known as Bracket, is a 24 year old Computer Consultant. Writing databases (and similar mundane tasks) by day, he works on game technology by night. He somehow also fits in time with Tina, his girlfriend, 2 pet rats, 1 pet snake, a guitar and weekly roleplaying sessions. Bracket needs to sleep more, and drink less caffeine!
参考文献
TANSTAAFL, "Isometric 'n ' Hexagonal Maps Part I "
TANSTAAFL, "Isometric 'n ' Hexagonal Maps Part II "
Jim Adams, "Isometric Views: Explanation and Interpretation "
Tobias Lensing, "Enhanced 2D "
Tricks of the Windows Game Programming Gurus
下载本文附带的源程序(146KB)
图片及源代码请登陆下面网站:
合作翻译:中国游戏开发者.CN – mays
http://mays.6to23.com
游戏制作天地 – wonyee(rocks_lee)
http://wonyee.top263.net/index.html
图片及源代码请登陆下面网站:
合作翻译:
中国游戏开发者.CN – mays
http://mays.6to23.com
游戏制作天地 – wonyee(rocks_lee)
http://wonyee.top263.net/index.html
译文版本:1.0 Beta
原文信息:http://www.gamedev.net
《Using Direct3D For 2D Tile Rendering》
Author:bracket@unforgettable.com
版权声明:中文版权属“中国游戏开发者.CN”、“游戏制作天地”共同所有。因为在我把文章翻译到后半部的时候,wonyee已经把这篇文章翻译好了。所以我就参考了他的部分翻译内容。
--------------------------------------------------------------------------------
目录
1 引言
2 传统的方法
3 如何获得3D架构的帮助
4 一些假设
5 第一步:设置DirectX
6 DXEngine的结构
7 设置DirectDraw
8 设置Direct3D
9 渲染一个Isometric视角的网格线
10 加入纹理因素
11 在引擎中加入光照
12 联合起来:带有高度映射及光照效果的图素引擎
13 将来的工作
--------------------------------------------------------------------------------
1、引言
Introduction
基于Isometric及Tile引擎的游戏已经出现很多年了。在此其间这类游戏的制作技术已经愈来愈成熟,但图素渲染方式还是没有多大的变化。当人们在争论3D技术是否会导致2D渲染技术(伪3D)被废弃的时候,我相信这个替代过程一定会发生,但可能需要一个好的理由 - 由时间去证明吧。有件事情是,在以前要制作一个动态3D引擎,对实力较小的游戏开发组来说是比较困难的。而且在3D图形硬件成熟之前,兼容性也是一个比较严重的问题。另外,直到所有3D加速卡都可以处理海量多边形的今天,让它与一个基于图素但组织良好的游戏相比,3D图形并不被看好。不管怎么说:3D加速卡有高速处理多边形、纹理填充的能力,它可以使不带加速渲染的程序的性能得到提高。如:Alpha 混合、纹理映射、高得洛着色及其它一些特效在2D程序中也可以实现,但3D加速卡可以为它们提供更多加速潜力。这篇文章讲述了如何使用 Direct3D 来提高 2D 图形性能,并且提出了每个人都希望掌握的基于Tile引擎的光照。关于“菱形及方形图素”的问题,在论坛上已经有很多年的争论了,每方都有各自的道理。这篇文章将集中讨论菱形图素的使用,因为大部读者都比较偏爱它(可给人立体视角)。当然,将此文的渲染技术转换到方型图素引擎上,应该是相当容易的。Jim Adams的《Isometric 引擎介绍》可给你对此类菱形图素渲染引擎有个总体的认识。
Isometric and tile-based games have been around for a very long time. Rendering techniques for this type of game have grown considerably in sophistication since then, but the style itself remains popular. While some have argued (vocally!) that the arrival of 3D graphics will render 2D (and "psuedo-3D ") rendering techniques obsolete, there is good reason to believe that if this is the case - it will not be true for some time. For one thing, writing a dynamic 3D engine is difficult - hard enough to be prohibitive for small development groups. Until 3D graphics hardware matures, compatibility remains an issue. Likewise, until the average 3D card can handle a significantly higher polygon throughput than today, 3D graphics will not always look significantly better than a well-planned tile-based game. 3D-renderers do, however, offer 2D graphics a significant boost in performance. After all, all a "3D card " does is spit out textured polygons at high speed - something that can seriously slow down a non-accelerated rendering routine. Alpha blending, texture mapping, gouraud shading and other features have all been implemented in 2D routines - but a 3D card offers the potential to make them fast enough for regular use. This article describes the use of Direct3D to enhance 2D graphics - and suggests that nicely lit tile-based engines are within everybody 's grasp. Tile-based and Isometric Views A significant volume of material has been written on both tile-based and isometric rendering. The "diamond tiles versus square tiles " argument has raged across many forums for years, with both sides having advantages. This article will focus on diamond-shaped tiles, largely because of author preference (they look nicer!) It should be relatively easy to switch to a square tile rendering engine using these techniques. Jim Adams ' Guide to Isometric Engines gives a great overview of this kind of isometric rendering:
2、传统的方法
Traditional Methods
惯例上,处理菱形图素是一件令人比较头痛的事情。图素首先要设计成菱形,其次要定义透明背景关键色。接着要建立菱形图素的无缝拼接。因此,程序员就得面对相当大的程序开销:在位块传送过程中,必须明度背景关键色不被绘制到屏幕上。虽然现在有很多的位块传送优化方案及其它技巧,可事实总是无法改变的,你的游戏必须负担额外的程序开销。
Traditionally, diamond tiles have been something of a pain. Tiles needed to be designed as diamond-shaped bitmaps, usually with a color-keyed background. It could be extremely different to make tile boundaries appear seamless. Additionally, the programmer was faced with a fair degree of overhead: the color keyed area of each bitmap had to be ignored during the blitting process. Many innovative ways of reducing this overhead exist (and many artists have proven innovative in tile design), but the fact remained: isometric rendering was tricky.
3、如何获得3D架构的帮助
How 3D Architectures Can Help
3D加速卡可以用很方法完美地表现基于菱形图素的isometric引擎。作为3D引擎中的一个主要功能,三角形纹理够实现将位图应用到各种形状奇怪的区域上。而菱形可以被简单的分解成两个三角形(as do any other quad),因此对3D加速卡来说,在处理矩型及菱形图素的位块传送时,不会有速度上的差别。美工的生活也为此轻松许多:在3D 引擎中纹理一般用矩形表示,并且通过映射将它们贴到相应的多边形上。这样,美工就需要设计方块形状的纹理(which tend to tile much more nicely),剩下的菱形图素无缝拼接工作则由3D加速卡去完成。另外,其它一些原本很复杂的问题,比如高度映射等也由此迎刃而解了。
(CGD.CN注:架构是一种规格说明。它决定系统如何构成,确定功能模块以及允许模块间进行通信和协同的协议与接口)。
3D cards are in many ways ideally suited to rendering diamond-tile based isometric views. Textured triangles, the staple of 3D engines, require the application of bitmaps to odd-shaped regions. Diamonds break down simply into two triangles (as do any other quad), so there is no difference in raw blitting overhead between square and diamond tiles. The artist 's life is made easier, too: textures in 3D engines are typically rectangular, and are mapped to fit polygons. Thus, artists are able to design square textures (which tend to tile much more nicely), and let the 3D hardware worry about making it look good when stretched over a diamond-shaped tile. Additionally, height-mapping and other concerns become significantly easier to implement because a textured quad, unlike a static bitmap, may be readily warped.
Note: Most 3D accelerators are really 2D accelerators. With the exception of some new ones that help with transformation & lighting, all these cards can do is pump out textured triangles - onto a 2D surface. Thus, there is absolutely nothing unnatural about using a 3D card to speed up 2D rendering - in fact, its just doing what the card always does, without some heavy matrix math!
4、一些假设
Some Assumptions
这篇文章使用到Isometric/Tile引擎的应用知识。假如你对此不慎了解,你应该去看看参考书目,以获得更多的基础信息。一些关于isometric的基本内容将会在实现引擎时给出,但文章不会把主要时间花在基本概念的介绍上。对不起,这篇文章同样也不会教给你DirectDraw(not Direct3D)及Windows程序设计的方法。因为在因特网有很多此类的教程。Andr LaMothe编写的《Tricks of Windows Game Programming Gurus 》就是初学这些主题的比较好的书(当然还有更多此类的书)。
This article assumes a working knowledge of isometric/tile-based graphics. If you are unsure of how these work, you are advised to check out the bibliography for more information. While plenty of information is given on implementing an isometric engine, this article doesn 't take the time to introduce the basic concepts. Sorry! This article also assumes a basic knowledge of DirectDraw (not Direct3D) and Windows programming. There are many tutorials on these on the Internet. Andr?LaMothe 's book Tricks of the Windows Game Programming Gurus makes an excellent starting point for learning these subjects (and many others).
5、第一步:设置DirectX
First Steps: Setting Up DirectX
在学习如何利用“Enhanced 2D”时,需要创建一个最简的Win32应用程序。这你可以看看文章附带例程的WinMain.cpp,它便是一个极简单的Win32应用程序。对于这个程序,须注意以下的代码项目:
The first stage in learning to use "Enhanced 2D " requires the creation of a minimal Win32 application. The sources that come with this article include WinMain.cpp (for each example), which is about as minimal as you can get if you still want your program to be called "Win32 "! The only significant items to note in this code are the following:
含一个全局变量CEngine *Engine。CEngine是我实现图素引擎的基类。
在WinMain主消息循环之前,注意调用Engine-> GameInit()。
在主循环内重复地调用Engine-> GameMain() 当程序结束时,调用Engine-> GameDone()。
The inclusion of CEngine *Engine as a global variable.CEngine is my basic tile engine class.
Just before the main message loop in WinMain, note the call to Engine-> GameInit().
The main loop itself repeatedly calls Engine-> GameMain(). When closing down, Engine-> GameDone() is called.
6、DXEngine的结构
The DXEngine Framework
我是一个喜爱C++的顽固分子。因为爱C++,所以我在设计DXEngine结构及例程的时候,全采用了面向对象的编程方法。DXEngine本身就是游戏底层的基类,也是一个抽象类(虚类)- 这意味你不可以实例化这个类的对象。InitDirectX()已经包含隐藏了初始化DirectDraw及Direct3D的必要代码。假如你要在你的程序中使用这些函数,那你只需继承这个类,并且改写一下GameInit (),GameMain ()和GameDone ()函数即可。那不是很好吗?
I 'm a diehard C++ fan. I admit it. Classes, Object Oriented Programming and similar concepts appeal to me in ways that probably can 't be described to minors?or at least intellectually, anyway. ;-) Because of my love for C++, I 've designed the framework used for all the demonstrations in an object oriented manner. DXEngine is the base class for the game itself. Its an abstract class - meaning you can 't instantiate it. It includes all of the code necessary to initialize DirectDraw and Direct3D, and hides it away in a single call to InitDirectX(). All a programmer wanting this functionality in his/her program needs to do is inherit this class and write GameInit(), GameMain() and GameDone(). Is that not nifty?
7、设置DirectDraw
Setting up DirectDraw
DXEngine做相当很多的DirectDraw 处理工作。这个程序的代码大部分来自Lamothe 编写的引擎中(although the BOB engine itself was trashed very quickly - talk about slow)。InitDirectDraw ()负责完成所有DirectDraw的设置工作(没有什么令人兴奋的)。它所做的与你经常做的步骤没有什么显著地区别 - 只不过在建立窗口及全屏模式上花了些时间。总体说,它定义了一个DirectDraw 接口,设置协作层,创建屏幕模式,创建主表面及缓冲区,然后建立一个裁剪器。没有革命性的东西。
DXEngine does quite a lot of work to bring DirectDraw to life. Much of this code originally came from LaMothe 's work (although the BOB engine itself was trashed very quickly - talk about slow). The function that does all the work for setting up DirectDraw is InitDirectDraw() (no surprises there!). It doesn 't differ significantly from the steps that you would need in general - although it does take the time to work in both windowed and fullscreen modes. Basically, it gets an interface to DirectDraw, sets the cooperation level, creates the screen mode, creates a primary and secondary surface and then sets up a clipper. Nothing too revolutionary.
可在Direct3D部分的内容中,有几点必须要指明:
A few points in it need to be highlighted, however, in the context of Direct3D:
我们建立协作层的时候,我设置了DDSCL_FPUSETUP标识。这是告诉DirectDraw,Direct3D将会使用到浮点指针单元。你不一定要这样做,但是使用了标识可以提高性能。其次,你应该避免在你的程序中使用到double类型。
当我们设置表面特征(ddscaps)时,我使用了DDSCAPS_3DDEVICE标识。你可以推测一下它是做什么用的,有没有奖金?它告诉Windows你要创建的表面应该支持3D设备渲染。如果没有包含这个标识,那么你在渲染的时候就得到意想不到的结果。
When setting up the Cooperation Level, I included the keyword DDSCL_FPUSETUP. This tells DirectDraw that Direct3D is likely to be using the floating point unit. Its not compulsory, but it can improve performance. You should avoid putting doubles in your code if you include this statement.
When setting the surface description (ddscaps), I included the keyword DDSCAPS_3DDEVICE. No prizes for guessing what this does?it tells Windows that the surface you want should be compatible with 3D device rendering. Not including this can result in nothing happening when you start rendering.
8、设置Direct3D
Setting Up Direct3D
这些工作都由InitDirect3D()来处理。相信许多人都是刚接触此类东西,幸好我已经详细的注解了整个程序初始化过程的,你应该可以很好的理解它。Direct3D 7 实际上包含了一些辅助函数,它使得初始化过程变得容易 - 但是还有许多难懂的方法需要我们去学习。(具体的做法参看 DirectX 7 SDK)
InitDirect3D() handles most of this work. Since this is all new material to most people, I 'll go through the initialization process in detail. Fortunately, its not as complicated as it could be. Direct3D 7 actually includes some helper functions to make this even easier - but we 'll learn more doing it the hard way! (See the DirectX 7 SDK for details of the easy way to do this).
第一步 初始化Direct3D,用QueryInterface()用询问DirectDraw接口是否可用(ReportError是我DXEngine类的一个部分,用来显示错误信息):
The first step in initializing Direct3D is to query DirectDraw for an interface (ReportError is part of my DXEngine class - it displays a message box):
LPDIRECT3D3 Direct3D;
if (FAILED(DirectDraw-> QueryInterface(IID_IDirect3D3,(LPVOID *)&Direct3D))){
ReportError( "Direct3D Query Failed ");
};
第二步 枚举3D设备。这项工作可以用FindDevice命令来完成(Direct3D的接口部分)。 查找支持硬件加速的Direct3D设备的代码:
The second step is to enumerate 3D devices. This is done with the FindDevice command (part of the Direct3D interface). The following code looks for hardware accelerated Direct3D devices:
D3DFINDDEVICESEARCH search;
D3DFINDDEVICERESULT result;
memset(&search,0,sizeof(search));
search.dwSize = sizeof(search);
search.bHardware = 1;
memset(&result,0,sizeof(result));
result.dwSize = sizeof(result);
if (FAILED(Direct3D-> FindDevice(&search,&result))) {
ReportError( "3D Hardware Not Found! ");
exit(10);
};
假设发现一个设备,下一步就是要创建它。这你可以用CreateDevice来完成调用(Direct3D 的接口部分)。下面就是创建硬件加速设备的代码:
Assuming that this found a device, the next stage is to go ahead and create it. This can be as simple as calling CreateDevice (again, part of the Direct3D interface). The following line of code searches for a hardware accelerated device:
LPDIRECT3DDEVICE3 D3DDevice;
Direct3D-> CreateDevice(IID_IDirect3DHALDevice,BBuffer,&D3DDevice,NULL);
在我的程序中,我扩展了一些代码:如果没有发现HAL设备,DXEngine将查找MMX设备,然后使用常规的RGB(软件仿真) 设备。不过,这并不是必要的。在Direct3D 设置的最后一个部分是创建视口(viewport)。视口会指令Direct3D 如何渲染世界。结构的另一部分还将建立 3D 裁剪功能 - 这个仅当你正在使用 Direct3D 变换函数的时候才有用。但在这篇文章中,我们可以忽略它。比较重要的部分如下:
In my code, I actually expand on this somewhat; if a HAL device isn 't found, DXEngine will look for an MMX device, then a regular RGB (software emulated) device. This isn 't strictly necessary, though. The final stage in starting up Direct3D involves the creation of a viewport. Viewports tell Direct3D how to render the world. Part of this structure sets up 3D clipping - and is only really of use if you are using Direct3D 's transformation functions. It 's of no use to this article, so we can ignore it. The important part is this:
LPDIRECT3DVIEWPORT3 Viewport;
D3DVIEWPORT2 Viewdata;
memset(&Viewdata,0,sizeof(Viewdata));
Viewdata.dwSize = sizeof(Viewdata);
Viewdata.dwWidth = ScreenWidth;
Viewdata.dwHeight = ScreenHeight;
// Create the viewport
if (FAILED(Direct3D-> CreateViewport(&Viewport,NULL))) {
ReportError( "Failed to create a viewport ");
};
if (FAILED(D3DDevice-> AddViewport(Viewport))) {
ReportError( "Failed to add a viewport ");
};
if (FAILED(Viewport-> SetViewport2(&Viewdata))) {
ReportError( "Failed to set Viewport data ");
};
D3DDevice-> SetCurrentViewport(Viewport);
这样做,是为建立一个与屏幕大小的视口(viewport)结构(如果你想要渲染一个更小的窗口,你可以改变这些设定值)。接着创建视口,把它加载到设备上,增加视口自身的数据,并且告诉设备使用它。真是太复杂了,但是我们已经完成设置。当你完成Direct3D后,你还应该记得释放你申请的变量:
What this does is it sets up a viewport structure with the screen size (if you wanted to render a smaller window, you can change these values). Then it creates the viewport, adds it to the device, adds the data to the viewport itself, and tells the device to use it. Overly complicated, if you ask me, but it gets the job done. When you are finished with Direct3D, it 's a good idea to release the variables you 've allocated:
if (Viewport) Viewport-> Release();
if (Direct3D) Direct3D-> Release();
假如不这么做,你的内存有多大。你也无法再运行其它的Direct3D程序(直到重新启动电脑)?不错的想法 - 如果你希望自己是一个顽固者,你可以这么做。
If you don 't do this, memory leaks are pretty likely. You may also make it impossible to run other Direct3D programs (until you reboot)?not a good idea - especially if you want to keep any fans you might have!
9、渲染一个Isometric视角的网格线
Rendering a Wireframe Isometric View
DEMO1(参看附带的源代码)绘制了一个滚轴画面,显示isometric类的网格线(Wireframe)- 由一些相对三角形组成的轮廓。这是一个isometric图素引擎交错贴图的好例证,然而它看起来相当的无聊?但是,它却是学习Direct3D的一个很好的开始。与其它演示一样,Demo 1是基于CEngine类的。Cengine继承了DXEngine底层部分的初始化及释放代码。
DEMO1 (see the accompanying source code) draws a scrolling, wireframe isometric display - little more than a bunch of outlined triangles adjacent to one another. This is a good illustration of how isometric tile engines stagger tiles (and of how quads may be broken down into triangles), and is pretty boring?but it 's a great start to learning Direct3D. Demo 1, like all of my other demos, is based around a class CEngine. CEngine inherits all of its low-level DirectX initialisation/destruction code from DXEngine.
GameInit,基础的游戏初始化函数,就这个例程来说是再简单不过了。整个过程包含了2个方法。一个是初始化DirectDraw/Direct3D,另一个是将滚动位置计数器清零。
GameInit, the basic game initialisation, couldn 't be simpler for this example. The entire method includes just 2 calls, 1 to initialise DirectDraw/Direct3D and the other to zero my scrolling position counter:
void CEngine :: GameInit()
{
InitDirectX();
ScrollX = 0;
};
GameMain,每帧都要调用的函数,这非常简单:
GameMain, the function that gets called for each frame, is also pretty simple:
void CEngine :: GameMain() {
ScrollX++;
if (ScrollX > 64) ScrollX = 0;
FillSurface(BBuffer,0,NULL);
Demo1Render(ScrollX, 0);
Flip();
// check of user is trying to exit
if (KEY_DOWN(VK_ESCAPE)) {
PostMessage(MainWindow, WM_DESTROY,0,0);
} // end if
};
滚屏计数值的大小将递增,直到超过一块图素的宽度时再被清零。FillSurface是我创建的应用程序段,用于设定某种纯色来填充一个DirectDraw表面;假如我不是每帧都花些时间来清除后缓冲区(back buffer),那么屏幕很快就会变得一塌糊涂。下面详细描述了Demo1 Render - 这个函数是此例程中真正的Direct3D渲染程序段。最后,后缓冲被翻转,同时程序检查用户是否按下了Esc键退出。
The scroller location is incremented and zeroed again if it exceeds the width of a tile. FillSurface is a utility routine I created that simply sets a DirectDraw surface to a solid color; if I didn 't take the time to clear the back buffer each frame, things quickly become pretty ugly. Demo1Render is described in detail below - it 's the actual Direct3D rendering routine for this example. Finally, the back buffer is flipped, and the program checks to see if the user has pressed ESC to quit.
GameDone 这么简单吗?它在这个程序中什么也不做!( 保证Direct3D/DirectDraw对象被完全释放)。
GameDone is really simple?it does nothing in this example! (The underlying class makes sure that Direct3D/DirectDraw are released properly).
例程中重要部分是渲染代码(我打赌,你认为自己从来没有使用过它)。例程本身附带了详细的注释,不过在这里我们还是一步一步学习它是如何工作的。
The real meat of this example is the rendering code (I bet you thought I 'd never get to it!). The example itself is heavily commented, but here is a step-by-step breakdown of how it works:
首先,我定义了一些变量。用ScreenX及ScreenY里存储渲染时的屏幕坐标。WhereX及Where用来保存世界坐标。(假如你对这些术语存在些疑惑,那么可以参见图素渲染教程?一般来说,世界坐标是相对整块大地图中某一图素的位置而言的,而屏幕坐标以象素位置而言的)。最重要的变量则是以下这个:
First of all, I declare some variables. ScreenX and ScreenY are used to store screen coordinates for rendering. WhereX and WhereY are used to store world coordinates. (If you are confused by these terms, check out one of the tile rendering tutorials?basically, world coordinates work in terms of whole tiles on a larger map, screen coordinates work in terms of pixel locations). The most important variable, however, is the following:
D3DTLVERTEX Vertex[4];
D3DTLVERTEX 结构对使用Direct3D来改进2D性能是中心所在。它是Direct3d“可塑顶点格式”系统的一部分。其它预先规定了的顶点格式包括 D3DVERTEX 及D3DLVERTEX。每个定义都有一个不同数据集合,并且它们告诉 Direct3D 管道如何工作。
The D3DTLVERTEX structure is central to using Direct3D to improve 2D performance. It is part of Direct3D 's "flexible vertex format " system. Other predefined vertex formats include D3DVERTEX and D3DLVERTEX. Each defines a different set of data, and tells the Direct3D pipeline what it needs to do.
3DVERTEX需要使用到变换(transformed)及照明(lit)顶点数据。
D3DLVERTEX已经包含了光照信息数据,但是还需要对其进行变换。
D3DTLVERTEX已经有屏幕坐标系及光照信息的数据。它是在提高2D环境(此文)中使用得较多的数据结构。
D3DVERTEX data needs to be both transformed and lit.
D3DLVERTEX data already has lighting information, but needs to be transformed.
D3DTLVERTEX data already has screen coordinates and lighting information included. As such, it 's of the most use in an Enhanced 2D context.
下一步是一个普通的3D渲染系统。每一帧3D图形的由BenginScene()调用而完成:
The next step is common to most 3D rendering systems. Every frame of 3D graphics has to be preceded by a call to BeginScene():
D3DDevice-> BeginScene();
接着,告诉Direct3D,不使用光照效果。虽然没有使用到光照,但是我依然指定了D3DTLVERTEX结构,Direct3D 有想要使用它自己系统的一个习惯吗?因此需要显式的告诉它不使用光照。虽然这并不需要每一帧都说明,但是为了清楚起见,我还是把它放在了渲染过程中。
Next, I inform Direct3D that I have no intention of using its lighting routines. Even though I 'm specifying D3DTLVERTEX structures, Direct3D has a habit of wanting to use its own system?so this tells it not to. This doesn 't really need to be called every frame, but I kept it in the rendering routine for clarity.
D3DDevice-> SetLightState(D3DLIGHTSTATE_MATERIAL,NULL);
再下一步,仅在这个演示中,我通知Direct3D我只想在网格线(Wireframe)模式下渲染。例子中使用了SetRenderState命令,它是Direct3D的概念之一。D3D会保存一张变量列表,表中的变量可以通过SetRenderState命令改变 - 包括雾化设置,过滤,透视修正等。详情请参考DirectX SDK手册。
Next, and only in this demo, I inform Direct3D that I 'd like to render in wireframe mode. This illustrates the SetRenderState command, one of Direct3D 's most powerful concepts. D3D maintains a list of variables that may be changed with this command - including fog settings, filtering, perspective correction, and more. Check the SDK for more information.
D3DDevice-> SetRenderState(D3DRENDERSTATE_FILLMODE,D3DFILL_WIREFRAME);
由于我们正在使网格线(Wireframe)作为渲染演示,并且我们希望线是白色的。这是很容易做到。每个网格的4个顶点上色彩属性都设置为白色的。Direct3D包含了一个宏定义D3DRGB。它可以接收三个浮点数(大小从0.0f 到1.0f)并将它们转换成D3DCOLOR格式:
Because we are rendering a wireframe demo, we want the lines to be white. This is nice and easy to achieve. For each of the 4 vertices (points at the edges of the square), the color property can be set to white. Direct3D includes a macro, D3DRGB that takes 3 floats (from 0.0f to 1.0f) and converts them into its own D3DCOLOR format:
Vertex[0].color = D3DRGB(1.0f,1.0f,1.0f);
Vertex[1].color = D3DRGB(1.0f,1.0f,1.0f);
Vertex[2].color = D3DRGB(1.0f,1.0f,1.0f);
Vertex[3].color = D3DRGB(1.0f,1.0f,1.0f);
最后的工作就是初始化渲染定位,WhereY及WhereX设置为0。ScreenY设置为-16,这样可以保证即使滚屏偏移量很大也不会留下空隙。
Finishing up the initialisation phase of rendering, WhereY, and WhereX are set to 0. ScreenY is set to -16, ensuring that even if a large scroll offset is in use it will not leave any gaps.
实际的isometric渲染循环与其它大部分文章介绍的没有什么不同。它可以总结成如下的伪代码(伪代码 - 实际代码参见压缩包):
The actual isometric rendering loop is pretty much the same as that described in numerous other articles. It may be summarized as (in pseudocode - see the example for actual code):
While (ScreenY < ScreenHeight) {
If (WhereY MOD 2) = 1 then ScreenX = -64 else ScreenX = -96
While (ScreenX < ScreenWidth) {
Setup Vertex Information
Render The Triangles
ScreenX = ScreenX + 64 (tile width)
WhereX = WhereX + 1
}
WhereY = WhereY + 1
WhereX = 0
ScreenY = ScreenY + 16 (half the tile height)
}
为什么我会在这里使用伪代码?因为它相当标准,而且可以保持文章简短些,我希望用更长的篇幅去描述渲染的代码部分。就此,伪代码是一个不错的选择。我用斜体字注明了与Direct3D相关部分,在后面将会详述它们。
Why did I leave that in pseudocode? Because its pretty standard stuff, and to keep this article short I 'd rather focus on the actual rendering code. Besides, pseudocode is good practice. ;-) I 've italicized the parts of this loop that concern Direct3D and will be expanded upon.
为了渲染一个方块建立起顶点信息并不是很难的事。尽管可以更简单,但Direct3D不支持四方行(Quads)作为基本类型(OpenGL支持)。幸运的是将一个菱形切分为两个三角形并不是很麻烦的事情。图中白色的数字代表了每4个顶点的位置:
Setting up the vertex information for rendering a square isn 't too hard?although it could be easier. Direct3D doesn 't support Quads as a primitive type (OpenGL does). Fortunately, its not all that hard to break a diamond into two triangles. The yellow numbers represent the location of each of the 4 vertices:
在这里存在一个显然问题?为什么2是底部数而不最右边的顶点数呢?答案是由于使用了一点TRIANGLESTRIP优化。假如能将大量三角形分批处理并一起传送到管道(pipeline),那么Direct3D的性能将发挥得更出色。不幸的是一起传送的三角形必须使用同一纹理。由于我们很可能希望邻接的图块看起来不一样,因此我们只能把相邻的两个三角形作为一组。多个三角形必须以顺时针顺序传送给Direct3D-否则它们们就无法被绘制!由此,我们从左面的三角形开始:
An obvious question at this point is?why is 2 the bottom and not the rightmost vertex? The answer is a little optimization known as the TRIANGLESTRIP. Direct3D performs much better if you can batch triangles and send them through the pipeline together. Unfortunately, the triangles you send together have to be using the same texture. Since we will probably want adjacent tiles to look different, I 've just grouped two triangles together. Triangles have to be sent to Direct3D in clockwise order - or they don 't draw at all! Because of this, I start with the left most triangle:
然后再加上一个顶点,形成第二个三角形:
and then add one more vertex to get a second one:
对顶点的排列处理使得我可以仅仅加上一个1顶点,而不需要使用三角形列表来详细列出两个三角形的顶点。不错!
The vertex arrangement has allowed me to just add 1 vertex rather than using a triangle list and listing both triangles in detail. Neat!
(mays注:三角带一个三角带是一些相互连接的三角形。因为这些三角形相互连接,所以程序不必再重复说明每个三角形中的三个顶点。)
SDK包含了一些图片举例进一步说明三角带(triangle strips)。不过有一个问题,在渲染一个三角带时,你只能使用同一个纹理;有时候你可能希望将一个大纹理贴到多个图素上,但是一般来说由于存在以上的限制,这是无法实现的。
The SDK includes some nice pictures illustrating how much farther triangle strips may be taken. Its a problem that you can 't change texture during the rendering of a texture strip; sometimes, you might want to glue together a big texture for multiple tiles, but in general their utility is greatly diminished because of this.
不管怎样,以下的代码示例如何被填充D3DTLVERTEX结构:
Anyway, in terms of this example, the following code fills up the D3DTLVERTEX structures
Vertex[0].sx = ScreenX + OffsetX;
Vertex[0].sy = ScreenY+16 + OffsetY;
Vertex[1].sx = ScreenX+32 + OffsetX;
Vertex[1].sy = ScreenY + OffsetY;
Vertex[2].sx = ScreenX+32 + OffsetX;
Vertex[2].sy = ScreenY+32 + OffsetY;
Vertex[3].sx = ScreenX+64 + OffsetX;
Vertex[3].sy = ScreenY+16 + OffsetY;
以上的分配方式有很大的优化余地-不过我希望我的代码保持清晰。OffsetX和OffsetY是平滑滚屏时的关键值-它们是一个象素大小的偏移,也就是说每次移动屏幕时只移动一个象素(所谓象素级移动)。每个顶点的sx和sy定义了其屏幕位置。Vertex[0]是菱形的左顶点,Vertex[1]是上顶点,Vertex[2]是下顶点,Vertex[3]在右边。这里没有什么革命性的东西。
There is plenty of room to optimize these allocations - but I wanted the code to remain clear. OffsetX and OffsetY are the key to smooth pixel scrolling - they are simply a pixel offset by which the entire image is shunted. sx and sy in each vertex define screen positions. Vertex[0] is the left of the diamond, Vertex[1] if the top of the diamond, Vertex[2] is the bottom, and Vertex[3] is the right side. Nothing too revolutionary here!
最后,在内层循环中,quad被传送以渲染:
Finally for the inner loop, the quad is sent off to be rendered:
D3DDevice-> DrawPrimitive(D3DPT_TRIANGLESTRIP,D3DFVF_TLVERTEX,Vertex,4,D3DDP_WAIT);
DrawPrimitive是目前Direct3D渲染的基础。它所有要做的如下:
DrawPrimitive is the basis of modern Direct3D rendering. All this does is:
1) 告诉DrawPrimitive顶点包含的是三角带(triangle strip)(而不是需要使用6个顶点的三角形列表(triangle list)) 。
Tell DrawPrimitive that Vertex contains a triangle strip (as opposed to a triangle list - which would need 6 vertices)
2) 说明顶点已经变换并且包含了光照值(D3DFVF_TLVERTEX)。
Explains that the vertices are already transformed and lit (D3DFVF_TLVERTEX)
3) 指明D3D在那里可以找到顶点信息。
Indicates where D3D may find the vertex information.
4) 通知D3D有4个顶点 [0-3] 。
Notes that there are 4 vertices [0-3]
5) 告知DrawPrimitive在渲染之前等待前一步工作。你也可以把这个参数设为0,它仍然可以正常工作
Tells DrawPrimitive to wait if it has to before rendering. You can use 0 for this parameter and it should still work.
渲染过程的最后部分,你必须调用EndScene:D3DDevice-> EndScene();
Lastly for the render routine, you have to call EndScene: D3DDevice-> EndScene();
总结,这个例程向你展示了如何建立Direct3D,渲染网格线,以及如何执行简单的isometric滚屏。在引擎中我使用了大小为64X32的图素,不过你可以很简单的改为其它的任何尺寸。对于一个仅47k的可执行文件来说,不错了!
To recap, this example has shown you how to fire up Direct3D, render wireframe quads, and perform basic isometric scrolling. The tile engine assumes 64x32 tiles, but could be easily adapted for almost any other size. Not bad for a 47 k executable!
10、加入纹理因素
Adding Textures to the Equation
DEMO2 (以及接下来的其它演示)扩展了上述的程序。与DEMO1相比,代码并没有很大的改动,但是已经能够实现对渲染的图素贴上纹理。
DEMO2 (and all subsequent demos) extends the code described above. Not an awful lot changes - but the code gains the ability to texture the tiles it renders, as opposed to the demonstrative (but painfully retro) wireframe graphics of DEMO1.
DEMO2中最大部分的改动就是加入了一个新的类,CTexture。CTexture封装了一段代码,用于将位图加载到内存中,然后把控制权交给Direct3D的纹理管理接口,最后在你完成纹理的使用后销毁它。代码令人吃惊的简单,所有的处理问题都考虑了吗?实际上,LoadTexture函数的代码可以完全使用DirectX 7 的同名工具函数代替。但我编写自己版本的函数是为了在加载位图的时候可以控制位深(bit depths)- 这个问题好象与我们要讨论的问题无关。因此,我想你既可以拷贝我的代码也可以使用SDK里的例程。
The biggest addition to the mix in DEMO2 is a new class, CTexture. CTexture encapsulates code required to load a bitmap into memory, hand control of it over to Direct3D 's texture management interface, and finally destroy the texture when you are done with it. The code itself is surprisingly simple, all things considered?although the bitmap loading routine can be simplified considerably if you so wish. In fact, the code for LoadTexture can be entirely replaced with the DirectX 7 utility function of the same name! The only reason I 've included my own version is that I like to be able to tweak bit depths during loading - something that isn 't too relevant for this article. So, I suggest that you either copy my code or use the SDK example!
同这里讨论有关系的代码部分已经修改包含在新版本的CEngine类中:
The changes to the code most relevant to this article are contained within the new version of CEngine:
GameInit()现在包含了初始化SampleTexture的代码(一个CTexture类的实例)。
GameMain()除了调用Demo1Render改成了调用Demo2Render外,其它没有改变。
GameDone()包含了对SampleTexture的删除。
GameInit() now includes code to instantiate SampleTexture (a CTexture object).
GameMain() is unchanged, except that Demo2Render is called instead of Demo1Render.
GameDone() now deletes SampleTexture.
重要的代码组织在Demo2Render中。变量声明后,第一部分是处理纹理对齐(texture alignment)。Direct3D及OpenGL都使用浮点数来表示纹理的位置(相对于顶点位置)。值为0表示坐标轴的起点,值为1.0 表示纹理的结束。因此,就这点来说纹理的尺寸大小都是没有关系的。由此程序员和美工可以很好的把精力放在对模型贴图的控制上而无需担心象素位置的精度影响。它同时使得将纹理应用于Quad的过程变得简单容易了。我决定顶点0 是左上角的顶点,顶点1 是右上角的,顶点2 是左下角的,而顶点3是右下角的。一般习惯在讨论材质时使用u,v而不是x,y(估计是为了不至于混淆)。如果使用D3DTLVERTEX,那么这些坐标将保存在tu和tv中。因此,DEMO2的材质对齐的建立过程如下:
The meat of the texturing code is found in Demo2Render. Once variables have been declared, the first new section deals with texture alignment. Direct3D (and OpenGL for that matter) both use floats to indicate texture location relative to a vertex. A value of 0 indicates the beginning of an axis, a value of 1.0 indicates the end of a texture. Thus, the size of a texture is irrelevant at this point. This lets a programmer/artist have pretty fine control over model skinning without needing to worry about precise pixel coordinates. It also makes it nice and easy to apply any texture you want to a quad. I decided that vertex 0 would be the top left, vertex 1 would be the top right, vertex 2 would be bottom left and vertex 3 would be bottom right. Its traditional when talking about textures to use u and v instead of x and y (presumably for the sake of clarity). Within a D3DTLVERTEX, these coordinates are stored as tu and tv. Thus, texture alignment for DEMO2 is setup as follows:
Vertex[0].tu = 0.0f;
Vertex[0].tv = 0.0f;
Vertex[1].tu = 1.0f;
Vertex[1].tv = 0.0f;
Vertex[2].tu = 0.0f;
Vertex[2].tv = 1.0f;
Vertex[3].tu = 1.0f;
Vertex[3].tv = 1.0f;
无论你的坐标是如何选择的,纹理都会变形以符合多边形的形状。这也使得旋转、缩放和平移纹理变得快捷而容易!
Its worth noting that whatever coordinates you choose, the texture will be warped to fit your polygon. This gives a very quick and easy way of rotating, zooming and panning textures!
使用Direct3D贴图,下一步需要考虑的是D3DTLVERTEX的RHW组件。RHW在2D的环境下一般是不使用的。“often 1 divided by the distance from the origin to the object along the z-axis”(引自 SDK)。由于不需要使用z轴,我们就不会遇到透视校正,因此我们只要把RHW的值设成1.0f 就可以了:(mays注:RHW是指齐次W的倒数,reciprocal homogenous W)
The next consideration when texturing in Direct3D is the rhw component of D3DTLVERTEX. rhw generally isn 't used in a 2D context, since its (to quote the SDK) "often 1 divided by the distance from the origin to the object along the z-axis. " We don 't have a z axis, so perspective correction isn 't going to happen... so we 'll just set this to 1.0f:
Vertex[0].rhw = 1.0f;
Vertex[1].rhw = 1.0f;
Vertex[2].rhw = 1.0f;
Vertex[3].rhw = 1.0f;
最后,由于我们只使用一个纹理,因此我通知Direct3D在接下来场景中所有的多边形上都应用这个纹理:
Finally, since I 'm only using one texture, I tell Direct3D to use it to texture all subsequent polygons for the scene:
D3DDevice-> SetTexture(0,SampleTexture-> Texture);
SetTexture是一个有趣的命令。参数中的0代表纹理stage(texture平台)。有时候可能在使用一个渲染命令的时候设置多个纹理。如果硬件支持多重纹理(multitexturing),那么在处理器性能损耗不大的情况下,您可以得到相当漂亮的效果。当您要使用凹凸贴图, 光线映射,覆盖等效果时就需要使用多重纹理。如果使用了透明处理,甚至还能在一个渲染调用中实现多层图素渲染!就此而言,SetTexture可能是你最有用的函数了。不过也需要注意几点:它运行有点慢。你只有在必须的时候才可以调用它。每个图块调用一次就可以了,但是假如你能找到方法确保相同的图块无需重复调用,那么你能提高每秒帧数。
SetTexture is an interesting command. The 0 represents the texture stage. It is possible (and a good idea, sometimes!) to set several textures to apply to the same render command. With hardware multitexturing, this can be inexpensive in terms of processor performance, and you can create some really neat effects. This is where you would specify bump maps, lightmaps, overlays, etc. With transparency, its even possible to perform multilayer tile rendering this way with only one render call! SetTexture may be your best friend in this respect, but be warned: it is a little slow. You really only want to call it when you have to. Once per tile will work, but if you can find a way to make sure that runs of identical tiles don 't require a call to it, then you 'll get a frame rate boost.
渲染过程的内层循环保持不变!在我测试DEMO用的机器上,使用了纹理的与仅使用网格线的渲染程序在速度上没有多大区别,不过这可能和使用的硬件有很大关系。
The renderer 's inner loop remains unchanged! On the machines I tested the demo on, there wasn 't much speed difference between texturing the polygons and just rendering them in wireframe, although this may vary depending upon hardware..
在我看来,这种渲染中最重要的特征很可能就是纹理处理引擎了。纹理是矩形的位图,不需要预先扭曲(不用处理透明关键色)。这使得美工的工作负担大大减轻了。
For me, probably the best feature of this type of rendering is the texturing engine. Textures are rectangular bitmaps, and don 't need to be pre-distorted (with wasted space for color keying). This makes life a lot easier for your artist!
重要提示:引擎的尺寸必须为2的整次幂(mays注:在数学中,一个量自身相乘的次数),才能在Direct3D中被正确的渲染。那么,64x64, 128x32 和 256x16 等都可以吗?但是 15x15 就不行了。不过这通常不是个问题。另外一个问题是,很多3D卡不能使用大纹理。超过 256x256 的纹理很可能在相当一部分显卡上(比如到现在为止所有3DFX出品的显卡)都无法使用。大纹理也非常耗费材质内存,所以最好还是保持纹理的小尺寸-尤其对一个图素引擎而言。
Important Note: Texture sizes must be powers of 2 to render properly in Direct3D. Thus, 64x64, 128x32 and 256x16 (etc) are all fine?but 15x15 isn 't. This generally isn 't a problem, though. Many 3D cards, however, choke on larger textures. Anything above 256x256 probably won 't work on a large portion of cards out there (such as everything 3DFX have released when I wrote this article). Larger textures also use up a lot of texture memory, so its probably a good idea to keep textures small - particularly for a tiled engine.
11、在引擎中加入光照
Adding Lighting to the Engine
DEMO3在渲染的时候加入了随机的彩色光(colored lights)。在每一帧,彩色光被加到每一块图素的每一个顶点上。得到的杂乱效果很容易让我们想起Disco舞厅。除此以外,这个例程很好的表现了大多数支持Direct3D的显卡在应用Gouraud明暗处理模式时能达到的速度。我记得曾经很费劲的使用通用的2D来实现这个效果,仅仅使用DirectDraw - 得到的速度是差不多为5 fps。而Direct3D使得实现这种灯光效果的过程变得如此的简单,以至于我非常后悔当初不先尝试使用Direct3D来实现它。
DEMO3 adds random colored lights to the renderer. A random color light is applied to each vertex of every tile, on every frame. The result is decidedly trippy, and has a nasty habit of making me think of disco. Despite this, it is a good example of the speed at which Direct3D can apply Gouraud shading on most cards. I remember working hard to do this in regular 2D, with just DirectDraw - and arriving with framerates around 5 fps. Direct3D makes it so ridiculously easy to use this form of lighting that I 've been kicking myself ever since for not trying it earlier!
一种更高级的灯光处理是使用光照图(lightmaps)。他们主要上是一张灰度纹理,使用SetTexture(1,texture)作为第二纹理被应用于对象,同时通知渲染过程第二纹理以alpha混合方式进行。Direct3D运行起来有着不可思议的速度。此外,光照映射(lightmapping)这个主题大得足够专门写一篇文章了 - 所以在这里我只是点一下题 (基本上比我见过的所有DirectDraw的实现方案要快而且效率很高)。
A more advanced form of lighting uses lightmaps. These are basically a grayscale texture, applied as the second texture with SetTexture(1,texture) and a call to ensure that the renderer knows that it should alpha-blend the second texture. Direct3D makes doing this incredibly fast. However, lightmapping is a sufficiently large topic to warrant its own article - so I 'm only going to say that it can be done (and quickly, substantially more quickly than any DirectDraw solution I 've seen thus far). Vertex lighting should be more than enough to get you started on the Direct3D road.
DEMO3的GameInit及GameDone 同DEMO2的一样,没有什么变化。GameMain调用的是Demo3Render而不是Demo2Render,但是这已是非渲染代码部分的所有的改动了。
DEMO3 's GameInit and GameDone are unchanged from DEMO2. GameMain calls Demo3Render instead of Demo2Render, but that 's really all that changes in the non-rendering code.
Demo3Render本身与Demo2Render也并没有什么实质的区别。唯一的不同在于,在渲染的内层循环中,在DrawPrimitive被调用之前,我将每个顶点的颜色值设置成一个随机的浮点数。D3DRGB将处于0到1之间的rgb浮点值转化为Direct3D在D3DCOLOR中实际使用的颜色格式。全部的彩色光(colored lights)的代码如下(看起来有点不雅,我仅仅是想举例说明颜色属性的威力罢了)。
Demo3Render itself isn 't substantially different from Demo2Render, either. The only difference is that inside the inner loop, before DrawPrimitive is called, I set the color value of each vertex to a random float. D3DRGB meshes red, green and blue floating point values between 0 and 1 into whatever color format Direct3D happens to be using within D3DCOLOR. The entirety of the random lighting code is this (it 's a little inelegant, I just wanted to demonstrate the color property 's power:
Vertex[0].color = D3DRGB(rand()/5000,rand()/5000,rand()/5000);
Vertex[1].color = D3DRGB(rand()/5000,rand()/5000,rand()/5000);
Vertex[2].color = D3DRGB(rand()/5000,rand()/5000,rand()/5000);
Vertex[3].color = D3DRGB(rand()/5000,rand()/5000,rand()/5000);
12、联合起来:带有高度映射及光照效果的图素引擎
Pulling This Together: A Height-mapped, Lit Tile Engine
DEMO4被设计用于显示前面所做的东西的实际用处 - 它把前面大部分的工作合到一个引擎中。要使它成为一个真正意义上的游戏还需要做很多的工作,但是这是一个很好的开始。这个DEMO中我加入了一个map结构,储存了每个顶点的高度及光照度的级别。这在一个通常意义的图素引擎中是极难实现的 - 制作垂直元素通常需要很厉害的美工技巧才能实现,而处理斜坡效果也是美工极头疼的事情。
DEMO4 is designed to demonstrate the advantages to having made it this far - it pulls most of the work from earlier sections into one, reasonably coherent engine. A lot would need to be done to make this into a working game, but it 's a pretty good start. What this demo does is add a map structure, storing a height and lighting level for each vertex. This is something that would be extremely hard to do in a regular tile engine - the vertical element usually had to be added with clever artist tricks, and ramps had been known to give artists serious headaches.
第一个改变是在DEMO4中定义了tMapNode类。整个类定义如下
The first major change incorporated in DEMO4 is the tMapNode class. This is simply a storage class (I 'd have used a struct, except that in C++ structs are stored as classes anyway so there really isn 't a lot of point!). The entire class definition is as follows:
class tMapNode
{
public:
int VertexHeight[4];
D3DCOLOR VertexColor[4];
int Texture;
};
基本上,这个类存储了VertexHeight(相对于普通象素位置的偏移量),一个颜色值,以及一个用来指定图素所用纹理的索引值。在一个真正的游戏中,这个类应该有更多的东西 - 比如一个指向图素集(所有位于此图素之上的图素)的指针,让你觉得视图中有无限多的图层。
Basically, this class stores VertexHeight (an offset from the normal pixel location), a color for each vertex, and an index number designed to identify that tile 's texture. In a real game, there would be plenty more - possibly including a pointer to any tiles that live above this one, giving you the option of infinite layering (something I 'm fond of in this age of super-fast rendering!).
在CEngine中,我扩展了滚屏的代码,使得它可以处理包含有高度图素的情况。ScrollX现在表示的是将被渲染的左上方的图素。ScrollXOffset则表示的是用于调整渲染位置的象素偏移量。另外我还加入了Direction标识,使得滚屏可以先从左到右,然后从右到左。我没有加入垂直滚屏,因为它的实现原理基本是相同的。GameInit的代码段初始化了这些变量。GameMain也调用了一个新的函数,MakeDemoMap。MakeDemoMap是一个相对简单的函数,用来初始化图素的随机高度。它包含了一些相邻图素要素,这是直接从《TANSTAAFL 's Isometric Tutorial》中提取出来的。它还分配了基于高度的光照度 - 高的图素比较亮。对于一个简单的地形渲染来说,这已经是不错的了;在实时游戏中,你也许需要更高级的东西。光照系统是另一主题了,因此我仅仅告诉你得到这些效果的基本点 - 并且让你体验一下。没准什么时候我会另写一篇关于光照的教程!
Within CEngine, I 've expanded the scrolling code to cope with having a map defined, rather than just static tiles. ScrollX now indicates the upper-left tile to be rendered. ScrollXOffset indicates the pixel offset by which to adjust the rendering pass. Direction has been added so that the scroller will bounce left, then right, then left, etc. I didn 't add vertical scrolling, even though it would have been easy to do so - exactly the same principles apply. GameInit has gained code to initialize these variables. GameMain now also calls a new method, MakeDemoMap. MakeDemoMap is a relatively simple routine to initialize the map with random heights. It includes some tile adjacency stuff taken straight from TANSTAAFL 's isometric tutorial (q.v.). It also assigns a lighting level based on the height of a vertex - higher equals brighter. This isn 't a bad lighting system for a simple landscape render; in a real game, you would probably want something a little more sophisticated. Lighting systems is another topic that could easily fill another tutorial, so for now I 'll give you the basics of how to apply the results - and let you experiment. Who knows, I may be talked into writing a lighting tutorial someday!
GameMain 包含了一些基础滚屏的逻辑,如下:
GameMain includes some basic scrolling logic, now:
if (Direction == 0) {
ScrollXOffset--;
if (ScrollXOffset < 1) {
ScrollX++;
ScrollXOffset = 63;
if (ScrollX > 20) Direction = 1;
};
}
else {
ScrollXOffset++;
if (ScrollXOffset > 63) {
ScrollX--;
ScrollXOffset = 0;
if (ScrollX < 1) Direction = 0;
};
};
这些东西太基础了。如果Direction 为 0, 那么从ScrollXOffset减去1。如果ScrollXOffset小于1,那么移动图素。如果到了地图的最边缘,那么就反向滚屏。当Direction等于1时,同样的过程,不过方向相反。
This is pretty basic. If Direction is 0, it subtracts 1 from ScrollXOffset. If ScrollXOffset is less than 1, it moves tile. If it has run out of map, it reverses direction. When Direction is equal to 1, it does the same thing but in the other direction!
GameMain同样调用了Demo4Render。这个函数现在多了几个参数。WorldX和WorldY目前也作为参数传给它了,他们指定了要渲染的图素视图的左上角的坐标位置。
GameMain also includes a call to Demo4Render, which has gained some extra parameters. WorldX and WorldY are now passed to it, and these specify the coordinates (in tilespace) of the top-left tile to be rendered.
GameDone还没有涉及到。其实并不需要这个函数!
GameDone hasn 't been touched. It didn 't really need to be!
Demo4Render和前面几个阶段的基本上还是一样的,不过为了实现map结构和高度图的渲染加入了一些东西。一个看起来最不重要(其实非常重要)的变化在于WhereX和WhereY被重置为世界坐标,而不是0。如果不这么做,那么每一帧渲染的就是地图中的同一部分了。几乎Demo4中所有的改变都在渲染过程的内层循环中:
Demo4Render is largely the same as previous incarnations, but some changes have been made to incorporate both the map structure and heightmapped rendering. The most trivial (but important) change is that WhereX and WhereY are now reset to World coordinates rather than zero. If I didn 't do this, I 'd be rendering the same portion of map every frame - which is boring! Almost all of the changes in Demo4 come within the renderer 's inner loop:
顶点sx和sy的赋值部分改变了。每个顶点的高度值现在需要减去它的sy坐标值;我选择减法,这样正数的高度值看起来就像小山,而负数的高度值看起来就像山谷了。这是符合逻辑的。现在的顶点位置赋值过程如下:
The Vertex sx and sy assignment section has changed. The height for each vertex is now subtracted from its sy coordinate; I chose subtraction so that positive height numbers would give the appearance of hills, while negative would give valleys?it just seemed more logical to me, that way around. The vertex position assignments now look like this:
Vertex[0].sx = ScreenX + OffsetX;
Vertex[0].sy = ScreenY+16 + OffsetY - Map[WhereX][WhereY].VertexHeight[0];
Vertex[1].sx = ScreenX+32 + OffsetX;
Vertex[1].sy = ScreenY + OffsetY - Map[WhereX][WhereY].VertexHeight[1];
Vertex[2].sx = ScreenX+32 + OffsetX;
Vertex[2].sy = ScreenY+32 + OffsetY - Map[WhereX][WhereY].VertexHeight[2];
Vertex[3].sx = ScreenX+64 + OffsetX;
Vertex[3].sy = ScreenY+16 + OffsetY - Map[WhereX][WhereY].VertexHeight[3];
这些并不是很难呀!最幸的是,这些小变化导致了整个纹理系统的巨大变化。而渲染/位块传送的代码却丝毫没有改变!难道这不是很令人吃惊的吗?我想到这里我应该去要一杯味道甜美的冷饮。记得以前我试着用很复杂的方法去实现(2D),现在不快乐吗?
That wasn 't too difficult! The neat thing is, this tiny change causes all of the textures to warp as appropriate with NO change to the rendering/blitting code! Isn 't that wonderful? I like this so much that I should probably go and take a nice, long, cold drink. I remember trying to do this the hard way (2D!)?not pleasant. Enough to give me gray hair, anyway!
颜色部分的代码也改变了,现在使用事先存储好的颜色值而不是刚才的随机颜色。具体做法如下:
The color code has also changed, so that it uses the stored colors rather than making you wonder if I 'm on drugs. ;-) This is another pretty simple change:
Vertex[0].color = Map[WhereX][WhereY].VertexColor[0];
Vertex[1].color = Map[WhereX][WhereY].VertexColor[1];
Vertex[2].color = Map[WhereX][WhereY].VertexColor[2];
Vertex[3].color = Map[WhereX][WhereY].VertexColor[3];
不管你信不信,这便是生成这样一个漂亮的阴影,带有高度映射并实现光照效果的引擎的所有代码了!您可以进一步使用更多优化的技巧,也还有很多改进的空间,这仅仅是一个初级的引擎。
Believe it or not, that 's all that it took to produce a neatly shaded, height-mapped tiling engine. There are plenty of optimizations that could be applied, lots of room for additional innovation, but that 's the basics. With luck, lots of vertex-shaded tile games will appear, now! (If you write something cool, and think this article helped, I 'd love to get email from you!)
13、将来的工作(或我没有写到的)
Where To Go From Here (Or What I didn 't cover!)
有些方面我在这个教程中没有提及到,因为我不想把这篇文章变成一本教科书。其中的一部分如下,但是如果你要得到进一步的信息,那么就去看关于它们的专门教程。如下:
A number of topics could have been in this tutorial, but were omitted so that it didn 't turn into a book. The basics of each is here, but a more advanced treatment of any of these would require its own tutorial. Some of these things are:
精灵。我没有生成一个通常的Quad-Drawing系统,虽然我在文中提到的已经足以让你轻松的编写出一个。但我还是要提示一下:将精灵作为纹理加载(使用透明色,如果你没有使用任何纹理过滤)-这和2D的处理道理是一样的,不过要记得至少要使用一次SetRenderState(D3DRENDERSTATE_COLORKEYENABLE,TRUE)。
复杂光照。顶点光照虽然酷,但是如果使用动态光照或者更先进的光照生成系统将会更酷。光照图(Lightmaps)也很酷,虽然他们很快就会吃掉大量的资源。
层。我有意的避免文章涉及到它;网上有大量的文章谈到层(图像的遮挡)问题 - 况且3D中的层和2D中的没有任何区别。
Sprites. I didn 't produce a general quad-drawing system, although I gave out enough information that it should be really easy to do. Hint: load the sprite as a texture (using color keying if you aren 't using any form of texture filter - it works the same was as in 2D, just remember to use SetRenderState(D3DRENDERSTATE_COLORKEYENABLE, TRUE) at least once.
Complex Lighting. Vertex lighting is cool, and can be made a lot cooler if you apply dynamic lighting routines, more sophisticated light generation systems in the first place, etc. Lightmaps can also be cool, although they can eat up a lot of resources pretty quickly.
Layering. I deliberately didn 't go into this; there are a lot of articles out there covering layering in tile based engines - and layering in Direct3D is no different from layering in 2D.
当然,我愿意接受任何提议。作为利用Direct3D 提高图素渲染的初级文章,希望它对您有帮助。你可以就任何问题,评价,询问或是抱怨给我发电子邮件。
bracket@unforgettable.com。 I 'm open to any other suggestions, of course. This tutorial was intended as a primer in using Direct3D to enhance one 's tile rendering experience, and I hope it has helped. You can contact me at bracket@unforgettable.com with any questions, comments, queries or complaints!
14、源代码
Source Code
你在想从那里开始学习吗?如果是我,那么一定从这里开始!点击下载源代码。
Did you start here? I know I would have! Here is the accompanying source code.
15、关于作者
About The Author
作者Herbert Wolverson,大家通常叫他做 Bracket,是一个24岁的电脑顾问。白天编写数据库(类似世俗的任务),晚上钻研游戏制作技术。他也得抽时间去陪蒂娜,他的女朋友,以及他的2只宠物鼠、一条宠物蛇、一把吉他和参加每周都要召开的角色扮演会议。Bracket真的需要更多的时间去睡眠,还应该少喝点咖啡!
Herbert Wolverson, commonly known as Bracket, is a 24 year old Computer Consultant. Writing databases (and similar mundane tasks) by day, he works on game technology by night. He somehow also fits in time with Tina, his girlfriend, 2 pet rats, 1 pet snake, a guitar and weekly roleplaying sessions. Bracket needs to sleep more, and drink less caffeine!
参考文献
TANSTAAFL, "Isometric 'n ' Hexagonal Maps Part I "
TANSTAAFL, "Isometric 'n ' Hexagonal Maps Part II "
Jim Adams, "Isometric Views: Explanation and Interpretation "
Tobias Lensing, "Enhanced 2D "
Tricks of the Windows Game Programming Gurus
下载本文附带的源程序(146KB)
图片及源代码请登陆下面网站:
合作翻译:中国游戏开发者.CN – mays
http://mays.6to23.com
游戏制作天地 – wonyee(rocks_lee)
http://wonyee.top263.net/index.html