聚焦3D地形编程

来源:互联网 发布:淘宝新手开店一本通 编辑:程序博客网 时间:2024/04/25 09:27

聚焦3D地形编程第一章 The Journey into the great Outdoors

聚焦3D地形编程

翻译的烂,请见谅, 还请阅读原著 《Focus on 3D Terrain Programming》
译者: microsoftxiao@163.com 邵小宁 神杀中龙
第一部分
地形编程入门

第一章室外旅行 The Journey into the great outdoors

欢迎来到奇妙的3D地形编程世界!我将指导你愉快的阅读地形书籍,并且我们一起渲染最高的山脉,低的山谷,甚至一些草地。总之,本书你将学到关于地形编程和它的游戏开发应用的很酷的东西。所以,装好你必要的背包(你知道,音乐, 咖啡, 零食, 还有一点teddy bear在你的卧室中)因为我们将要开始了。

地形? 谢谢, 我已经吃了。

我知道第一个问题你会问这个: “什么是地形, anyway?” 好,我将立刻回答它。地形是陆地: 多岩石的山脉,绿色的平原,起伏的小山,所有形态组合成美丽的风景。地形渲染领域关心怎样实时的渲染所有这些宏伟的自然风貌。之后你要作出地形的计算,你需要解决怎样渲染其他的自然风貌,像水,云,太阳,雾还有其他有趣的素材。

通过这本书,你至少可以理解怎样创建一个不能相信的真实的室外场景,有非常多的细节且速度极快。要通过一些通常的地形信息,从地形(不在游戏开发方面)的一些通常的应用开始。

常规应用

在我们非常酷的游戏开发应用之前, 让我们看下它的其他应用。我从Virtual Terrain Project(http://www.vterrain.org/)找到了一些信息,这是一个伟大的站点,有所有关于地形的应用。下面是地形的一些应用:

n        虚拟游览(travel planning)

n        可视化天气和环境拓扑

n        Real estate walkthroughs

n        军事用途,像飞行模拟(以训练为目的)

有许多地形的应用,正如你看到的,地形可视化和渲染是很重要的一个领域。使用工具来制作地形, 它有足够的细节和每帧平滑的速率。(缓慢的应用将扰乱任何地形场景,真实是最重要的。)这些信息仅仅是冰山一角; 如果你对地形感兴趣但是不在游戏开发,看一看上述站点。

地形和游戏开发

3D地形在游戏开发界是巨大的应用,尤其是出现了最新的连续等级细节(CLOD)算法后。(CLOD的定义将在第5章得到解释), “Geomipmapping for the CLOD impaired”)3D游戏以前在游戏内削弱了大量室外图形。他们更趋向于室内在小的卧室和不透气的走廊。(这在第一人称设计类型里尤其明显。)

在过去的几年内,作为玩家,已经见到一系列基于室外的游戏,跨越了各种类型: 策略,动作和第一人称设计。像Black and White(1.1)Starsiege: 部落是两个最主要的基于室外的游戏使用了广阔的地形。

所有的室外游戏最近两年内发布的,一个特殊的游戏可以代表了3D地形在游戏内的特殊应用和一些话题: Treadmarks

Treadmarks

发行于2000年一月, Seamus McNallyTreadmarks在人们回想的地形引擎领域是十分具有革命性的。游戏,如图1.2, 是一个基于坦克战斗和围绕着一种基于ROAM地形风景的竞赛游戏(这些细节将在第7章讲述, “Wherever You May ROAM”)还有包括一些大的爆炸。最好的部分是关于在游戏每个子弹爆炸将影响到地形。例如,正常情况下炮轰会留下洞在地形上,然而大量武器创造了更大的弹坑。

甚至现在, 这个游戏已经3岁了,它仍然跟人的印象很深刻。这要归功于McNallyROAM实现算法,显示出了新思想和改变算法的更多可应性到a fast-paced 图形应用像Treadmarks或者任何其他游戏。

不幸的是,Seamus McNallyHodgkin’s Lymphoma抗争了3年死于2000330日,享年21岁。

虽然我不了解Seamus或他的家人(创建Treadmarks的全体工作人员), 但我想要感谢他和他难以置信的主意和想法在地形可视化方面并且希望他静静的安眠。为了纪念他在GameDev.net创建了

(http://www.gamedev.net/community/memorial/seumas)

因为Treadmarks 是地形编程世界的里程碑,我可以包含这个dmo(来自于http://www.treadmarks.com/)在本书的附件CD(Demos/TM_16_Demo.exe).我强烈的推荐给你。这个游戏有很多伟大的地形效果, 然而更伟大的是我们将在本书讨论它。甚至it’s just such an addicting game!

Demo Building Made Easy!

这本书的demos被分成了三个组: 主要章节demos, 预备的主要章节demos和各种无偿的任意的demos包含在内。所有这些可以在本书的极好的伴随CD内。捐献的demos将不被包含; they will be left as a project for you to figure out.

The Main Demos

主要的demos是官方附加demos在每章的代码,提供给你。这些demos使用了OpenGL渲染API和自定义的Windows代码,所以你可以仅仅运行它们在Windwos操作系统。主要的demos也使用C++Microsoft Visual C++ 6.0

每个章节的代码被划分了两个部分: demo代码和基础代码。Demo代码被所有书的理论和内容的实现,而base代码包含了应用程序的初始化,摄影机例程(routine), 数学操作等。这所有在VC++(Microsoft Visual C++)工作区被命名为demoXX_YY.dsw, XX是章节的号,YYdemo的当前章节编号。

Note 注意

It is import to note that although this book’s accompanying demos stick to a certain API,

虽然这本书附加的demos关联的API是重要的,实际的文本API和操作系统独立的。无论你使用OpenGL, Direct3D, 或者其他API, 你将可以理解本书的内容。

当你用VC++打开工作区时, 你可以构造应用程序,并且被平稳的编译。让我们一步步的实现这些,你可以在附加CDCode/Chapter 1/demo1_1.First内找到,打开Microsoft Visual C++demo1_1.dsw(文件, 打开工作区). 之后你照着做,如图1.3

从那里,你可以简单的构造(Build, Build demo1_1.exe)demo和可执行的EXEDemo几乎精确的demo7_1的复制品(7章的第一个demo), 所有你可以期望看到本书后面相似的东西。For now though, 将快速的解释。Demo展示了看起来像ROAM算法的tessellation例子。现在有点是在戏弄你,直到你看到第7!如果你正确的Build demo1_1,它将像如图1.4。也可以看表1.1的控制。

Table 1.1 Controls for demo 1_1()

这不是一个美丽的截图么?我认为。无论如何,在这本书将完成的例子中这是没有说服力的,但是它的工作只是戏弄别人了点。

The Book at an Itty-Bitty Glance

本书将关于地形,地形,地形,喘口气甚至更多地形! 我们将包含从不规则高度图的产生到三种不同的CLOD算法。我们将以可怕的特效结束以增加3D地形场景的真实感。这些效果由像雾, 云渲染, 镜头光晕,和其他各种各样的技巧方法和效果组成。所以,without further ado, 让我们开始摘要。

我们将以一部分一部分的代替一章一章的。

第一部分: 地形编程入门 ()

第二部分: 高级地形编程 ()

The Demos

我编制了在本书讨论的每个主要主题的demo程序。As we proceed through the book, 作为贯彻本书始终,这需要你不断留意我提供的仅仅是你自己实现的一些步骤。It is imperative that you keep in mind the demos I provide are only to be used as a stepping stone for your own implementation. 我的实现制作成好的教学指南给你;不要仅仅复制粘贴demo代码到你自己的项目内。Demos提供的不是高度优化的,没有提供最佳的细节,并且所有各式我们讨论技术的bellswhistles没有被实现。

因为我是个和蔼的家伙,我决定帮助你个小忙。地形是一个动态问题: 技术导致工作一天也许需要更多天来检查。因此,我决定奉献我的站点(http://trent.codershq.com/)为地形研究和实现,我将保留一些不变的数据库在3D地形编程领域。我将尝试开发大量细节和我可以实现的最快的实现,我将保留一份开发日志。如果demos在本书CD没有提供,那么到我的站点来看一系列demos和将供应这些无价的信息。

摘要

本章包含了地形的基本常识和它的应用。它也说明了怎样编译和执行不同类型的demos和提供整本书的概述。准备好: 你的旅行将通向奇妙的地形渲染世界。

 

 

翻译的烂,请见谅,期望深刻理解还请阅读原著,本译文仅供参考。
译:microsoftxiao@163.com 邵小宁 神杀中龙

好,在这里——你将进入3D地形编程的世界!本章将包括地形渲染的所有方面,在你开始有趣的纹理化/光照化技术之前,你需要知道这些,各种各样的硬编码地形算法。在本章,你将学习到下面的关键概念:

n         什么是高度图,怎样创建它,并且怎样加载它。

n         怎样使用burte force算法渲染地形

n         怎样使用fault formationmidpoint displacement产生不规则地形。

那么,让我们开始吧!

Heightmaps – 高度图

假设你有一个规则的多边形格子沿XZ轴展开。这种情况下你不知道我要谈论什么,如图2.1可以更新你的内存。

现在那是一张漂亮但有令人讨厌的图像!我们怎样可以正确的制作它呢,好,terrain-ish? 答案是使用高度图。高度图,在我们这儿,是一系列unsigned char变量(让我们把值取在0-255, 这样在灰度图上产生一系列渐变灰度值)我们将实时创建且在绘图程序内。高度图为我们的地形提供高度值,所以如果我们沿着XZ轴展开的话,高度图定义的值将沿着Y轴展开。快举个例子, 看如图2.2 之后我们加载它并且把它运用到我们的地形中,在格子2.1的格子中将变换出美丽地形(虽然它非常的缺乏颜色和光照)你看如图2.3

必须承认,图2.3的地形开起来即漂亮又讨厌,没有非常酷的纹理和光照,但是我们需要从某处开始!同样我刚刚解释了,高度图给我们的格子的顶点提高到一个宏伟的风景,增添了力量。问题是,严密的说什么是高度图?通常,一个高度图是一个每像素描绘的灰度值。(在这里,高度范围从0-255, 一系列渐变的灰度图。)暗色描绘低海拔,那么亮色描绘高海拔。再看如图2.22.3; notice how the 3D terrain(Figure 2.3) corresponds exactly to the heightmap in Figure 2.2, 从山顶任何一点,到山谷,是平滑的颜色么?那么我们想要我们高度图做什么呢:作为我们地形顶点的模型。

在这里,我们高度图的格式将是RAW格式。(虽然大部分demos动态创建高度图,我包含了加载和保存RAW格式的选项。) 我选择这个格式因为它难以置信的简单容易使用。另外,因为RAW格式仅仅包含纯数据,它更容易加载高度图。(我们也要加载灰度的RAW图,更容易制作。)在我们加载RAW图像前,我们需要几个东西。首先,我们需要创建一个简单的可以描绘高度图的数据结构。我们这个结构是一个unsigned char类型的缓冲区(我们需要可以动态分配内存)并且变量可以存储高度图的大小。这足够了么, 恩?好,这里:

struct SHEIGHT_DATA

{        unsigned char* m_pucData; // the height data

      int   m_iSize;     // the height size(power of 2)

};

创建地形基础类 The Creation of a Base Terrain Class

我们需要从所有特定的地形引擎中(brute force, geomipmapping)创建基础类将被继承。我们不希望用户创建这类的实例;我们仅仅想让这个类成为我们今后开发特定实现的父类。看图2.4在我们头脑获得一个可视化概念。

CTERRAIN

CBRUTE_FORCE

CGEOMIPMAPPING

CROAM

CQUADTREE

继承自

2.4 CTERRAIN和四个地形实现类的关系

注意: CTERRAIN类是我们C++内的一种抽象类。抽象类是给所有它的子类提供公共的接口。用途是: 一个目前有红色头发但是性格另人讨厌。虽然所有它的孩子继承了母亲的红色头发,但每个有独特的性格。同样一个抽象类;虽然一个抽象类令人讨厌,its traits carry on to its children, 并且那些孩子可以被定义更多它们自己的另人激动的行为。(即通过覆写虚函数)

至此我们的类需要三个变量: 一个SHEIGHT_DATA的实例,一个高度缩放比列变量(将让我们可以为地形动态缩放高度), 和一个大小变量(可以存储SHEIGHT_DATA的大小)。至于函数,我们需要一些操作高度图的函数和设置高度缩放变量的函数。这里我们展示出:

class CTERRAIN

{

       protected:

              SHEIGHT_DATA m_heightData;       // the height data

              Float                    m_fHeightScale;     // scaling variable

public:

    int                        m_iSize;            // must be a power of two

    virtual void Render(void) = 0;

    bool LoadHeightMap(char* szFilename, int iSize);

    bool SaveHeightMap(char* szFilename);

    bool UnloadHeightMap(void);

//-------------------------------------------------------------------------------------

// Name:                 SetHeightScale – public

// Description:             Set the height scaling variable

// Arguments:              -fScale: how much to scale the terrain

// Return Value: None

//---------------------------------------------------------------------------------------

inline void SetHeightScale(float fScale)

{ m_fHeightScale = fScale; }

//------------------------------------------------------------------------------------------

// Name:                   SetHeightAtPoint – public

// Description:              Set the true height value at the given point

// Arguments:               -unHeight: the new height value for the point

//                         -iX, iZ: which height value to retrieve

// Return Value:             None

//-------------------------------------------------------------------------------------------

inline void SetHeightAtPoint(unsigned char ucHeight, int iX, int iZ)

{ m_heightData.m_pucData[( iZ*m_iSize )+iX] = ucHeight; }

//---------------------------------------------------------------------------------------------

// Name:                      GetTrueHeightAtPoint – public

// Description:      A function to get the true height value(0-255) at a point

// Arguments:       -iX, iZ: which height value to retrieve

// Returen Value:    An unsigned char value: the true height at

//                 the given point

//----------------------------------------------------------------------------------------------

inline unsigned char GetTrueHeightAtPoint(int iX, int iZ)

{ return ( m_heightData.m_pucData[( iZ*m_iSize )+iX ]; }

//-------------------------------------------------------------------------------------------------

// Name:                  GetScaledHeightAtPoint – public

// Description:              Retrieve the scaled height at a given point

// Arguments:                -iX, iZ: which height value to retrieve

// Return Value:             A float value: the scaled height at the given point

//--------------------------------------------------------------------------------------------------

inline float GetScaledHeightAtPoint( int iX, int iZ )

{ return ( ( m_heightData.m_pucData[( iZ*m_iSize )+iX])*m_fHeightScale); }

CTERRAIN(void)

{    }

~CTERRAIN(void)

{    }

};

Not too shabby if I do say so myself! 好那是我们的地形父类!每个我们开发的其他实现从这个类派生。我为用户添加了两个容易使用的操作高度图的函数。然而我们,作为开发者,将使用true函数,用户将使用被缩放了的函数来执行碰撞检测(我们将在第八章做, “封装它: 特效和其他”)

加载和卸载高度图 Loading and Unloading a Heightmap

我已经谈论了这些例程,并且我们最后要使用它们。这些例程是简单的,所以对比他们没有任何难度。我们仅仅使用一些C风格的文件I/O来做。

注意: 我趋向于使用严格的C风格I/O因为它比C++风格的更容易阅读。如果你已经是真正的C++死忠,并且完全厌恶C做事的方式,那么可以自由改变例程为C++式的。另一方面,我确实喜欢C++风格的内存操作,所以,如果你是C死忠,那么你就那么做。

我需要谈论怎样加载,保存和卸载高度图。最好的地方是在开始时加载例程因为你不能在没有加载前卸载任何事物。我们需要两个参数:文件名和地图大小。在函数内,我们想创建FILE的实例来加载高度图。那么我们想确认高度图类的实例是否已经加载了信息;如果是这样,那么我们需要调用卸载例程并继续我们操作。我们讨论的代码像这样:

bool CTERRAIN::LoadHeightMap(char* szFilename, int iSize)

{

       FILE* pFile;

//check to see if the data has been set

if( m_heightData.m_pucData)

    UnloadHeightMap();

}

其次, 我们需要打开文件并为我们的高度图实例数据缓冲区分配内存(m_heightData.m_pucData)我们需要确认内存是否被正确的分配,是否没有发生可怕的错误。

// allocate the memory for our height data

m_heightData.m_pucData = new unsigned char [ iSize*iSize];

// check to see wether the memory was successfully allocated

if (m_heightData.m_pucData == NULL)

{

// the memory could not bel allocated

// something is seriously wrong here

printf(“Could not allocate memory for%s"n”, szFilename);

return false;
}

我们的加载过程继续,我们将加载实际的数据把它们放置在高度图实例的数据缓冲区内。然后我们将关闭文件,设置一些类成员变量,然后输出成功消息。

// read the heightmap into context

fread(m_heightData.m_pucDat, 1, iSize*iSize, pFile);

// close the file

fclose(pFile);

// set the size data

m_heightData.m_iSize = iSize;

m_iSize = m_heightData.m_iSize;

// Yahoo! The height has been successfully loaded!

Printf(“Loaded %s"n”, szFilename);

Return true;

}

注意: 高度图保存例程几乎是和加载同样的东西。基本上,我们仅仅需要替换freadfwriteThat’s all there is to it!

那就是加载例程。让我们在我们注意力被分散前转到卸载例程。卸载程序是简单的。我们仅仅必须检查内存是否已经被分配,然后如果分配了,我们需要删除它。

bool CTERRAIN::UnloadHeightMap(void)

{

// check to see if the data has been set

if(m_heightData.m_pucData)

{

    // delete the data

    delete[] m_heightData.m_pucData;

    // reset the map dimensions, also

    m_heightData.m_iSize = 0;

}

// the heightmap has been unloaded

printf(“Successfully unloaded the heightmap"n”);

return true;

}

我真的不需要检查数据缓冲是一个NULL指针(指针是否为NULL在中心会检查) , 所有我的检查有点是多余的。这个检查已经成为了习惯,然而,这本书就是这么做的。你可以不检查是否为NULL指针就删除它。现在我将展示给你我们已经讨论的渲染方法。

The Brute Force of the Matter 硬渲染

渲染地形使用brute force算法直接而简单,而且它提供了最大化的细节。不幸的是,它是这本书里讲的最慢的算法。基本上,如果你有一个64x64像素的高度图,那么地形,当使用brute force渲染时,由64x64个顶点组成,规则的重复模式。如图(2.5)

这种情况下你不能立即重新组织它,我们将每行的顶点作为三角形带渲染因为这是大部分渲染顶点的方式。你不能单独的渲染一个三角形或使用像图2.5那样的方式渲染三角扇形,would you?

这章的demo, 我留着它作为一种简单的可能。顶点的颜色基于它的高度,所以所有顶点将利用灰色着色。并且所有这些都使用brute force渲染地形。这里快速的摘录一小片OpenGL来展示怎样渲染地形:

void CBRUTE_FORCE::Render(void)

{

unsigned char unColor;

int iZ;

int iX;

// loop throught the Z axis of the terrain

for (iZ = 0; iZ<m_iSize-1; iZ++)

{

       // begin a new triangle strip

       glBegin(GL_TRIANGLE_STRIP);

       //loop through the X axis of the terrain

       //this is where the triangle strip is constructed

       for(iX=0; iX<m_iSize-1;iX++)

       {

              //Use height-based coloring. (High-points are

        //light, and low points are dark.)

        ucColor = GetTrueHeightAtPoint(iX, iZ);

              // set the color with OpenGL, and reader the point

        glColor3ub(ucColor, ucColor, ucColor);

        glVertex3f(iX, GetScaledHeightAtPoint(iX, iZ), iZ);

              // Use height-based coloring. (High-points are

        // light, and low points are dark.)

        ucColor = GetTrueHeightAtPoint(iX, iZ+1);

              // set the color with OpenGL, and render the point

        glColor3ub(ucColor, ucColor, ucColor);

        glVertex3f(iX,

GetScaledHeightAtPoint(iX, iZ+1),

iZ+1);

       }

    // end the triangle strip

    glEnd();

}
}

现在到了创建实际demo的时候了!拿出在CD上的demo2_1。到Cod"Chapter 2"demo2_1, Microsoft Visual C++打开工作区, 然后开始娱乐!这个demo展示了我们刚刚讨论的梭鱼东西。如图2.6展示了demo的截图,如表2.1提供了控制demo的描述。移动你的视点,仅仅可以使鼠标向左,右和拖拽。

Woohoo!现在,我说过我们将创建大量我们的动态高度图。你也许会问你自己,我该怎样做? 好的,我很高兴回答你。(甚至如果你不问,我仍然要解释它!)现在我们将学习怎样以程序的方式使用两种不规则地形产生技术生成高度图。准备!

Fractal Terrain Generatoin 不规则地形生成

Fractal terrain generation被用来产生地形的算法, 虽然这里,我们将高度图作为我们地形的蓝图。但是我们将通过这里的两个算法,第一个是fault formation和第二个midpoint displacement。我们将自始至终的使用fault formation算法在本书因为它不会被地点尺寸所限制,(如果用一般高度图将限制在0-255的高度) midpoint displacement需要2N次方才可以。(尺寸也必须是相等的,所以你可以产生1024x1024的高度图,你不能产生产生一个512x1024的高度图。)所以,不要再耽搁了,让我们从不规则地形生成算法开始!

Fault Formation缺点形成算法

一种不规则地形生成算法叫做fault formation. Fault formation是在生成地形过程中”faults”; 大部分时,它产生比较平滑的地形。基本上,所有我们做的随机线在blank高度区域, 而且然后我们添加随机高到一边。看图2.7如果你讨厌可视化或者如果你刚刚想要证实你脑中的图(或者, 如果你喜欢, 注意你的头脑我很奇怪)是正确的。

原版 43 fault-formation algorithm

这是整个过程的第一步,当然。在你提高到高级阶段之前,这里还有一些你需要知道的算法。首先,更早时我谈论过需要减少每次反复。你也许会问为什么?好,如果你不减少每个高度,你最后使用的高度将像2.8。看2.9的高度图。

注意, 在图2.8, /暗斑点是多么的不和谐就是这个原因;他们仅仅在所有地方被展开。这就好像一个混乱的地形,但是我们像创建一个平滑的,起伏的小山。不要担心;解决这个问题相当简单。我们想用线性递减高度值没有在0结束。这么做,我们将使用下面的等式(拿出demo2_2):

iHeight = iMaxDelta – ((iMaxDelta-iMinDelta)*iCurrentIteration)/iIterations;

iMinDelta, iMaxDelta, iIterations作为函数参数提供。 iMinDeltaiMaxDelta描绘了最低值和最高值,你想要当新faults时的高度。我趋向于严格的一个0作为iMinDelta255作为iMaxDeltaiIterations 我之前说过,描绘fault passes一系列过程(多么不同的时间被划分)。最后,but certainly not least, iCurrentIteration描绘了当前iteration值。

我早说过,我们就年斤毫年 想提升一边,然后我们想升起每个边线点的高度值。因此,我们将循环处理整个高度图的所有高度。所有这些容易实现;它仅仅是解决一个简单的数学问题。我有一个vector在我们线的方向上(我们之前创建了两个随机点,那么它的方向被存储在(iDirX1, iDirZ1)。下一个vector我们想创建一个从最初随机点(iRandX1, iRandZ1)到当前循环点(x, z) 之后完成,我们需要找到Z分量的叉乘, 然后如果它比0大,那么我们需要增加当前点。所有之前的解释都将从这的代码展示出来。

// iDirX1, iDirZ1 is a vector going the same direction as the line

iDirX1 = iRandX2 – iRandX1;

iDirZ1 = iRandZ2 – iRandZ1;

for(x = 0; x<m_iSize;x++)

{

       for(z=0; z<m_iSize; z++)

       {

              // iDirX2, iDirZ2 is a vector from iRandX1, iRandZ1 to the

              // current point (in the loop).

              iDirX2 = x-iRandX1;

             iDirZ2 = z-iRandZ1;

              // if the result of (iDirX2*iDirZ1 – iDirX1*iDirZ2) is “up”

              //(above 0), then raise this point by iHeight

              if((iDirX2*iDirZ1 – iDirX1*iDirZ2) > 0)

                     fTempBuffer[( z*m_iSize)+x]+=(float)iHeight;

       }

}

 

注意: demo2_2这两段你看到了fault formationmidpoint displacement代码在demo2_2内的两个片段,你也许注意到我怎样创建临时缓冲区,fTempBuffer, 所有的高度值严格用浮点表示。如果你记得,虽然,我谈论过我们的高度图是一个unsigned char类型的数组。为什么我在这种情形使用浮点变量?我这么做是因为算法需要比我们的默认unsigned char高度缓冲区有更高的精确性。之后我们创建整个高度图并规格化,我从fTempBuffer传送所有信息到CTERRAIN类内的高度缓冲区, m_heightData

检查图2.9看一些使用fault formation产生的高度图,和各种fault-line iterations. 紧接着, 我们也还没有完成这个算法!万一你没注意, 地图看起来像之前的图(-terrainish)(新世界)。我们需要经过一个腐蚀(erosion)过滤器来过滤整个地图直到我们形成一个新的平滑的值的。这个过程非常好, 如果不精确, 像经过污点过滤器通过你喜欢的绘图程序来处理下。 如果它帮助你理解了下面的解释, 正好是这样的理解。

我们将要应用一个简单的FIR过滤器, 作为Jason Shankel的建议。 这个过滤器意味着模拟地形侵蚀(erosion),就像自然界频繁发生的那种。你曾经在自然界里看到过的一系列的高山看起来如图2.9?) 我们将获得波形(bands)数据,胜于立刻过滤整个高度图。过滤函数看起来像这样:

void CTERRAIN::FilterHeightBand( float* fpBand, int iStride, int iCount, float fFilter)

{

       float v = ucpBand[0];

       int j = iStride;

       int i;

       // Go through the height band and apply the ersion filter

       for(i = 0; i < iCount-1; i++ )

       {

              ucpBand[j] = fFilter*v + (1-fFilter)*ucpBand[j];

              v = ucpBand[j];

              j+= iStride;

       }

}

这个函数获取高度值的单个边并且goes through them value by value, 通过iStride规定在每次循环内的向前的值日。iStride也规定出我们过滤整个高度图从上到下的方向,从下到上,从坐到右,从右到左。整个函数最重要的是这行:

ucpBand[j] = fFilter*v + (1-fFilter)*ucpBand[j];

这行是涂污/侵蚀。 各种各样的值为了fFilter影响模糊。0.0f是根本不模糊, 1.0f是最模糊。通常,我们想要值在0.3f0.6f之间,这依赖于你想要地形的平滑程度。现在,例如, 我们说出过滤器的值0.25f, 且当前边值为0.9f。前一个等式看起来像这样:

ucpBand[j] = 0.25f*v + (1-0.25f)*0.9f;

之后我们执行初始化计算, 之前的等式将简单化为这样:

ucpBand[j] = 0.25f*v + 0.675f;

0.675f是高度图像素被模糊的新值, 但是现在它需要被和之前的像素值进行插值。(我们将给出像素值为0.87f)。我们应用0.25模糊过滤器到该像素且加上非插值的像素值到此像素,以致于我们有这样的计算。

ucpBand[j] = 0.25f*0.87f + 0.675f;

执行最后的计算, 我们得到0.8925f的最终值。 所以,你看, 所有我们真实的行动这里混合成了当前像素到前一像素间的值。拿出图2.10看我们之前讨论的每像素过滤看起来是非常大攀登。

玩弄下demo2_2. 我为高度图操作制作了菜单, 并且现在你可以动态创建新的高度图。如果你找到了,仅仅选择保存当前选项, 那么高度图将被保存到程序目录下。当你选择Fault Formation选项时,弹出一个对话框你可以输入细节值。这个值是一个整数,取值范围为1-100。现在该介绍些有趣的midpoint displacement(中点位移)的时候了。

Midpoint Displacement 中点位移算法

Fault formation在一些小场景组成一些小山工作的非常好,但是如果你想产生一些比这混乱的,甚至像山脉那样的地貌, Fault formation就不行了。好,继续看。Midpoint displacement将可以满足你的期待!这个算法也被认为是plasma fractaldiamond-square算法。然而,midpoint displacement发出的声音更酷,并且它提供给读者(就是你)一个继续整个过程更好的观点,所以我将坚持大部分时间使用这一术语。

注意: 重要的是注意midpoint displacement算法有一个轻微的缺点: 算法仅可以生成方形的高度图,并且尺寸必须似乎2N次方。这不像fault formation算法,你可以指定任何你想要的尺寸。

我们将完成这个算法,本质上,它是对单条边的中点进行位移。让我给你一个一维空间的概念。如果我们有一条线,像如图2.11 AB 我们找到它的重点,标记出来为C。并且移动它。现在,我们将位移线中点的高度值,我们叫fHeight吧。(看图2.12)我们将使得产生的两条线相等,并且我们将在-fHeight/2fHeight/2到范围内位移中点。(我们想要每次细分出(subdivide)两条线,而且我们将要将其位移到线的某个高度在一定范围内。

之后我们需要递减fHeight的直到我们期望的粗糙程度。就这么做,我们简单的用2-fRoughness来进行乘法, fRoughness是未加工地形的一个常量值。用户将指定该值存储到fRoughtness内,所以你需要知道一点关于你可以设置各种值的信息。这个值是可以的,从技术上讲,任何可以是你任何期望的浮点值,但是最好的结果应该是0.25f1.5f。看图2.13,可视化的指示出各种可以达到的粗糙程度的情形。

正如你看到的,这个值即fRoughness对高度图的影响相当大。值小于1.0f将创建一个无序地形,值正好为1.0将是平行的,大于1.0f将创建一个平滑的地形。现在让我们继续深入解释二维的情形。

1D的解释留在你的大脑中,我们将改变到2D因为你刚刚学习了单条线的相关概念。有个例外是这样的,代替单线的中点计算,我们现在必须计算四条不同边的中点,平均它们,然后为正方形的中心的高度值增加这个值。如图2.14所示的正方形(ABCD)开始。

像我之前说的第二点,我们必须计算所有四边的中点(AB, BD, DC, CA)结果点为E, 将直接在正方形的中心。然后位移EABCD高度值的平均值,并加上在-fHeight/2fHeight/2范围内的随机值。结果将如图2.15所示。

这还仅仅是第一次位移的一半阶段。现在我们必须计算出每个中点的高度值,是我们先前找到的那个。跟我们之前做的是相似的,;我们仅仅平均围绕顶点的高度值并加上-fHeight/2fHeight/2范围内随机值日。最后将如图2.6所示。

然后你可以继续找到下一个矩形执行同样的处理。如果你理解了1D解释,然而,你确定理解了2D解释并实习那代码,demo2_2, 找到CDCode"Chapter 2"demo2_2

编译信息,照常,提供了文本文件在demo的目录下。去查看这个demo。控制与最后一次的(2.1提示)的相同,但是这次,当你点下细节区域的重点时,你想要的值范围为0(真是无序的地形)150(简单地形 ). 真有趣。

摘要

本章,你收到了进入地形编程的入门级训练。你学到了所有关于高度图的信息: 它们是,怎样产生它们,还有怎样加载/卸载它们。然后你学习了怎样使用burte force渲染那些高度图,是市面上最简单的地形渲染算法。最后,你学习两种程序式产生高度图的算法。下两章,我们将学习所有和地形的纹理化和光照化的有趣技术。

聚焦3D地形编程第三章纹理化地形

翻译: 神杀中龙 邵小宁 microsoftxiao@163.com
原著: 《Focus on 3D Terrain Programming》
翻译的烂请见谅

现在你已经可以制作简单的地形网格,
你需要知道怎样如何使用纹理贴图来为那令人讨厌的网格添加细节。我将继续讨论简单的纹理并且一直到我们可以使用真正有趣的片段(地形算法)作为开始。我将停止浪费空间现在,并且仅仅告诉你将在本章学习什么:

n         怎样应用巨大的单模式纹理映射到地形网格上。

n         怎样以程序式手段来产生复杂的纹理地图使用各种各种的地形 tiles

n         怎样为地形添加纹理细节甚至以前产生的纹理的更多细节。

Simple Texture Mapping 简单纹理映射

我们将使用简单纹理映射。 你将学习怎样延长一个纹理到整个地形网格。大部分时间, 如果这个技术看起来是糟糕的,当然,你已经真的制作好了纹理映射,我们将在下节继续工作。立刻计算出你学习的怎样延长纹理最终结果看起来像什么。

延长单张纹理越过整个地形, 我们将为地形上的每个顶点带上范围在0.0f-1.0f(标准的纹理坐标范围)的分量。这么做比听起来容易。出发了, 看图3.1

像图3.1展示的, 左下角地形网格( 我们将选择256x256的高度图), (0, 0)纹理坐标为(0.0f, 0.0f), 而左上角地形(255, 255),纹理坐标为(1.0f, 1.0f) 基本上,所有我们需要的是找到顶点,包括当前渲染的和划分它使用高度图。(这么做是可以产生我们期望范围内的值日, 0.0f-1.0f, 没有超过我们的范围。注意这是重要的,因为我们在后续章节将超过这个范围。)在渲染每个顶点前, 我们需要计算三个东西: 纹理值的x, zz+1, 我们将叫做fTexLeft, fTexBottom, fTexTop, 这里我们该如何计算这些值呢:

fTexLeft = (float)x/m_iSize;

fTexBottom = (float)z/m_iSize;

fTexTop = (float)(z+1)/m_iSize;

那么你可以思考下这将是困难的!无论如何, 我们需要在每个顶点被渲染前计算他们内并且然后将纹理坐标传送给我们的渲染API。当我们渲染顶点(x, z)时, 我们发送(fTexLeft, fTexBottom)作为我们的纹理坐标, 并且当我们渲染(x, z+1)时, 我们发送(fTexLeft, fTexTop)作为我们的纹理坐标。看图3.2和在CDCode"Chapter3"demo3_1上的demo3_1将看到你劳动的成果。

屏幕截图后了比我们第二章地形更多的细节,地形 101”(然而注意我移除了渐变), 但是目睹了实际形式的地形它是困难的。如图3.3延长单一纹理, 甚至如果纹理在demo3_1内使用纹理被高分辨率, 在我们的纹理地形上我们可以捕捉到一切细节。

我们需要更多细节。 我们想要纹理地图像图3.4一样,通过使用一系列程序式方法产生 tiles(泥土, 草地, 岩石还有雪地等)

如图3.4有多少细节?这个纹理帮助我们区别高山区域到低矮的平原,如图3.3 你需要知道怎样产生真正酷的纹理像下面展示的。继续阅读!

Procedural Texture Generation 程序式产生纹理

Procedural texture generation是很酷并且很有用的技术对于任何地形引擎。之后我们完成我们的程序化纹理产生器,我们将允许用户加载一系列24个它可选择的tiles。然后我们将调用我们的纹理生成函数。(所有用户都需要知道他想要创建的纹理的大小。) 那时!我们怎样着手创建我们的纹理生成函数呢?首先,你需要知道我们这里实际的目标。我们将让它和产生的高度图表现一致(如高山,那就用高山纹理,如平原,就产生平原纹理)。我们将遍历我们纹理地图的每个像素,找到高度符合该像素的并计算出每个纹理tile的像素。(每个tile有一个规定好的区域。)非常罕见的tile将被100%的可见,所有我们需要合并tile到其他tile(RGB颜色上进行插值)。这个结果将如图3.5你可以看到插值是在草地和岩石tile间进行的。

The Region System 系统区域

开始编码前,我们需要创建一个表示每个tile信息的结构。一个region, 被使用,是一系列定义好高度值范围的三个值。这个结果我们创建出来:

struct STRN_TEXTURE_REGIONS

{

       int m_iLowHeight;                // lowest possible height(0%)

       int m_iOptimalHeight;             // optimal height(100%)

    int m_iHighHeight;                // highest possible height(0%)

};

解释每个值意思将由图3.6来完成。

根据解释, 我们将让m_iLowHeight等于63并且m_iOptimalHeight等于128 计算m_iHighHeight的值需要些简单的数学。我们将让m_iOptimalHeight减去m_iLowHeight 然后我们将加上m_iOptimalHeight和前一个处理的值。我们有我们的范围集(low:63, optimal:128, high:193) 所以如图3.6那样替换那些值。 现在想象一下怎样描绘当前tile的高度值, 说, 150 想象一这个值如图3.6在线上所示, 计算出我们的范围。To save you the trouble of trying to figure it out, 如图3.7

正如你看到的图像, 纹理presence在高度(150)时为70% 现在我们知道许多信息, 那我们怎么做呢,我们计算出RGB三个一组从纹理图内并且乘以0.7f 这个结果正似乎我们当前像素想要的值。

我们需要创建一个函数将计算出提供给我们的百分区域。 这个函数相当简单。它需要两个微不足道的测试看这个高度是否真的在这个范围内;如果不在这个范围内, 退出函数。 接着,我们需要标记出高度的位于哪个区域。它在最佳值之下还是之上,或者等于最佳值?微不足道的情况是高度等于最佳值;如果是这样, 那么当前tile纹理presence为当前像素的100% 而且我们不需要担心插值过程。

如果高度在最佳值之下, 我们需要减少这些片段的值。 之后,我们用这个高度值减去这个区域较低的值。然后我们最理想的范围值是减去低范围值。我们然后从第一个计算到最后一个计算值划分这个结果, 恩迷糊! 我们有了我们的百分比presence

这是我刚刚讨论的代码:

// 高度是位于最佳值之下

if(ucHeight<m_tiles.m_regions[tileType].m_iOptimalHeight)

{

       // calculate the texture percentage for the given tile’s region

       fTemp1 = (float)m_tiles.m_regions[tileType].m_iLowHeight – ucHeight;

       fTemp2 = (float)m_tiles.m_regions[tileType].m_iOptimalHeight – m_tiles.m_regions[tileType].m_iLowHeight;

       return (fTemp1/fTemp2);

}

最后的情况是如果这个高度值在最佳范围之上。计算就要考虑的复杂些比当高度在范围下复杂,但是他们仍然不是非常难的。代码形式比文字更容易理解:

// height is above the optimal height

else if(ucHeight>m_tiles.m_regions[tileType].m_iOptimalHeight)

{

       // calculate the texture percentage for the given tile’s region

       fTemp1 = (float)m_tiles.m_regions[tileType].m_iHighHeight – m_tiles.m_regions[tileType].m_iOptimalHeight;

       return ((fTemp1 – (ucHeight – m_tiles.m_regions[tileType].m_iOptimalHeight))/fTemp1);

}

这个计算,理论上,是基于同样的低比最佳,高好的情况,除了我们必须获取场景中100%时的小部分值,它低于某高度,取代的值高过某高度!

The Tile System Tile系统

好的,现在你知道如何从一个纹理tile内获得纹理的presence了和纹理像素内。现在你需要应用你刚刚学到的来为每个纹理tiles加上这个计算并创建出整个纹理地图。这虽然比听起来容易,所以别受打击!

首先,我们需要创建纹理tile结构以管理所有的纹理tiles每个tile我们不需要更多的信息;所有我们需要替换纹理信息值且每个纹理的结构区域。我们将也想明白tiles的数量,有多少个被加载。根据这些需求,我创建了STR_TEXTURE_TILES结构体,它是这样的:

struct STRN_TEXTURE_TILES

{

       STRN_TEXTURE_REGIONS m_regions[TRN_NUM_TILES]; // texture regions

       CIMAGE                  textureTiles[TRN_NUM_TILES]; // texture tiles

       Int                         iNumTiles;

};

其次,你需要一些管理纹理tile的函数。我需要加载和卸载单个tile的函数,还有卸载所有tiles。这些函数的实现不值一提,所以我在这里不展示它们。如果你感兴趣可以去看代码。除此之外,你要阅读地形生成函数!

开始生成函数,我们需要计算出实际加载多少纹理。(我们想要用户可以没有纹理时产生纹理。)之后要完成它,我们需要重新循环的计算出区域范围内的每个tile(我们想要tile区域被0-255的范围内隔开)。这现在我将这么做:

iLastHeight = -1;

for(i = 0; i<TRN_NUM_TILES; i++)

{

       // we only want to perform these calculations if we

       // actually have a tile loaded

       if(m_tiles.textureTiles[i].IsLoaded())

       {

              // calculate the three height boundaries

              m_tiles.m_regions[i].m_iLowHeight = iLastHeight+1;

              iLastHeight+=255/m_tiles.iNumTiles;

              m_tiles.m_regions[i].m_iOptimalHeight = iLastHeight;

              m_tiles.m_regions[i].m_iHeighHeight = (iLastHeight – m_tiles.m_regions[i].m_iLowHeight) + iLastHeight;

       }

}

The only thing that should look remotely odd here is the last segment where we calculate m_iHighHeight, 甚至它看上去不是个单数。(如果它是单数,就涉及到开始我解释的区域范围了。)

Creating the Texture Data 创建纹理数据

现在到了创建纹理数据的时候了。要做这个,我们需要创建三个不同的循环: 一个是纹理地图的Z轴,一个是X轴,还有每个tile (在函数内tile将是第三个循环。)我们也许哟啊创建三个保存运行时我们计算出的每像素的当前RGB分量。实际的生成纹理函数像这样:

for( z = 0; z<uSize; z++)

{

       for(x = 0; x<uiSize; x++)

       {

              // set out total color counters to 0.0f

              fTotalRed = 0.0f;

              fTotalGreen = 0.0f;

              fTotalBlue = 0.0f;

              // loop through the tiles

              // for the third time in this function

              for(i=0; i<TRN_NUM_TILES;i++)

              {

                     // if the tile is loaded, we can perform the calculations

                     if( m_tiles.textureTiles[i].IsLoaded())

                     {

                     }

              }

       }

}

下面,我们需要从纹理中提取出RGB值的分量(当前像素)到我们的临时RGB unsigned char变量。一旦完成,我们

需要标记出当前tilepresence在当前像素上(使用我们前面创建的函数), 临时RGB变量乘以结果,并加上我们的总RGB数量。现在我们需要设置前面解释的代码:

// get the current color in the texture at the coordinates that we

// got in GetTexCoords

m_Tiles.textureTiles[i].GetColor(uiTexX, uiTexZ, &ucRed, &ucGreen, &ucBlue);

// get the current coordinate’s blending percentage for this tile

fBlend[i] = RegionPercent( I, InterpolateHeight(x, z, fMapRatio ) );

// calculate the RGB values that will be used

fTotalRed += ucRed*fBlend[i];

fTotalGreen += ucGreen*fBlend[i];

fTotalBlue += ucBlue*fBlend[i];

之后我们需要循环处理四个tiles 我们然后设置纹理上的像素颜色,然后对下一像素重复这一过程。当我们完全完成产生纹理完成值时, 我们使用我们的图形API来设置!

Improving the Texture Generator 改进纹理生成器

好的,我要倒下了。我们还没有准备就绪。我们的纹理生成函数还有一些纹理。这些问题是:

n         我们仅仅可以使用一种分辨率或在这之下,或与我们高度图相同的方式来创建纹理。

n         如果解决这个问题,那么我们仅可以创建在我们的纹理tiles之下分辨率的tiles

然而这两个问题是关联在一起的。让我们从高度图的分辨率开始。

Getting Rid of the Heightmap Resolution Dependency 摆脱高度图的分辨率依赖性

我需要让用户选择任何他想要的纹理大小。(好,几乎任何纹理大小。我想应该是2N次方)。前面的纹理生成函数中,我们输入巨大的一系列循环,我们需要计算出高度图内纹理地图像素的比率,可以这样完成:

fMapRatio = (float)m_iSize/uiSize;

然后我们需要创建一个函数将这个值和我们展开的高度图进行插值。我们将插值划分成两个部分: 一部分是X轴到Z轴。我们将获取两个部分结果的平均值,作为我们插值的高度。最后也许最好的方式是这个东西,但是它可以工作,工作的很快!

这个函数,我们需要三个参数。前两个为X, Z坐标,我们获取它们。这非常high, 像这样,超过高度图范围。第三个是我们计算出的纹理图像素比率(fMapRatio)。函数内,我们将缩放(x,z)坐标使用比率。计算出X轴的插值像这样:

// set the middle boundary

ucLow = GetTrueHeightAtPoint( ( int )fScaledX, (int)fScaledZ );

// set the high boundary

if( (fScaledX+1) > m_iSize )

       return ucLow;

else

       ucHighX = GetTrueHeightAtPoint( (int)fScaledX+1, (int)fScaledZ);

// calculate the interpolation (For the X axis)

fInterpolation = (fScaledX – (int)fScaledX);

ucX            = ((ucHighX-ucLow)*fInterpolation)+ucLow;

正如你看到的,我们这么做获取了低高度的值给第一个东西。然后我们核对看它是否超过高度图下一个高度。如果没有,那么我们必须满足低的值。如果下个高度在高度图上,那么我们可以获取它并准备为两个值插值。我们获取不同的浮点值Xunsigned char X值。(它将低值的精确到度,将定义出总的插值。)下个计算,我们计算沿着X轴的插值。然后同样为Z轴做一次将两个结果合并,并除以2这样就完成了。

Getting Rid of the Tile Resolution Dependency 摆脱Tile的分辨率依赖

好的,我们差不多了。我们仅仅才完成一个计算,我们还需要排除tile大小的限制。解决方法也许会让你惊讶,为什么我这么说? “ 好,相信我,这需要很长时间的一个计算来解决,所以感觉很坏。我们需要重复tile!我创建了简单的函数来新建纹理坐标并重复到我们的纹理。它是这样的:

void CTERRAIN::GetTexCoords(CIMAGE texture, unsigned int* x, unsigned int* y)

{

       unsigned int uiWidth = texture.GetWidth();

       unsigned int uiHeight = texture.GetHeight();

       int iRepeatX = -1;

       int iRepeatY = -1;

       int i = 0;

       // loop until we figure out how many time the tile

       // has repeated (on the X axis)

       while(iRepeatX == -1)

       {

              i++;

              //if x is less than the total width,

              // then we found a winner!

              if(*x<(uiWidth*i))

                     iRepeatX = i-1;

       }

       // prepare to figure out the repetition on the Y axis

       i = 0;

       // loop until we figure out how many times the tile has repeated

       // (on the Y axis)

       while(iRepeatY == -1)

       {

              i++;

              // if y is less than the total height, then we have a bingo!

              if( *y<(uiHeight*i))

                     iRepeatY = i-1;

       }

       // update the given texture coordinates

       *x = *x-(uiWidth*iRepeatX);

       *y = *y-(uiHeight*iRepeatY);

}

这个函数的大部分由两个while循环组成主要目的是为计算出需要重复多少个纹理,以在它到达纹理坐标时传递参数。之后计算,我们缩放他们使用纹理的范围值。(我们不想试着开出纹理范围外的信息。这将导致一个错误,错误是不好的。)

那么!我们的纹理产生函数现在完成了!如图3.8这个demo, 你将注意到菜单里的新选项叫做纹理映射。这个选项,你可以产生高分辨率的纹理并保存纹理到当前目录。谈到这个demo, 你可以看到你努力工作的成果在CD内的Code"Chapter3"demo3_2. 打开工作区用Microsoft Visual C++,开始有趣的把玩吧。

Using Detail Maps 使用细节地图

1024x1024是相当大的数据量,我想在我们的纹理上达到这个细节。必须通过另外一种方式来达到我们不浪费资源又兼有这些细节愿望。好的,不要多想!一种酷的方式可以使用细节地图来为你的地形添加这些细节。细节地图是如图3.9那样的灰度图重复多次加到地形上并添加上细微差别,像裂缝,凹陷,岩石和其他有趣的东西。

为你的地形引擎添加细节地图支持是个简单的过程。添加一些加载和卸载细节图的管理函数,函数将允许用户决定在地形上重复填充多少次,然后你必须编辑你的渲染代码。最难的决定是是否使用硬件多纹理或仅仅使用两个分离的渲染过程。因为地形网格变得相当大,你最好坚持赌上硬件多纹理。实现硬件多纹理在这本书之外了,但是如果你不知道怎样利用你的图形API做的话,它的实现也是相当简单的,你将学习它。这种情况是你不知道一种好的地方去学习API拿出OpenGL Game Programming(Astle/Hawkins)Special Effect Game Programming with DirectX 8.0)McCuskey), 都是Premier Press出版的——一个伟大的出版商,所以我这么说!

编辑你的渲染代码,仅仅设置基本纹理颜色(例如,我们前面产生的) 作为第一个纹理单位,然后设置你的细节纹理到第二个纹理单位。记得怎样为纹理坐标添加上颜色是这样计算的?

fTexLeft = ( float )x/m_iSize;

fTexBottom = ( float)z/m_iSize;

fTexTop = ( float)(z+1)/m_iSize;

好的,我仅仅必须做下轻微的修改这些计算获得我们细节纹理的纹理坐标:

fTexLeft = (float)(x/m_iSize)*m_iRepeatDetailMap);

fTexBottom = (float)(z/m_iSize)*m_iRepeatDetailMap);

fTexTop = (float)((z+1)/m_iSize)*m_iRepeatDetailMap);

m_iRepeatDetailMap是用户希望重复填充地形的次数。最难的部分是使用你的图形API设置多纹理,大拿市如果你使用OpenGL, 我将都给你设置好。 (Yeah, 我知道我是个和蔼的家伙,并且如果你感激要报答我的话,我的生日在311日!)看你的地形上的细节地图,使用256x256的程序式纹理没有使用细节图(如图3.10左边) 而使用了256x256细节图的在3.10右边。看这么多的细节看来是正确的?最好的部分是添加这些昂贵的细节是简单的。拿出CDCode"Chapter3"demo3_3下的demo3_3, 在运动中看新的细节图。仅仅是改变了休息T关闭细节图, D开起细节。(默认是开启的。)

摘要 Summary

我们学习了一些关于纹理化地形的东西。我们从简单的纹理出发(延长但个纹理覆盖整个地形)然后我们踢开它调整到程序式纹理生成。我们最后使用了简单而酷的技术叫做细节贴图。在下一章,我们将学习提高我们地形更多真实感的步骤: 光照化。如果喜欢纹理技术,你也许想看到Tobias Franke的文章标题”Terrain Textuere Generation” 或者Yordan Gyurchev’s 的文章标题”Generating Terrain Textures.”你也许对Jeff Lander的文章标题”Terrain Texturing,”感兴趣,描述了动态纹理tiling的解决方法。

聚焦3D地形编程第四章光照化地形

翻译: 邵小宁 microsoftxiao@163.com 神杀中龙
原著  《Focus on 3D Terrain Programming》
翻译的烂请见谅

纹理化地形致使我们的地形进入一个新的细节等级,而光照化纹理将使整体进入新的真实感境界。问题是这样的:我们如何快速的光照我们的地形同时有保持高度的真实感?好,所有的技术让我将所有快速光照化地形的技术传授给你。我不会讲述复杂的全局光照算法
(虽然我将为你指出一些获取这些信息的地方)因为地形真实感大概需要整本书来讲述。好的,下面是本章的议程:

n         基于高度的光照

n         硬件光照

n         应用光照图到地形

n         超酷的斜坡光照算法

我会继续简短的讨论光照因为我了解你(否则如果你不知道,而我知道怎么做)想从比较酷的地形算法开始,我将在写三章就描述。让我们走吧!

Height-Based Lighting

Height-based lighting是简单且非真实的,但是它是光照的一种类型,所以我至少要简要的描述它。我们在第二章的demos里采用基于高度的光照,所以如果你不知道的话在这之前你就习惯了.

Height-based Lighting仅仅是——在顶点的高度上进行光照。高顶点(从地形碎片的高度数据)是比低顶点更亮一些,and that’s all there is to it.到那种程度我们需要使用我们的GetTrueHeightAtPoint函数(member of the CTERRAIN class)来求出在当前(x,z)位置(范围在0-255)的明亮像素从高度图里,and that is our brightness value. 它是简单的!如图4.1 加强这个概念。

在图中,顶点A几乎是黑的,顶点B有点亮,而顶点C将是完全被照亮了(白色)。你可以——用三段文字和一符图来结实基于高度的光照。

现在你知道了什么是height-based lighting, 但是你怎样计算你代码中的光照值呢?好的,它相当简单,考虑你已经加载了高度图。例如,假设你试着计算(157,227)的亮度。好的,顶点的亮度将从高度图中获取一个高度直,很简单。

ucShade = GetTrueHeightAtPoint(157, 227);

ucShade是一个我们存储光照值的变量,而GetTrueHeightAtPoint是从我们高度图中计算出信息在顶点(157,227)这个位置,范围从0()255(明亮)。现在,让我们为我们的光照添加上颜色。

Coloring the Light Source 光源色彩

我们并不总是想让我们的光颜色是灰色(黑到白)。大部分时候,我们喜欢我们光是各种各样的色彩。例如,如果没有云的时候,用户希望有漂亮的日落,所以我们希望我们的光颜色逐渐变为橘黄色,粉红色或者紫色。我们需要创建一个向量以存储我们的光颜色信息并且一个简单函数类设置光的颜色。(我们想将光的值指定在0.0f-.0f之间。你将在下一步找到为什么。)然后,我们可以找到前面的值再使用下面等式乘以每个RGB光颜色。

Intensity = shade*color

现在,使用那个等式,我们可以计算出RGB颜色的分量。然后发送给渲染API:

ColorToAPI((unsigned char)(ucShade*m_vecLightColor[0]),

           (unsigned char)(ucShade*m_vecLightColor[1]),

           (unsigned char)(ucShade*m_vecLightColor[2]));

ucShade是一个我们前面计算出的亮度值,而vecLightColor是我们光的颜色。现在拿出demo4_1(CDCod"Chaptter4"de4_1)如图4.2。如果你需要恢复demo的控制,看表4.1

当你看到这个图时,你注意到地形比较低的区域暗而高的区域是亮的。这说明height-based lighting正在发挥作用: 高区域亮,低区域暗。问题是这是什么算法?首先它是难以置信的不真实。这个算法不会考虑太阳光直接照到低区域的情形,而导致那个区域非常亮。问题如图4.3

你看,在4.3内,阳光照到顶点AB, 但是根据我们使用height-based lighting技术计算出来的光信息,顶点A是亮的而顶点B是暗的,而在这里这是错误的(如图所示)

另外的问题是这个技术给你带来了非常小的自由度对于你想要的地形光照。现在我们需要继续并讨论更灵活和真实的光照化地形。

Hardwre Lighting 硬件光照

这个几书有两个主要的问题。首先,它依赖于高级API所以我不会给你展示代码或给你演示。第二,它对于动态地形网格是没有用的——这个性质我们将在后三章来处理。因为这些原因,我将给你一些译本的细节实现。

硬件光照许哟啊你计算出每个三角形的面法线。最好的时候是在你demo进行预处理时;那样,程序的计算量不会陷入困境。之后计算法线,你仅仅发送它到API给当前三角形到你想要渲染的地方,然后就完成了。

警告

必须确认你恰当的在做任何事之前设置了你的API,开启硬件光照。硬件光照可以偶尔有个痛苦的甚至,所以如果你的地形没有或只有一点光请不要太惊讶。你需要确认你已经设置好了光源(使用正确的衰减,漫反射/镜面反射/环境光值日,等)。之后设置你的光,确定你开启了光源以及光分量的API。许多人向我问起关于硬件光照的问题, 75%都是他们已经忘记了开启他们的光源!这简直难以统计。

对于静态地形这个技术工作的很好其性质我们也已经在过去的两章使用了并且其中一个就是在本章使用的。它轻微的产生了动态光白天/野外模拟。然而,因为硬件光照主要基于顶点,动态地形网格加一点硬件光照看起来则不那么好。(动态网格经常移动顶点。)这是我们的一点硬件光照讨论。希望你别无视。

Lightmapping 光照图

遍及本书我们将使用光照图。一个lightmap正确的像高度图(在第2章讨论的)除了将高度信息进行取代,光照图包含的仅仅是光信息。所有加载,保存和卸载光照图的代码与高度图的一样(除了光照操作函数与高度图函数处理的变量不同), 所以我就不在这些函数上浪费宝贵的时间了。我们的光照图信息也将被存储在灰度的RAW纹理当中,就像我们的高度图,在光照图中除了只有属于光照的信息。例如,看高度图还有高度图4.4那么看到4.5达到的结果。看怎样将光照图影响到地形光照?

看图4.5光照怎样在球面通过球形来表示出图4.4的光照图? 这就是为什么我们使用光照图:可以为地形碎片定义出精确的光照类型。并且因为你可以一预先创建光照图,你可以使用各种各样的产生它们的算法。通过多种方式你可以产生光照图。这些方式很复杂——但是看起来很像全局照明技术。下一节我将为你展示创建光照图的方法。

之后你已经将光照图采用加载高度图的方式加载完毕,你需要创建一个函数将求出每个像素的亮度:

inline unsigned char GetBrightnessAtPoint(int x, int z)

{ return (m_lightmap.m_ucpData[(z*m_lightmap.m_iSize)+x]); }

记住我们怎样使用GetTrueHeightAtPoint来为height-based lighting获取亮度信息在demo4_2? 我们必须替换的调用GetBrightnessAtPoint并且设置它!看这些光照技术是多么容易?拿出位于CD上的Code"Chapter4"demo4_2demo4_2上,并且试着创建一些你自己的小的高度图然后观看它们开启和关闭的情形。我创建了如图4.6一个有趣的光照图,结果如图4.7

As if it weren’t obvious before, I am suffering from an extreme combination of having too much time on my hands and having far too much with this book!

光照图是种在游戏紧要时候强烈需要高级特性时的一种实现手段。我谈到了如何将其粘贴到地形上,但是更多高级光照图概念将集中在如何生成光照图。(我在稍后将展示给你这个算法。)

许多游戏,像Max-PayneQuake 2, 使用了radiosity方法。(Visit http://freespace.virgin.net/hugo.elias/radiosity/radiosity.htm 如果你喜欢技术解释。) 注意那是两款室内游戏,而地形是室外的话题,你也许会猜测,我必须找到另外的我们的计算光照图的技术。幸运的是,有许多不同的技术可以实现(几乎使人混乱,事实上)。我将提供一个简单的算法,但是之所以说它简单并不是说它不强大,我将在最后提及全局照明算法。

Slope Lighting 斜坡光照

Let me get this out of my system right now: 这是一个我使用的最长时间的算法。它难以置信的易于似乎用,并且它提供了非常锐利的视觉效果。 Slope lighting是一种简单的光照技术,用于根据顶点和相关附近的顶点的高度来进行着色。

Okay, Slope Lighting Is Cool, But How Is It Performed? 好的,斜面光照是酷的,但是怎样执行它呢?

斜面光到地形上,我们将获取从此顶点到当前顶点(方向将被指示通过光方向)然后减去当前顶点的高度。基本上,我们核对看如果其他顶点正被一个阴影投射到当前顶点。我认为这样现实例子的完美再现。假设你站在一个大楼的前面,阳光从你的视点照向你。这个大楼对你投射了阴影,如图4.8

如图所示,光源射线将不能照到你的位置,被大建筑阻挡了。结果是你位于阴影的区,使你表现出人获得的光射线很少而发暗。同理完成斜坡光照——着色顶点似的光源的射线被高的顶点所阻挡。概念如图4.9所示。

正如你看到的,光源发射出各种光线(在一个无限空间内,实际上)但是,这种情况下,它们将无法到达顶点B因为顶点A阻挡了光线。仅仅因为顶点B接受不到直射光不意味着将完全黑暗;一些光被其他的顶点接受并反射,产生轻微的照明。A顶点将不再完全黑暗。

这个算法中有一个缺陷,当你设置光方向时,它必须是45度的增量。例如,图4.1左边的光照使用(1,1)的方向。如果我们想移动光到左边,我们必须改变光方向到(0, 1)这将导致光移动了45度取代了平滑移动的过程。一次2-5度。

现在,即使你注意了,两个图区别不是巨大的。因此,你可以很容易的实时的改变光的方向。大部分用户不会注意到跳跃式的着色。

Create a Slope-Lighting System 创建斜坡光照系统

在开始编写slop-lighting代码前我们需要添加一点新的变量在CTERRAIN类内。(如果你认为这个类现在已经有太多特性了,just wait until later!)我们需要可以定义亮度的最小值/最大值给我们的地形,因为,我前面说过,很少有顶点完全变黑。我们也需要柔和光的变量和光的方向,both of which need to be used if we want to achieve realistic results. 另外,我们希望函数将容易让用户自定义slope lighting系统的参数:

inline void CustomizeSlopeLighting(int iDirX, int iDirZ,

                            float fMinBrightness,

                            float fMaxBrightness,

                            float fSoftness)

{

       // set the light direction

       m_iDirectionX = iDirX;

       m_iDirectionZ = iDirZ;

       // set the minimum/maximum shading values

       m_fMinBrightness = fMinBrightness;

       m_fMaxBrightness = fMaxBrightness;

       // set the light’s softness

       m_fLightSoftness = fSoftness;

}

Dynamically Creating Lightmaps 动态创建光照图

在这之前,你回想我说过光照图是危急时的算法?好,现在我们将为了使用地形而创建光照图。你可以选择为每帧计算光照图(我描述了这个过程不是很慢的算法。)但是好的计算方式是在开始时仅计算一次,然后在需要的时候再计算。这里首先我创建了函数的一半:

void CTERRAIN::CalculateLighting(void)

{

       float fShade;

       int x, z;

       // a lightmap has already been provided, no need to create one

       if(m_lightingType == LIGHTMAP)

              return;

       // allocate memory if it is needed

       if(m_lightmap.m_iSize!=m_iSize || m_lightmap.m_ucpData == NULL)

       {

              // delete the memory for the old data

              delete[] m_lightmap.m_ucpData;

              // allocate memory for the new lightmap data buffer

              m_lightmap.m_ucpData = new unsigned char[m_iSize*m_iSize];

              m_lightmap.m_iSize = m_iSize;

       }

       // loop through all vertices

       for( z=0; z<m_iSize; z++)

       {

              for( x=0; x<m_iSize; x++)

              {

                     // using height-based lighting, trivial

                     if(m_lightingType == HEIGHT_BASED)

                 SetBrightnessAtPoint(x, z, GetTrueHeightAtPoint(x, z));

              }

       }

}

直到这里,你将可以领会任何东西。我们预制作光照图出发。如果这么做了, 那么我们不能在光照图内写过多的信息。然后我们需要为光照图分配内存。之后,我们在所有顶点遍历,光照图需要和我们高度图有相同的大小——并检测用户是否使用height-based lighting。如果是,我们设置当前像素和高度像素相同。其余的函数将进行slop lighting吹,所以我分两段描述:

// using the slope-lighting technique

else if(m_lightingType == SLOPE_LIGHT)

{

       // ensure that we won’t be stepping over array boundaries by

       // doing this

       if( z>=m_iDirectionZ && x >= m_iDirectionX)

       {

              // calculate the shading value using the “slope lighting”

              // algorithm

              fShade = 1.0f-(GetTrueHeightAtPoint(x-m_iDirectionX, z-m_iDirectionZ) –

                            GetTrueHeightAtPoint( x, z ) )/ m_fLieghtSoftness;

       }

}

这里将发生slope-lighting计算。正如你看到的,我们减去前一顶点的高度值——在用户指定的方向上——即当前顶点。我们试图看到之前的顶点投射了多少阴影。我们然后切分光柔和且从此值减去1.0f之后我们再分开。我真的不在谈论怎样处理柔和光了,所以看图4.11, 使用两个等级不同的柔和光.

现在,放在这个函数里:

       // if we are stepping over a boundary, then just

       // return a very bright color value (white)

       else

              fShade = 1.0f;

       // clamp the shading value to the minimum/maximum

       // brightness boundaries

       if( fShade<m_fMinBrightness )

              fShade = m_fMinBrightness;

       if( fShade>m_fMaxBrightness )

              fShade = m_fMaxBrightness;

       // set the new brightness for our lightmap

       SetBrightnessAtPoint(x, z,

                     (unsigned char )( fShade*255 ) );

}

}

}

}

在这节,我们夹取fShademinimum/maximum亮度范围并且在光照图内设置当前点的亮度。demo4_3(CDCode"Chapter4"demo4_3)我添加了很酷的对话框(4.12)让你完全可以动态自定义slop-lighting系统。祝你愉快。

摘要 Summary

本章已经快速的从头到尾讲述了简单的光照技术以使你的地形的真实感提升一个层次。我们谈论了height-based lighting硬件光照, 光照图,还有斜坡光照。斜坡光照大概是你简单地形演示最好的选择。我没有时间提及非常酷的全局光照技术,在HoffmanMitchell的文章 “Real-Time Photorealisitic Terrain Lighting”,但是如果你对它感兴趣的话,它干脆而拥有价值。无论如何,你自己最好做好准备——你已经要进入地形编程的核心部分,将由所有高级地形算法信息组成。

参考 References

1 Van Noland, Charlie. “Slope Lighting Terrain. “ 2002.

http://www.gamedev.net/reference/articels/article1436.asp

 

 

聚焦3D地形编程第五章GeomipMapping for the CLOD

译者: 神杀中龙 邵小宁 microsoftxiao@163.com
翻译的烂请见谅
原著 《Focus on 3D Terrain Programming》

Woohoo!你现在将要学习地形编程的核心部分了,它们是由难以置信的复杂算法。实际上,那是谎话。本章我将解释三个算法,因为它们的简单和高效所以选择了它们。And , for once in this book, 下面的列表是本章将要讲述的内容:

n         解释持续层次细节的意思

n         geomipmapping的背后理论

n         实现geomipmapping的方法

为了简单那, 我打破了议程的三个部分。不管这些细节,本章将相当长。然而,不要让本章的长度吓到你。The content will be presented, as always, in a fun and simple manner. 注意,虽然我改变了一点学习风格。第567将集中在算法的解释上相比前几章将使用伪码表示。在以后几章,我将为你提供一些演示并且给出我的实现,但是实现是简单的,并且you should use them only in conjunction with the tex. With that said, 我们开始吧。

CLOD Terrain 101

在本书你已经听说了术语 Continuous Level of Detail(CLOD) 持续层次细节很久了,但是现在到了我告诉你实际情况的时候了。CLOD算法, in one sentence, 是一个动态多边形网格,通过增加额外的三角形来得到更多细节。这是个简单的陈述,但是读完本节你将了解更多关于CLOD的东西,并且知道本章结束。不要烦恼现在还不理解CLOD

Why Bother with CLOD Terrain? CLOD地形为什么麻烦?

CLOD算法需要更多的研究,难于编码,平均比brute force实现占用更多的CPU周期。在你脑中,为什么CLOD算法麻烦?它真的简单吗: 为了创建更真实,更多细节,更重要,更快的地形碎片。

More Detail Is Added

Where More Detail Is Needed

CLOD的基本思想是添加更多的细节(更多三角形)是必须的。举例来说,如果我们有一个相当光滑的地形片(看图5.1),我们将想要一个平均更少些的地形(如图5.2)。然而,不是所有更多细节的算法都很糟糕。Geomipmapping可以似的三角形不是很分散但又拥有更多细节,但是Rottger的四叉树算法(在第六章,攀登四叉树)来做。因此,话说CLOD, 总体上,添加了更多细节到区域上,但并不总是这样,但是大部分是这样的,我彻底的把你搞晕了?

Cull Like You’ve Never Culled Before!  

另外实际的CLOD基础算法是它们允许比brute force方法拣选更多的多边形。这意味着这些多边形发送给API但却看不见。举例来说,我们通过geomipmapping实现一系列的地形碎片。如果碎片不可见,我们排除潜在的289个被渲染的顶点(一个17x17的顶点碎片)in one fell swoop. 大量的顶点不会被加载到显卡里,而是被CPU裁剪了。使用简单的方法,我们满足GPUCPU两者,让你的主板同时高兴。

Not Everything Is Happy

In the Land of CLOD Terrain

CLOD地形大陆不是任何事物都是愉快的

虽尽管使用CLOD地形算法有一些缺点,Oddly enough, 世界帮助我们写下了这一段在 Game Developer Magazine里。

主要确定是包括每帧更新多边形网格。

当大部分这些算法还不是很流行时(geomipmapping, Rottger’s quadtree algorithm, and ROAM)被设计。这是因为该算法想要将更多的工作量放置在CPU里而且发送了一点必要的信息到了GPU。从此一直,然而,这些东西有了一丁点的变化。现在我们需要把焦点放在GPUCPU更多些。

Wrapping Up Your Introduction to CLOD Terrain 封装你的初级CLOD地形

明显的,如果geomipmapping, 四叉树,ROAM算法过时了,你将不会再读到它们了。

这意味着某人,某地(heck, 甚至我)哪出了一些优化的,在今天的地形渲染也是最重要的优化。随即,我将解除前面胡说的关于geomipmapping的东西。

Geomipmapping Theory for the Semi-CLOD Impaired

Geomipmapping, Willerm H. de Boer开发,一个友好的GPU CLOD算法。它也是转变到CLOD大陆最完美的简单算法。那么我们继续前进,你也许想要查阅实际geomipmapping白皮书,我放在 CD-ROM里希望给你提供方便。(它的名字叫 /geomipmapping.pdf算法白皮书。)

Simply the Basics 简单的基础

如果你熟悉mipmapping的纹理化概念,那么geomipmapping似乎是很浅显的对你来说。概念同样,除了处理纹理被取代,我们处理地形片上的顶点。推动geomipmapping概念的是你有一个地形片集合。为了解释它,我有大小为5的顶点作为一片(5x5的片)5x5的片将有一系列的等级细节,第0级是最详细的,这样,第2级是较详细的。

看图5.3如果你需要可视化解释每个各种等级的话。在图中,黑色顶点没有被发送到渲染API, 但是白色被发送到了。

如果查阅了Willern de Boergeomipmapping的白皮书,你也许注意到图5.3和白皮书里展示的一点不同。原因是我做的改变了一点为了更清晰,但是现在,just know that I did it for a reason.

到了我们讨论更多geomipmapping的时候了。我前面已经给介绍了些基础,但是现在到了你知道所有东西的时候了….

好,几乎所有东西。我也许会保留些信息。

[译者注: Mip Mapping (Mip贴图) 这 项纹理贴图的技术,是依据不同精度的要求,而使用不同版本的材质图样进行贴图。例如:当物体移近使用者时,程序会在物体表面贴上较精细、清晰度较高的材质 图案,于是让物体呈现出更高层、更加真实的效果;而当物体远离使用者时,程序就会贴上较单纯、清晰度较低的材质图样,进而提升图形处理的整体效率。LOD(细节水平)是协调纹理像素和实际像素之间关系的一个标准。一般用于中、低档显卡中。

注意 Mip-Mapping和三线性过滤有区别。]

[

译者注 MipMapping 和三线性区别

Mip-Mapping

  我第一次看到Mip-mapping技术是在游戏QUAKE里,而现在这种技术早已是随处可见了。这种技术是由Williams1983年发明的,“Mip”这个名称起源于“multum in parvo”,大概就是在一小块地方有很多东西的意思。

  具体说来,Mip-Mapping的思想就是构建一套纹理,总共需要大约1.3倍的内存。其中,每块子纹理是通过对父纹理过滤而得到,它的长和宽都是其父纹理的1/2,其面积为父纹理的1/4。接下来,在应用的时候,你根据距离选取最合适的一块来进行映射,实践证明,这种技术虽然简单,但对提高纹理映射的质量确实非常有效。

  通过Mip-Mapping,可以为较小的多边形映射上面积较小的纹理,这对减少纹理的扰动大有好处。举个例子,你有一块256x256大小的纹理,当它开始向远离观察者的方向开始移动时,你会看到它开始闪烁和颤动。这种现象的出现是因为我们把一大块纹理映射到一个很小的区域而引起的。你可能在上一帧时,画的是纹理中(50,20)处的像素,到了下一帧,却画的是纹理中(60,30)处的像素。如果这两个像素相差很大,你就会观察到前面所说的现象了。总的来说,这种剧烈的纹理座标的变化,会损害图像的品质,并且影响CACHE的效率,而Mip-Mapping无疑是解决这个问题的好办法。

Tri-linear Interpolation

  在介绍了双线性插值和Mip-Mapping以后,该来讲讲三线性插值了。其实三线性插值也很简单,它就是前两种技术的结合。它在对Mip-Mapping的每块纹理做双线性插值的同时,还要对Mip-Mapping中相邻的两块纹理按距离再做一次插值。既算出较大的一块纹理上的某点双线性插值像素值和较小的一块纹理上的某点双线性插值像素值,再按目标同两块纹理的距离做一次类似的插值。

  使用三线性插值,可以消除Mip-Mapping里纹理切换(既上一帧时用的是某个大小的一块纹理,而下一帧时又换了一块的情况)时的突然变化,从而可以提供很平畅的高质图像输出。

  同前两种技术相比,三线性插值的运算量非常大,目前只能依靠硬件来实现。

原文链接: http://www.chinagcn.com/blog/?43/viewspace-702

]

正如我前面说的,geomipmapping与纹理的MipMapping类似除了我们使用陆地片来替代纹理片。我们需要怎么做呢,从3D空间用户的出发点(摄影机眼睛的位置), 使所有的片的大部分细节都围绕者摄影机因为这些片是用户看得见的。

At a certain distance way, 我们将片切换到低的细节上。而更远的距离,我将切换到甚至更低的细节。图5.4是可视化解释。

如图所示,这些片是摄影机位置处的细节层次(LOD)0以为这这些片是最高等级的细节。这些片一旦拉远,细节将调整到1是第二高的细节。如果距离摄影机更远的话,片将降低到等级2是图中最低的细节。

Triangle Arrangement Made Easy

前面,你也许注意到5.3排列出的三角形不同于我提供给你的geomipmapping paper中的排列。这种情况你不需要立刻访问白皮书,看图5.5看,论文的建议三角形这么排列。

这个排列也许像更好的想法,and it is for the most part. (警告: I’m going to get slightly sidetracked right here.)三角形带如果你计划使用顶点缓冲渲染片的话,这是我给你的建议。然而,因为使用顶点缓冲区进行渲染辉高度依赖API,我选择使用直接渲染模式因为它更容易转换到其他API语法。使用顶点缓冲区渲染对地形实现来说更高速因为它的函数当你发送每个顶点,纹理坐标,颜色等到API时是单独的。另外,大部分图形卡更喜欢每个顶点信息被发送到顶点缓冲的形式。最后,我推荐你使用顶点缓冲区渲染地形。你将获得极大的速度,完成这个是相当有价值的。如果你喜欢看一个Direct3D顶点缓冲区实现geomipmapping-esque的例子,可以看“Simplified Terrain Using Interlocking Tiles” Game Programming Gems, 2里。

总之,该回到前面的话题了。如图5.3是我们将要渲染好的排列好的片。这个排列对我们来说是非常有益的:它允许我们更容易的跳过被渲染的顶点当我们需要时,which is quite often. 接着下一个话题。

Hacks and Cracks, But Mostly Just Cracks   砍,爆裂,砍,更多爆裂

常常当你处理CLOD地形算法时,你必须处理裂缝的问题。裂缝出现,geomipmapping出现这种情况,当高细节挨着低细节时(看图5.6)

正如你看到的图,片左边比右边细节高。我们的问题位于点AB。问题是当A点比B细节高时,这意味着左边的精确度高于A点,但是右边的片刚刚是从那个高度平均过来的位于高度之下。这个破裂的东西也许不像个大问题,但是图5.7所示,展示了我的geomipmapping 实现没有裂缝吗?

这不是一个平滑的地形,是吧?这是所有洞穴的景色。我们来解决它!

Crack-Proofing Your Geomipmapping Engine 你的Geomipmapping引擎裂缝实验

裂缝穿透你的geomipmapping地形是比听着来的容易。你添加些有意的东西(that would be me)解释这个概念给你,整个处理过程是容易的好,好像是容易的。

我有两个可能的解决裂缝问题的方法。一个方法是在低细节地方添加顶点片以至于这个片区域将达到和高细节区同样的细节等级。这个解决方法是丑陋的,可是,它意味着我们必须对片进行重排列(添加其他的三角形扇)

另外的方法是从更多细节片内删掉顶点。这个方法更有效一些。看图5.8看删掉顶点多么容易解决裂缝。

Where Art Thou Crack? 你在哪里裂缝了?

你知道怎么导致裂缝和怎样解决它们。真正的问题是: 你如何确定它们?基本上,当你当前渲染片,你需要测试片周围(看图5.9)看是否有低细节。如果它们是,你知道你需要忽略一些顶点了。

测试每个片不困难。你将需要实现一系列的简单的if-else语句。(Pseudo-code如下).

If LeftPath.LOD is less than CurrentPath.LOD

       RenderLeftVertex = true;

Else

       RenderLeftVertex = false;

If RightPath.LOD is less than CurrentPath.LOD

       RenderRightVertex = true

Else

       RenderRightVertex = false;

If UpperPath.LOD is less than CurrentPath.LOD

       RenderUpperVertex = true;

Else

       RenderUpperVertex = false;

If LowerPath.LOD is less than CurrentPath.LOD

       RenderLowerVertex = true;

Else

       RenderLowerVertex = false;

看多么简单啊?之后测试,渲染你的三角形扇,你跳过粗糙片的方向顶点。例如,右边片是低级等级的,那么你的当前片是高等级细节(行列翻倍被渲染), 那么你仅仅想要跳过这些顶点到远处的片(看图5.10)。

警告:

注意你省略的仅仅是必须的顶点。否则,你将结束整个片而不意味着省略。例如,图5.10, 片由多行多列组成的三角形扇。你不想省略右边的顶点;你仅仅想省略右边的顶点在最右列。

这是你简单的geomipmapping理论!现在到我们实现我们学过所有东西的时候了。

Implementing Geomipmapping for the Very Slightly CLOD Impaired 实现Geomipmapping CLOD

你了解了geomiapping背后的基础,但是现在我们需要实现它。这将使你的大脑负担太重。做完它将是最难的部分,照常,我们将每次做一步。来点咖啡,锁好你的们,然后来点好听的音乐!

Path It Up 拼凑

由于geomipmapping是由一系列碎片组成的,大概可以从一个好的想法出发,创建一个patch数据结构。这个结构不需要包含太多信息,而且我们至少需要包含,最好。实际上,这将是从本书里你见过的最小的结构。不要太习惯漂亮的大小!

所有的patch结构真的只需要两个变量。一个变量将记录当前细节等级,另一个变量存储从patch到摄影机的位置的距离。保持头脑正常!整个patch数据结构。在这里,代码:

struct SGEOMM_PATTH

{

       float m_fDistance;

       int m_iLOD;

};

它也许是微笑结构,但是记住: 大东西将由小东西组成。虽然小,我们将经常使用它,所以确认你已经花了几小时记住它的成员。

Creating the Basic Geomipmapping Implementation 创建基本的Geomimapping实现

Yeah, 两个成员的数据结构不要哭哭啼啼的。现在我们将开始geomipmapping引擎的重负荷工作——geomimapping类。开始,我们需要获得我们的patch信息指针,我们将动态的分配一些指针在我们的demo内。继续,我们需要计算出patch大小(顶点数)和地形的边需要多少个patchs

警告

geomipmmaping实现是基于2N次方+1个像素块高度图。这意味着你不能使用midpoint displacement不规则高度图产生高度图。所有你的高度图只能使用fault formation产生。

Patch的大小由用户来指定,所以我们可以让它指定patch大小当它初始化类时。(我趋向于严格的大约17x17顶点提供一个漂亮的细节和速度。本章将解释为什么采取这么大。)

Geomipmapping Initialization Geomipmapping初始化

为了初始化geomipmapping系统,我们需要用户指定他们的patch大小。(我将恢复我的建议17x17的顶点。)After we have that, 我们初始化系统。

首先我们许哟啊计算地形每个边的片。我们描绘出高度图和划分出独立的patch, 如图5.11

P描绘一系列的每个边, h描绘出高度图的大小, s描绘出单独patch的大小。使用一个等式,看图5.12我们将在之后插入一个等式。

之后我们计算每个边的patch我们需要为每个片分配正方形的缓冲区。(这个值我们仅仅完成这个计算。)

m_pPatches = new SGEOMM_PATH[SQUARE(m_iNumPathcesPerSide)];

继续,虽然它不是初始化必要的部分,我想计算出这个片可以达到的最大细节等级。注意最大等级是最小细节等级,最大细节等级是0如果等级增加,细节减少。这的计算:

iDivisor = m_iPatchSize – 1;

while(iDivisor > 2)

{

       iDivisor = iDivisor >> 1;

       iLOD++;

}

所有我们做的许哟啊多少个循环获得iDivisor下至2iDivisor到达2时,我们不会继续减,并且我们计算一些我们处理的细节。例如17x17片大小,我们的最大细节为3意味着我们有四个不同的细节等级(0, 1, 2, 3)从任何单个片选择。初始化吧!现在我们将移动到可怕的下一节。

Geomipmapping Shutdown 清理Geomipmapping

关闭geomipmapping系统是简单的。我们需要释放我们为patch缓冲分配的所有内存和让类成员变量都休息。

Geomipmapping Maintenance Geomipmapping维护

不像我们前三章的地形, CLOD地形算法需要在每帧更新(这也是为什么叫做Continuous Level of Detail的原因)。大部分基于CLOD的算法需要在更新阶段进行一些维护工作,但是geomipmapping不那么做。这个工作我们必须在更新函数彻底到最小限度时处理;它简单由我们的patch来计算得到。

为了实现我们的geomipmapping更新函数,我们需要为每个patch进行更新; 因此,我们需要制作一个双循环:

for(z = 0; z<m_iNumPatchesPerSide; z++)

{

       for(x = 0; x<m_iNumPatchesPerSide; x++)

       {

       }

}

首先的东西我们需要在循环内计算从摄影机位置到当前片中心的距离。这个计算将是你在高校熟悉的数学,他们操练一个距离公式在你的大脑里。这种情况你可以像我一样所有课在睡觉,这里重复。

Dist = sqrt((x2-x1)^2 + (y2-y1)^2 + (z2-z1)^2); 3D距离公式

带着这个灯市,看图5.13变量将被插入到等式中。

这是距离计算的代码;

m_pPatches[iPatch].m_fDistance = sqrtf(

                                                 SQUARE( ( fX-camera.m_vecEyePos[0] ) )+

                                                 SQUARE( ( fY –camera.m_vecEyePos[1]) )+

                                                 SQUARE( ( fZ –camera.m_vecEyePos[2] ) ) );

之后我们从摄影机开始计算距离,我们可以计算出该片的细节等级。代码,我计算出硬编码的距离。(我的代码仅仅有一点是这样的;你也许想撇去这样的代码)对于你的引擎,虽然,你想要更多严格的方式计算出细节等级。例如,在geomipmapping白皮书内, Willem de Boer描述了屏幕像素决定算法以至于当片改变登记,too mch popping won’t be present.

Popping是当多边形改变到不同的细节去出现的。这个改变也许或不会很明显。例如,从1改变到0没有引起popping因为1 等级片仍然是端正的细节(17x17至少) 然而,3级改变到2导致一点popping因为你从8个三角形到了32虽然那些同样比率的三角形被在第一个片添加了,32级是明显的。CLOD算法一个主要的目标是减少甚至完全消除popping。我将在稍后部分讲述它。

总之,这本书要实现我的geomipmapping我简单硬编码远离了LOD(我想排除和你的经验差距,更容易理解。是的,我知道我是个和蔼的家伙。) 这是改变的LOD代码小片:

if(m_pPatches[iPath].m_fDistance < 500)

       m_pPatches[iPath].m_iLOD = 0;

else if(m_pPathes[iPatch].m_fDistance<1000 )

       m_pPatches[iPatch].m_iLOD = 1;

else if(m_pPatches[iPatch].m_fDistance<2500)

       m_pPatches[iPatch].m_iLOD = 2;

else if(m_pPatches[iPatch].m_fDistance>=2500)

       m_pPatches[iPatch].m_iLOD = 3;

这些距离有效的速度整合和细节。如果demo有点在你的视频卡迟缓,你也许需要改变下这些距离。之后的章节我们将优化下速度,所以不要失望!那么更新所有geomipmapping片吧现在,至少。现在到了地形实现最有趣的部分了:渲染它!

 

 

 

原创粉丝点击