OpenGL (LWJGL 3) culling terrain vertices/triangles that are not in the view frustum



I am trying to implement frustum culling in my 3D Game currently and it has worked efficiently with the entities because they have a bounding box (AABB) and its easier to check a box against the frustum. On saying that, how would I cull the terrain? (it physically cannot have a AABB or sphere)

The frustum class (I use the inbuilt JOML one):

import org.joml.FrustumIntersection;
import org.joml.Matrix4f;

import engine.Terrians.Terrain;
import engine.maths.Matrices;
import engine.maths.Vector3f;
import engine.objects.Camera;
import engine.physics.AABB;

public class FrustumG {

    private final Matrix4f projectionViewMatrix;

    private FrustumIntersection frustumInt;

    public FrustumG() {
        projectionViewMatrix = new Matrix4f().identity();
        frustumInt = new FrustumIntersection();
    }
    
    public void update(Matrix4f projectionMatrix, Matrix4f viewMatrix) {
        projectionViewMatrix.set(projectionMatrix);
        projectionViewMatrix.mul(viewMatrix);
        
        frustumInt.set(projectionViewMatrix);
    }
    
    public boolean intersectsAABB(AABB aabb) {
        return frustumInt.testAab(aabb.getWorldMinX(), aabb.getWorldMinY(), aabb.getWorldMinZ(), 
                                  aabb.getWorldMaxX(), aabb.getWorldMaxY(), aabb.getWorldMaxZ());
    }
    
    public boolean intersectsPoint(Vector3f point) {
        return frustumInt.testPoint(point.getX(), point.getY(), point.getZ());
    }
    
}

My Mesh Class stores vertices information for the Terrain. I do not want to edit and update the vertices VBO every frame, as I have googled that it can affect the performance of the game (and I would also have to edit the indices list, and loop through the two lists every frame). I saw some websites saying to use GL_DYNAMIC_DRAW instead of GL_STATIC_DRAW, but I did not understand it.

Here is the Mesh Class:

import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.system.MemoryUtil;

public class Mesh {
    private float[] verticesData;
    private int[] indicesData;
    private float[] textureData;
    private float[] normalsData;
    private int vao, pbo, ibo, cbo, tbo, nbo;
    private Texture texture;
    
    public Mesh(float[] verticesArray, int[] indices, float[] normalsArray, float[] texturesArray) {
        this.verticesData = verticesArray;
        this.indicesData = indices;
        this.textureData = texturesArray;
        this.normalsData = normalsArray;
    }
    
    public Mesh(float[] positions, int dimensions) {
        this.verticesData = positions;
        vao = GL30.glGenVertexArrays();
        GL30.glBindVertexArray(vao);
         
        FloatBuffer positionBuffer = MemoryUtil.memAllocFloat(positions.length);
        positionBuffer.put(positions).flip();
        
        pbo = storeData(positionBuffer, 0, dimensions);
    }
    
    public void createTextures(String filepath) {
        Texture texture = new Texture(filepath);
        texture.create();
        this.texture = texture;
    }
    
    public int loadCubeMap(String[] textureFiles, String textureMainSystemPath) {
        int texID = GL11.glGenTextures();
        GL30.glActiveTexture(GL30.GL_TEXTURE0);
        GL30.glBindTexture(GL30.GL_TEXTURE_CUBE_MAP, texID);
        
        for(int i = 0; i < textureFiles.length; i++) {
            Texture.createSkybox(textureMainSystemPath + textureFiles[i] + ".png", i);
        }
        
        GL30.glTexParameteri(GL30.GL_TEXTURE_CUBE_MAP, GL30.GL_TEXTURE_MAG_FILTER, GL30.GL_LINEAR);
        GL30.glTexParameteri(GL30.GL_TEXTURE_CUBE_MAP, GL30.GL_TEXTURE_MIN_FILTER, GL30.GL_LINEAR);
        
        return texID;
    }
    
    public void createMeshes() {    
        vao = GL30.glGenVertexArrays();
        GL30.glBindVertexArray(vao);
        
        FloatBuffer positionBuffer = MemoryUtil.memAllocFloat(verticesData.length);
        positionBuffer.put(verticesData).flip();
        
        pbo = storeData(positionBuffer, 0, 3);
        
        /**FloatBuffer colorBuffer = MemoryUtil.memAllocFloat(vertices.length * 3);
        float[] colorData = new float[vertices.length * 3];
        for (int i = 0; i < vertices.length; i++) {
            colorData[i * 3] = vertices[i].getColor().getX();
            colorData[i * 3 + 1] = vertices[i].getColor().getY();
            colorData[i * 3 + 2] = vertices[i].getColor().getZ();
        }
        colorBuffer.put(colorData).flip();
        
        cbo = storeData(colorBuffer, 1, 3);**/
        
        FloatBuffer textureBuffer = MemoryUtil.memAllocFloat(verticesData.length);
        textureBuffer.put(textureData).flip();
        tbo = storeData(textureBuffer, 1, 2);
        
        FloatBuffer normalBuffer = MemoryUtil.memAllocFloat(verticesData.length);
        normalBuffer.put(normalsData).flip();
        nbo = storeData(normalBuffer, 2, 3);
        
        IntBuffer indicesBuffer = MemoryUtil.memAllocInt(indicesData.length);
        indicesBuffer.put(indicesData).flip();
        
        ibo = GL15.glGenBuffers();
        GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ibo);
        GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL15.GL_STATIC_DRAW);
        GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
    }
    
    private int storeData(FloatBuffer buffer, int index, int size) {
        int bufferID = GL15.glGenBuffers();
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, bufferID);
        GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buffer, GL15.GL_STATIC_DRAW);
        GL20.glVertexAttribPointer(index, size, GL11.GL_FLOAT, false, 0, 0);
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
        return bufferID;
    }
    
    public void destroyBuffers() {
        GL15.glDeleteBuffers(pbo);
        GL15.glDeleteBuffers(cbo);
        GL15.glDeleteBuffers(ibo);
        GL15.glDeleteBuffers(tbo);
        GL15.glDeleteBuffers(nbo);
        
        GL30.glDeleteVertexArrays(vao);
    }

    public float[] getVertices() {
        return verticesData;
    }
    
    public float[] getPositions2D() {
        return verticesData;
    }

    public int[] getIndices() {
        return indicesData;
    }

    public int getVAO() {
        return vao;
    }

    public int getPBO() {
        return pbo;
    }
    
    public int getCBO() {
        return cbo;
    }

    public int getIBO() {
        return ibo;
    }
    
    public int getTBO() {
        return tbo;
    }
    
    public int getNBO() {
        return nbo;
    }

    public Texture getTexture() {
        return texture;
    }
}

Is there a more efficient way to frustum cull the vertices / triangles of the terrain?

Answer

One way to determine what section of your terrain should be culled is to use a quadtree (for a heightmap) or an octree (for a voxel map). Basically, you divide your terrain into little chunks that then get divided further accordingly. You can then test if these chunks are in your viewing frustum and cull them if necessary. This technique was already discussed in great detail:

I saw some websites saying to use GL_DYNAMIC_DRAW instead of GL_STATIC_DRAW, but I did not understand it.

These are usage hints to OpenGL on how the data will be accessed so the implementation has the ability to apply certain optimizations on how to store/use it.

usage is a hint to the GL implementation as to how a buffer object’s data store will be accessed. This enables the GL implementation to make more intelligent decisions that may significantly impact buffer object performance. (https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glBufferData.xhtml)

Please note that these are only indications, no restrictions:

It does not, however, constrain the actual usage of the data store.

Because you will likely update your VBO’s and IBO’s constantly (see culling) and only want to draw them GL_DYNAMIC_DRAW would be a good choice:

The data store contents will be modified repeatedly (because of culling) and used many times. The data store contents are modified by the application and used as the source for GL drawing and image specification commands.

as I have googled that it can affect the performance of the game

Well, it will cost some performance to cull your terrain but in the end, it will likely gain performance because many vertices (triangles) can be discarded. This performance gain may grow with larger terrains.



Source: stackoverflow