最近做了刷地形的初版效果,由于近期较忙,所以就简单地说一下大致的方案。
本身刷地形的原理并不难,即在鼠标选择位置的一个范围内,通过指定的高度模板,以叠加的方式改变地形高度。
画刷模板
在画刷模板这一块没有过多讲究,直接使用了高斯算子。
根据画刷大小生成不同大小的画刷算子。为了保证不同大小的画刷算子形状一致,此处限制了高斯函数中x,y的范围为[-10,10],根据插值计算出当前网格对应的x,y,再带入高斯函数计算。
通过alpha参数可控制形状。
生成代码如下:
void Terrain::UpdateGaussTemplate()
{
int n = m_nBrushSize;
int len = 2 * n + 1;
m_vecGuassTemplate.resize(static_cast<size_t>(4 * len * len));
float alpha = 2.0;
float square_alpha = alpha * alpha;
for(int i = -n; i <= n; i++)
{
for(int j = -n; j <= n; j++)
{
size_t idx = static_cast<size_t>((i + n) * len + j + n);
float x = 10.0f * i / n;
float y = 10.0f * j / n;
m_vecGuassTemplate[idx] = exp(-(x * x + y * y) / (2 * square_alpha)) / sqrt(2 * PI * square_alpha);
}
}
}
计算鼠标点选世界坐标
当鼠标按下的时候,我们需要得知它当前选中的物体,以及当前选中位置的世界坐标。我们知道dx拾取物体的方式是射线拾取,而OpenGL(1.0)则维护了一个拾取缓冲区。
我们本次模拟了后一种方法,具体做法为:
(1) 做一次离屏渲染,把每个像素可见物体的id、深度记录到一张纹理中。
(2) 获取鼠标当前点击像素对应的id、深度,根据视图/投影矩阵的逆矩阵来还原世界坐标。
(3) 确定作用的地形网格,叠加对应的高斯模板。
实际操作上由于我本身还维护了透明物体的渲染,要多次渲染并叠加,所以整体操作流程要更为复杂一些,但基本思路是不变的。
此处的深度我记录的是相机空间下的线性深度,具体的记录深度/深度还原世界坐标的方式在我这篇文章中有介绍:https://blog.youkuaiyun.com/ZJU_fish1996/article/details/87211119
接下来的问题就是从我们预先生成的缓冲区里读取数据了。OpenGL提供了一个叫glReadPixels的函数,可以基本满足我们的需求,不过据说效率很一般,好在我们只需要一个像素的数据,不需要把整个屏幕的像素都拷贝到CPU。
大致的方法是:
(1) 绑定当前的读缓冲区(深度,id写入的那个缓冲区):
gl->glBindFramebuffer(GL_READ_FRAMEBUFFER, gFrameBuffer->frameBuffer);
(2) 设定当前激活的缓冲区(由于我用了多重缓冲区,所以需要指明是哪一个,如果没有使用多重缓冲区就不必了):
gl->glReadBuffer(GL_COLOR_ATTACHMENT1);
(3) 读取数据:
vector<float> pixels(4, 0);
gl->glReadPixels(posX, posY, 1, 1, GL_RGBA, GL_FLOAT, pixels.data());
(4) 解除读缓冲区绑定:
gl->glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
我的这部分的代码如下:
void RenderCommon::OnSelectedPos(bool bNeedClear)
{
if(selectedPos == QVector2D(-1, -1)) return;
QOpenGLExtraFunctions* gl = QOpenGLContext::currentContext()->extraFunctions();
gl->glBindFramebuffer(GL_FRAMEBUFFER, 0);
int posX = static_cast<int>(selectedPos.x());
int posY = static_cast<int>(selectedPos.y());
auto gFrameBuffer = CResourceInfo::Inst()->CreateFrameBuffer("GBuffer", screenX, screenY, 4);
{
vector<float> pixels(4, 0);
vector<float> depths(4, 0);
gl->glBindFramebuffer(GL_READ_FRAMEBUFFER, gFrameBuffer->frameBuffer);
gl->glReadBuffer(GL_COLOR_ATTACHMENT3);
gl->glReadPixels(posX, posY, 1, 1, GL_RGBA, GL_FLOAT, pixels.data());
gl->glReadBuffer(GL_COLOR_ATTACHMENT1);
gl->glReadPixels(posX, posY, 1, 1, GL_RGBA, GL_FLOAT, depths.data());
gl->glReadBuffer(GL_NONE);
gl->glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
int id = static_cast<int>(pixels[0] * 255);
float depth = depths[3];
if(!fIsZero(depth))
{
float t = tan(GetFov()/2);
float vx = static_cast<float>(posX) / screenX;
float vy = static_cast<float>(posY) / screenY;
float x = (vx * 2 - 1) * zFar * t * aspect * depth;
float y = (vy * 2 - 1) * zFar * t * depth;
QVector4D pos = QVector4D(x, y, -zFar * depth , 1);
QMatrix4x4 viewMatrix = Camera::Inst()->GetViewMatrix();
QVector4D ret = viewMatrix.inverted() * pos;
selectedWorldPos = QVector3D(ret.x(), ret.y(), ret.z()) / ret.w();
}
if(id != 0)
{
selectedId = id;
}
}
if(bNeedClear)
{
size_t idx = static_cast<size_t>(selectedId - 1);
if(ObjectInfo::Inst()->GetObject(idx)->m_strType == "Terrain")
{
Object* obj = ObjectInfo::Inst()->GetObject(idx).get();
Terrain* terrain = static_cast<Terrain*>(obj);
terrain->DoBrush(selectedWorldPos.x(), selectedWorldPos.z());
selectedPos = QVector2D(-1, -1);
}
}
}
地形绘制更新
地形由一个高度图来维护,是一个二维的数组,此处给出高度图的一个定义:
class HeightMap
{
private:
bool m_bDirty = true;
float m_fDelta = 0.1f;
vector<vector<float>> m_vecHeights;
public:
// vector<float>& operator[](size_t i)
// {
// return m_vecHeights[i];
// }
HeightMap(size_t x = 0, size_t y = 0)
{
m_vecHeights.resize(x, vector<float>(y));
}
float GetDelta() const
{
return m_fDelta;
}
void SetDelta(float fDelta)
{
m_fDelta = fDelta;
}
int GetX() const
{
return static_cast<int>(m_vecHeights.size());
}
int GetY() const
{
return m_vecHeights.size() == 0 ? 0 : static_cast<int>(m_vecHeights[0].size());
}
float Get(int _i, int _j) const
{
size_t i = static_cast<size_t>(_i);
size_t j = static_cast<size_t>(_j);
return m_vecHeights[i][j];
}
bool Set(int _i, int _j, float data)
{
size_t i = static_cast<size_t>(_i);
size_t j = static_cast<size_t>(_j);
if(i >= m_vecHeights.size() || (m_vecHeights.size() != 0 && j >= m_vecHeights[0].size()))
{
return false;
}
if(!fIsEqual(data, m_vecHeights[i][j]))
{
m_bDirty = true;
m_vecHeights[i][j] = data;
}
return true;
}
bool Append(int _i, int _j, float data)
{
size_t i = static_cast<size_t>(_i);
size_t j = static_cast<size_t>(_j);
if(i >= m_vecHeights.size() || (m_vecHeights.size() != 0 && j >= m_vecHeights[0].size()))
{
return false;
}
if(!fIsZero(data))
{
m_bDirty = true;
m_vecHeights[i][j] += data;
}
return true;
}
bool IsDirty() const
{
return m_bDirty;
}
void SetDirty(bool bDirty)
{
m_bDirty = bDirty;
}
friend ostream& operator<<(ostream& out, const HeightMap& h)
{
size_t m = h.m_vecHeights.size();
size_t n = m == 0 ? 0 : h.m_vecHeights[0].size();
out << m << " " << n << " ";
for(size_t i = 0;i < m;i++)
{
for(size_t j = 0;j < n; j++)
{
out << h.m_vecHeights[i][j] << " ";
}
}
return out;
}
friend istream& operator>>(istream& in, HeightMap& h)
{
size_t m, n;
in >> m >> n;
h.m_vecHeights.resize(m, vector<float>(n));
for(size_t i = 0;i < m;i++)
{
for(size_t j = 0;j < n; j++)
{
in >> h.m_vecHeights[i][j];
}
}
return in;
}
};
地形的数据改变后就需要重新绑定vb了,这里也是简单粗暴地重新写入了buffer里的所有数据,调用的是QOpenGLBuffer封装的write函数,它本身也可以进行vb数据的局部更新,有一定优化空间。不过画刷修改的地形数据在buffer里分布可能较为分散,分批次写入不好说会不会更优。
buffer结构的定义:
struct MeshBuffer
{
QOpenGLBuffer arrayBuf;
QOpenGLBuffer indexBuf;
int vertexNum = 0;
int indiceNum = 0;
MeshBuffer() : indexBuf(QOpenGLBuffer::IndexBuffer)
{
arrayBuf.create();
indexBuf.create();
}
~MeshBuffer()
{
arrayBuf.destroy();
indexBuf.destroy();
}
bool IsInit()
{
return vertexNum && indiceNum;
}
void Init(VertexData* vertex, int num)
{
vertexNum = num;
arrayBuf.bind();
arrayBuf.allocate(vertex, vertexNum * static_cast<int>(sizeof(VertexData)));
}
void Init(GLushort* indice, int num)
{
indiceNum = num;
indexBuf.bind();
indexBuf.allocate(indice, indiceNum * static_cast<int>(sizeof(GLushort)));
}
void bind()
{
arrayBuf.bind();
indexBuf.bind();
}
void realloc(VertexData* vertex, int num, int offset = 0)
{
arrayBuf.bind();
arrayBuf.write(offset, vertex, num * static_cast<int>(sizeof(VertexData)));
}
void realloc(GLushort* indice, int num, int offset = 0)
{
indexBuf.bind();
indexBuf.write(offset, indice, num * static_cast<int>(sizeof(GLushort)));
}
MeshBuffer* Clone()
{
MeshBuffer* buffer = new MeshBuffer();
buffer->arrayBuf = arrayBuf;
buffer->indexBuf = indexBuf;
buffer->vertexNum = vertexNum;
buffer->indiceNum = indiceNum;
return buffer;
}
};
地形buffer初始化部分,程序控制生成x * y的网格数据:
void GeometryEngine::initGridGeometry(const HeightMap& heightMap)
{
bool bInit = gridBuffer.IsInit();
int x = heightMap.GetX();
int y = heightMap.GetY();
float delta = heightMap.GetDelta();
const int vertexNum = x * y;
const int indiceNum = (x - 1) * (y - 1) * 6;
vector<VertexData> Vertices(static_cast<size_t>(vertexNum));
vector<GLushort> Indices;
for(int i = 0; i < x; i++)
{
for(int j = 0; j < y; j++)
{
size_t idx = static_cast<size_t>(i * y + j);
Vertices[idx].position = QVector3D(delta * (j - x/2), delta * (i - y/2), heightMap.Get(i, j));
Vertices[idx].texcoord = QVector2D(float(j) / (x - 1), float(i) / (y - 1));
}
}
for(int i = 0;i < x - 1; i++)
{
for(int j = 0;j < y - 1; j++)
{
GLushort i1 = static_cast<GLushort>(i * y + j);
GLushort i2 = static_cast<GLushort>(i * y + j + 1);
GLushort i3 = static_cast<GLushort>(y * (i + 1) + j);
Indices.push_back(i1);
Indices.push_back(i2);
Indices.push_back(i3);
}
}
for(int i = 0;i < x - 1; i++)
{
for(int j = 0;j < y - 1; j++)
{
GLushort i1 = static_cast<GLushort>(i * y + j + 1);
GLushort i2 = static_cast<GLushort>(y * (i + 1) + j + 1);
GLushort i3 = static_cast<GLushort>(y * (i + 1) + j);
Indices.push_back(i1);
Indices.push_back(i2);
Indices.push_back(i3);
}
}
for(size_t i = 0;i < Indices.size() / 3;i++)
{
CalNormalAndTangent(Vertices[Indices[3 * i]],Vertices[Indices[3 * i + 1]],Vertices[Indices[3 * i + 2]]);
}
gridBuffer.Init(Vertices.data(), vertexNum);
if(!bInit) gridBuffer.Init(Indices.data(), indiceNum);
}
地形数据绘制部分:
void GeometryEngine::drawGrid(QOpenGLShaderProgram* program, HeightMap& heightMap, bool bTess)
{
int x = heightMap.GetX();
int y = heightMap.GetY();
if(heightMap.IsDirty())
{
heightMap.SetDirty(false);
initGridGeometry(heightMap);
}
auto gl = QOpenGLContext::currentContext()->extraFunctions();
gridBuffer.bind();
int offset = 0;
int vertexLocation = program->attributeLocation("a_position");
program->enableAttributeArray(vertexLocation);
program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 3, sizeof(VertexData));
offset += sizeof(QVector3D);
int tangentLocation = program->attributeLocation("a_tangent");
program->enableAttributeArray(tangentLocation);
program->setAttributeBuffer(tangentLocation, GL_FLOAT, offset, 3, sizeof(VertexData));
offset += sizeof(QVector3D);
int normalLocation = program->attributeLocation("a_normal");
program->enableAttributeArray(normalLocation);
program->setAttributeBuffer(normalLocation, GL_FLOAT, offset, 3, sizeof(VertexData));
offset += sizeof(QVector3D);
int texcoordLocation = program->attributeLocation("a_texcoord");
program->enableAttributeArray(texcoordLocation);
program->setAttributeBuffer(texcoordLocation, GL_FLOAT, offset, 2, sizeof(VertexData));
if(bTess)
{
gl->glPatchParameteri(GL_PATCH_VERTICES, 3);
gl->glDrawElements(GL_PATCHES, 6 * (x - 1) * (y - 1), GL_UNSIGNED_SHORT, nullptr);
}
else
{
gl->glDrawElements(GL_TRIANGLES, 6 * (x - 1) * (y - 1), GL_UNSIGNED_SHORT, nullptr);
}
}
最后是画刷修改地形高度部分,我们只需要修改heightMap的数据,检测到数据“脏"后,绘制部分会自动重新生成地形buffer数据。
void Terrain::DoBrush(float x, float y)
{
int sizeX = m_heightMap.GetX();
int sizeY = m_heightMap.GetY();
float delta = m_heightMap.GetDelta();
int pos_j = static_cast<int>(( x / delta/ m_f3Scale.x + sizeX / 2));
int pos_i = static_cast<int>((-y / delta/ m_f3Scale.z + sizeY / 2));
int n = static_cast<int>(m_nBrushSize);
int len = (n * 2 + 1);
for(int i = -n;i <= n; i++)
{
for(int j = -n; j <= n; j++)
{
size_t idx = static_cast<size_t>((i + n) * len + j + n);
m_heightMap.Append(i + pos_i + n / 2, j + pos_j + n / 2, m_fBrushStrength * m_vecGuassTemplate[idx]);
}
}
}