Last active
July 8, 2023 14:46
-
-
Save HungryProton/c5b368e87f8598d1b1bc2788b9a52b47 to your computer and use it in GitHub Desktop.
Autosmooth meshes in GDScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Extra data (like colors, weight, UVs and so on) are NOT preserved, but should be easy enough to add.