地形渲染是三维图形学中必然不可缺席的一环,在游戏之类的应用中,地形效果的好坏更是直接影响整个产品的最终评价。既然具有如此重要性,那么我们也好应该确切了解一下一个最基本的地形系统一般包括些什么东西,鉴于本博客以前貌似很少提及这一块,接下来我就自己实现了一次地形渲染。——ZwqXin.com
联动文章:
本站涉及地形相关的文章:
本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
原文地址:http://www.zwqxin.cn/archives/opengl/lod-terrain-rendering-1.html
最开始接触OpenGL的那段时间,做地形的渲染是很简单的——一般我们有一张高程图(height map)和一张地形表面纹理(diffuse map),我们准备一个大型顶点网格(在X-Z平面上),然后以(网格顶点坐标 / 网格尺寸)的方式产生纹理坐标去检索高程图,从而获得该顶点的高度,渲染出来,再直接覆盖式贴上表面纹理,就是一个地形了。这个地形的外形直接取决于高度图的分辨率、网格大小和表面贴图的质量,但是它确实表达了一个“任意地形”的概念,是直至今天地形渲染技术的雏形。
通常我们评价一个地形的“效果”,会至少从这两个方面去评价:
- 渲染效率。地形通常是一个大范围覆盖视野的物件,加上构筑一个具有大量高低起伏的地形通常需要大量的顶点数据,所以怎样在保证视觉质量的前提下减少本地向GPU提交的顶点数,将极大地影响整个场景最终的FPS。除了减少网格顶点数,通过纹理组合来尽量以低分辨率纹理模拟高分辨率纹理也是一个提高地形渲染效率的方向。当然了,影响效率的肯定还有很多因素,但顶点数的控制的确是最为重要的;
- 视觉效果。这个是不用多说的了,实时渲染图形学所追求的是什么?地形的视觉效果更多由表面贴图去控制。但是考虑真实环境的山脉、山坡这些地方,它们所包含的东西很可能不是单一贴图就能表现出来的,譬如植被随着高度的变化、不同区域环境的变化、地表随风飘荡的植物,等等。除了上面提及的分辨率问题,一个通过编辑器生成的地形如何也能动态地匹配对应的预生成纹理,也是一个地形系统要考虑的。
在以前写的一个小demo([Terrain Texture-Array Demo] )中,我通过Texture Array的layer坐标的浮点插值特性模拟了一个“草-雪-岩”过渡的地形,但毕竟只是为了展示TextureArray特性而已(看看我在该文中是怎样构造顶峰地形的就明白了),真正的地形的多层贴图不会这么简单粗暴的。关于视觉效果这块我也不算接触得太多,但后文会提及的,本篇主要从渲染效率——顶点数量控制这一块谈谈一个具有Level of Detail(Lod,细节层次)的基本地形系统是怎样生成的。
也作为一篇记录文章,记录一下我这次制作这个地形的历程。
当然,我还是从雏形般的地形渲染开始。但是正如在[水效果Ⅱ - 涟漪]中一样,我们可以直接在Vertex Shader中检索高程纹理,作为该顶点的y数据值。这是[Vertex Texture Fetch 顶点纹理拾取]给我们带来的硬件加速,比起以前直接读内存中(或通过额外pass预渲染后回读)的高程图,这样显得高效好多。先来看看我们需要什么数据:
- class ZWRenderer
- {
- public:
- //...
- bool Load();
- void Render();
- protected:
- //...
- }
- class ZWChunkedTerrain : public ZWRenderer
- {
- public:
- //...
- protected:
- //
- virtual bool PreLoad();
- virtual bool SetupData(...);
- virtual bool PostLoad();
- virtual void Draw();
- virtual void Release();
- private:
- GLint m_nGridX;
- GLint m_nGridZ;
- GLfloat m_fGridLength;
- GLfloat m_fBaseHeightValue;
- GLfloat m_fHeightScale;
- GLuint m_nHeightMapHandle;
- ZWMatrix16 *m_pProjMatrixRef;
- ZWMatrix16 *m_pViewMatrixRef;
- ZWMatrix16 m_mtModelMatrix;
- ZWVector3 m_vPosition;
- ZWQuaternion m_qRotation;
- ZWGLSL m_ChunkedTerrainShader;
- };
除了那些很明显的矩阵信息和height map外,还有网格数(Grid)和一个格子的尺寸长度(正方形格子),这些都需要在网格的VBO们产生之前准备好。当然我们可以直接只生成一个位置属性的VBO,在Shader中计算对应的纹理坐标(毕竟是网格),但我不太喜欢这种精简:
- bool ZWChunkedTerrain::SetupData(std::vector<ZWVector3> &posVec, std::vector<ZWTexcoord> &texcoordVec,
- std::vector<ZWVector3> &normalVec, std::vector<GLushort> &indexVec)
- {
- if (m_nGridX > 0 && m_nGridZ > 0 && m_fGridLength > 0)
- {
- int nXVertCount = m_nGridX + 1;
- int nZVertCount = m_nGridZ + 1;
- for (int iz = 0; iz < nZVertCount; ++iz)
- {
- for (int ix = 0; ix < nXVertCount; ++ix)
- {
- posVec.push_back(ZWVector3(ix * m_fGridLength, 0, iz * m_fGridLength));
- texcoordVec.push_back(ZWTexcoord(GLfloat(ix) / m_nGridX, GLfloat(iz) / m_nGridZ));
- }
- }
- int nLocIndex = 0;
- for (int iz = 0; iz < m_nGridZ; iz += 1)
- {
- for (int ix = 0; ix < m_nGridX; ix += 1)
- {
- nLocIndex = iz * nXVertCount + ix;
- indexVec.push_back(nLocIndex);
- indexVec.push_back(nLocIndex + nXVertCount);
- indexVec.push_back(nLocIndex + 1);
- indexVec.push_back(nLocIndex + 1);
- indexVec.push_back(nLocIndex + nXVertCount);
- indexVec.push_back(nLocIndex + nXVertCount + 1);
- }
- }
- return true;
- }
- return false;
- }
好了,根据数据生成VBO后(当然了,我还会直接用一个VAO保存好VBO数据检索格式等状态[AB是一家?VAO与VBO] ),渲染的时候直接Draw出来就可以了。接下来是对应的Shader,其实就是很直接的输出而已,稍微特别点的就是VTF(Vertex Texture Fetch)了:
- float terrainHeight(vec2 vCoordPos)
- {
- vec2 vLength = vec2(lengthX, lengthZ);
- float fValueHeight = texture(heightTex, vec2(vCoordPos.x / vLength.x, vCoordPos.y / vLength.y)).r;
- return heightScale * (fValueHeight - baseHeightValue);
- }
在上述获取顶点的y值的函数中,首先把传入的顶点(x,z)值规范化到0~1(length是整个网格尺寸),然后直接检索height map,得到的高度值的范围是0~1,根据我们传入的两个uniform(在类声明中也可以看到,它们也是可以通过外部设置的值,作为offset的 baseHeightValue和作为scaler的heightScale )调整成我们实际需要的高度值范围输出。注意我们的高度图其实只要是个单通道图片就OK了(生成纹理时内部格式可用GL_R)。
当然我们也可以不用高度图,而是通过一些连续的noise值来形成高度数据集。不得不提的就是在这方面很著名的柏林噪声(Perlin noise),你可以认为通过这种算法,我们输入的连续递增或递减值会以各种高低起伏的固定的连续值输出。这要是拓展成二维的话就跟一个实际的地形太类似了吧——只是我们是无法控制输出的。以前我使用的时候都是CPU计算的,但noise()系列函数自GLSL诞生以来就存在了,GPU的计算肯定更好啊——可惜我从来没用这族函数成功过,无论是在vertex shader还是fragment shader,noise()输出总是0,可能只好怪具体的显卡制造商吧……《GPU Gems 2》里提及了一种增强型的柏林噪声算法 ,有兴趣的同鞋可以去及及。但我比较贪快,就直接取来用了(可以兼容GLSL的perlin noise算法函数实现,见此网页,里面还有个简化版的):
- float terrainHeight(vec2 vCoordPos)
- {
- vec2 vLength = vec2(lengthX, lengthZ);
- float fNoseCoordScale = 5.0;
- float fValueHeight = perlin_noise2(vec2(vCoordPos.x / vLength.x * fNoseCoordScale, vCoordPos.y / vLength.y * fNoseCoordScale));;
- return heightScale * (fValueHeight - baseHeightValue);
- }
当然了,具体实际情况下,我们的地形还是需要可以控制的值的(通过改变height map),以下都以height map方式为准。
恩,其实以上这也就是很久以前的地形渲染方式,转变为可编程渲染管道下,结合Shader实现的版本而已——实现了本质上一模一样的结果,但还差一样:如何获得地形上某一点的高度值(height value)呢?以前让摄像机紧贴地面,也就是每帧另外检索一次高度图而已——如今也是一样的。譬如我现在就是要在地形上“植草”,每个草簇的(x,z)坐标值都是已知的,怎么嵌入到我们现在的渲染流程里呢?要知道,我现在不是要渲染些什么东西,而只是要个结果而已。当然你也可以另外用个渲染Pass把结果“渲染”出来再回读,但实在没必要啊,毕竟只要Vertex Shader的结果而已——没错,就是transform feedback([乱弹纪录IV:Transform Feedback] )。
在没有Geometry Shader的情况下,Transform Feedback的输出结果就是Vertex Shader的结果了。我们输入多少个点(x,0,z),输出就是多少个点(x,y,z),这跟上述计算Terrain网格各顶点高度值是同一个逻辑嘛,都是检索高度图。
- class ZWChunkedTerrain : public ZWRenderer
- {
- public:
- struct PlantingPointInfo
- {
- GLint nStartIndex;
- GLint nCount;
- };
- //...
- void AddPlantingPointVBO(GLuint nPosVBO, std::vector<ZWVector3> &PosDataVec);
- void RemovePlantingPointVBO(GLuint nPosVBO);
- void UpdatePlantingPointHeightForVBO(GLuint nPosVBO);
- void UpdateAllPlantingPointHeight();
- protected:
- //...
- virtual void UpdatePlantingPointHeight(GLuint nPosVBO, PlantingPointInfo &plantingPointInfo);
- private:
- //...
- GLuint m_nPlantingPointVAO;
- GLuint m_nPlantingPointVBO;
- ZWGLSL m_PlantingPointShader;
- std::vector<ZWVector3> m_PlantingPointVec;
- std::map<GLuint, PlantingPointInfo> m_PlantVBORefMap;
- };
这里,需要计算高程的数据集PosDataVec,连带其对应的VBO,传入这个地形类,并用m_PlantingPointVec集结,另外用一个map(m_PlantVBORefMap)记录下这个用于输出的VBO和它对应的数据集在m_PlantingPointVec中的位置和数量。在UpdatePlantingPointHeight这些函数执行时,其实是新建了一个Pass,m_PlantingPointVec中的所有点数据会经由一个VBO/VAO为媒介作为输入,需要更新y值的数据集,通过tranform feedback,输出到其对应的VBO中。为啥不采用VBO1->VBO1、VBO2->VBO2的方式呢?要记住,我们的Buffer Object不能同时担任输入和输出两个角色([乱弹纪录IV:Transform Feedback] )。那么用单独的Pass,执行1 x VBO -> n x VBO呢?这个是OpenGL 4.x才有的功能哪!(穷人再次画圈圈~)
- void ZWChunkedTerrain::UpdatePlantingPointHeight(GLuint nPosVBO, PlantingPointInfo &plantingPointInfo)
- {
- if (m_bPlantingPointUpdated)
- {
- glBindBuffer(GL_ARRAY_BUFFER, m_nPlantingPointVBO);
- glBufferData(GL_ARRAY_BUFFER, m_PlantingPointVec.size() * sizeof(ZWVector3), &m_PlantingPointVec[0], GL_STATIC_DRAW);
- m_bPlantingPointUpdated = false;
- }
- m_PlantingPointShader.Enable();
- m_PlantingPointShader.SendUniform("nForPointsUpdating", 1);
- //...
- glActiveTexture(GL_TEXTURE0);
- glBindTexture(GL_TEXTURE_2D, m_nHeightMapHandle);
- glEnable(GL_RASTERIZER_DISCARD);
- glBindVertexArray(m_nPlantingPointVAO);
- glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, nPosVBO);
- glBeginTransformFeedback(GL_POINTS);
- glDrawArrays(GL_POINTS, plantingPointInfo.nStartIndex, plantingPointInfo.nCount);
- glEndTransformFeedback();
- glDisable(GL_RASTERIZER_DISCARD);
- m_PlantingPointShader.Disable();
- }
m_PlantingPointShader里的vertex shader与地形渲染的Shader(m_ChunkedTerrainShader)里的vertex Shader是同一个,而且前者里既没有Geometry Shader也没有Fragment Shader。Vertex Shader是同一个主要确保高度计算的逻辑是统一的,但是关于矩阵计算这些可要区别开来——输入点要调整到跟地形网格的本地坐标系一致来计算检索height map的纹理坐标,而且要在terrain的世界坐标系下输出。
- void main(void)
- {
- float fHeightValue = 0.0;
- if(nForPointsUpdating > 0)
- {
- fHeightValue = terrainHeight(...);
- varying_output_pos = (matModel * vec4(attrib_position.x, fHeightValue + attrib_position.y, attrib_position.z, 1.0)).xyz;
- }
- else
- {
- fHeightValue = terrainHeight(...);
- vec4 pos = vec4(...);
- gl_Position = matProj * matView * matModel * pos;
- }
- }
由于作为举例的草簇的位置都是固定的,所以这个对应的UpdatePlantingPoint函数只要在初始化时执行一次足矣,输出的VBO在以后的渲染中会直接用于草体渲染的了。除了草簇、那些树木啊房子之类静止的东西都可以这样做,但那些会动的物体,譬如动物啊、摄像机啊,就得每帧更新了。(感觉还是会有点麻烦哦,能不能直接射线检测完事呢哈哈。)
终于到主菜了——怎么减小每帧传输的顶点数据量呢?这种上世纪的传统地形渲染方式的一个最大的弊端就是很难对之进行场景空间管理上的优化。所谓场景空间管理,最常见的有广度上的视锥体剔除、深度上的遮挡剔除、内容上的细节层次选择,等等。但是传统的地形(也可以把上面的地形看作传统地形,因为本质一致)只是一块大饼,对它来说只有渲染和不渲染这两个选项,而且通常它只能选择前一个,就是总是渲染,总是把所有顶点数据提交。那么,可以想到的优化方式就是——把它打散,分成一块一块(chunk)。这样,譬如一个1000X1000大小的地形就可以分成10X10块(每块100X100的大小),每一块独立地进行渲染。这样当然会增加每一帧的DrawCall(Batch),违背了GPU渲染的宁可一批次多渲染而不多批次渲染的特点,但它对地形来说,是同时引进了场景空间管理的方便性。譬如现在视锥体剔除就可以针对每个块(chunk)执行,只有通过视锥体剔除的块,才执行渲染。这样,虽然Batch还是显著增加了,但一帧内提交的顶点数目也会显著减少——对于地形这种很多时候只会看到其中一小部分的物件来说。总的来说,这样提高了地形渲染的灵活性。
当然也可以进行遮挡剔除地形块,但地形的高度毕竟是起伏不平的,注定基于包围盒的方式不会太精准。视锥体剔除还可以给个高度大概值,影响不会十分大,但深度方向上可能就会引入大的准确性失准了,需要很小心。那么,还有内容上的细节层次选择呢?确实,这相比前两者来说,是对地形渲染来说,减少顶点数的大杀器!本文的重心也在此——具有细节层次(Level of Deatil, Lod)的地形渲染——Lod of A Terrain。
关于Lod,其实通常是针对模型和纹理来说的。场景中的模型有时候为了表现它表面的各种细节美,其自身的顶点数会很多,对于实时渲染来说,这种精细如果多起来的话,是很致命的。所以有个取巧的方法,就是在距离视点近的模型用精模、距离远的模型用低模。美术资源方面可以预先针对同一个物体制作几个对应的模型(或者用算法实现),精细程度由高到低,程序导入模型时把这些模型全数导入,并对同一个物体的这几个模型给予同一个tag和Lod值,这样渲染时一个物体就可以根据视距选择同一个tag下的各种lod的模型数据,越近的时候选lod值越小的,越远的时候选lod值越大的。纹理这就不用说了,其实就是指texture level,视像素-纹素比越靠近1选择越低的level值,反之亦然,设置好插值模式还可以实现level间的插值呢。另外,关于Lod其实本站之前也涉及过——就是阴影(Shadow)的lod啊(Cascade Shadow Map[Shadow Map阴影贴图技术之探Ⅳ] )。
地形的Lod其实更类似模型的Lod,不过它确实也有其自身的特点。就本质来说,Lod其实很简单——就是距离摄像机越近的块(chunk)的顶点数越多,距离摄像机越远的块的顶点数越少。那么究竟怎样产生变化呢?这里采用Geomipmap的那一套——让每个Chunk拥有同样数量的Lod数、其网格中两个维度上的格子数满足是2的幂次方。
如上图,假设细节最大(Lod=0,注意Lod值越大细节程度越低)的网格是8X8,那么下一个细节级别(Lod=1)则取4x4的网格,再下一级 (Lod=2)取2x2的网格,最后一级(Lod=3)就是单个格子(1x1)了。这样,通过取上一级别顶点集中相隔1的顶点,就可以构成本级别的顶点集 ——在实操中,其实只用一份最高细节顶点数据集(1份顶点VBO),然后每个Lod的顶点数据,只需要在渲染前改变对这个顶点集的索引值(index)就 OK了。在lod=0时(眼前的邻近Chunks),传输的是81个顶点,在lod=1时(对应稍微远点的Chunks)传输的顶点数就降为25个,再远点就降为9个、4个!达到了我们需要的大幅减少整体顶点传输量的目的。我干脆就预先保存好这LodCount份索引数据,渲染时按需取用!
- class ZWChunkedTerrain : public ZWRenderer
- {
- public:
- struct IndexVBOInfo
- {
- GLuint nVBO;
- GLint nIndexCount;
- GLint nLodIndex;
- };
- //...
- void SetChunkCount(GLint nChunkX, GLint nChunkZ);
- void SetGridCount(GLint nGridX, GLint nGridZ);
- //...
- inline void SetViewingCenter(ZWVector3 vEyePos){ m_vViewingCenter = vEyePos; }
- protected:
- //...
- bool IsChunkInsideViewFrustum(ZWVector3 vWorldPos, ZWVector3 vChunkExtent);
- private:
- GLint m_nChunkX;
- GLint m_nChunkZ;
- GLint m_nGridX;
- GLint m_nGridZ;
- GLint m_nGridLodCount;
- //...
- ZWVector3 m_vViewingCenter;
- std::vector<IndexVBOInfo> m_IndexVBOInfoVec;
- };
- void ZWChunkedTerrain::SetGridCount(GLint nGridX, GLint nGridZ)
- {
- if (nGridX > 0 && nGridZ > 0 && 0 == (nGridX & (nGridX - 1)) && 0 == (nGridZ & (nGridZ - 1)))
- {
- m_nGridX = nGridX;
- m_nGridZ = nGridZ;
- m_nGridLodCount = GLint(ceilf(logf(float(max(m_nGridX, m_nGridZ))) / logf(2.0f))) + 1;
- }
- }
- bool ZWChunkedTerrain::PostLoad()
- {
- std::vector<GLushort> indexVec;
- GLuint nIndexVBO = 0;
- int nLocIndex = 0;
- int nLocStep = 0;
- int nXVertCount = m_nGridX + 1;
- for (int iLod = 0; iLod < m_nGridLodCount; ++iLod)
- {
- indexVec.clear();
- nLocStep = int(powf(2.0f, float(iLod)));
- for (int iz = 0; iz < m_nGridZ; iz += nLocStep)
- {
- for (int ix = 0; ix < m_nGridX; ix += nLocStep)
- {
- nLocIndex = iz * nXVertCount + ix;
- indexVec.push_back(nLocIndex);
- indexVec.push_back(nLocIndex + nLocStep * nXVertCount);
- indexVec.push_back(nLocIndex + nLocStep);
- indexVec.push_back(nLocIndex + nLocStep);
- indexVec.push_back(nLocIndex + nLocStep * nXVertCount);
- indexVec.push_back(nLocIndex + nLocStep * nXVertCount + nLocStep);
- }
- }
- glGenBuffers(1, &nIndexVBO);
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, nIndexVBO);
- glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexVec.size() * sizeof(GLushort), (void*)(&(indexVec[0])), GL_STATIC_DRAW);
- IndexVBOInfo indexVBOInfo;
- indexVBOInfo.nVBO = nIndexVBO;
- indexVBOInfo.nIndexCount = indexVec.size();
- indexVBOInfo.nLodIndex = iLod;
- m_IndexVBOInfoVec.push_back(indexVBOInfo);
- }
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, NULL);
- //...
- return true;
- }
- void ZWChunkedTerrain::Draw()
- {
- m_ChunkedTerrainShader.Enable();
- //...
- ZWVector3 vChunkExtent(...);
- for (int cz = 0; cz < m_nChunkZ; ++cz)
- {
- for (int cx = 0; cx < m_nChunkX; ++cx)
- {
- ZWVector4D vChunkCenter4D(...);
- ZWVector3 vChunkCenter(vChunkCenter4D.x, vChunkCenter4D.y, vChunkCenter4D.z);
- if (IsChunkInsideViewFrustum(vChunkCenter, vChunkExtent))
- {
- vChunkCenter4D = m_mtModelMatrix * vChunkCenter4D;
- vChunkCenter = ZWVector3(vChunkCenter4D.x, vChunkCenter4D.y, vChunkCenter4D.z);
- GLfloat fDistToViewingCenter = (m_vViewingCenter - vChunkCenter).length();
- int nLodIndex = max(min(int(fDistToViewingCenter / 40.0f), m_nGridLodCount - 1), 0);
- m_ChunkedTerrainShader.SendUniform("chunkCoord", cx, cz);
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_IndexVBOInfoVec[nLodIndex].nVBO);
- glDrawElements( GL_TRIANGLES, m_IndexVBOInfoVec[nLodIndex].nIndexCount, GL_UNSIGNED_SHORT, NULL);
- }
- }
- }
- m_ChunkedTerrainShader.Disable();
- }
在设置网格的格子数时,先检测一下格子数是否满足2的次方幂(0 == x&(x-1)),另外对应Lod的总数量可以按此计算:
LodCount = Log2(max(m_nGridX, m_nGridZ)) + 1
初始化时,以对应步伐创建好这LodCount个索引集,并对应保存成LodCount个Index-VBO([索引顶点的VBO与多重纹理下的VBO] )和数量。渲染时遍历每个块,先进行粗略包围盒的视锥体剔除,测试通过的块,再根据视距选择一个Lod级别,采用对应的Index-VBO来渲染。这里为了方便,Lod的选取就是单纯按距离分段了(每距离40就向下替换一个Lod级别,当然这个数值应该向外暴露,作为可调参数),更理想的分法是近小远大以更合理分配Lod,尽量让近处远处格子占的屏幕像素量大致相当,就像在[Shadow Map阴影贴图技术之探Ⅳ]中提到的那样。
稍微改良一下上述代码吧。我们知道,opengl的bind之类进行绑定的操作是很重型的渲染效率减速具(VAO记录VBOs的渲染状态也是为了节省和加速[AB是一家?VAO与VBO] ),上面我在一个双重循环里不停地Bind一个新的Index-VBO,如果Chunk数大的时候,那效率是很糟糕的。那我可以把所有lod的索引数据全部保存到一个Index-VBO里,记录下Offset值和count值交给glDrawElements,这样就去除了这些繁多的Bind绑定了:
- struct IndexVBOInfo
- {
- GLuint nVBO;
- GLint nIndexOffset;
- GLint nIndexCount;
- GLint nLodIndex;
- };
- bool ZWChunkedTerrain::SetupData(...)
- {
- if (m_nGridX > 0 && m_nGridZ > 0 && m_fGridLength > 0)
- {
- //...
- unsigned int nLastIndexCount = 0;
- for (int iLod = 0; iLod < m_nGridLodCount; ++iLod)
- {
- nLastIndexCount = indexVec.size();
- nLocStep = int(powf(2.0f, float(iLod)));
- //...
- IndexVBOInfo indexVBOInfo;
- indexVBOInfo.nVBO = 0;
- indexVBOInfo.nIndexOffset = nLastIndexCount;
- indexVBOInfo.nIndexCount = indexVec.size() - nLastIndexCount;
- indexVBOInfo.nLodIndex = iLod;
- m_IndexVBOInfoVec.push_back(indexVBOInfo);
- }
- return true;
- }
- return false;
- }
- void ZWChunkedTerrain::Draw()
- {
- //...
- for (int cz = 0; cz < m_nChunkZ; ++cz)
- {
- for (int cx = 0; cx < m_nChunkX; ++cx)
- {
- if (IsChunkInsideViewFrustum(vChunkCenter, vChunkExtent))
- {
- //...
- m_ChunkedTerrainShader.SendUniform("chunkCoord", cx, cz);
- glDrawElements(GL_TRIANGLES, m_IndexVBOInfoVec[nLodIndex].nIndexCount, GL_UNSIGNED_SHORT,
- (GLvoid*)(sizeof(GLushort) * m_IndexVBOInfoVec[nLodIndex].nIndexOffset));
- }
- }
- }
- //...
- }
下面是通过颜色区分各个Lod层次下的Chunk的样图:
线框模式看上去有点模样,但是一到实体模式,马上出现问题了:不同Lod的Chunk块在交界处会产生明显的裂缝(crack)。而且,当你让摄像机移动时,会明显地感到Lod值变化带来该Chunk形状的突变,造成视觉上的不连续。然后,我认为上面的双重循环逐个执行视锥体剔除测试也太丑陋了点,还有没有优化的方法呢?——好吧,这些我都留给下一篇文章:[地形渲染II - Continuity of Terrain Lod]。
————————我是分割线————————
再在这里插播一个实现期间做的尝试:上面都是一个一个Chunk去轮询的,可不可以一个一个Lod地去轮询,然后直接绘制出每个Lod下对应的所有Chunk呢?事实上所有Chunk都使用的同一份顶点数据,只要能把(x,z)位置的chunk归类到某一个Lod,轮询Lod的时候就可以利用Geometry Instancing([乱弹纪录III:Geometry Instancing] )一次性提交该Lod的顶点数据给GPU,节省传输带宽。怎样做到这个归类操作呢?我是利用一个额外的Pass预先把各个chunk的中心x-z坐标(Coord)当作顶点属性传入一个Shader,顺便也把视锥体裁剪做了,然后按照视距确定出每个可视Coord的Lod,然后在Geometry Shader中选择性输出(transform feedback)。因为没有GL4.x的选择性输出到多个feedback buffer中的其中一个(multi stream out)的功能,所以我就把这个Pass拆分成LodCount个,每一个输出对应一个Lod的Coord数据到对应的一个Feedback Buffer中,并用对应的Query Object来查询输出的Coord的个数。在正常的渲染Pass就可以利用这些数据生成每个Lod对应的Insatnces了。
- m_LodAndCullingShader.Enable();
- //...
- glEnable(GL_RASTERIZER_DISCARD);
- for (GLint nLodIndex = 0; nLodIndex < m_nGridLodCount; ++nLodIndex)
- {
- m_LodAndCullingShader.SendUniform("nLod", nLodIndex);
- glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_IndexVBOInfoVec[nLodIndex].nFeedVBO);
- glBeginTransformFeedback(GL_POINTS);
- glBeginQuery(GL_PRIMITIVES_GENERATED, m_IndexVBOInfoVec[nLodIndex].nCoordQuery);
- glDrawArrays(GL_POINTS, 0, 1);
- glEndQuery(GL_PRIMITIVES_GENERATED);
- glEndTransformFeedback();
- }
- glDisable(GL_RASTERIZER_DISCARD);
- m_ChunkedTerrainShader.Enable();
- //...
- for (GLint nLod = 0; nLod < m_nGridLodCount; ++nLod)
- {
- glBindBuffer(GL_ARRAY_BUFFER, m_IndexVBOInfoVec[nLod].nFeedVBO);
- glEnableVertexAttribArray(VAT_LODCOORD);
- glVertexAttribIPointer(VAT_LODCOORD, 2, GL_INT, 0, NULL);
- glVertexAttribDivisor(VAT_LODCOORD, 1);
- glGetQueryObjectiv(m_IndexVBOInfoVec[nLod].nCoordQuery, GL_QUERY_RESULT, &m_IndexVBOInfoVec[nLod].nCoodCount);
- glDrawElementsInstanced( GL_TRIANGLES, m_IndexVBOInfoVec[nLod].nIndexCount, GL_UNSIGNED_SHORT,
- (GLvoid*)(sizeof(GLushort) * m_IndexVBOInfoVec[nLod].nIndexOffset), m_IndexVBOInfoVec[nLod].nCoodCount);
- }
结果是惊人地……差。分析原因有几种可能或其组合:1.很明显地这里的第一个Pass被分为多个子Pass了,虽然输入的数据量少但仍然会造成传输瓶颈(当然对于GL4.x的话是可以合并成单一Pass的,但目前暂时没这条件);2.Query Object的使用(虽然我觉得实际渲染的循环多少能Hide掉query object造成的查询延迟,但或许并没想象中那么好。当然在GL4.x的Transform Feedback Object下没必要使用query object,但没条件);3.过多的Bind调用(每一个Lod下的渲染都得绑定一次对应的Feedback VBO,等等);4.用了Geometry Shader(在[乱弹纪录I:Geometry Shader] 中提过使用这个Shader stage会损伤并行性,能不用就不用。但这里要选择性输出,所以还是得用的)。FPS的大幅下降也使我放弃这个方案了,但既然本文聊作记录,就把所有成功不成功的尝试也列出来罢。
于是,小插曲完毕,还是回归正题的方案吧。继续之前写的……哦,对了之前写的就是让客观换页了:) 请移步[地形渲染II - Continuity of Terrain Lod]。
本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
原文地址:http://www.zwqxin.cn/archives/opengl/lod-terrain-rendering-1.html