在前篇文章中,一个基于Lod的地形渲染基本成型了,但还遗留着一些与连续性相关的细节问题需要处理,本文将着重记录这一次地形渲染中为了解决或缓解这些问题所采取的方法,另外也将引入四叉树数据结构重新组织Chunk数据,以及进行多层的表面贴图。——ZwqXin.com
本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
原文地址:http://www.zwqxin.cn/archives/opengl/lod-terrain-rendering-2.html
所谓地形的连续性,一是空间上的连续性,地形块应该无缝连接,反之则反映为裂缝(crack)问题;二是时间上的连续性,在整个渲染程序运行过程中,摄像机位置方向变换过程中,视觉上不产生突然的形态上的改变。但是,目前的Lod Terrain则明显地存在这两个问题:从下图明显看出裂缝(分别显示近景和远景),而突变问题则会从移动摄像机过程中体现出来。其实两者本质上都同样是由于相邻的块具有不同的Lod值造成的模型精度不匹配。
地形裂缝的消失
先来看解决裂缝问题。如图,对于相邻Lod最多只会相差1的情况,左边是后方地形块的Lod>前方地形快的Lod时出现的情况,右边是后方地形块的Lod<前方地形快的Lod时出现的情况,低Lod(高细节)的块在该边缘线段上拥有多一个的顶点,而高Lod(低细节)的块在对应点上并没有顶点,所以也不会在该点执行高度图采样,而其实际的值是相邻顶点的插值(在这里也就是平均值),所以高细节的块在该多出的顶点的影响下产生朝上凸或朝下凹的效果。所以要修补的话,可以很简单粗暴——就是让高细节块的邻接边上相隔为1的两顶点连成线段,与那个多出的顶点组成一个三角形。这个三角形恰恰好可以完美填充该裂缝,而且它不产生新的顶点,而只是使用原来的高细节块的顶点数据(应该说,是全部Chunk块都在使用的那份包含最高细节所有顶点的VBO),通过新增索引数据产生新的三角形(trianles)而已。那么相邻Lod相差大于1的情况呢?嘛……调节好Lod和地形块大小的关系,不要让这些反常的情况出现……
- bool ZWChunkedTerrain::SetupData(...)
- {
- //..
- for (int iLod = 0; iLod < m_nGridLodCount; ++iLod)
- {
- //...
- //Try to fix crack
- if (iLod < m_nGridLodCount - 1)
- {
- int nLocVertOffset = nLocStep * nXVertCount;
- for (int ix = 0; ix < m_nGridX; ix += nLocStep * 2)
- {
- indexVec.push_back(ix);
- indexVec.push_back(ix + nLocStep);
- indexVec.push_back(ix + nLocStep * 2);
- indexVec.push_back(nZGridOffset + ix);
- indexVec.push_back(nZGridOffset + ix + nLocStep * 2);
- indexVec.push_back(nZGridOffset + ix + nLocStep);
- }
- for (int iz = 0; iz < m_nGridZ; iz += nLocStep * 2)
- {
- indexVec.push_back(iz * nXVertCount);
- indexVec.push_back(iz * nXVertCount + nLocVertOffset * 2);
- indexVec.push_back(iz * nXVertCount + nLocVertOffset);
- indexVec.push_back(nXGridOffset + iz * nXVertCount);
- indexVec.push_back(nXGridOffset + iz * nXVertCount + nLocVertOffset);
- indexVec.push_back(nXGridOffset + iz * nXVertCount + nLocVertOffset * 2);
- }
- }
- //...
- }
- }
上面给每份Lod的对应的Index-VBO都加了四条尾巴。分别都缝补四条边,其他都不变。但是很遗憾,尽管缝隙是消失了,却出现了额外的面片突出的现象(见图)。为啥呢?因为粗心大意的“一刀切”啊,上述只讨论相邻块Lod不一样的情况,如果Lod相同呢?那邻接边的缝补三角形不就重叠了么?对于下凹的段,这重叠的缝补三角形就暴露出来了——尖锐的边界。
正确的做法应该是具体块具体分析,我们要分析哪些边需要缝补,只有需要缝补的邻接边才需要让高细节那边的块产生对应的一条缝补边。也就是说,我们要对每个要渲染出来的块检测它的四个边界邻接的是不是低一个细节级别(Lod)的块,若是,才绘制缝补边(单独的DrawCall)。
- enum
- {
- CHUNK_LEFT = 0,
- CHUNK_RIGHT,
- CHUNK_BACK,
- CHUNK_FRONT,
- CHUNK_DIRECTION_COUNT
- };
- struct IndexVBOInfo
- {
- GLint nLodIndex;
- GLint nIndexOffset;
- GLint nIndexCount;
- GLint nCrackIndexOffset[CHUNK_DIRECTION_COUNT];
- GLint nCrackIndexCount[CHUNK_DIRECTION_COUNT];
- };
- bool ZWChunkedTerrain::SetupData(...)
- {
- //..
- for (int iLod = 0; iLod < m_nGridLodCount; ++iLod)
- {
- //...
- if (iLod < m_nGridLodCount - 1)
- {
- int nLocVertOffset = nLocStep * nXVertCount;
- indexVBOInfo.nCrackIndexOffset[CHUNK_LEFT] = indexVec.size();
- for (int iz = 0; iz < m_nGridZ; iz += nLocStep * 2)
- {
- indexVec.push_back(iz * nXVertCount);
- indexVec.push_back(iz * nXVertCount + nLocVertOffset * 2);
- indexVec.push_back(iz * nXVertCount + nLocVertOffset);
- }
- indexVBOInfo.nCrackIndexCount[CHUNK_LEFT] = indexVec.size() - indexVBOInfo.nCrackIndexOffset[CHUNK_LEFT];
- indexVBOInfo.nCrackIndexOffset[CHUNK_RIGHT] = indexVec.size();
- for (int iz = 0; iz < m_nGridZ; iz += nLocStep * 2)
- {
- indexVec.push_back(nXGridOffset + iz * nXVertCount);
- indexVec.push_back(nXGridOffset + iz * nXVertCount + nLocVertOffset);
- indexVec.push_back(nXGridOffset + iz * nXVertCount + nLocVertOffset * 2);
- }
- indexVBOInfo.nCrackIndexCount[CHUNK_RIGHT] = indexVec.size() - indexVBOInfo.nCrackIndexOffset[CHUNK_RIGHT];
- indexVBOInfo.nCrackIndexOffset[CHUNK_BACK] = indexVec.size();
- for (int ix = 0; ix < m_nGridX; ix += nLocStep * 2)
- {
- indexVec.push_back(ix);
- indexVec.push_back(ix + nLocStep);
- indexVec.push_back(ix + nLocStep * 2);
- }
- indexVBOInfo.nCrackIndexCount[CHUNK_BACK] = indexVec.size() - indexVBOInfo.nCrackIndexOffset[CHUNK_BACK];
- indexVBOInfo.nCrackIndexOffset[CHUNK_FRONT] = indexVec.size();
- for (int ix = 0; ix < m_nGridX; ix += nLocStep * 2)
- {
- indexVec.push_back(nZGridOffset + ix);
- indexVec.push_back(nZGridOffset + ix + nLocStep * 2);
- indexVec.push_back(nZGridOffset + ix + nLocStep);
- }
- indexVBOInfo.nCrackIndexCount[CHUNK_FRONT] = indexVec.size() - indexVBOInfo.nCrackIndexOffset[CHUNK_FRONT];
- }
- //...
- }
- }
初始化的时候,每个Lod的索引数据分别添加四条修补边的三角形的索引,并记录下位置(offset)和数量(count)。
- void ZWChunkedTerrain::Draw()
- {
- for (unsigned int i = 0; i < m_ChunkInfoVec.size(); ++i)
- {
- //...
- chunkInfo.bVisible = false;
- if (IsChunkInsideViewFrustum(...)
- {
- chunkInfo.bVisible = true;
- //...
- glDrawElements(...);
- }
- }
- for (unsigned int i = 0; i < m_ChunkInfoVec.size(); ++i)
- {
- TerrainChunk &chunkInfo = m_ChunkInfoVec[i];
- if(chunkInfo.bVisible)
- {
- GLint nNeighbourIndex = 0;
- GLint nCrackLodIndex = chunkInfo.nMorphLod;
- m_ChunkedTerrainShader.SendUniform("chunkCoord", chunkInfo.nX, chunkInfo.nZ);
- if (chunkInfo.nX > 0) //Left
- {
- nNeighbourIndex = chunkInfo.nZ * m_nChunkX + chunkInfo.nX - 1;
- DrawTerrainCrack(chunkInfo, nNeighbourIndex, CHUNK_LEFT);
- }
- if (chunkInfo.nX < m_nChunkX - 1) //Right
- {
- nNeighbourIndex = chunkInfo.nZ * m_nChunkX + chunkInfo.nX + 1;
- DrawTerrainCrack(chunkInfo, nNeighbourIndex, CHUNK_RIGHT);
- }
- if (chunkInfo.nZ > 0) //Back
- {
- nNeighbourIndex = (chunkInfo.nZ - 1) * m_nChunkX + chunkInfo.nX;
- DrawTerrainCrack(chunkInfo, nNeighbourIndex, CHUNK_BACK);
- }
- if (chunkInfo.nZ < m_nChunkZ - 1) //Front
- {
- nNeighbourIndex = (chunkInfo.nZ + 1) * m_nChunkX + chunkInfo.nX;
- DrawTerrainCrack(chunkInfo, nNeighbourIndex, CHUNK_FRONT);
- }
- }
- }
- }
- void ZWChunkedTerrain::DrawTerrainCrack(TerrainChunk &chunkInfo, GLuint nNeighbourIndex, GLenum NeighbourDirection)
- {
- if(m_ChunkInfoVec[nNeighbourIndex].bVisible && (m_ChunkInfoVec[nNeighbourIndex].nLod > chunkInfo.nLod ))
- {
- glDrawElements(GL_TRIANGLES, m_IndexVBOInfoVec[chunkInfo.nMorphLod].nCrackIndexCount[NeighbourDirection], GL_UNSIGNED_SHORT,
- (GLvoid*)(sizeof(GLushort) * m_IndexVBOInfoVec[chunkInfo.nMorphLod].nCrackIndexOffset[NeighbourDirection]));
- }
- }
这里m_ChunkInfoVec保存了所有Chunk的信息,后文会看到。很直观地,上面代码就是对可见的Chunk,检测四个方向上的邻接块并决定是否绘制缝补三角形。这样就能让地形快在空间上的连续性问题得到初步的解决了,下图蓝色部分标示了地形的Crack的缝补边。接下来是时间上动态连续性的问题了。
地形块的平滑过渡
想来想去也就只有动态地Morph(变形)了。简言之,就是让Lod的切换不再简单直接,而是有一定的延迟,这段延迟内根据视距的变化,决定当前每个Chunk的Lod是渐渐变形至高细节Lod的块的样子,还是渐渐变形至低细节Lod的样子。但是,执行一个顶点变形动画(Morph - Animation,可参考[MD2格式模型的格式、导入与帧动画] 一文)的关键在于变形的源(source)和目标(destination)的模型的顶点数量需要一致。而我们这里不同Lod的地形块是拥有不同顶点数目的,所以,无论是高细节向低细节的过渡还是相反的过渡,我们都得选择高细节(Lod值较小的一方)的索引顶点数据来执行变形动画——如果一个Chunk要从高细节过渡到低细节(摄像机远离中),那么它就得保持高细节状态的Lod从高细节的状态慢慢变形(morph)到低细节的状态,一旦达到与低细节状态一致,则立即切换成低细节状态的Lod的顶点集;如果一个Chunk要从低细节过渡到高细节(摄像机接近中),那么它一开始就得切换成高细节状态的Lod的顶点集,但这时这个顶点集的高度数据要跟低细节状态时的形态一模一样,然后再慢慢变形(morph)到应有的高细节状态。无论是哪种情况,在该Chunk被确定要切换Lod的时候,都得以较高细节的Lod为初始状态——我称这个虚拟的Lod为MorphLod,说虚拟是因为它不适于原本的Lod决定法则,但它却决定着当前Chunk是要使用哪一个Lod顶点集。
- struct TerrainChunk
- {
- GLint nX; //X方向排序
- GLint nZ; //Z方向排序
- GLint nLod; //真Lod
- GLint nLastLod; //上一个状态的真Lod
- GLint nMorphLod; //实质使用的虚拟Lod
- GLfloat fBlendValue; //从0过渡到1,状态的morph值
- GLfloat fShaderBlender;//实质传入Shader计算的morph值
- ZWTexcoord vCenterCoordPos;//Chunk的中心
- GLboolean bIsVisible; //Chunk是否可见
- };
- //std::vector<TerrainChunk> m_ChunkInfoVec;
- void ZWChunkedTerrain::Draw()
- {
- m_ChunkedTerrainShader.Enable();
- //...
- for (unsigned int i = 0; i < m_ChunkInfoVec.size(); ++i)
- {
- TerrainChunk &chunkInfo = m_ChunkInfoVec[i];
- chunkInfo.bIsVisible = false;
- if (IsChunkInsideViewFrustum(...)
- {
- chunkInfo.bIsVisible = true;
- ZWVector4D vChunkCenter4D = m_mtModelMatrix * ZWVector4D(chunkInfo.vCenterCoordPos.u, 0, chunkInfo.vCenterCoordPos.v, 1.0f);
- ZWVector3 vChunkCenter(vChunkCenter4D.x, vChunkCenter4D.y, vChunkCenter4D.z);
- GLfloat fDistToViewingCenter = (m_vViewingCenter - vChunkCenter).length();
- int nLodIndex = max(min(int(fDistToViewingCenter / m_fAvgLodDistance), m_nGridLodCount - 1), 0);
- chunkInfo.nMorphLod = nLodIndex;
- chunkInfo.fShaderBlender = 1.0f;
- if (m_bIsMorphedChunks)
- {
- if (-1 == chunkInfo.nLastLod)
- {
- chunkInfo.nLastLod = chunkInfo.nLod = nLodIndex;
- }
- else
- {
- GLfloat fCurBlendVal = chunkInfo.fBlendValue;
- if (chunkInfo.nLod > chunkInfo.nLastLod)
- {
- fCurBlendVal = max(min((fDistToViewingCenter - nLodIndex * m_fAvgLodDistance) / m_fAvgBlendDistance, 1.0f), 0.0f);
- }
- else if (chunkInfo.nLod < chunkInfo.nLastLod)
- {
- fCurBlendVal = max(min(((nLodIndex + 1) * m_fAvgLodDistance - fDistToViewingCenter) / m_fAvgBlendDistance, 1.0f), 0.0f);
- }
- if (fCurBlendVal >= 1.0f || fCurBlendVal <= 0.0f)
- {
- chunkInfo.nLod = nLodIndex;
- }
- if (nLodIndex != chunkInfo.nLod)
- {
- chunkInfo.nLastLod = chunkInfo.nLod;
- chunkInfo.nLod = nLodIndex;
- }
- chunkInfo.fShaderBlender = chunkInfo.fBlendValue = 1.0f;
- if (chunkInfo.nLod > chunkInfo.nLastLod) //Leaving
- {
- chunkInfo.fBlendValue = max(min((fDistToViewingCenter - nLodIndex * m_fAvgLodDistance) / m_fAvgBlendDistance, 1.0f), 0.0f);
- chunkInfo.nMorphLod = chunkInfo.nLastLod;
- if (chunkInfo.fBlendValue >= 1.0f)
- {
- chunkInfo.nLastLod = chunkInfo.nLod;
- }
- chunkInfo.fShaderBlender = 1.0f - chunkInfo.fBlendValue;
- }
- else if (chunkInfo.nLod < chunkInfo.nLastLod) //Approaching
- {
- chunkInfo.fBlendValue = max(min(((nLodIndex + 1) * m_fAvgLodDistance - fDistToViewingCenter) / m_fAvgBlendDistance, 1.0f), 0.0f);
- if (chunkInfo.fBlendValue >= 1.0f)
- {
- chunkInfo.nLastLod = chunkInfo.nLod;
- }
- chunkInfo.fShaderBlender = chunkInfo.fBlendValue;
- }
- }
- }
- else
- {
- chunkInfo.nLod = nLodIndex;
- chunkInfo.fShaderBlender = 1.2f;// >1.0
- }
- m_ChunkedTerrainShader.SendUniform("fAvgBlendValue", chunkInfo.fShaderBlender);
- m_ChunkedTerrainShader.SendUniform("chunkCoord", chunkInfo.nX, chunkInfo.nZ);
- glDrawElements(GL_TRIANGLES, m_IndexVBOInfoVec[chunkInfo.nMorphLod].nIndexCount, GL_UNSIGNED_SHORT,
- (GLvoid*)(sizeof(GLushort) * m_IndexVBOInfoVec[chunkInfo.nMorphLod].nIndexOffset));
- }
- }
- //DrawTerrainCrack
- m_ChunkedTerrainShader.Disable();
- }
这里的细逻辑要思路清晰不然很容易失误(譬如morph完成的刹那的闪烁)。我们的目的是要得到这样一个morph值,它跟视距有个线性相关的关系,把它传入Shader。Vertex Shader里,我们让一个顶点有两个边界值,一个是它确切从Height map检索出来的合适变换的高度值,另一个是它相邻两顶点的高度值的插值(平均值),也就是当它处于低细节状态时的位置值。怎么知道它由哪两个相邻顶点插值而来的呢?如下图,这是我们的网格的一部分,其中,所有顶点在内的顶点集(25个)隶属于较高细节层次(Lod1),而红色缘的顶点(9个)所组成的顶点集隶属于较低细节层次(Lod2),并且Lod2 = Lod1 + 1,是相邻细节层。那么,当某个地形块当前正在变形(morph)时,使用的都是MorphLod = Lod1,这下面所有顶点都传送到Shader插值出实际的高度值,其中,红色缘的顶点(譬如图中A、B、C、D)直接取对应高度图中的值,而紫色的顶点(G、H等等)则取横向两邻接顶点位置处的高度值两者的均值(譬如G取A、C位置中值),嫣红色的顶点(E、F等等)则取竖向两邻接顶点位置处的高度值两者的均值(譬如E取A、B位置中值),最后,灰色的顶点(譬如图中的I)则取对角线两邻接顶点位置处的高度值两者的均值(I取B、C位置中值)——为什么不是A、D中值呢?这是因为我们的索引是三角形索引(GL_TRIANGLES),图中正是我的程序中按索引绘制的三角形形状(每个格子的对角线从右上到左下)。假如我的索引稍微改变一下使得格子的对角线从左上到右下,这时I才应该取A、D中值(这一点我开始没有注意到,还想当然的取了四邻点均值,Bilinear么喂……这里自嘲一下)。
- void main()
- {
- //...
- fHeightValue = terrainHeight(vTerrainCoord);
- if(fAvgBlendValue < 1.0)
- {
- float fLocStep = pow(2.0, nLod);
- float fGridLenStep = fGridLength * fLocStep;
- ivec2 vArrange = ivec2(((attrib_texcoord) / fLocStep) * vGridCount);
- float fPreHeightValue = fHeightValue;
- if(1 == vArrange.s % 2 && 0 == vArrange.t % 2)
- {
- float fHValue1 = terrainHeight(vTerrainCoord + vec2(-fGridLenStep, 0.0));
- float fHValue2 = terrainHeight(vTerrainCoord + vec2( fGridLenStep, 0.0));
- fPreHeightValue = (fHValue1 + fHValue2) / 2.0;
- }
- else if(0 == vArrange.s % 2 && 1 == vArrange.t % 2)
- {
- float fHValue1 = terrainHeight(vTerrainCoord + vec2(0.0, -fGridLenStep));
- float fHValue2 = terrainHeight(vTerrainCoord + vec2(0.0, fGridLenStep));
- fPreHeightValue = (fHValue1 + fHValue2) / 2.0;
- }
- else if(1 == vArrange.s % 2 && 1 == vArrange.t % 2)
- {
- //On LeftFront-RightBack(index buffer specified) Diagonal
- float fHValueLF = terrainHeight(vTerrainCoord + vec2(-fGridLenStep, fGridLenStep));
- float fHValueRB = terrainHeight(vTerrainCoord + vec2( fGridLenStep, -fGridLenStep));
- fPreHeightValue = (fHValueLF + fHValueRB) / 2.0;
- }
- fHeightValue = mix(fPreHeightValue, fHeightValue, fAvgBlendValue);
- }
- //...
这看似利用Morph解决了时间方向上的不连续问题。但是此后出现了一个“惊人”的问题——空间上的缝隙又再出现了!
回想上面解决缝隙时我们是针对相邻Chunk的“静止的”真Lod值来选择缝补边的,但是当引入MorphLod的时候,问题来了:现在相邻的两个Chunk可能是同一个Lod甚至同一个MorphLod,但是它们在变形Morph的过程中,某些顶点与邻近块的对应位置顶点的高度可能是不相同的。考虑下面图示的情况:
我想到的一个方法,就是让Morph过程中的Chunk产生不同的缝补边。现在双方的顶点数是一致的,那么缺口处在竖直方向连接一条线段,构成左右两个三角形就可以了。但是我们的Chunk都是分开渲染的,所以要让变形中的Chunk自己去生成一个与邻接Chunk对应点一致的顶点,也就是说,要增加顶点了!为了对应各种Lod的情况,在最开始构筑那份全场通用的顶点数据时,就要让边缘的各顶点(除了四个角点)都再后续一个拷贝顶点(x,z值一致),因为我们要让这拷贝顶点与被拷贝顶点同时被渲出来:
- struct IndexVBOInfo
- {
- GLint nLodIndex;
- GLint nIndexOffset;
- GLint nIndexCount;
- GLint nCrackIndexOffset[CHUNK_DIRECTION_COUNT];
- GLint nCrackIndexCount[CHUNK_DIRECTION_COUNT];
- GLint nMorphCrackIndexOffset[CHUNK_DIRECTION_COUNT];
- GLint nMorphCrackIndexCount[CHUNK_DIRECTION_COUNT];
- };
- bool ZWChunkedTerrain::SetupData(...)
- {
- //Orignal Vertex and texcoord
- /// Morphing vertices's
- GLfloat fRecogValue = -m_fHeightScale * 2.0f - 1.0f;
- GLint nMorphVertOffset = posVec.size();
- for (int iz = 1; iz < m_nGridZ; ++iz)
- {
- posVec.push_back(ZWVector3(0, fRecogValue, iz * m_fGridLength));
- texcoordVec.push_back(ZWTexcoord(0, GLfloat(iz) / m_nGridZ));
- }
- for (int iz = 1; iz < m_nGridZ; ++iz)
- {
- posVec.push_back(ZWVector3(m_nGridX * m_fGridLength, fRecogValue, iz * m_fGridLength));
- texcoordVec.push_back(ZWTexcoord(1.0f, GLfloat(iz) / m_nGridZ));
- }
- for (int ix = 1; ix < m_nGridX; ++ix)
- {
- posVec.push_back(ZWVector3(ix * m_fGridLength, fRecogValue, 0));
- texcoordVec.push_back(ZWTexcoord(GLfloat(ix) / m_nGridX, 0));
- }
- for (int ix = 1; ix < m_nGridX; ++ix)
- {
- posVec.push_back(ZWVector3(ix * m_fGridLength, fRecogValue, m_nGridZ * m_fGridLength));
- texcoordVec.push_back(ZWTexcoord(GLfloat(ix) / m_nGridX, 1.0f));
- }
- GLint nMorphingVertCount = posVec.size() - nMorphVertOffset;
- for (int iLod = 0; iLod < m_nGridLodCount; ++iLod)
- {
- IndexVBOInfo indexVBOInfo;
- //....original index info
- /// Morphing vertices's
- GLushort nMorphvertIndex = 0;
- GLint nMorphingEdgeVertCount = nMorphingVertCount / 4;
- indexVBOInfo.nMorphCrackIndexOffset[CHUNK_LEFT] = indexVec.size();
- for (int iz = 0; iz < m_nGridZ; iz += nLocStep * 2)
- {
- nMorphvertIndex = nMorphVertOffset + CHUNK_LEFT * nMorphingEdgeVertCount + iz + nLocStep - 1;
- indexVec.push_back(iz * nXVertCount);
- indexVec.push_back(nMorphvertIndex);
- indexVec.push_back(iz * nXVertCount + nLocVertOffset);
- indexVec.push_back(iz * nXVertCount + nLocVertOffset);
- indexVec.push_back(nMorphvertIndex);
- indexVec.push_back(iz * nXVertCount + nLocVertOffset * 2);
- }
- indexVBOInfo.nMorphCrackIndexCount[CHUNK_LEFT] = indexVec.size() - indexVBOInfo.nMorphCrackIndexOffset[CHUNK_LEFT];
- indexVBOInfo.nMorphCrackIndexOffset[CHUNK_RIGHT] = indexVec.size();
- for (int iz = 0; iz < m_nGridZ; iz += nLocStep * 2)
- {
- nMorphvertIndex = nMorphVertOffset + CHUNK_RIGHT * nMorphingEdgeVertCount + iz + nLocStep - 1;
- indexVec.push_back(nXGridOffset + iz * nXVertCount);
- indexVec.push_back(nXGridOffset + iz * nXVertCount + nLocVertOffset);
- indexVec.push_back(nMorphvertIndex);
- indexVec.push_back(nMorphvertIndex);
- indexVec.push_back(nXGridOffset + iz * nXVertCount + nLocVertOffset);
- indexVec.push_back(nXGridOffset + iz * nXVertCount + nLocVertOffset * 2);
- }
- indexVBOInfo.nMorphCrackIndexCount[CHUNK_RIGHT] = indexVec.size() - indexVBOInfo.nMorphCrackIndexOffset[CHUNK_RIGHT];
- indexVBOInfo.nMorphCrackIndexOffset[CHUNK_BACK] = indexVec.size();
- for (int ix = 0; ix < m_nGridX; ix += nLocStep * 2)
- {
- nMorphvertIndex = nMorphVertOffset + CHUNK_BACK * nMorphingEdgeVertCount + ix + nLocStep - 1;
- indexVec.push_back(ix);
- indexVec.push_back(ix + nLocStep);
- indexVec.push_back(nMorphvertIndex);
- indexVec.push_back(nMorphvertIndex);
- indexVec.push_back(ix + nLocStep);
- indexVec.push_back(ix + nLocStep * 2);
- }
- indexVBOInfo.nMorphCrackIndexCount[CHUNK_BACK] = indexVec.size() - indexVBOInfo.nMorphCrackIndexOffset[CHUNK_BACK];
- indexVBOInfo.nMorphCrackIndexOffset[CHUNK_FRONT] = indexVec.size();
- for (int ix = 0; ix < m_nGridX; ix += nLocStep * 2)
- {
- nMorphvertIndex = nMorphVertOffset + CHUNK_FRONT * nMorphingEdgeVertCount + ix + nLocStep - 1;
- indexVec.push_back(nZGridOffset + ix);
- indexVec.push_back(nMorphvertIndex);
- indexVec.push_back(nZGridOffset + ix + nLocStep);
- indexVec.push_back(nZGridOffset + ix + nLocStep);
- indexVec.push_back(nMorphvertIndex);
- indexVec.push_back(nZGridOffset + ix + nLocStep * 2);
- }
- indexVBOInfo.nMorphCrackIndexCount[CHUNK_FRONT] = indexVec.size() - indexVBOInfo.nMorphCrackIndexOffset[CHUNK_FRONT];
- m_IndexVBOInfoVec.push_back(indexVBOInfo);
- }
-
- return true;
- }
- return false;
- }
- void ZWChunkedTerrain::DrawTerrainCrack(TerrainChunk &chunkInfo, GLuint nNeighbourIndex, GLenum NeighbourDirection)
- {
- if(m_ChunkInfoVec[nNeighbourIndex].bIsVisible)
- {
- if(m_ChunkInfoVec[nNeighbourIndex].nMorphLod > chunkInfo.nMorphLod)
- {
- glDrawElements(GL_TRIANGLES, m_IndexVBOInfoVec[chunkInfo.nMorphLod].nCrackIndexCount[NeighbourDirection], GL_UNSIGNED_SHORT,
- (GLvoid*)(sizeof(GLushort) * m_IndexVBOInfoVec[chunkInfo.nMorphLod].nCrackIndexOffset[NeighbourDirection]));
- }
- else if (m_bIsMorphedChunks)
- {
- glDrawElements(GL_TRIANGLES, m_IndexVBOInfoVec[chunkInfo.nMorphLod].nMorphCrackIndexCount[NeighbourDirection], GL_UNSIGNED_SHORT,
- (GLvoid*)(sizeof(GLushort) * m_IndexVBOInfoVec[chunkInfo.nMorphLod].nMorphCrackIndexOffset[NeighbourDirection]));
- }
- }
- }
我们要在Vertex Shader中识别这个点究竟是Morphing中的顶点(需要插值),还是这些额外的拷贝点(只对之输出目标点的高度值),需要一个识别的方式。这里我简单粗暴地使用了原输入点的y值作为fRecogValue。给予一个固定值给它以区分,有点丑陋但是避免了更丑陋的:一个额外的bool型顶点属性数据。绘制缝隙缝补时,通过判断当前Chunk与邻接Chunk的MorphLod判断当前Chunk是否存在需要Morph的情况,若是则绘制上述针对Morph的缝补三角形,若不是(neighbour.MorphLod > self.MorphLod)则继续绘制以前的静态缝补边。
上图所示是这种Morph缝隙的缝补边,可见与每单位使用单一三角形缝补的一般缝隙不同,这里每单位之用两个三角形来作缝补。
这种方式很好地处理了同数量顶点的邻近块中的一个正在变形的情况,但是它其实是不适用于两个Chunk都在变形的情况的(事实上这样的情况也常见)。这算一个缺陷吧,我还没想到能很好解决的方法。但毕竟产生这种情况的Chunk不会太靠近视点,它们也一般都是变向同一个Lod,仅仅是morph值的些少偏差导致有点错位而已,而且它们作为morphing chunk还是会产生上面的动态缝补边的,所以很多时候都会遮盖掉缝隙(Crack)留下单纯的贴图不连续而已。
下图总结两种Crack的应用场合,相同的位置,视距前者略大于后者(可以想象一下摄像机正向前行进),期间后方的地形块(Chunk)开始进入Morph变形至高细节层次。不同MorphLod之间标示不同颜色(红色和黄色),而静态缝隙用深蓝色标示,Morph缝隙用天蓝色标示。
四叉树组织Chunk
这是我查资料的时候发现其他人实现地形(Geomipmap或Clipmap)时都会率先使用四叉树这种数据结构来组织。考虑到可以减少对Chunk遍历计算视锥体剔除造成的计算浪费,所以我也自己捉摸着实现了个简单的四叉树来组织这次地形渲染中的Chunk。区别于一般用于管理室内场景的八叉树,四叉树主要用于管理像地形这类主要向二维拓展的物件。
重新考虑一下,现在渲染的时候是遍历所有Chunk来执行视锥体剔除测试的,如果有N个Chunk就得执行N遍,对于N很大的时候,这就很影响渲染效率了。更好的做法是把场景先分成田字型的四大块,分别对之执行测试,如果某一大块测试不通过(在视锥体外),那就根本不用检测它里面的各个Chunk了,因为肯定都是不可见的;如果该某大块通过测试,那就对该大块再细分成四块,再分别执行测试……这种迭代的方式可以有效地减少视锥体剔除测试的次数。四叉树就是这种把平面物件不断一层一层地分块到尽并储存各种块的信息的数据结构。另外,像那种查询物体在哪一块chunk上的操作,也可以通过四叉树查询。反正涉及原本需要轮询Terrain Chunks的操作,都可以用四叉树完成。我们只需要预先建立好一棵根据地理信息容纳地形Chunk数据的四叉树就可以了。
- //ZWRectQuadTree<TerrainChunk> m_TerrainQuadTree;
- //std::vector<TerrainChunk *> m_VisibleChunkVec;
- bool ZWChunkedTerrain::PostLoad()
- {
- m_TerrainQuadTree.BuildTree(ZWRectQuadTree<TerrainChunk>::Rect(0, 0, m_nChunkX * m_fGridLength * m_nGridX, m_nChunkZ * m_fGridLength * m_nGridZ),
- m_nChunkX, m_nChunkZ);
- //...
- for (unsigned int i = 0; i < m_ChunkInfoVec.size(); ++i)
- {
- m_TerrainQuadTree.InsertDataReference(ZWRectQuadTree<TerrainChunk>::Coord(m_ChunkInfoVec[i].vCenterCoordPos.u, m_ChunkInfoVec[i].vCenterCoordPos.v),
- &m_ChunkInfoVec[i]);
- }
- //...
- }
- void ZWChunkedTerrain::Draw()
- {
- //...
- CollectChunksInsideViewFrustum();
- for (unsigned int i = 0; i < m_VisibleChunkVec.size(); ++i)
- {
- TerrainChunk *chunkInfo = m_VisibleChunkVec[i];
- //...Draw chunk and cracks
- }
- //...
- }
- void ZWChunkedTerrain::CollectChunksInsideViewFrustum()
- {
- ZWMatrix16 mtModelViewProj(m_mtModelMatrix);
- if (m_pProjMatrixRef && m_pViewMatrixRef)
- {
- mtModelViewProj = (*m_pProjMatrixRef) * (*m_pViewMatrixRef) * mtModelViewProj;
- }
- m_VisibleChunkVec.clear();
- ZWRectQuadTree<TerrainChunk>::Node *pRootNode = m_TerrainQuadTree.GetRootNode();
- if (pRootNode)
- {
- CollectChunksInsideViewFrustum(pRootNode, mtModelViewProj);
- }
- }
- void ZWChunkedTerrain::CollectChunksInsideViewFrustum(ZWRectQuadTree<TerrainChunk>::Node *pNode, const ZWMatrix16 &mtModelViewProj)
- {
- if (IsChunkInsideViewFrustum(ZWVector3(pNode->rect.center.x, 0, pNode->rect.center.y),
- ZWVector3(pNode->rect.bound.bx / 2.0f, m_fHeightScale, pNode->rect.bound.by / 2.0f), mtModelViewProj))
- {
- if (m_TerrainQuadTree.IsLeafNode(pNode))
- {
- m_TerrainQuadTree.RetrieveNodeData(pNode, m_VisibleChunkVec);
- }
- else
- {
- for (int i = 0; i < ZWRectQuadTree<TerrainChunk>::CHILD_NODE_COUNT; ++i)
- {
- if (pNode->subChild[i])
- {
- CollectChunksInsideViewFrustum(pNode->subChild[i], mtModelViewProj);
- }
- }
- }
- }
- }
场景层次贴图
其实这个在上篇([地形渲染I - Lod of A Terrain])中的开头也提到,是涉及地形视觉质量的方面了。譬如我们有一张雪地的贴图、一张岩石的贴图、一张草地的贴图,怎样随心所欲地混合成贴合我们的地形的表面贴图呢?其实一个简单的方案就是提供三张对应的alpha贴图(或者一张颜色纹理中的RGB三个通道,又或者两个alpha图/两个通道,而最后一张贴图对应的就由1.0减去前两者的值总和),在Fragment Shader里把这些alpha值提取出来与对应的表面贴图相乘,各结果相加,就是结果了。我们实际上就只要控制这些Alpha值图就可以了,可以通过编辑工具生成嘛。
FinalDiffuseCol = diffuseCol1 * fAlpha1 + diffuseCol2 * fAlpha2 + diffuseCol3 * fAlpha3 + ......
- vec4 texCol = vec4(0.0); //Diffuse Color
- int nTailCompositorCount = nDiffuseTexCount - min(nDiffuseTexCount, nCompositorCount);
- vec4 texDiffuse = vec4(0.0);
- vec4 texComposite = vec4(0.0);
- float fComposite = 0.0;
- float fCurComposition = 0.0;
- for(int i = 0; i < nDiffuseTexCount; ++i)
- {
- vec4 texDiffuse = texture2DArray(diffuseTexArray, vec3(varying_vf_texcoord, float(i)));
- if(nCompositorCount == i)
- {
- fComposite = (1.0 - fComposite) / nTailCompositorCount;
- }
- else
- {
- if(nCompositorPerChannel > 0)
- {
- if(0 == i % 4)
- {
- texComposite = texture2DArray(compositeTexArray, vec3(varying_vf_texcoord, float(i / 4)));
- }
- fComposite = texComposite[i % 4];
- }
- else
- {
- fComposite = texture2DArray(compositeTexArray, vec3(varying_vf_texcoord, float(i))).r;
- }
- }
- texCol += texDiffuse * fComposite;
- fCurComposition += fComposite;
- }
除了颜色贴图外,一般地形还包括一些Tile贴图,平铺式地叠加到颜色贴图上,这样可以提高地形局部位置呈现出来的精细感,而不致于因为分辨率过低的表面贴图,使摄像机靠近地表时感觉不致于太“粗糙”……还值得一提的就是光照贴图(Light-Map),对地形来说,因其体积庞大,所以一般不适宜用常用的实时阴影算法(可参考本站文章[Shadow Volume 阴影锥技术之探Ⅰ] [Shadow Map阴影贴图技术之探Ⅰ] )去计算自阴影(譬如斜阳下一个山峰的影子投到其山腰山脚上),而且一般室外场景会限制为单一日光段,所以像游戏那类应用,常常会让制作高度图的美术资源那方面也同时烘焙一张对应的LigtMap出来。程序直接让最终像素颜色乘以该静态的LightMap对应位置的通道值(0.0~1.0),这样无论是多大规模的阴影还是需要漂亮的自阴影、软阴影效果,都可以轻易搞掂了——当然了,前提是-静止-的。
- enum CompositeMappingMode
- {
- CMM_PER_CHANNEL,
- CMM_PER_TEXTURE,
- };
- struct DiffuseMapInfo
- {
- CompositeMappingMode compositeMappingMode;
- GLuint nDiffuseTexArray;
- GLuint nCompositeTexArray;
- GLint nDiffuseTexCount;
- GLint nCompositeTexCount;
- GLuint nDefaultDiffuseTex;//只需要单一表面纹理时
- GLuint nDetailTex;
- GLint nDetailTileCount;
- };
表面纹理和Alpha纹理(Composite)都用纹理数组([学一学, Texture Array纹理数组] )载入,便于上面Fragment Shader的读取。
好了,这两篇记录这次地形渲染过程的流水文到此完结了。相信随着OpenGL4.x的细分曲面技术的发展,地形的渲染可能会趋向更便捷更灵活更丰富的方式;随着Virtual texture的技术的发展,今后使用Page(页面)来动态加载地形的方式也越来越普遍了,Google Earth那种相应和完美度也能变得寻常起来吧。不过,今天,还是立此为碑,本站的Terrain Rendering文章也有了,以后技术拓展也有个参照物了吧~
本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
原文地址:http://www.zwqxin.cn/archives/opengl/lod-terrain-rendering-2.html