本教程将重点介绍场景中的渲染地形。我们将介绍需要完成的基本设置,并将介绍使用带地形的照明。我们还将简要介绍使用Skyboxes,Skydomes和Skyplanes模拟天空。最后,我们将解释如何向场景添加雾效果。
可以在样本目录Samples / Terrain / include / Terrain.h中找到本教程的完整源代码
Note
有关如何设置Ogre项目并成功编译的说明,请参阅设置OGRE项目。
忽略屏幕截图中的FPS统计信息。它们是在老旧计算机上渲染的。
An Introduction to Terrain
对于旧版本的Ogre,我们不得不使用“地形场景管理器”来渲染场景中的地形。这是一个独立的SceneManager,与你的其他管理器一起运行。新的Ogre Terrain组件不需要使用单独的管理器。自Ogre 1.7(Cthugha)以来,有两个地形组件:Terrian and Paging。Paging组件用于优化大型地形。它将在后面的教程中介绍。本教程将主要关注Terrain组件。
要设置地形,我们将重点关注两个主要类:
Terrain类代表一块地形,TerrainGroup拥有一系列Terrain碎片。它用于LOD(细节级别)渲染。LOD渲染会降低距离摄像机较远的地形的分辨率。单个地形对象由带有映射到它们的材质的图块组成。我们将使用单个TerrainGroup而不进行Paging。Paging将在后面的教程中介绍。
Set up the Camera
我们首先设置我们的相机。将以下内容添加到setup
:
mCameraNode->setPosition(mTerrainPos + Vector3(1683, 50, 2116));
mCameraNode->lookAt(Vector3(1963, 50, 1660), Node::TS_PARENT);
mCamera->setNearClipDistance(40); // tight near plane important for shadows
mCamera->setFarClipDistance(50000);
这应该跟上一个教程中看起来很熟悉。
if (mRoot->getRenderSystem()->getCapabilities()->hasCapability(RSC_INFINITE_FAR_PLANE))
{
mCamera->setFarClipDistance(0); // enable infinite far clip distance if we can
}
我们做的最后一件事是检查我们当前的渲染系统是否具有处理无限远裁剪距离的能力。如果是,那么我们将远裁剪距离设置为零(这意味着''没有''远裁剪)
Setting Up a Light for Our Terrain
Terrain组件可以使用定向光来计算光照贴图。让我们为此添加一个Light,并在我们进入时为场景添加一些环境光。
Vector3 lightdir(0.55,-0.3,0.75);
lightdir.normalise();
Ogre :: Light * l = mSceneMgr-> createLight(“tstLight”);
l-> setType(Light :: LT_DIRECTIONAL);
1-> setDirection(lightdir);
l-> setDiffuseColour(ColourValue :: White);
l-> setSpecularColour(ColourValue(0.4,0.4,0.4));
如果你对它中的任何一个感到困惑,这也在前一个教程中有所介绍。该normalise
方法将使向量的长度等于1,同时保持其方向。在使用向量时,你会看到很多内容。这样做是为了避免在计算中出现额外的因素。
Terrain loading overview
现在我们将进入实际的地形设置。首先,我们创建TerrainGlobalOptions。
mTerrainGlobals = new Ogre :: TerrainGlobalOptions();
这是一个包含我们可能创建的所有地形的信息的类 - 这就是为什么它们被称为“全局”选项。它还提供了一些getter和setter。我们将在本教程后面看到每个TerrainGroup的本地选项。
接下来,我们构造TerrainGroup对象。这将管理一个Terrains网格。
mTerrainGroup = new Ogre::TerrainGroup(mSceneMgr, Ogre::Terrain::ALIGN_X_Z, TERRAIN_SIZE, TERRAIN_WORLD_SIZE);
mTerrainGroup->setFilenameConvention(TERRAIN_FILE_PREFIX, TERRAIN_FILE_SUFFIX);
mTerrainGroup->setOrigin(mTerrainPos);
TerrainGroup构造函数将SceneManager作为其第一个参数。然后它采用对齐选项,地形大小和地形世界大小。您可以阅读Ogre :: TerrainGroup以获取更多信息。setFilenameConvention
允许我们选择如何保存我们的地形。最后,我们设置了用于我们地形的原点。
我们接下来要做的就是调用我们的地形配置方法,我们将尽快填写。确保将我们创建的Light作为参数传递
configureTerrainDefaults(light);
我们接下来要做的是定义我们的地形并要求TerrainGroup加载它们。
for (long x = TERRAIN_PAGE_MIN_X; x <= TERRAIN_PAGE_MAX_X; ++x)
for (long y = TERRAIN_PAGE_MIN_Y; y <= TERRAIN_PAGE_MAX_Y; ++y)
defineTerrain(x, y);
// sync load since we want everything in place when we start
mTerrainGroup->loadAllTerrains(true);
我们只使用单个地形,因此该函数只会被调用一次。在我们的例子中for循环仅用于说明。我们将尽快填写defineTerrain函数。
我们现在将初始化地形的混合贴图。
if (mTerrainsImported)
{
TerrainGroup::TerrainIterator ti = mTerrainGroup->getTerrainIterator();
while(ti.hasMoreElements())
{
Terrain* t = ti.getNext()->instance;
initBlendMaps(t);
}
}
mTerrainGroup->freeTemporaryResources();
我们从TerrainGroup获得TerrainIterator,然后循环遍历任何Terrain元素并初始化它们的混合贴图 - initBlendMaps
也将很快编写。当我们完成它,会在configureTerrainDefaults函数中设置
mTerrainsImported
变量。
我们要做的最后一件事是确保清理在配置地形时创建的任何临时资源。
这完成了我们的createScene
方法。现在我们只需要完成我们跳过的所有方法。
Terrain appearance
该render地形组件有大量的可以设置来改变地形的渲染方式的选项。首先,我们将层次细节程度配置为:
mTerrainGlobals->setMaxPixelError(8);
mTerrainGlobals->setCompositeMapDistance(3000);
我们在这里设置两个全局选项。第一个调用设置理想地形与为渲染它而创建的网格之间允许的最大像素误差。较小的数字意味着更精确的地形,因为它需要更多的顶点来减少误差。第二个调用确定Ogre仍将应用我们的光照贴图的距离。如果你增加这个,那么你会看到Ogre将光照效果应用到更远的距离。
接下来我们要做的就是将我们的照明信息传递给我们的地形。
// Important to set these so that the terrain knows what to use for baked (non-realtime) data
mTerrainGlobals->setLightMapDirection(l->getDerivedDirection());
mTerrainGlobals->setCompositeMapAmbient(mSceneMgr->getAmbientLight());
mTerrainGlobals->setCompositeMapDiffuse(l->getDiffuseColour());
在第一次调用中,我们肯定会调用getDerivedDirection
,因为这将任何可能附加到其上的SceneNode应用于Light方向的变换。由于我们的Light连接到根节点,这与调用getDirection
相同,但了解区别很重要。接下来的两个调用应该是不言自明的。我们只需为我们的地形设置环境光和漫反射颜色,以匹配我们的场景照明。
接下来我们要做的是引用TerrainGroup的导入设置并设置一些基本值。
Ogre::Terrain::ImportData& defaultimp = mTerrainGroup->getDefaultImportSettings();
defaultimp.terrainSize = TERRAIN_SIZE;
defaultimp.worldSize = TERRAIN_WORLD_SIZE;
defaultimp.inputScale = 600;
defaultimp.minBatchSize = 33;
defaultimp.maxBatchSize = 65;
我们不打算在本教程中介绍这些选项的确切含义,但你可能已经注意到terrainSize
和worldSize被
设置来匹配我们在createScene
设置的全局选项。在inputScale
决定了高度图将如何扩大场景的规模。由于我们的高度图图像精度有限,因此我们使用的是比较大的比例。您可以使用浮点原始高度图来避免应用任何输入缩放,但这些图像通常需要一些数据压缩。
最后一步是添加我们的地形将使用的纹理。首先,我们调整列表大小以容纳三个纹理。之后,我们设置每个纹理worldSize
并将它们添加到列表中。
defaultimp.layerList.resize(3);
defaultimp.layerList[0].worldSize = 100;
defaultimp.layerList[0].textureNames.push_back("grass_green-01_diffusespecular.dds");
defaultimp.layerList[0].textureNames.push_back("grass_green-01_normalheight.dds");
defaultimp.layerList[1].worldSize = 100;
defaultimp.layerList[1].textureNames.push_back("dirt_grayrocky_diffusespecular.dds");
defaultimp.layerList[1].textureNames.push_back("dirt_grayrocky_normalheight.dds");
defaultimp.layerList[2].worldSize = 200;
defaultimp.layerList[2].textureNames.push_back("growth_weirdfungus-03_diffusespecular.dds");
defaultimp.layerList[2].textureNames.push_back("growth_weirdfungus-03_normalheight.dds");
纹理worldSize
决定了应用于地形时每个纹理贴图的大小。较小的值将增加渲染纹理图层的分辨率,因为每个部分将被拉伸较少以填充地形。
默认材质生成器每层需要两个纹理:
- 漫反射的镜面纹理和
- 高度贴图纹理。
- 本教程中使用的纹理位于SDK或源代码分发的Samples目录中。在撰写本文时,它们包含在此目录中:
Samples/Media/materials/textures/nvidia/
。请记住,Ogre在加载资源时不会自动搜索子目录,因此您必须在resources.cfg
文件中添加一行,告诉它包含nvidia目录,当然,您必须将实际纹理复制到项目的media文件夹中。
Defining a terrain chunk
现在我们将解决我们的defineTerrain函数
。我们要做的第一件事就是要求TerrainGroup为这个Terrain定义一个唯一的文件名。如果它已经生成,那么我们可以调用TerrainGroup::defineTerrain函数
来自动设置此网格位置和先前生成的文件名。如果尚未生成,则我们生成一个图像,getTerrainImage
然后调用另一个重载,TerrainGroup::defineTerrain
该重载将引用我们生成的图像。最后,我们将mTerrainsImported
标志设置为true。
String filename = mTerrainGroup->generateFilename(x, y);
if (ResourceGroupManager::getSingleton().resourceExists(mTerrainGroup->getResourceGroup(), filename))
{
mTerrainGroup->defineTerrain(x, y);
}
else
{
Image img;
getTerrainImage(x % 2 != 0, y % 2 != 0, img);
mTerrainGroup->defineTerrain(x, y, &img);
mTerrainsImported = true;
}
您可能需要查看此函数一段时间才能完全理解它。确保您注意到使用了“三种”不同的defineTerrain
方法。其中一个来自TutorialApplication,其中两个来自TerrainGroup
Loading a heightmap
最后一步,我们需要编写defineTerrain
使用的辅助函数。这将加载我们的terrain.png
高度图。确保它已添加到您的一个资源加载路径中。它也包含在Ogre Samples目录中。
img.load("terrain.png", mTerrainGroup->getResourceGroup());
if (flipX)
img.flipAroundY();
if (flipY)
img.flipAroundX();
Fliping用于创建无缝地形,以便使用单个高度图创建无限地形。如果你的地形的高度图已经是无缝的,那么您不需要使用此技巧。在我们的例子中,翻转代码也没用,因为我们使用的是1x1 TerrainGroup。翻转1x1瓦片不会改变任何内容。它只是为了演示。
Height based blending
最后,我们将通过完成initBlendMaps函数
来完成配置方法。此方法将我们在configureTerrainDefaults
定义的不同层混合在一起。现在,您应该将此方法视为一种魔力。本教程不会介绍详细信息。基本上,该方法基于该点处的地形高度来混合纹理。这不是进行混合的唯一方法。这是一个复杂的话题,正好位于Ogre和它试图抽象出去的东西之间。
using namespace Ogre;
TerrainLayerBlendMap* blendMap0 = terrain->getLayerBlendMap(1);
TerrainLayerBlendMap* blendMap1 = terrain->getLayerBlendMap(2);
float minHeight0 = 20;
float fadeDist0 = 15;
float minHeight1 = 70;
float fadeDist1 = 15;
float* pBlend0 = blendMap0->getBlendPointer();
float* pBlend1 = blendMap1->getBlendPointer();
for (uint16 y = 0; y < terrain->getLayerBlendMapSize(); ++y)
{
for (uint16 x = 0; x < terrain->getLayerBlendMapSize(); ++x)
{
Real tx, ty;
blendMap0->convertImageToTerrainSpace(x, y, &tx, &ty);
float height = terrain->getHeightAtTerrainPosition(tx, ty);
*pBlend0++ = Math::saturate((height - minHeight0) / fadeDist0);
*pBlend1++ = Math::saturate((height - minHeight1) / fadeDist1);
}
}
blendMap0->dirty();
blendMap1->dirty();
blendMap0->update();
blendMap1->update();
Terrain Loading Label
我们将改进一些事情。我们将在叠加层上添加一个标签,以便我们查看地形生成何时完成。我们还将确保保存我们的地形,以便可以重新加载而不是每次都重建它。最后,我们将确保自己清理完毕。
首先,我们需要将数据成员添加到Sample_Terrain标头的私有部分。
OgreBites::Label* mInfoLabel = nullptr;
让我们在createFrameListener
方法中构造这个标签。
mInfoLabel = mTrayMgr-> createLabel(TL_TOP,“TInfo”,“”,350);
我们使用SdkSample中定义的TrayManager指针来请求创建新标签。此函数包括TrayLocation,标签的名称,要显示的标题和宽度。
接下来,我们将给frameRenderingQueued
添加逻辑来跟踪地形是否仍在加载。我们还将在加载后保存我们的地形。frameRenderingQueued
在调用父方法后立即添加以下内容:
if (mTerrainGroup->isDerivedDataUpdateInProgress())
{
mTrayMgr->moveWidgetToTray(mInfoLabel, TL_TOP, 0);
mInfoLabel->show();
if (mTerrainsImported)
{
mInfoLabel->setCaption("Building terrain, please wait...");
}
else
{
mInfoLabel->setCaption("Updating textures, patience...");
}
}
else
{
mTrayMgr->removeWidgetFromTray(mInfoLabel);
mInfoLabel->hide();
if (mTerrainsImported)
{
saveTerrains(true);
mTerrainsImported = false;
}
}
我们要做的第一件事就是确定我们的地形是否仍在建造中。如果是,那么我们将标签添加到托盘并要求显示它。然后我们检查是否有任何新的地形被导入。如果有,那么我们会显示说明地形仍在建造的文字。否则我们假设纹理正在更新。
如果不再更新地形,那么我们要求OgreBites :: TrayManager删除我们的Label小部件并隐藏Label。我们还检查是否已导入新地形并将其保存以备将来使用。在我们的示例中,该文件将命名为“terrain_00000000.dat”,它将与应用程序的可执行文件一起位于“bin”目录中。保存任何新地形后,我们重置mTerrainsImported
标志。
再次编译并运行您的应用程序。现在,您应该在构建地形时在屏幕顶部看到一个标签。当地形正在加载时,您将无法按退出以退出并且您的移动控件将不稳定。这就是加载屏幕在游戏中的用途。但是如果你第二次退出并运行应用程序,那么它应该加载第一次保存的terrain文件。这应该是一个更快的过程。
Simulating(模拟) a sky
skyBoxes(天空盒)
SkyBox基本上是一个巨大的纹理立方体,围绕场景中的所有对象。它是模拟天空的方法之一。我们需要六个纹理来覆盖SkyBox的所有内部面。
在场景中包含SkyBox非常容易。将以下内容添加到以下内容createScene。
mSceneMgr-> setSkyBox(true,“Examples / SpaceSkyBox”);
编译并运行您的应用程序。这里的所有都是它的。SkyBox看起来非常有颗粒感,因为我们使用的分辨率相当低。
这个函数的第一个参数确定是否立即启用SkyBox。如果您想稍后禁用SkyBox,您可以拨打电话mSceneMgr->setSkyBox(false, "")
。这会禁用SkyBox。
第三和第四个参数setSkyBox
对于理解很重要。我们允许他们在我们的调用中采用默认值。第三个参数是Camera和SkyBox之间的距离。对你的函数调用进行如下更改:
mSceneMgr->setSkyBox(true, "Examples/SpaceSkyBox", 300);
编译并运行您的应用程序。什么也没有变。这是因为第四个参数设置是否在场景的其余部分之前渲染SkyBox。如果首先渲染SkyBox,那么无论它有多接近,其余的场景对象都将呈现在它上面。现在试试这个调用:
mSceneMgr-> setSkyBox(true,“Examples / SpaceSkyBox”,300,false);
再次编译并运行你的应用程序。这次你肯定会看到不同的东西。相机下方只应存在一小块地形。四处移动,注意发生了什么。SkyBox的渲染距离相机仅300像素,并且不再在其他所有内容之前渲染。这意味着SkyBox被绘制在远离相机300个单位的地形上。
通过先不渲染SkyBox可以获得适度的性能提升,但正如您所看到的,您需要确保在执行此操作时不会出现类似的奇怪问题。在大多数情况下,将这些附加参数保留为默认值就足够了。虽然您可能希望在应用程序中故意使用这种奇怪的剔除行为。尽量不要陷入事情“应该工作”的方式。如果有什么东西引起你的注意,那就玩吧。
SkyDomes
另一种模拟天空的方法是SkyDome。天空纹理仍然应用于围绕场景的巨大立方体,但是纹理以这样(在场景上创建圆顶)的方式投影。理解这一点的最好方法是在实践中观察它。注释掉我们setSkyBox函数调用
并添加以下行:
mSceneMgr->setSkyDome(true, "Examples/CloudySky", 5, 8);
编译并运行您的应用程序。确保将相机移动到地形边缘,以便更好地了解正在进行的操作。这种方法的主要缺点是纹理不会覆盖立方体的底面。您需要确保用户不会意外地看到幕后。
该setSkyDome
函数的前两个参数与setSkyBox相同
。你也可以以相同的方式禁用SkyDome。第三个参数是圆顶投影的曲率。建议使用2到65之间的值。较低的值将在远距离处产生更好的效果,但较高的值将导致较少的纹理失真。第四个参数是纹理平铺的次数。此参数是Ogre :: Real值。如果需要,可以将纹理平铺3.14次。最后两个参数是距离以及是否先绘制SkyDome。这些是我们使用的最后两个参数setSkyBox
。
SkyPlanes
模拟天空的第三种方法与前两种方法有很大不同。此方法将使用单个平面。我们需要做的第一件事是创建一个Plane对象。注释掉我们setSkyDome函数调用
并添加以下内容:
Ogre::Plane plane;
plane.d = 1000;
plane.normal = Ogre::Vector3::NEGATIVE_UNIT_Y;
我们通过提供距离原点(d)和垂直于我们平面的矢量(法线)的距离来定义平面。通过沿y轴选择“负”单位向量,我们得到一个平行于地面并朝下的平面。
现在我们可以创建SkyPlane。
mSceneMgr-> setSkyPlane(true,plane,“Examples / SpaceSkyPlane”,1500,75);
第四个参数是SkyPlane的大小(1500x1500单位),第五个参数是平铺纹理的次数。
编译并运行您的应用程序。同样,我们使用的纹理分辨率相当低。高清晰度纹理看起来会更好。它也不是很好。这些问题都可以通过使用更高质量的资源来解决。真正的问题是,一旦用户移动到靠近地形边缘的任何地方,他们很可能会看到SkyPlane的边缘。出于这个原因,SkyPlane经常用于具有高墙的场景中。在这些情况下,SkyPlane的性能比其他技术提高了不少。
SkyPlane还有一些其他属性可用于产生更好的效果。该setSkyPlane
方法的第六个参数是我们在过去两个方法中介绍的“renderFirst”参数。第七个参数允许我们为SkyPlane指定曲率。这将拉下SkyPlane的角落,将其变成曲面而不是平面。如果我们将曲率设置为其他平面,我们还需要设置Ogre用于渲染SkyPlane 的段数。当SkyPlane是一个平面时,一切都是一个大的正方形,但是如果我们增加曲率,那么它将需要更复杂的几何形状。该函数的第八和第九参数是平面的每个维度的段数。
我们来试试吧。对我们的函数进行以下更改:
mSceneMgr->setSkyPlane(
true, plane, "Examples/SpaceSkyPlane", 1500, 50, true, 1.5, 150, 150);
编译并运行应用程序。这应该有助于我们的SkyPlane的外观。转到地形的边缘以更好地了解曲率的作用。
Fog
就像图形编程中的几乎所有东西一样,Ogre中的雾效果是一种幻觉。Ogre不会将雾对象渲染到场景中。相反,它只是将滤镜应用于我们的场景。此滤镜允许视口的背景颜色根据物体与相机的距离在不同程度上显示我们的风景。这意味着您的雾具有与Viewport背景颜色相同的颜色。
Ogre中有两种基本类型的雾:线性和指数。不同之处在于当您离开相机时雾变得更厚。
Adding Fog to our Scene
我们将首先为场景添加线性雾。我们需要确保将Viewport的背景颜色设置为我们所需的雾色。在我们设置地形的代码之前向createScene
添加以下:
Ogre::ColourValue fadeColour(0.9, 0.9, 0.9);
mWindow->getViewport(0)->setBackgroundColour(fadeColour);
确保在地形代码之前添加它,否则它将无法工作。如果您使用多个Viewport,则可能必须使用Ogre :: RenderTarget :: getNumViewports迭代它们。
现在我们可以创造雾。
mSceneMgr-> setFog(Ogre :: FOG_LINEAR,fadeColour,0,600,900);
第一个参数是雾型。第二个参数是我们用于设置Viewport背景颜色的颜色。第三个参数不用于线性雾。第四个和第五个参数指定雾范围的开始和结束。在我们的示例中,雾将从距离相机的600个单位开始,并且将从900个单位结束。这被称为线性雾的原因是因为厚度在这两个值之间以线性方式增加。编译并运行您的应用程序。
下一种雾是指数雾。如图所示,指数雾首先缓慢增长,然后很快变得密集。我们没有为这种雾设定范围,而是提供所需的密度。
mSceneMgr-> setFog(Ogre :: FOG_EXP,fadeColour,0.002);
编译并运行应用程序。你可以看到这会产生一种不同的雾效果。它更像是填充相机周围区域的阴霾。指数雾的变化以更快的速度增加。
mSceneMgr-> setFog(Ogre :: FOG_EXP2,fadeColour,0.002);
编译并运行您的应用程序以查看其产生的差异。
Conclusion
本教程介绍了使用Ogre Terrain组件的基础知识。我们简要概述了为将地形高度图导入场景而需要进行的设置。我们提到了Ogre :: TerrainGroup的概念,尽管我们在本教程的“组”中只使用了一个Ogre :: Terrain对象。我们还确保用定向光初始化我们的地形,这样我们就可以在我们的地形上获得镜面反射和阴影。
我们还介绍了Ogre提供的不同方法来模拟场景中的天空。其中包括:SkyBoxes,SkyDomes和SkyPlanes。最后,我们介绍了ogre的雾效果。通过在我们的场景中应用滤镜来渲染雾,该滤镜允许视口的背景颜色根据与相机的距离在我们的场景中渗透。
这是一个你应该花很多时间进行实验的教程。所有这些功能都可以配置很多,你可以使用我们到目前为止所涵盖的内容来创建一些非常有说服力的场景。