[OpenGL] 刷地形初版实现

        最近做了刷地形的初版效果,由于近期较忙,所以就简单地说一下大致的方案。

        本身刷地形的原理并不难,即在鼠标选择位置的一个范围内,通过指定的高度模板,以叠加的方式改变地形高度。

画刷模板

        在画刷模板这一块没有过多讲究,直接使用了高斯算子。

        根据画刷大小生成不同大小的画刷算子。为了保证不同大小的画刷算子形状一致,此处限制了高斯函数中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.csdn.net/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]);
        }
    }
}

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值