TokisanGames / Terrain3D

A high performance, editable terrain system for Godot 4.
MIT License
2.14k stars 127 forks source link

Support baking the mesh on editor export to glft #525

Open scottdavis opened 1 week ago

scottdavis commented 1 week ago

Description

Currently if you export a scene with a Terrian3D node it exports everything except the Terrian3D mesh. This would be super helpful for using 3rd party navmesh libraries and blender workflows.

Currently i have a workflow where I dump the baked mesh to an obj file then import it into blender and merge the gltf export. Its super time consuming.

Hoping there is a callback that can bake the mesh on export when the editor makes the gltf.

if anyone happens to stumble on this needing a solutions my work around involves baking the mesh then passing it to this function.

func export_mesh_to_obj(mesh: Mesh, filepath: String):
    if mesh == null:
        print("No mesh found")
        return

    var file = FileAccess.open(filepath, FileAccess.ModeFlags.WRITE)
    if file == null:
        print("Failed to open file for writing")
        return

    # Write the OBJ header
    file.store_line("# Exported Mesh OBJ\n")

    # Prepare MeshDataTool
    var mesh_data_tool: MeshDataTool = MeshDataTool.new()

    # Iterate through surfaces of the mesh
    for surface_index in range(mesh.get_surface_count()):
        if mesh.surface_get_primitive_type(surface_index) != Mesh.PRIMITIVE_TRIANGLES:
            print("Surface is not made of triangles, skipping.")
            continue

        # Create MeshDataTool from the surface
        if mesh_data_tool.create_from_surface(mesh, surface_index) != OK:
            print("Failed to create MeshDataTool from surface")
            continue

        # Write vertices
        var vertex_count = mesh_data_tool.get_vertex_count()
        for i in range(vertex_count):
            var vertex = mesh_data_tool.get_vertex(i)
            file.store_line("v %f %f %f" % [vertex.x, vertex.y, vertex.z])

        # Write faces (OBJ uses 1-based indexing, so we add +1 to the indices)
        var face_count = mesh_data_tool.get_face_count()
        for i in range(face_count):
            var i1 = mesh_data_tool.get_face_vertex(i, 0) + 1
            var i2 = mesh_data_tool.get_face_vertex(i, 1) + 1
            var i3 = mesh_data_tool.get_face_vertex(i, 2) + 1
            file.store_line("f %d %d %d" % [i1, i2, i3])

    # Close the file after writing
    file.close()
    print("Mesh exported successfully to: ", filepath)

func export_terrain_to_obj(terrain: Terrain3D, filepath: String):
    var file = FileAccess.open(filepath, FileAccess.ModeFlags.WRITE)
    if file == null:
        print("Failed to open file for writing")
        return

    # Write the OBJ header
    file.store_line("# Exported Terrain3D OBJ\n")

    # Get terrain meshes (each chunk of terrain is handled separately)
    var meshes = terrain.get_meshes()  # Get the meshes for the terrain chunks
    for mesh_data in meshes:
        var mesh = mesh_data.mesh

        if mesh == null:
            continue

        var mesh_data_tool = MeshDataTool.new()

        # Iterate through all surfaces in the mesh
        for surface_index in range(mesh.get_surface_count()):
            if mesh.surface_get_primitive_type(surface_index) != Mesh.PRIMITIVE_TRIANGLES:
                print("Surface is not made of triangles, skipping.")
                continue

            if mesh_data_tool.create_from_surface(mesh, surface_index) != OK:
                print("Failed to create MeshDataTool from surface")
                continue

            # Write vertices
            var vertex_count = mesh_data_tool.get_vertex_count()
            for i in range(vertex_count):
                var vertex = mesh_data_tool.get_vertex(i)
                file.store_line("v %f %f %f" % [vertex.x, vertex.y, vertex.z])

            # Write faces (OBJ uses 1-based indexing, so we add +1 to the indices)
            var face_count = mesh_data_tool.get_face_count()
            for i in range(face_count):
                var i1 = mesh_data_tool.get_face_vertex(i, 0) + 1
                var i2 = mesh_data_tool.get_face_vertex(i, 1) + 1
                var i3 = mesh_data_tool.get_face_vertex(i, 2) + 1
                file.store_line("f %d %d %d" % [i1, i2, i3])

    # Close the file after writing
    file.close()
    print("Terrain exported successfully to: ", filepath)

Usage would look something like this

    var t3: Terrain3D = find_child("Terrain3D")
    var mesh: Mesh = t3.bake_mesh(0, Terrain3DStorage.HEIGHT_FILTER_NEAREST)
    export_mesh_to_obj(mesh, "res://test_dump.obj")
scottdavis commented 1 week ago

After realizing that making this issue was my rubber duck i realized if you bake the mesh array then export you get the geometry. However it would still be nice it that was automatic.

TokisanGames commented 1 week ago

Are there any signals or function calls the editor emits when the user selects export to gltf? I didn't see any. If there aren't, we can't do anything about that.

Baking beforehand when needed is only 3-4 clicks.

Does your script do anything that our mesh baker doesn't already do? It looks like it bakes at lod0, whereas we can bake at any lod, and likely faster. Note that the terrain can be huge, up to 65.5 x 65.5km. Large terrains are not something you want to dump into text. Export to glb is more optimal than obj.

scottdavis commented 1 week ago

Yea i ended up removing most of what my script does and doing it manually. My goal was to run it headless and generate my nav mesh via CI/CD for my server. However the closest thing i can find that the editor does would be this. https://docs.godotengine.org/en/stable/classes/class_gltfdocumentextension.html