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:
@@ -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:
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://bjtqjywscfgdy
|
||||
uid://xsjnhahhyein
|
||||
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
uid://pfjqfqn3imye
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://dy80wnu2py4dk
|
||||
uid://bursmx2g1betd
|
||||
|
||||
Reference in New Issue
Block a user