Skip to content

Instantly share code, notes, and snippets.

@HungryProton
Last active July 8, 2023 14:46
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save HungryProton/c5b368e87f8598d1b1bc2788b9a52b47 to your computer and use it in GitHub Desktop.
Save HungryProton/c5b368e87f8598d1b1bc2788b9a52b47 to your computer and use it in GitHub Desktop.
Autosmooth meshes in GDScript
class_name MeshUtils
static func auto_smooth(mesh: Mesh, threshold_degrees := 30.0) -> Mesh:
var result := ArrayMesh.new()
var threshold := deg_to_rad(threshold_degrees)
var sanitized_mesh := merge_duplicate_vertices(mesh)
# Auto smooth each surfaces.
# If you need to auto smooth the whole mesh at once, merge all surfaces first.
for surface_index in sanitized_mesh.get_surface_count():
var mdt := MeshDataTool.new()
mdt.create_from_surface(sanitized_mesh, surface_index)
# Final arrays from where the mesh will be reconstructed
var vertices: Array[Vector3] = []
var normals: Array[Vector3] = []
var colors: Array[Color] = []
var indices: Array[int] = []
# Gather data about each faces
# ------
var vertex_map := {} # key: vertex id, value: array with face indices connected to the vertex.
var face_map := {} # key: face id, value: array with the 3 original vertex id
var new_face_map := {} # Same as face_map, but holds the new vertex id instead
for face_index in mdt.get_face_count():
if not face_index in face_map:
face_map[face_index] = []
for i in [0, 1, 2]: # 3 vertex per face
var original_vertex := mdt.get_face_vertex(face_index, i)
if not original_vertex in vertex_map:
vertex_map[original_vertex] = []
face_map[face_index].push_back(original_vertex)
vertex_map[original_vertex].push_back(face_index)
new_face_map = face_map.duplicate(true)
# Utility lambdas
# ------
# Returns the three edges indices associated with a face
var get_face_edges := func (face_index: int) -> Array[int]:
var edges : Array[int] = []
for i in [0, 1, 2]:
edges.push_back(mdt.get_face_edge(face_index, i))
return edges
# Returns true if both edges array contains at least one common edge with normals
# below the angle threshold
var has_common_soft_edges := func(edges_1: Array[int], edges_2: Array[int]) -> bool:
for edge_1 in edges_1:
for edge_2 in edges_2:
if edge_1 != edge_2:
continue
# One common edge found
var edge_faces := mdt.get_edge_faces(edge_1)
if edge_faces.size() != 2:
print_debug("Non manifold geometry detected. Can't handle this case.")
continue
# Compare normals
var normal_1 := mdt.get_face_normal(edge_faces[0]).normalized()
var normal_2 := mdt.get_face_normal(edge_faces[1]).normalized()
var angle := normal_1.angle_to(normal_2)
if angle < threshold:
return true # Soft edge found
# No soft edge found
return false
# Merge vertices together
# ------
for original_vertex in mdt.get_vertex_count():
# Get the faces touching this vertex
var parent_faces: Array[int] = []
parent_faces.assign(vertex_map[original_vertex])
var face_count: int = parent_faces.size()
var face_groups: Array = []
# Group together faces sharing common edges
# ------
var iterations := 0
while not parent_faces.is_empty():
# Pick a face not in a group yet
var p_face_1 := parent_faces.pop_front()
var edges_1 := get_face_edges.call(p_face_1)
# Either
# - First iteration, no existing groups yet
# - Too many iterations, remaining faces should move to their own group
if face_groups.is_empty() or iterations == parent_faces.size() + 1:
face_groups.push_back([p_face_1])
iterations = 0
continue
# Check against existing groups if it can fit
var fits_in_group := false
for group in face_groups:
for p_face_2 in group:
var edges_2 := get_face_edges.call(p_face_2)
if has_common_soft_edges.call(edges_1, edges_2):
group.push_back(p_face_1)
fits_in_group = true
break
if fits_in_group:
break
# Can't fit an existing group yet, add it to the back of the list
if not fits_in_group:
parent_faces.push_back(p_face_1)
iterations += 1
else:
iterations = 0
# For each group, average the faces normals and merge the vertices
# ------
for group in face_groups:
var normal := Vector3.ZERO
for face_index in group:
normal += mdt.get_face_normal(face_index)
var new_vertex_index := vertices.size()
vertices.push_back(mdt.get_vertex(original_vertex))
normals.push_back(normal.normalized())
for face_index in group:
var id: int = face_map[face_index].find(original_vertex)
new_face_map[face_index][id] = new_vertex_index
# Flatten the face map into a proper index array
for face_data in new_face_map.values():
for vertex_index in face_data:
indices.push_back(vertex_index)
## Recompose the mesh from the arrays above
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
st.set_smooth_group(0)
for i in vertices.size():
st.set_normal(normals[i])
st.add_vertex(vertices[i])
for index in indices:
st.add_index(index)
# Store the final result
result.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, st.commit_to_arrays())
return result
# Merge vertices sharing the same position into a single one.
# Normal data is not preserved and the resulting mesh will be smooth shaded.
static func merge_duplicate_vertices(mesh: Mesh) -> Mesh:
var result := ArrayMesh.new()
# Utility lambdas
var has_duplicate_values = func(array: Array) -> bool:
var size := array.size()
for i in size:
for j in range(i + 1, size):
if array[i] == array[j]:
return true
return false
# Apply to each individual surfaces
for surface_index in mesh.get_surface_count():
var mdt := MeshDataTool.new()
mdt.create_from_surface(mesh, surface_index)
# Final arrays from where the mesh will be reconstructed
var vertices: Array[Vector3] = []
var indices: Array[int] = []
# Create a map use to find the final merged vertex based on position alone
# Key: Vertex position, value: vertex index.
var vertex_map := {}
for vertex_index in mdt.get_vertex_count():
var vertex_pos := mdt.get_vertex(vertex_index)
if not vertex_pos in vertex_map:
var new_vertex_index = vertices.size()
vertices.push_back(vertex_pos)
vertex_map[vertex_pos] = new_vertex_index
# For each face, get the 3 vertices position and match them against the
# vertex map to find the actual vertices composing this face. Update
# indices accordingly
for face_index in mdt.get_face_count():
var new_face_indices: Array[int] = []
for i in [0, 1, 2]:
var vertex_index := mdt.get_face_vertex(face_index, i)
var vertex_pos := mdt.get_vertex(vertex_index)
new_face_indices.push_back(vertex_map[vertex_pos])
if has_duplicate_values.call(new_face_indices):
continue # Invalid triangle
for vertex_index in new_face_indices:
indices.push_back(vertex_index)
## Recreate the mesh from the arrays above
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
st.set_smooth_group(0)
for i in vertices.size():
st.add_vertex(vertices[i])
for index in indices:
st.add_index(index)
st.generate_normals()
# Store the final result
result.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, st.commit_to_arrays())
return result
@HungryProton
Copy link
Author

Extra data (like colors, weight, UVs and so on) are NOT preserved, but should be easy enough to add.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment