Updated to Godot 4.5 and func_godot 2025.9. Added grid_white and grid_taupe textures. Unfucked func_godot and navmesh settings. Began blocking out motel layout in map.

This commit is contained in:
2025-10-14 02:15:54 -04:00
parent 986d2f5666
commit b402a162e1
154 changed files with 10450 additions and 10097 deletions

View File

@@ -1,23 +1,32 @@
@tool
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
class_name FuncGodotLocalConfig extends Resource
## Local machine project wide settings. [color=red]WARNING![/color] Do not create your own! Use the resource in [i]addons/func_godot[/i].
##
## Local machine project wide settings. Can define global defaults for some FuncGodot properties.
## DO NOT CREATE A NEW RESOURCE! This resource works by saving a configuration file to your game's *user://* folder and pulling the properties from that config file rather than this resource.
## Use the premade `addons/func_godot/func_godot_local_config.tres` instead.
class_name FuncGodotLocalConfig
extends Resource
## [color=red][b]DO NOT CREATE A NEW RESOURCE![/b][/color] This resource works by saving a configuration file to your game's [b][i]user://[/i][/b] folder
## and pulling the properties from that config file rather than this resource. Use the premade [b][i]addons/func_godot/func_godot_local_config.tres[/i][/b] instead.
## [br][br]
## [b]Fgd Output Folder :[/b] Global directory path that [FuncGodotFGDFile] saves to when exported. Overridden when exported from a game configuration resource like [TrenchBroomGameConfig].[br][br]
## [b]Trenchbroom Game Config Folder :[/b] Global directory path where your TrenchBroom game configuration should be saved to. Consult the [url="https://trenchbroom.github.io/manual/latest/#game_configuration_files"]TrenchBroom Manual's Game Configuration documentation[/url] for more information.[br][br]
## [b]Netradiant Custom Gamepacks Folder :[/b] Global directory path where your NetRadiant Custom gamepacks are saved. On Windows this is the [i]gamepacks[/i] folder in your NetRadiant Custom installation.[br][br]
## [b]Map Editor Game Path :[/b] Global directory path to your mapping folder where all of your mapping assets exist. This is usually either your project folder or a subfolder within it.[br][br]
## [b]Game Path Models Folder :[/b] Relative directory path from your Map Editor Game Path to a subfolder containing any display models you might use for your map editor. Currently only used by [FuncGodotFGDModelPointClass].[br][br]
## [b]Default Inverse Scale Factor :[/b] Scale factor that affects how [FuncGodotFGDModelPointClass] entities scale their map editor display models. Not used with TrenchBroom, use [member TrenchBroomGameConfig.entity_scale] expression instead.[br][br]
enum PROPERTY {
FGD_OUTPUT_FOLDER,
TRENCHBROOM_GAME_CONFIG_FOLDER,
NETRADIANT_CUSTOM_GAMEPACKS_FOLDER,
MAP_EDITOR_GAME_PATH,
GAME_PATH_MODELS_FOLDER,
DEFAULT_INVERSE_SCALE
#GAME_PATH_MODELS_FOLDER,
#DEFAULT_INVERSE_SCALE
}
@export var export_func_godot_settings: bool: set = _save_settings
@export_tool_button("Export func_godot settings", "Save") var _save_settings = export_func_godot_settings
@export_tool_button("Reload func_godot settings", "Reload") var _load_settings = reload_func_godot_settings
const CONFIG_PROPERTIES: Array[Dictionary] = [
const _CONFIG_PROPERTIES: Array[Dictionary] = [
{
"name": "fgd_output_folder",
"usage": PROPERTY_USAGE_EDITOR,
@@ -46,44 +55,44 @@ const CONFIG_PROPERTIES: Array[Dictionary] = [
"hint": PROPERTY_HINT_GLOBAL_DIR,
"func_godot_type": PROPERTY.MAP_EDITOR_GAME_PATH
},
{
"name": "game_path_models_folder",
"usage": PROPERTY_USAGE_EDITOR,
"type": TYPE_STRING,
"func_godot_type": PROPERTY.GAME_PATH_MODELS_FOLDER
},
{
"name": "default_inverse_scale_factor",
"usage": PROPERTY_USAGE_EDITOR,
"type": TYPE_FLOAT,
"func_godot_type": PROPERTY.DEFAULT_INVERSE_SCALE
}
#{
#"name": "game_path_models_folder",
#"usage": PROPERTY_USAGE_EDITOR,
#"type": TYPE_STRING,
#"func_godot_type": PROPERTY.GAME_PATH_MODELS_FOLDER
#},
#{
#"name": "default_inverse_scale_factor",
#"usage": PROPERTY_USAGE_EDITOR,
#"type": TYPE_FLOAT,
#"func_godot_type": PROPERTY.DEFAULT_INVERSE_SCALE
#}
]
var settings_dict: Dictionary
var loaded := false
var _settings_dict: Dictionary
var _loaded := false
## Retrieve a setting from the local configuration.
static func get_setting(name: PROPERTY) -> Variant:
var settings = load("res://addons/func_godot/func_godot_local_config.tres")
if not settings.loaded:
settings._load_settings()
return settings.settings_dict.get(PROPERTY.keys()[name], '') as Variant
var settings: FuncGodotLocalConfig = load("res://addons/func_godot/func_godot_local_config.tres")
settings.reload_func_godot_settings()
return settings._settings_dict.get(PROPERTY.keys()[name], '') as Variant
func _get_property_list() -> Array:
return CONFIG_PROPERTIES.duplicate()
return _CONFIG_PROPERTIES.duplicate()
func _get(property: StringName) -> Variant:
var config = _get_config_property(property)
if config == null and not config is Dictionary:
return null
_try_loading()
return settings_dict.get(PROPERTY.keys()[config['func_godot_type']], _get_default_value(config['type']))
return _settings_dict.get(PROPERTY.keys()[config['func_godot_type']], _get_default_value(config['type']))
func _set(property: StringName, value: Variant) -> bool:
var config = _get_config_property(property)
if config == null and not config is Dictionary:
return false
settings_dict[PROPERTY.keys()[config['func_godot_type']]] = value
_settings_dict[PROPERTY.keys()[config['func_godot_type']]] = value
return true
func _get_default_value(type) -> Variant:
@@ -100,34 +109,39 @@ func _get_default_value(type) -> Variant:
return null
func _get_config_property(name: StringName) -> Variant:
for config in CONFIG_PROPERTIES:
for config in _CONFIG_PROPERTIES:
if config['name'] == name:
return config
return null
func _load_settings() -> void:
loaded = true
## Reload this system's configuration settings into the Local Config resource.
func reload_func_godot_settings() -> void:
_loaded = true
var path = _get_path()
if not FileAccess.file_exists(path): return
if not FileAccess.file_exists(path):
return
var settings = FileAccess.get_file_as_string(path)
settings_dict = {}
if not settings or settings.is_empty(): return
_settings_dict = {}
if not settings or settings.is_empty():
return
settings = JSON.parse_string(settings)
for key in settings.keys():
settings_dict[key] = settings[key]
_settings_dict[key] = settings[key]
notify_property_list_changed()
func _try_loading() -> void:
if not loaded: _load_settings()
if not _loaded:
reload_func_godot_settings()
func _save_settings(_s = null) -> void:
if settings_dict.size() == 0:
## Export the current resource settings to a configuration file in this game's [i]user://[/i] folder.
func export_func_godot_settings() -> void:
if _settings_dict.size() == 0:
return
var path = _get_path()
var file = FileAccess.open(path, FileAccess.WRITE)
var json = JSON.stringify(settings_dict)
var json = JSON.stringify(_settings_dict)
file.store_line(json)
loaded = false
_loaded = false
print("Saved settings to ", path)
func _get_path() -> String:

View File

@@ -1 +1 @@
uid://bjtqjywscfgdy
uid://xsjnhahhyein

View File

@@ -1,184 +0,0 @@
class_name FuncGodotTextureLoader
enum PBRSuffix {
ALBEDO,
NORMAL,
METALLIC,
ROUGHNESS,
EMISSION,
AO,
HEIGHT
}
# Suffix string / Godot enum / StandardMaterial3D property
const PBR_SUFFIX_NAMES: Dictionary = {
PBRSuffix.ALBEDO: 'albedo',
PBRSuffix.NORMAL: 'normal',
PBRSuffix.METALLIC: 'metallic',
PBRSuffix.ROUGHNESS: 'roughness',
PBRSuffix.EMISSION: 'emission',
PBRSuffix.AO: 'ao',
PBRSuffix.HEIGHT: 'height',
}
const PBR_SUFFIX_PATTERNS: Dictionary = {
PBRSuffix.ALBEDO: '%s_albedo.%s',
PBRSuffix.NORMAL: '%s_normal.%s',
PBRSuffix.METALLIC: '%s_metallic.%s',
PBRSuffix.ROUGHNESS: '%s_roughness.%s',
PBRSuffix.EMISSION: '%s_emission.%s',
PBRSuffix.AO: '%s_ao.%s',
PBRSuffix.HEIGHT: '%s_height.%s'
}
var PBR_SUFFIX_TEXTURES: Dictionary = {
PBRSuffix.ALBEDO: StandardMaterial3D.TEXTURE_ALBEDO,
PBRSuffix.NORMAL: StandardMaterial3D.TEXTURE_NORMAL,
PBRSuffix.METALLIC: StandardMaterial3D.TEXTURE_METALLIC,
PBRSuffix.ROUGHNESS: StandardMaterial3D.TEXTURE_ROUGHNESS,
PBRSuffix.EMISSION: StandardMaterial3D.TEXTURE_EMISSION,
PBRSuffix.AO: StandardMaterial3D.TEXTURE_AMBIENT_OCCLUSION,
PBRSuffix.HEIGHT: StandardMaterial3D.TEXTURE_HEIGHTMAP
}
const PBR_SUFFIX_PROPERTIES: Dictionary = {
PBRSuffix.NORMAL: 'normal_enabled',
PBRSuffix.EMISSION: 'emission_enabled',
PBRSuffix.AO: 'ao_enabled',
PBRSuffix.HEIGHT: 'heightmap_enabled',
}
var map_settings: FuncGodotMapSettings = FuncGodotMapSettings.new()
var texture_wad_resources: Array = []
# Overrides
func _init(new_map_settings: FuncGodotMapSettings) -> void:
map_settings = new_map_settings
load_texture_wad_resources()
# Business Logic
func load_texture_wad_resources() -> void:
texture_wad_resources.clear()
for texture_wad in map_settings.texture_wads:
if texture_wad and not texture_wad in texture_wad_resources:
texture_wad_resources.append(texture_wad)
func load_textures(texture_list: Array) -> Dictionary:
var texture_dict: Dictionary = {}
for texture_name in texture_list:
texture_dict[texture_name] = load_texture(texture_name)
return texture_dict
func load_texture(texture_name: String) -> Texture2D:
# Load albedo texture if it exists
for texture_extension in map_settings.texture_file_extensions:
var texture_path: String = "%s/%s.%s" % [map_settings.base_texture_dir, texture_name, texture_extension]
if ResourceLoader.exists(texture_path, "Texture2D"):
return load(texture_path) as Texture2D
var texture_name_lower: String = texture_name.to_lower()
for texture_wad in texture_wad_resources:
if texture_name_lower in texture_wad.textures:
return texture_wad.textures[texture_name_lower]
return load("res://addons/func_godot/textures/default_texture.png") as Texture2D
func create_materials(texture_list: Array) -> Dictionary:
var texture_materials: Dictionary = {}
#prints("TEXLI", texture_list)
for texture in texture_list:
texture_materials[texture] = create_material(texture)
return texture_materials
func create_material(texture_name: String) -> Material:
# Autoload material if it exists
var material_dict: Dictionary = {}
var material_path: String = "%s/%s.%s" % [map_settings.base_texture_dir, texture_name, map_settings.material_file_extension]
if not material_path in material_dict and FileAccess.file_exists(material_path):
var loaded_material: Material = load(material_path)
if loaded_material:
material_dict[material_path] = loaded_material
# If material already exists, use it
if material_path in material_dict:
return material_dict[material_path]
var material: Material = null
if map_settings.default_material:
material = map_settings.default_material.duplicate()
else:
material = StandardMaterial3D.new()
var texture: Texture2D = load_texture(texture_name)
if not texture:
return material
if material is BaseMaterial3D:
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED if map_settings.unshaded else BaseMaterial3D.SHADING_MODE_PER_PIXEL
if material is StandardMaterial3D:
material.set_texture(StandardMaterial3D.TEXTURE_ALBEDO, texture)
elif material is ShaderMaterial && map_settings.default_material_albedo_uniform != "":
material.set_shader_parameter(map_settings.default_material_albedo_uniform, texture)
var pbr_textures : Dictionary = get_pbr_textures(texture_name)
for pbr_suffix in PBRSuffix.values():
var suffix: int = pbr_suffix
var tex: Texture2D = pbr_textures[suffix]
if tex:
if material is ShaderMaterial:
material = StandardMaterial3D.new()
material.set_texture(StandardMaterial3D.TEXTURE_ALBEDO, texture)
var enable_prop: String = PBR_SUFFIX_PROPERTIES[suffix] if suffix in PBR_SUFFIX_PROPERTIES else ""
if(enable_prop != ""):
material.set(enable_prop, true)
material.set_texture(PBR_SUFFIX_TEXTURES[suffix], tex)
material_dict[material_path] = material
if (map_settings.save_generated_materials and material
and texture_name != map_settings.clip_texture
and texture_name != map_settings.skip_texture
and texture.resource_path != "res://addons/func_godot/textures/default_texture.png"):
ResourceSaver.save(material, material_path)
return material
# PBR texture fetching
func get_pbr_suffix_pattern(suffix: int) -> String:
if not suffix in PBR_SUFFIX_NAMES:
return ''
var pattern_setting: String = "%s_map_pattern" % [PBR_SUFFIX_NAMES[suffix]]
if pattern_setting in map_settings:
return map_settings.get(pattern_setting)
return PBR_SUFFIX_PATTERNS[suffix]
func get_pbr_texture(texture: String, suffix: PBRSuffix) -> Texture2D:
var texture_comps: PackedStringArray = texture.split('/')
if texture_comps.size() == 0:
return null
for texture_extension in map_settings.texture_file_extensions:
var path: String = "%s/%s/%s" % [
map_settings.base_texture_dir,
'/'.join(texture_comps),
get_pbr_suffix_pattern(suffix) % [
texture_comps[-1],
texture_extension
]
]
if(FileAccess.file_exists(path)):
return load(path) as Texture2D
return null
func get_pbr_textures(texture_name: String) -> Dictionary:
var pbr_textures: Dictionary = {}
for pbr_suffix in PBRSuffix.values():
pbr_textures[pbr_suffix] = get_pbr_texture(texture_name, pbr_suffix)
return pbr_textures

View File

@@ -1 +0,0 @@
uid://pfjqfqn3imye

View File

@@ -1,40 +1,408 @@
## General-purpose utility functions namespaced to FuncGodot for compatibility
class_name FuncGodotUtil
## Static class with a number of reuseable utility methods that can be called at Editor or Run Time.
## Print debug messages. True to print, false to ignore
const DEBUG : bool = true
const _VERTEX_EPSILON: float = 0.008
## Const-predicated print function to avoid excess log spam. Print msg if [constant DEBUG] is `true`.
static func debug_print(msg) -> void:
if(DEBUG):
print(msg)
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)
## Return a string that corresponds to the current OS's newline control character(s)
## 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"
## Create a dictionary suitable for creating a category with name when overriding [method Object._get_property_list]
static func category_dict(name: String) -> Dictionary:
return property_dict(name, TYPE_STRING, -1, "", PROPERTY_USAGE_CATEGORY)
#region MATH
## Creates a property with name and type from [enum @GlobalScope.Variant.Type].
## Optionally, provide hint from [enum @GlobalScope.PropertyHint] and corresponding hint_string, and usage from [enum @GlobalScope.PropertyUsageFlags].
static func property_dict(name: String, type: int, hint: int = -1, hint_string: String = "", usage: int = -1) -> Dictionary:
var dict := {
'name': name,
'type': type
}
static func op_vec3_sum(lhs: Vector3, rhs: Vector3) -> Vector3:
return lhs + rhs
if hint != -1:
dict['hint'] = hint
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()
if hint_string != "":
dict['hint_string'] = hint_string
## 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)
if usage != -1:
dict['usage'] = usage
## 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
return dict
#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
]
## 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):
return load(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_join(path)):
path = path.path_join(path)
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 texture_file_extension in map_settings.texture_file_extensions:
for i in pbr_suffixes.size():
if not pbr_suffixes[i].is_empty():
var pbr: String = pbr_suffixes[i] % [path, texture_file_extension]
if ResourceLoader.exists(pbr):
material.set_texture(_pbr_textures[i], load(pbr))
## 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(true)
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)
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):
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))
uv += (uv_basis.origin * uv_basis.get_scale())
uv.x /= uv_basis.x.x
uv.y /= uv_basis.y.y
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_basis := Transform2D.IDENTITY, texture_size := Vector2.ONE) -> Vector2:
var nx := absf(normal.dot(Vector3.RIGHT))
var ny := absf(normal.dot(Vector3.UP))
var nz := absf(normal.dot(Vector3.FORWARD))
var uv: Vector2
if ny >= nx and ny >= nz:
uv = Vector2(vertex.x, -vertex.z)
elif nx >= ny and nx >= nz:
uv = Vector2(vertex.y, -vertex.z)
else:
uv = Vector2(vertex.x, vertex.y)
var uv_out := uv.rotated(uv_basis.get_rotation())
uv_out /= uv_basis.get_scale()
uv_out += uv_basis.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) -> Mesh:
if not mesh:
push_error("Need a source mesh to smooth")
return
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

View File

@@ -1 +1 @@
uid://dy80wnu2py4dk
uid://bursmx2g1betd