a project i want to make is going to require a walkable , interactive world. the main issue i’m having is that the only editor-exposed collisions are either predetermined primitive shapes, or user defined convex hulls.
now, from what i gathered online, a fairly standard practice to making 3d terrain is to make a plane, then adjust vertex heights. so, i decided to do that with a quick model in blender
and, the model import went well enough.
but an issue became present: ground is solid. a model is not.
so, after looking around, i found a project example of someone doing 3d terrain in the engine, but upon looking at the project further, it was just showing off the terrain, LODs and some fancy heightmap shaders. unfortunately not on that list was collisions.
after more looking, i found that someone had previously made a tool to convert a collada into a convexhull file. i thought that it would be as simple as just running the tool but alas, no.
turns out, terrain is hardly convex, and then you get this
I agree that a single convex hull is not a good fit for terrain. It will wrap the whole shape and fill in concave areas, which is why you get that large artificial slope.
At the moment, Defold does not expose such an advanced built-in terrain collider workflow in the editor. The built-in options are primitive shapes and convex hulls. Convex hull generation tools can help with some models, but they do not solve the terrain case by themselves, because the terrain is usually concave.
Some ideas you can try:
Use the terrain mesh only for rendering, and build a separate simplified collision representation from boxes/capsules/spheres/convex hulls. For this to work with non-convex level geometry, the collision mesh has to be split into smaller convex pieces.
For a heightmap-style terrain, handle terrain collision yourself instead of relying on the physics engine. Sample the height at the character’s (X,Z) position, set or constrain the character’s Y position, calculate the ground normal from nearby samples, and handle movement in the character controller. This is often the best approach, especially if you mostly need it for characters movement rather than full rigid body complex and realistic simulation.
Generate chunked collision from the terrain: for example small convex patches, boxes, ramps, or simplified tile-like collision pieces and put them in your game like a grid based tile map.
Use a native extension for a custom physics solution. There has been work on alternative physics extensions like ReactPhysics3D by @d954mas
Also note that since 1.13.0 the deprecated .dae (Collada) support is removed.
For the future, we already had put some effort into making custom data components in Defold that will be now first battle tested with light components in the upcoming 1.13.1. We are also working further on Editor extensxibility. These together are planned to be used to creat such solutions similar to terrain editors in other game engines. But that is a vision for now, that we’ll be shaping.
build a separate simplified collision representation from boxes/capsules/spheres/convex hulls. For this to work with non-convex level geometry, the collision mesh has to be split into smaller convex pieces.
Generate chunked collision from the terrain: for example small convex patches, boxes, ramps, or simplified tile-like collision pieces and put them in your game like a grid based tile map.
i thought about these but was unsure how both how i’d generate them, as well as if the engine would handle so many collision objects well
Sample the height at the character’s (X,Z) position
i’m not sure how to do this since the height is baked into the mesh, and as far as i’m aware, there’s no clean way to get a mesh’s vertices with existing APIs. if there is then this might be what i do because i really only need physics for making things not fall through the ground.
Use a native extension for a custom physics solution.
thought about this too, but i’m pretty bad at math. the engine really doesn’t need a new physics simulation, because bullet has supported concave meshes for at least a decade now. i suppose if anything, it’d be a layer to manually set the collision shape to a concave one, but at that point i might as well just make a pull request to the engine
Note that this is just 10x10 height map, you may need higher resolution for good results. And since player can be between these sampled points, it may be good idea to get linear interpolation of the height between two closest points from heightmap. Heightmaps with higher resolution should be still pretty fast to read from, but size will sadly grow with the square of resolution.
I’m attaching the .blend file. Just open it, select mesh, go to “Scripting” tab and play the script and it should generate the heightmap for you.
Also, if for some reason you or somebody else need the script without the .blend file, it is below (as I said before, I’m not the author, but works good for me )
import bpy
import bmesh
import mathutils
import os
# =====================================================
# SETTINGS
# =====================================================
GRID_SIZE = 10
OUTPUT_FILE = bpy.path.abspath("//heightmap.lua")
# =====================================================
# GET SELECTED OBJECT
# =====================================================
obj = bpy.context.active_object
if obj is None or obj.type != 'MESH':
raise Exception("Select a mesh object.")
depsgraph = bpy.context.evaluated_depsgraph_get()
obj_eval = obj.evaluated_get(depsgraph)
# =====================================================
# BUILD BVH
# =====================================================
mesh = obj_eval.to_mesh()
bm = bmesh.new()
bm.from_mesh(mesh)
bm.transform(obj.matrix_world)
bvh = mathutils.bvhtree.BVHTree.FromBMesh(bm)
# =====================================================
# FIND WORLD BOUNDS
# =====================================================
world_corners = [obj.matrix_world @ mathutils.Vector(corner) for corner in obj.bound_box]
min_x = min(v.x for v in world_corners)
max_x = max(v.x for v in world_corners)
min_y = min(v.y for v in world_corners)
max_y = max(v.y for v in world_corners)
min_z = min(v.z for v in world_corners)
max_z = max(v.z for v in world_corners)
ray_start_z = max_z + 100.0
ray_distance = (max_z - min_z) + 200.0
# =====================================================
# SAMPLE HEIGHTS
# =====================================================
heightmap = []
for iy in range(GRID_SIZE):
row = []
fy = iy / (GRID_SIZE - 1)
y = min_y + fy * (max_y - min_y)
for ix in range(GRID_SIZE):
fx = ix / (GRID_SIZE - 1)
x = min_x + fx * (max_x - min_x)
origin = mathutils.Vector((x, y, ray_start_z))
direction = mathutils.Vector((0, 0, -1))
hit = bvh.ray_cast(origin, direction, ray_distance)
if hit[0] is None:
row.append(0.0)
else:
location = hit[0]
row.append(round(location.z, 5))
heightmap.append(row)
# =====================================================
# EXPORT LUA
# =====================================================
with open(OUTPUT_FILE, "w") as f:
f.write("return {\n")
for row in heightmap:
f.write(" {")
for i, h in enumerate(row):
if i > 0:
f.write(", ")
f.write(f"{h:.5f}")
f.write("},\n")
f.write("}\n")
print("Heightmap exported to:")
print(OUTPUT_FILE)
# =====================================================
# CLEANUP
# =====================================================
bm.free()
obj_eval.to_mesh_clear()
Python script that will export height data as lua table
that gave me the idea to try to use meshes and the .buffer format, since the API allows you to read vertex data from a mesh’s buffer.
unfortunately, it doesn’t seem as simple as “just shove the points in an array”, and the .buffer format doesn’t seem very well documented to understand what is going wrong.
i found the forum post announcing meshes, which did come with a project and python script to generate a mesh file, however it’s for an old version of blender and it doesn’t seem to run on newer versions anymore
well anyways, i abandoned trying to get the engine to play nice with models in the backend, so i cooped your (chatgpt’s?) python script, made some modifications to work better for me and my workflow, and bim bam wow it works fairly well, even with a very high poly count mesh.
took a small bit of tweaking on the player entity but hey results are results, even if they’re glued and taped together
interesting, however turns out you can’t get vertex data from meshes during runtime, as the compiled buffer is not compatible with the existing buffer API, and will give the following error if you give it (buffer expected, got userdata)
go.property("navmesh_buffer", resource.buffer("/example/assets/navmesh.buffer"))
function init(self)
local navmesh_buffer = resource.get_buffer(self.navmesh_buffer)
local buffer_stream = buffer.get_stream(navmesh_buffer, "position")
local total_elements = #buffer_stream
for i = 1, total_elements, 3 do
local x = buffer_stream[i]
local y = buffer_stream[i + 1]
local z = buffer_stream[i + 2]
print(string.format("Vertex %d: X=%.2f, Y=%.2f, Z=%.2f", (i - 1) / 3 + 1, x, y, z))
end
end
-- DEBUG:SCRIPT: Vertex 1: X=-8.16, Y=-1.69, Z=-7.06
-- DEBUG:SCRIPT: Vertex 2: X=-7.56, Y=-1.69, Z=-7.36
-- DEBUG:SCRIPT: Vertex 3: X=-9.36, Y=-1.69, Z=-7.96
does anyone know if meshes are any better to use than models?
after reviewing my code and thinking about it, chances are that i’m going to have to keep a dedicated collision heightmap file, since it means i don’t have to worry about manually interpolating points to determine the height at points not covered by a vertex, and as such reading vertex data at runtime becomes less useful.
but if i have 2 of the exact some object, just that one is in a model as a gltf, and the other is as a buffer in a mesh, is there much of a difference between the two in terms of resource use or anything of the sort?
The thing is that the lua API, at least, doesnt expost vertex data of the meshes of a model. Or I didnt find anything about that.
So if you have a gltf file, you wont be able to access its mesh and get vertex/color/normal data.
I think this should be exposed by the API, like a model.get_mesh_buffer(model_id, mesh_id) then you have your meshdata at least as readonly. It would be cool to add for example another generated mesh on top of it, or to copy it and deform it.
On the other side,
If you have a mesh ressource/buffer, you can do anything you want, but dont have animation, bones etc.
So you have to choose what you need based on this.
If you have a gltf, maybe you can also export vertex data only and build a mesh copy of it.
Or write a gltf parser in lua/cpp to access all data. Maybe somebody did it already?