464 lines
17 KiB
GDScript3
464 lines
17 KiB
GDScript3
|
|
class_name FuncGodotUtil
|
||
|
|
## Static class with a number of reuseable utility methods that can be called at Editor or Run Time.
|
||
|
|
|
||
|
|
const _VERTEX_EPSILON: float = 0.008
|
||
|
|
|
||
|
|
const _VEC3_UP_ID := Vector3(0.0, 0.0, 1.0)
|
||
|
|
const _VEC3_RIGHT_ID := Vector3(0.0, 1.0, 0.0)
|
||
|
|
const _VEC3_FORWARD_ID := Vector3(1.0, 0.0, 0.0)
|
||
|
|
|
||
|
|
## Connected by the [FuncGodotMap] node to the build process' sub-components if the
|
||
|
|
## [member FuncGodotMap.build_flags]'s SHOW_PROFILE_INFO flag is set.
|
||
|
|
static func print_profile_info(message: String, signature: String) -> void:
|
||
|
|
prints(signature, message)
|
||
|
|
|
||
|
|
## Return a [String] that corresponds to the current [OS]'s newline control characters.
|
||
|
|
static func newline() -> String:
|
||
|
|
if OS.get_name() == "Windows":
|
||
|
|
return "\r\n"
|
||
|
|
else:
|
||
|
|
return "\n"
|
||
|
|
|
||
|
|
#region MATH
|
||
|
|
|
||
|
|
static func op_vec3_sum(lhs: Vector3, rhs: Vector3) -> Vector3:
|
||
|
|
return lhs + rhs
|
||
|
|
|
||
|
|
static func op_vec3_avg(array: Array[Vector3]) -> Vector3:
|
||
|
|
if array.is_empty():
|
||
|
|
push_error("Cannot average empty Vector3 array!")
|
||
|
|
return Vector3()
|
||
|
|
return array.reduce(op_vec3_sum, Vector3()) / array.size()
|
||
|
|
|
||
|
|
## Conversion from id tech coordinate system to Godot, from a top-down perspective.
|
||
|
|
static func id_to_opengl(vec: Vector3) -> Vector3:
|
||
|
|
return Vector3(vec.y, vec.z, vec.x)
|
||
|
|
|
||
|
|
## Check if a point is inside a convex hull defined by a series of planes by an epsilon constant.
|
||
|
|
static func is_point_in_convex_hull(planes: Array[Plane], vertex: Vector3) -> bool:
|
||
|
|
for plane in planes:
|
||
|
|
var distance: float = plane.normal.dot(vertex) - plane.d
|
||
|
|
if distance > _VERTEX_EPSILON:
|
||
|
|
return false
|
||
|
|
return true
|
||
|
|
|
||
|
|
#endregion
|
||
|
|
|
||
|
|
#region PATCH DEF
|
||
|
|
|
||
|
|
## Returns the control points that defines a cubic curve for a equivalent input quadratic curve.
|
||
|
|
static func elevate_quadratic(p0: Vector3, p1: Vector3, p2: Vector3) -> Array[Vector3]:
|
||
|
|
return [p0, p0 + (2.0/3.0) * (p1 - p0), p2 + (2.0/3.0) * (p1 - p2), p2 ]
|
||
|
|
|
||
|
|
## Create a Curve3D and bake points.
|
||
|
|
static func create_curve(start: Vector3, control: Vector3, end: Vector3, bake_interval: float = 0.05) -> Curve3D:
|
||
|
|
var ret := Curve3D.new()
|
||
|
|
ret.bake_interval = bake_interval
|
||
|
|
update_ref_curve(ret, start, control, end, bake_interval)
|
||
|
|
return ret
|
||
|
|
|
||
|
|
## Update a Curve3D given quadratic inputs.
|
||
|
|
static func update_ref_curve(curve: Curve3D, p0: Vector3, p1: Vector3, p2: Vector3, bake_interval: float = 0.05) -> void:
|
||
|
|
curve.clear_points()
|
||
|
|
curve.bake_interval = bake_interval
|
||
|
|
curve.add_point(p0, (p1 - p0) * (2.0 / 3.0))
|
||
|
|
curve.add_point(p1, (p1 - p0) * (1.0 / 3.0), (p2 - p1) * (1.0 / 3.0))
|
||
|
|
curve.add_point(p2, (p2 - p1 * (2.0 / 3.0)))
|
||
|
|
|
||
|
|
#endregion
|
||
|
|
|
||
|
|
#region TEXTURES
|
||
|
|
|
||
|
|
## Fallback texture if the one defined in the [QuakeMapFile] cannot be found.
|
||
|
|
const default_texture_path: String = "res://addons/func_godot/textures/default_texture.png"
|
||
|
|
|
||
|
|
const _pbr_textures: PackedInt32Array = [
|
||
|
|
StandardMaterial3D.TEXTURE_ALBEDO,
|
||
|
|
StandardMaterial3D.TEXTURE_NORMAL,
|
||
|
|
StandardMaterial3D.TEXTURE_METALLIC,
|
||
|
|
StandardMaterial3D.TEXTURE_ROUGHNESS,
|
||
|
|
StandardMaterial3D.TEXTURE_EMISSION,
|
||
|
|
StandardMaterial3D.TEXTURE_AMBIENT_OCCLUSION,
|
||
|
|
StandardMaterial3D.TEXTURE_HEIGHTMAP,
|
||
|
|
ORMMaterial3D.TEXTURE_ORM,
|
||
|
|
]
|
||
|
|
|
||
|
|
# Used during auto-PBR processing. Must match the _pbr_textures order.
|
||
|
|
# -1 means the feature is permanantly enabled.
|
||
|
|
const _pbr_features: PackedInt32Array = [
|
||
|
|
-1,
|
||
|
|
BaseMaterial3D.FEATURE_NORMAL_MAPPING,
|
||
|
|
-1,
|
||
|
|
-1,
|
||
|
|
BaseMaterial3D.FEATURE_EMISSION,
|
||
|
|
BaseMaterial3D.FEATURE_AMBIENT_OCCLUSION,
|
||
|
|
BaseMaterial3D.FEATURE_HEIGHT_MAPPING,
|
||
|
|
-1,
|
||
|
|
]
|
||
|
|
|
||
|
|
## Searches for a Texture2D within the base texture directory or the WAD files added to map settings.
|
||
|
|
## If not found, a default texture is returned.
|
||
|
|
static func load_texture(texture_name: String, wad_resources: Array[QuakeWadFile], map_settings: FuncGodotMapSettings) -> Texture2D:
|
||
|
|
for texture_file_extension in map_settings.texture_file_extensions:
|
||
|
|
var texture_path: String = map_settings.base_texture_dir.path_join(texture_name + "." + texture_file_extension)
|
||
|
|
if ResourceLoader.exists(texture_path):
|
||
|
|
var texture_file = load(texture_path)
|
||
|
|
if texture_file is Texture2D:
|
||
|
|
return texture_file
|
||
|
|
else:
|
||
|
|
printerr("Error: Texture load failed! (%s) not a valid Texture2D resource", texture_path)
|
||
|
|
|
||
|
|
var texture_name_lower: String = texture_name.to_lower()
|
||
|
|
for wad in wad_resources:
|
||
|
|
if texture_name_lower in wad.textures:
|
||
|
|
return wad.textures[texture_name_lower]
|
||
|
|
|
||
|
|
return load(default_texture_path)
|
||
|
|
|
||
|
|
## Filters faces textured with Skip during the geometry generation step of the build process.
|
||
|
|
static func is_skip(texture: String, map_settings: FuncGodotMapSettings) -> bool:
|
||
|
|
if map_settings:
|
||
|
|
return texture.to_lower() == map_settings.skip_texture
|
||
|
|
return false
|
||
|
|
|
||
|
|
## Filters faces textured with Clip during the geometry generation step of the build process.
|
||
|
|
static func is_clip(texture: String, map_settings: FuncGodotMapSettings) -> bool:
|
||
|
|
if map_settings:
|
||
|
|
return texture.to_lower() == map_settings.clip_texture
|
||
|
|
return false
|
||
|
|
|
||
|
|
## Filters faces textured with Origin during the parsing and geometry generation steps of the build process.
|
||
|
|
static func is_origin(texture: String, map_settings: FuncGodotMapSettings) -> bool:
|
||
|
|
if map_settings:
|
||
|
|
return texture.to_lower() == map_settings.origin_texture
|
||
|
|
return false
|
||
|
|
|
||
|
|
## Filters faces textured with any of the tool textures during the geometry generation step of the build process.
|
||
|
|
static func filter_face(texture: String, map_settings: FuncGodotMapSettings) -> bool:
|
||
|
|
if map_settings:
|
||
|
|
texture = texture.to_lower()
|
||
|
|
if (texture == map_settings.skip_texture
|
||
|
|
or texture == map_settings.clip_texture
|
||
|
|
or texture == map_settings.origin_texture
|
||
|
|
):
|
||
|
|
return true
|
||
|
|
return false
|
||
|
|
|
||
|
|
## Adds PBR textures to an existing [BaseMaterial3D].
|
||
|
|
static func build_base_material(map_settings: FuncGodotMapSettings, material: BaseMaterial3D, texture: String) -> void:
|
||
|
|
var path: String = map_settings.base_texture_dir.path_join(texture)
|
||
|
|
# Check if there is a subfolder with our PBR textures
|
||
|
|
if DirAccess.open(path):
|
||
|
|
path = path.path_join(texture)
|
||
|
|
|
||
|
|
var pbr_suffixes: PackedStringArray = [
|
||
|
|
map_settings.albedo_map_pattern,
|
||
|
|
map_settings.normal_map_pattern,
|
||
|
|
map_settings.metallic_map_pattern,
|
||
|
|
map_settings.roughness_map_pattern,
|
||
|
|
map_settings.emission_map_pattern,
|
||
|
|
map_settings.ao_map_pattern,
|
||
|
|
map_settings.height_map_pattern,
|
||
|
|
map_settings.orm_map_pattern,
|
||
|
|
]
|
||
|
|
|
||
|
|
for i in pbr_suffixes.size():
|
||
|
|
if not pbr_suffixes[i].is_empty():
|
||
|
|
var pbr: String = pbr_suffixes[i]
|
||
|
|
var token: int = pbr.find("%s", 0)
|
||
|
|
if token != -1:
|
||
|
|
if pbr.find("%s", token + 1) != -1:
|
||
|
|
token = 2
|
||
|
|
else:
|
||
|
|
token = 1
|
||
|
|
|
||
|
|
if token < 1:
|
||
|
|
printerr("No string replacement tokens found in auto-PBR pattern \'" + pbr + "\'! Must have at least one instance of \'%s\' per pattern.")
|
||
|
|
continue
|
||
|
|
|
||
|
|
if token > 0:
|
||
|
|
for texture_file_extension in map_settings.texture_file_extensions:
|
||
|
|
if token > 1:
|
||
|
|
pbr = pbr_suffixes[i] % [path, texture_file_extension]
|
||
|
|
else:
|
||
|
|
pbr = pbr_suffixes[i] % [path]
|
||
|
|
pbr += "." + texture_file_extension
|
||
|
|
if ResourceLoader.exists(pbr):
|
||
|
|
print(pbr)
|
||
|
|
if _pbr_features[i] > -1:
|
||
|
|
material.set_feature(_pbr_features[i], true)
|
||
|
|
material.set_texture(_pbr_textures[i], load(pbr))
|
||
|
|
break
|
||
|
|
|
||
|
|
## Builds both materials and sizes dictionaries for use in the geometry generation step of the build process.
|
||
|
|
## Both dictionaries use texture names as keys. The materials dictionary uses [Material] as values,
|
||
|
|
## while the sizes dictionary saves the albedo texture sizes to aid in UV mapping.
|
||
|
|
static func build_texture_map(entity_data: Array[FuncGodotData.EntityData], map_settings: FuncGodotMapSettings) -> Array[Dictionary]:
|
||
|
|
var texture_materials: Dictionary[String, Material] = {}
|
||
|
|
var texture_sizes: Dictionary[String, Vector2] = {}
|
||
|
|
|
||
|
|
# Prepare WAD files
|
||
|
|
var wad_resources: Array[QuakeWadFile] = []
|
||
|
|
for wad in map_settings.texture_wads:
|
||
|
|
if wad and not wad in wad_resources:
|
||
|
|
wad_resources.append(wad)
|
||
|
|
|
||
|
|
for entity in entity_data:
|
||
|
|
if not entity.is_visual():
|
||
|
|
continue
|
||
|
|
|
||
|
|
for brush in entity.brushes:
|
||
|
|
for face in brush.faces:
|
||
|
|
var texture_name: String = face.texture
|
||
|
|
|
||
|
|
if filter_face(texture_name, map_settings):
|
||
|
|
continue
|
||
|
|
if texture_materials.has(texture_name):
|
||
|
|
continue
|
||
|
|
|
||
|
|
var material_path: String = map_settings.base_material_dir if not map_settings.base_material_dir.is_empty() else map_settings.base_texture_dir
|
||
|
|
material_path = material_path.path_join(texture_name) + "." + map_settings.material_file_extension
|
||
|
|
material_path = material_path.replace("*", "")
|
||
|
|
|
||
|
|
if ResourceLoader.exists(material_path):
|
||
|
|
var material: Material = load(material_path)
|
||
|
|
texture_materials[texture_name] = material
|
||
|
|
if material is BaseMaterial3D:
|
||
|
|
var albedo = material.albedo_texture
|
||
|
|
if albedo is Texture2D:
|
||
|
|
texture_sizes[texture_name] = material.albedo_texture.get_size()
|
||
|
|
elif material is ShaderMaterial:
|
||
|
|
var albedo = material.get_shader_parameter(map_settings.default_material_albedo_uniform)
|
||
|
|
if albedo is Texture2D:
|
||
|
|
texture_sizes[texture_name] = albedo.get_size()
|
||
|
|
if not texture_sizes.has(texture_name):
|
||
|
|
var texture: Texture2D = load_texture(texture_name, wad_resources, map_settings)
|
||
|
|
if texture:
|
||
|
|
texture_sizes[texture_name] = texture.get_size()
|
||
|
|
if not texture_sizes.has(texture_name):
|
||
|
|
texture_sizes[texture_name] = Vector2.ONE * map_settings.inverse_scale_factor
|
||
|
|
|
||
|
|
# Material generation
|
||
|
|
elif map_settings.default_material:
|
||
|
|
var material = map_settings.default_material.duplicate(false)
|
||
|
|
var texture: Texture2D = load_texture(texture_name, wad_resources, map_settings)
|
||
|
|
texture_sizes[texture_name] = texture.get_size()
|
||
|
|
|
||
|
|
if material is BaseMaterial3D:
|
||
|
|
material.albedo_texture = texture
|
||
|
|
build_base_material(map_settings, material, texture_name)
|
||
|
|
elif material is ShaderMaterial:
|
||
|
|
material.set_shader_parameter(map_settings.default_material_albedo_uniform, texture)
|
||
|
|
var path: String = map_settings.base_texture_dir
|
||
|
|
for uniform in map_settings.shader_material_uniform_map_patterns.keys():
|
||
|
|
if map_settings.shader_material_uniform_map_patterns[uniform].find("%s") < 0:
|
||
|
|
printerr("No string replacement tokens fuond in ShaderMaterial uniform map pattern \'" + map_settings.shader_material_uniform_map_patterns[uniform] + "\'! Must have one instance of \'%s\' per pattern.")
|
||
|
|
continue
|
||
|
|
for texture_file_extension in map_settings.texture_file_extensions:
|
||
|
|
var uniform_texture_path: String = map_settings.shader_material_uniform_map_patterns[uniform] % [texture_name] + "." + texture_file_extension
|
||
|
|
uniform_texture_path = path.path_join(uniform_texture_path)
|
||
|
|
if ResourceLoader.exists(uniform_texture_path):
|
||
|
|
material.set_shader_parameter(uniform, load(uniform_texture_path))
|
||
|
|
break
|
||
|
|
|
||
|
|
if (map_settings.save_generated_materials and material
|
||
|
|
and texture_name != map_settings.clip_texture
|
||
|
|
and texture_name != map_settings.skip_texture
|
||
|
|
and texture_name != map_settings.origin_texture
|
||
|
|
and texture.resource_path != default_texture_path):
|
||
|
|
# Make sure our material directory exists
|
||
|
|
var dir := DirAccess.open(material_path.get_base_dir())
|
||
|
|
if not dir:
|
||
|
|
dir = DirAccess.open("res://")
|
||
|
|
dir.make_dir_recursive(material_path.get_base_dir().trim_prefix("res://"))
|
||
|
|
# Save the new material
|
||
|
|
ResourceSaver.save(material, material_path)
|
||
|
|
|
||
|
|
texture_materials[texture_name] = material
|
||
|
|
else: # No default material exists
|
||
|
|
printerr("Error: No default material found in map settings")
|
||
|
|
|
||
|
|
return [texture_materials, texture_sizes]
|
||
|
|
|
||
|
|
#endregion
|
||
|
|
|
||
|
|
#region UV MAPPING
|
||
|
|
|
||
|
|
## Returns UV coordinate calculated from the Valve 220 UV format.
|
||
|
|
static func get_valve_uv(vertex: Vector3, u_axis: Vector3, v_axis: Vector3, uv_basis := Transform2D.IDENTITY, texture_size := Vector2.ONE) -> Vector2:
|
||
|
|
var uv := Vector2(u_axis.dot(vertex), v_axis.dot(vertex))
|
||
|
|
var scale := Vector2(uv_basis.x.x, uv_basis.y.y)
|
||
|
|
uv += (uv_basis.origin * scale)
|
||
|
|
uv /= scale;
|
||
|
|
uv.x /= texture_size.x
|
||
|
|
uv.y /= texture_size.y
|
||
|
|
return uv
|
||
|
|
|
||
|
|
## Returns UV coordinate calculated from the original id Standard UV format.
|
||
|
|
static func get_quake_uv(vertex: Vector3, normal: Vector3, uv_in := Transform2D.IDENTITY, texture_size := Vector2.ONE) -> Vector2:
|
||
|
|
var uv_out: Vector2
|
||
|
|
var nx := absf(normal.dot(Vector3.RIGHT))
|
||
|
|
var ny := absf(normal.dot(Vector3.UP))
|
||
|
|
var nz := absf(normal.dot(Vector3.FORWARD))
|
||
|
|
|
||
|
|
if ny >= nx and ny >= nz:
|
||
|
|
uv_out = Vector2(vertex.x, -vertex.z)
|
||
|
|
elif nx >= ny and nx >= nz:
|
||
|
|
uv_out = Vector2(vertex.y, -vertex.z)
|
||
|
|
else:
|
||
|
|
uv_out = Vector2(vertex.x, vertex.y)
|
||
|
|
|
||
|
|
uv_out = uv_out.rotated(uv_in.get_rotation())
|
||
|
|
uv_out /= uv_in.get_scale()
|
||
|
|
uv_out += uv_in.origin
|
||
|
|
uv_out /= texture_size
|
||
|
|
return uv_out
|
||
|
|
|
||
|
|
## Determines which UV format is being used and returns the UV coordinate.
|
||
|
|
static func get_face_vertex_uv(vertex: Vector3, face: FuncGodotData.FaceData, texture_size: Vector2) -> Vector2:
|
||
|
|
if face.uv_axes.size() >= 2:
|
||
|
|
return get_valve_uv(vertex, face.uv_axes[0], face.uv_axes[1], face.uv, texture_size)
|
||
|
|
else:
|
||
|
|
return get_quake_uv(vertex, face.plane.normal, face.uv, texture_size)
|
||
|
|
|
||
|
|
## Returns the tangent calculated from the Valve 220 UV format.
|
||
|
|
static func get_valve_tangent(u: Vector3, v: Vector3, normal: Vector3) -> PackedFloat32Array:
|
||
|
|
var u_axis: Vector3 = u.normalized()
|
||
|
|
var v_axis: Vector3 = v.normalized()
|
||
|
|
var v_sign: float = -signf(normal.cross(u_axis).dot(v_axis))
|
||
|
|
return [u_axis.x, u_axis.y, u_axis.z, v_sign]
|
||
|
|
|
||
|
|
# NOTE: we may still need to orthonormalize tangents. Just in case, here's a rough outline.
|
||
|
|
#var tangent: Vector3 = u.normalized()
|
||
|
|
#tangent = (tangent - normal * normal.dot(tangent)).normalized()
|
||
|
|
#
|
||
|
|
## in the case of parallel U or V axes to planar normal, reconstruct the tangent
|
||
|
|
#if tangent.length_squared() < 0.01:
|
||
|
|
# if absf(normal.y) < 0.9:
|
||
|
|
# tangent = Vector3.UP.cross(normal)
|
||
|
|
# else:
|
||
|
|
# tangent = Vector3.RIGHT.cross(normal)
|
||
|
|
#
|
||
|
|
#tangent = tangent.normalized()
|
||
|
|
#return [tangent.x, tangent.y, tangent.z, -signf(normal.cross(tangent).dot(v.normalized))]
|
||
|
|
|
||
|
|
## Returns the tangent calculated from the original id Standard UV format.
|
||
|
|
static func get_quake_tangent(normal: Vector3, uv_y_scale: float, uv_rotation: float) -> PackedFloat32Array:
|
||
|
|
var dx := normal.dot(_VEC3_RIGHT_ID)
|
||
|
|
var dy := normal.dot(_VEC3_UP_ID)
|
||
|
|
var dz := normal.dot(_VEC3_FORWARD_ID)
|
||
|
|
var dxa := absf(dx)
|
||
|
|
var dya := absf(dy)
|
||
|
|
var dza := absf(dz)
|
||
|
|
var u_axis: Vector3
|
||
|
|
var v_sign: float = 0.0
|
||
|
|
|
||
|
|
if dya >= dxa and dya >= dza:
|
||
|
|
u_axis = _VEC3_FORWARD_ID
|
||
|
|
v_sign = signf(dy)
|
||
|
|
elif dxa >= dya and dxa >= dza:
|
||
|
|
u_axis = _VEC3_FORWARD_ID
|
||
|
|
v_sign = -signf(dx)
|
||
|
|
elif dza >= dya and dza >= dxa:
|
||
|
|
u_axis = _VEC3_RIGHT_ID
|
||
|
|
v_sign = signf(dz)
|
||
|
|
|
||
|
|
v_sign *= signf(uv_y_scale)
|
||
|
|
u_axis = u_axis.rotated(normal, deg_to_rad(-uv_rotation) * v_sign)
|
||
|
|
return [u_axis.x, u_axis.y, u_axis.z, v_sign]
|
||
|
|
|
||
|
|
static func get_face_tangent(face: FuncGodotData.FaceData) -> PackedFloat32Array:
|
||
|
|
if face.uv_axes.size() >= 2:
|
||
|
|
return get_valve_tangent(face.uv_axes[0], face.uv_axes[1], face.plane.normal)
|
||
|
|
else:
|
||
|
|
return get_quake_tangent(face.plane.normal, face.uv.get_scale().y, face.uv.get_rotation())
|
||
|
|
|
||
|
|
#endregion
|
||
|
|
|
||
|
|
#region MESH
|
||
|
|
|
||
|
|
static func smooth_mesh_by_angle(mesh: ArrayMesh, angle_deg: float = 89.0) -> ArrayMesh:
|
||
|
|
if not mesh:
|
||
|
|
push_error("Need a source mesh to smooth")
|
||
|
|
return null
|
||
|
|
|
||
|
|
var angle: float = deg_to_rad(clampf(angle_deg, 0.0, 360.0))
|
||
|
|
|
||
|
|
var mesh_vertices: Array[Vector3] = []
|
||
|
|
var mesh_normals: Array[Vector3] = []
|
||
|
|
var surface_data: Array[Dictionary] = []
|
||
|
|
var mdt: MeshDataTool
|
||
|
|
var st := SurfaceTool.new()
|
||
|
|
|
||
|
|
# Collect surface information
|
||
|
|
for surface_index in mesh.get_surface_count():
|
||
|
|
mdt = MeshDataTool.new()
|
||
|
|
|
||
|
|
if mdt.create_from_surface(mesh, surface_index) != OK:
|
||
|
|
continue
|
||
|
|
|
||
|
|
var info: Dictionary = {
|
||
|
|
"mdt": mdt,
|
||
|
|
"ofs": mesh_vertices.size(),
|
||
|
|
"mat": mesh.surface_get_material(surface_index)
|
||
|
|
}
|
||
|
|
|
||
|
|
surface_data.append(info)
|
||
|
|
|
||
|
|
for i in mdt.get_vertex_count():
|
||
|
|
mesh_vertices.append(mdt.get_vertex(i))
|
||
|
|
mesh_normals.append(mdt.get_vertex_normal(i))
|
||
|
|
|
||
|
|
var groups: Dictionary = {}
|
||
|
|
|
||
|
|
# Group vertices by position
|
||
|
|
for i in mesh_vertices.size():
|
||
|
|
var pos := mesh_vertices[i]
|
||
|
|
|
||
|
|
# this is likely already snapped from the map building process
|
||
|
|
var key := pos.snappedf(_VERTEX_EPSILON)
|
||
|
|
|
||
|
|
if not groups.has(key):
|
||
|
|
groups[key] = [i]
|
||
|
|
else:
|
||
|
|
groups[key].append(i)
|
||
|
|
|
||
|
|
# Collect normals. Likely optimizable.
|
||
|
|
for group in groups.values():
|
||
|
|
for i in group:
|
||
|
|
var this := mesh_normals[i]
|
||
|
|
var normal_out := Vector3()
|
||
|
|
for j in group:
|
||
|
|
var other := mesh_normals[j]
|
||
|
|
if this.angle_to(other) <= angle:
|
||
|
|
normal_out += other
|
||
|
|
|
||
|
|
mesh_normals[i] = normal_out.normalized()
|
||
|
|
|
||
|
|
var smoothed_mesh := ArrayMesh.new()
|
||
|
|
|
||
|
|
# Construct smoothed output mesh
|
||
|
|
for dict in surface_data:
|
||
|
|
mdt = dict["mdt"]
|
||
|
|
var offset: int = dict["ofs"]
|
||
|
|
for i in mdt.get_vertex_count():
|
||
|
|
mdt.set_vertex_normal(i, mesh_normals[offset + i])
|
||
|
|
|
||
|
|
st = SurfaceTool.new()
|
||
|
|
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||
|
|
st.set_material(dict["mat"])
|
||
|
|
|
||
|
|
for i in mdt.get_face_count():
|
||
|
|
for j in 3:
|
||
|
|
var index := mdt.get_face_vertex(i, j)
|
||
|
|
st.set_normal(mdt.get_vertex_normal(index))
|
||
|
|
st.set_uv(mdt.get_vertex_uv(index))
|
||
|
|
st.set_tangent(mdt.get_vertex_tangent(index))
|
||
|
|
st.add_vertex(mdt.get_vertex(index))
|
||
|
|
|
||
|
|
smoothed_mesh = st.commit(smoothed_mesh)
|
||
|
|
|
||
|
|
return smoothed_mesh
|
||
|
|
|
||
|
|
#endregion
|