Updated func_godot

This commit is contained in:
MaddoScientisto 2025-12-28 22:53:18 +01:00
commit 01a852de9b
170 changed files with 1705 additions and 2296 deletions

21
addons/func_godot/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 func-godot
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,15 @@
[gd_resource type="Resource" script_class="FuncGodotFGDBaseClass" load_steps=2 format=3 uid="uid://bcdsueg5pysfq"]
[ext_resource type="Script" uid="uid://ck575aqs1sbrb" path="res://addons/func_godot/src/fgd/func_godot_fgd_base_class.gd" id="1_21jph"]
[resource]
script = ExtResource("1_21jph")
classname = "CullInteriorFaces"
description = "Cull interior faces option for SolidClass Geometry"
class_properties = Dictionary[String, Variant]({
"_cull_interior_faces": false
})
class_property_descriptions = Dictionary[String, Variant]({
"_cull_interior_faces": "If true, cull interior faces with matching vertices or faces that are flush within a larger face. Note: This has a performance impact that scales with how many brushes are in the brush entity."
})
metadata/_custom_type_script = "uid://cgkrrgcimlr8y"

View file

@ -1,39 +1,18 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=4 format=3 uid="uid://cxy7jnh6d7msn"]
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=5 format=3 uid="uid://cxy7jnh6d7msn"]
[ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="1_0fsmp"]
[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_c3bns"]
[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="2_c03gr"]
[ext_resource type="Resource" uid="uid://bcdsueg5pysfq" path="res://addons/func_godot/fgd/cull_interior_faces.tres" id="3_wuxhx"]
[resource]
script = ExtResource("1_0fsmp")
spawn_type = 2
origin_type = 3
build_visuals = true
global_illumination_mode = 1
use_in_baked_light = true
shadow_casting_setting = 1
build_occlusion = false
render_layers = 1
collision_shape_type = 2
collision_layer = 1
collision_mask = 0
collision_priority = 1.0
collision_shape_margin = 0.04
add_textures_metadata = false
add_vertex_metadata = false
add_face_position_metadata = false
add_face_normal_metadata = false
add_collision_shape_to_face_indices_metadata = false
add_collision_shape_face_range_metadata = false
classname = "func_detail"
description = "Static collidable geometry. Builds a StaticBody3D with a MeshInstance3D and a single concave CollisionShape3D. Does not occlude other VisualInstance3D nodes."
func_godot_internal = false
base_classes = Array[Resource]([ExtResource("1_c3bns"), ExtResource("2_c03gr")])
class_properties = {}
class_property_descriptions = {}
auto_apply_to_matching_node_properties = false
meta_properties = {
base_classes = Array[Resource]([ExtResource("1_c3bns"), ExtResource("2_c03gr"), ExtResource("3_wuxhx")])
meta_properties = Dictionary[String, Variant]({
"color": Color(0.8, 0.8, 0.8, 1)
}
})
node_class = "StaticBody3D"
name_property = ""

View file

@ -1,39 +1,17 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=4 format=3 uid="uid://ch3e0dix85uhb"]
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=5 format=3 uid="uid://ch3e0dix85uhb"]
[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_ar63x"]
[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="2_j7vgq"]
[ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_lhb87"]
[ext_resource type="Resource" uid="uid://bcdsueg5pysfq" path="res://addons/func_godot/fgd/cull_interior_faces.tres" id="3_1mhrv"]
[resource]
script = ExtResource("2_lhb87")
spawn_type = 2
origin_type = 3
build_visuals = true
global_illumination_mode = 1
use_in_baked_light = true
shadow_casting_setting = 1
build_occlusion = false
render_layers = 1
collision_shape_type = 0
collision_layer = 1
collision_mask = 1
collision_priority = 1.0
collision_shape_margin = 0.04
add_textures_metadata = false
add_vertex_metadata = false
add_face_position_metadata = false
add_face_normal_metadata = false
add_collision_shape_to_face_indices_metadata = false
add_collision_shape_face_range_metadata = false
classname = "func_detail_illusionary"
description = "Static geometry with no collision. Builds a Node3D with a MeshInstance3D. Does not occlude other VisualInstance3D nodes."
func_godot_internal = false
base_classes = Array[Resource]([ExtResource("1_ar63x"), ExtResource("2_j7vgq")])
class_properties = {}
class_property_descriptions = {}
auto_apply_to_matching_node_properties = false
meta_properties = {
base_classes = Array[Resource]([ExtResource("1_ar63x"), ExtResource("2_j7vgq"), ExtResource("3_1mhrv")])
meta_properties = Dictionary[String, Variant]({
"color": Color(0.8, 0.8, 0.8, 1)
}
})
node_class = "Node3D"
name_property = ""

View file

@ -1,39 +1,19 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=4 format=3 uid="uid://b70vf4t5dc70t"]
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=5 format=3 uid="uid://b70vf4t5dc70t"]
[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_5mwee"]
[ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_8o081"]
[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="2_bp8pb"]
[ext_resource type="Resource" uid="uid://bcdsueg5pysfq" path="res://addons/func_godot/fgd/cull_interior_faces.tres" id="3_xnsya"]
[resource]
script = ExtResource("2_8o081")
spawn_type = 2
origin_type = 3
build_visuals = true
global_illumination_mode = 1
use_in_baked_light = true
shadow_casting_setting = 1
build_occlusion = true
render_layers = 1
collision_shape_type = 2
collision_layer = 1
collision_mask = 0
collision_priority = 1.0
collision_shape_margin = 0.04
add_textures_metadata = false
add_vertex_metadata = false
add_face_position_metadata = false
add_face_normal_metadata = false
add_collision_shape_to_face_indices_metadata = false
add_collision_shape_face_range_metadata = false
classname = "func_geo"
description = "Static collidable geometry. Builds a StaticBody3D with a MeshInstance3D, a single concave CollisionShape3D, and an OccluderInstance3D."
func_godot_internal = false
base_classes = Array[Resource]([ExtResource("1_5mwee"), ExtResource("2_bp8pb")])
class_properties = {}
class_property_descriptions = {}
auto_apply_to_matching_node_properties = false
meta_properties = {
base_classes = Array[Resource]([ExtResource("1_5mwee"), ExtResource("2_bp8pb"), ExtResource("3_xnsya")])
meta_properties = Dictionary[String, Variant]({
"color": Color(0.8, 0.8, 0.8, 1)
}
})
node_class = "StaticBody3D"
name_property = ""

View file

@ -1,4 +1,4 @@
[gd_resource type="Resource" script_class="FuncGodotFGDFile" load_steps=9 format=3 uid="uid://crgpdahjaj"]
[gd_resource type="Resource" script_class="FuncGodotFGDFile" load_steps=10 format=3 uid="uid://crgpdahjaj"]
[ext_resource type="Script" uid="uid://drlmgulwbjwqu" path="res://addons/func_godot/src/fgd/func_godot_fgd_file.gd" id="1_axt3h"]
[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_ehab8"]
@ -6,12 +6,10 @@
[ext_resource type="Resource" uid="uid://bdji3873bg32h" path="res://addons/func_godot/fgd/worldspawn.tres" id="2_ri2rx"]
[ext_resource type="Resource" uid="uid://b70vf4t5dc70t" path="res://addons/func_godot/fgd/func_geo.tres" id="3_7jigp"]
[ext_resource type="Resource" uid="uid://cxy7jnh6d7msn" path="res://addons/func_godot/fgd/func_detail.tres" id="3_fqfww"]
[ext_resource type="Resource" uid="uid://bcdsueg5pysfq" path="res://addons/func_godot/fgd/cull_interior_faces.tres" id="3_h5cmk"]
[ext_resource type="Resource" uid="uid://dg5x44cc7flew" path="res://addons/func_godot/fgd/func_illusionary.tres" id="4_c4ucw"]
[ext_resource type="Resource" uid="uid://ch3e0dix85uhb" path="res://addons/func_godot/fgd/func_detail_illusionary.tres" id="5_b2q3p"]
[resource]
script = ExtResource("1_axt3h")
target_map_editor = 1
fgd_name = "FuncGodot"
base_fgd_files = Array[Resource]([])
entity_definitions = Array[Resource]([ExtResource("1_ehab8"), ExtResource("2_7jebp"), ExtResource("2_ri2rx"), ExtResource("3_7jigp"), ExtResource("3_fqfww"), ExtResource("5_b2q3p"), ExtResource("4_c4ucw")])
entity_definitions = Array[Resource]([ExtResource("1_ehab8"), ExtResource("2_7jebp"), ExtResource("3_h5cmk"), ExtResource("2_ri2rx"), ExtResource("3_7jigp"), ExtResource("3_fqfww"), ExtResource("5_b2q3p"), ExtResource("4_c4ucw")])

View file

@ -1,39 +1,18 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=4 format=3 uid="uid://dg5x44cc7flew"]
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=5 format=3 uid="uid://dg5x44cc7flew"]
[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_kv0mq"]
[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="2_hovr4"]
[ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_uffhi"]
[ext_resource type="Resource" uid="uid://bcdsueg5pysfq" path="res://addons/func_godot/fgd/cull_interior_faces.tres" id="3_woywv"]
[resource]
script = ExtResource("2_uffhi")
spawn_type = 2
origin_type = 3
build_visuals = true
global_illumination_mode = 1
use_in_baked_light = true
shadow_casting_setting = 1
build_occlusion = true
render_layers = 1
collision_shape_type = 0
collision_layer = 1
collision_mask = 1
collision_priority = 1.0
collision_shape_margin = 0.04
add_textures_metadata = false
add_vertex_metadata = false
add_face_position_metadata = false
add_face_normal_metadata = false
add_collision_shape_to_face_indices_metadata = false
add_collision_shape_face_range_metadata = false
classname = "func_illusionary"
description = "Static geometry with no collision. Builds a Node3D with a MeshInstance3D and an Occluder3D to aid in render culling of other VisualInstance3D nodes."
func_godot_internal = false
base_classes = Array[Resource]([ExtResource("1_kv0mq"), ExtResource("2_hovr4")])
class_properties = {}
class_property_descriptions = {}
auto_apply_to_matching_node_properties = false
meta_properties = {
base_classes = Array[Resource]([ExtResource("1_kv0mq"), ExtResource("2_hovr4"), ExtResource("3_woywv")])
meta_properties = Dictionary[String, Variant]({
"color": Color(0.8, 0.8, 0.8, 1)
}
})
node_class = "Node3D"
name_property = ""

View file

@ -6,23 +6,14 @@
script = ExtResource("1_04y3n")
classname = "Phong"
description = "Phong shading options for SolidClass geometry."
func_godot_internal = false
base_classes = Array[Resource]([])
class_properties = {
class_properties = Dictionary[String, Variant]({
"_phong": {
"Disabled": 0,
"Smooth shading": 1
},
"_phong_angle": 89.0
}
class_property_descriptions = {
})
class_property_descriptions = Dictionary[String, Variant]({
"_phong": ["Phong shading", 0],
"_phong_angle": "Phong smoothing angle"
}
auto_apply_to_matching_node_properties = false
meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1),
"size": AABB(-8, -8, -8, 8, 8, 8)
}
node_class = ""
name_property = ""
})

View file

@ -6,19 +6,10 @@
script = ExtResource("1_h3atm")
classname = "VertexMergeDistance"
description = "Adjustable value to snap vertices to on map build. This can reduce instances of seams between polygons."
func_godot_internal = false
base_classes = Array[Resource]([])
class_properties = {
"_vertex_merge_distance": 0.008
}
class_property_descriptions = {
class_properties = Dictionary[String, Variant]({
"_vertex_merge_distance": 0.03125
})
class_property_descriptions = Dictionary[String, Variant]({
"_vertex_merge_distance": "Adjustable value to snap vertices to on map build. This can reduce instances of seams between polygons."
}
auto_apply_to_matching_node_properties = false
meta_properties = {
"color": Color(0.8, 0.8, 0.8, 1),
"size": AABB(-8, -8, -8, 8, 8, 8)
}
node_class = ""
name_property = ""
})
metadata/_custom_type_script = "uid://ck575aqs1sbrb"

View file

@ -1,38 +1,18 @@
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 format=3 uid="uid://bdji3873bg32h"]
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=4 format=3 uid="uid://bdji3873bg32h"]
[ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="1_62t8m"]
[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="1_h1046"]
[ext_resource type="Resource" uid="uid://bcdsueg5pysfq" path="res://addons/func_godot/fgd/cull_interior_faces.tres" id="2_ky6lr"]
[resource]
script = ExtResource("1_62t8m")
spawn_type = 0
origin_type = 1
build_visuals = true
global_illumination_mode = 1
use_in_baked_light = true
shadow_casting_setting = 1
build_occlusion = false
render_layers = 1
collision_shape_type = 1
collision_layer = 1
collision_mask = 0
collision_priority = 1.0
collision_shape_margin = 0.04
add_textures_metadata = false
add_vertex_metadata = false
add_face_position_metadata = false
add_face_normal_metadata = false
add_collision_shape_to_face_indices_metadata = false
add_collision_shape_face_range_metadata = false
classname = "worldspawn"
description = "Default static world geometry. Builds a StaticBody3D with a single MeshInstance3D and a single convex CollisionShape3D shape. Also builds Occluder3D to aid in render culling of other VisualInstance3D nodes."
func_godot_internal = false
base_classes = Array[Resource]([ExtResource("1_h1046")])
class_properties = {}
class_property_descriptions = {}
auto_apply_to_matching_node_properties = false
meta_properties = {
description = "Default static world geometry. Builds a StaticBody3D with a single MeshInstance3D and a single convex CollisionShape3D shape."
base_classes = Array[Resource]([ExtResource("1_h1046"), ExtResource("2_ky6lr")])
meta_properties = Dictionary[String, Variant]({
"color": Color(0.8, 0.8, 0.8, 1)
}
})
node_class = "StaticBody3D"
name_property = ""

View file

@ -1,31 +1,9 @@
[gd_resource type="Resource" script_class="FuncGodotMapSettings" load_steps=4 format=3 uid="uid://bkhxcqsquw1yg"]
[gd_resource type="Resource" script_class="FuncGodotMapSettings" load_steps=5 format=3 uid="uid://bkhxcqsquw1yg"]
[ext_resource type="Material" uid="uid://cvex6toty8yn7" path="res://addons/func_godot/textures/default_material.tres" id="1_8l5wm"]
[ext_resource type="Script" uid="uid://bctwech0sq0kh" path="res://addons/func_godot/src/map/func_godot_map_settings.gd" id="1_dlf23"]
[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_hd7se"]
[ext_resource type="Script" uid="uid://38q6k0ctahjn" path="res://addons/func_godot/src/map/func_godot_map_settings.gd" id="1_dlf23"]
[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="2_hf4oi"]
[ext_resource type="Script" uid="uid://cij36hpqc46c" path="res://addons/func_godot/src/import/quake_wad_file.gd" id="4_576s4"]
[resource]
script = ExtResource("1_dlf23")
inverse_scale_factor = 16.0
entity_fgd = ExtResource("1_hd7se")
entity_name_property = ""
base_texture_dir = "res://textures"
texture_file_extensions = Array[String](["png", "jpg", "jpeg", "bmp", "tga", "webp"])
clip_texture = "special/clip"
skip_texture = "special/skip"
origin_texture = "special/origin"
texture_wads = Array[Resource]([])
material_file_extension = "tres"
default_material = ExtResource("1_8l5wm")
default_material_albedo_uniform = ""
albedo_map_pattern = "%s_albedo.%s"
normal_map_pattern = "%s_normal.%s"
metallic_map_pattern = "%s_metallic.%s"
roughness_map_pattern = "%s_roughness.%s"
emission_map_pattern = "%s_emission.%s"
ao_map_pattern = "%s_ao.%s"
height_map_pattern = "%s_height.%s"
orm_map_pattern = "%s_orm.%s"
save_generated_materials = true
uv_unwrap_texel_size = 2.0
use_trenchbroom_groups_hierarchy = false

View file

@ -8,17 +8,6 @@
[resource]
script = ExtResource("2_en8ro")
gamepack_name = "func_godot"
game_name = "FuncGodot"
base_game_path = ""
fgd_file = ExtResource("1_gct4v")
netradiant_custom_shaders = Array[Resource]([ExtResource("2_w7psh"), ExtResource("3_6gpk8"), ExtResource("4_8rl60")])
texture_types = PackedStringArray("png", "jpg", "jpeg", "bmp", "tga")
model_types = PackedStringArray("glb", "gltf", "obj")
sound_types = PackedStringArray("wav", "ogg")
default_scale = "1.0"
clip_texture = "textures/special/clip"
skip_texture = "textures/special/skip"
map_type = 1
default_build_menu_variables = {}
default_build_menu_commands = {}
texture_types = PackedStringArray("png", "jpg", "jpeg", "bmp", "tga")

View file

@ -4,5 +4,4 @@
[resource]
script = ExtResource("1_cuylw")
texture_path = "textures/special/clip"
shader_attributes = Array[String](["qer_trans 0.4"])
texture_path = "textures/clip"

View file

@ -4,5 +4,4 @@
[resource]
script = ExtResource("1_ah2cp")
texture_path = "textures/special/origin"
shader_attributes = Array[String](["qer_trans 0.4"])
texture_path = "textures/origin"

View file

@ -4,5 +4,4 @@
[resource]
script = ExtResource("1_4ja6h")
texture_path = "textures/special/skip"
shader_attributes = Array[String](["qer_trans 0.4"])
texture_path = "textures/skip"

View file

@ -9,26 +9,3 @@
[resource]
script = ExtResource("2_ns6ah")
game_name = "FuncGodot"
icon = ExtResource("6_tex5j")
map_formats = Array[Dictionary]([{
"format": "Valve",
"initialmap": "initial_valve.map"
}, {
"format": "Standard",
"initialmap": "initial_standard.map"
}, {
"format": "Quake2",
"initialmap": "initial_quake2.map"
}, {
"format": "Quake3"
}])
textures_root_folder = "textures"
texture_exclusion_patterns = Array[String](["*_albedo", "*_ao", "*_emission", "*_height", "*_metallic", "*_normal", "*_orm", "*_roughness", "*_sss"])
palette_path = "textures/palette.lmp"
fgd_file = ExtResource("1_8u1vq")
entity_scale = "32"
brush_tags = Array[Resource]([])
brushface_tags = Array[Resource]([ExtResource("1_rsp20"), ExtResource("2_166i2"), ExtResource("3_stisi")])
default_uv_scale = Vector2(1, 1)
game_config_version = 0

View file

@ -8,4 +8,3 @@ tag_name = "Func"
tag_attributes = Array[String]([])
tag_match_type = 1
tag_pattern = "func*"
texture_name = ""

View file

@ -5,7 +5,6 @@
[resource]
script = ExtResource("1_msqpk")
tag_name = "Trigger"
tag_attributes = Array[String](["transparent"])
tag_match_type = 1
tag_pattern = "trigger*"
texture_name = "special/trigger"
texture_name = "trigger"

View file

@ -5,7 +5,4 @@
[resource]
script = ExtResource("1_7td58")
tag_name = "Clip"
tag_attributes = Array[String](["transparent"])
tag_match_type = 0
tag_pattern = "clip"
texture_name = ""

View file

@ -5,7 +5,4 @@
[resource]
script = ExtResource("1_enkfc")
tag_name = "Origin"
tag_attributes = Array[String](["transparent"])
tag_match_type = 0
tag_pattern = "origin"
texture_name = ""

View file

@ -5,7 +5,4 @@
[resource]
script = ExtResource("1_2teqe")
tag_name = "Skip"
tag_attributes = Array[String](["transparent"])
tag_match_type = 0
tag_pattern = "skip"
texture_name = ""

View file

@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon.png-6db43b6a52df1ce3744a82f15cbdbbea.cte
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
@ -25,6 +27,10 @@ mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false

View file

@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon32.png-7025e2d95a64a3066b7947e1900b4daf.c
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
@ -25,6 +27,10 @@ mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false

View file

@ -2,6 +2,6 @@
name="FuncGodot"
description="Quake .map and Half-Life .vmf file support for Godot."
author="Josh Palmer, Hannah Crawford, Emberlynn Bland, Tim Maccabe, Vera Lux"
version="2025.9"
author="Josh Palmer, Hannah Crawford, Emberlynn Bland, Tim Maccabe, Vera Lux, func_godot Community"
version="2025.12"
script="src/func_godot_plugin.gd"

View file

@ -122,7 +122,7 @@ class GroupData extends RefCounted:
class EntityData extends RefCounted:
## All of the entity's key value pairs from the map file, retrieved during parsing.
## The func_godot_properties dictionary generated at the end of entity assembly is derived from this.
var properties: Dictionary = {}
var properties: Dictionary[String, Variant] = {}
## The entity's brush data collected during the parsing stage. If the entity's FGD resource cannot be found,
## the presence of a single brush determines this entity to be a Solid Entity.
var brushes: Array[BrushData] = []
@ -154,6 +154,12 @@ class EntityData extends RefCounted:
return (definition
and definition is FuncGodotFGDSolidClass
and definition.build_visuals)
func is_gi_enabled() -> bool:
return (definition
and definition is FuncGodotFGDSolidClass
and definition.global_illumination_mode
)
## Checks the entity's FGD resource definition, returning whether the Solid Class CollisionShapeType is set to Convex.
func is_collision_convex() -> bool:
@ -172,12 +178,12 @@ class EntityData extends RefCounted:
## Determines if the entity's mesh should be processed for normal smoothing.
## The smoothing property can be retrieved from [member FuncGodotMapSettings.entity_smoothing_property].
func is_smooth_shaded(smoothing_property: String = "_phong") -> bool:
return properties.get(smoothing_property, "0").to_int()
return properties.get(smoothing_property, 0)
## Retrieves the entity's smoothing angle to determine if the face should be smoothed.
## The smoothing angle property can be retrieved from [member FuncGodotMapSettings.entity_smoothing_angle_property].
func get_smoothing_angle(smoothing_angle_property: String = "_phong_angle") -> float:
return properties.get(smoothing_angle_property, "89.0").to_float()
return properties.get(smoothing_angle_property, 89.0)
class VertexGroupData:
## Faces this vertex appears in.

View file

@ -51,14 +51,14 @@ func generate_solid_entity_node(node: Node, node_name: String, data: _EntityData
node = ClassDB.instantiate(definition.node_class)
else:
var script: Script = get_script_by_class_name(definition.node_class)
if script is GDScript:
node = (script as GDScript).new()
if script is Script:
node = script.new()
else:
node = Node3D.new()
node.name = node_name
node_name = node_name.trim_suffix(definition.classname).trim_suffix("_")
var properties: Dictionary = data.properties
var properties: Dictionary[String, Variant] = data.properties
# Mesh Instance generation
if data.mesh:
@ -95,9 +95,17 @@ func generate_solid_entity_node(node: Node, node_name: String, data: _EntityData
node.add_child(occluder_instance)
data.occluder_instance = occluder_instance
# NOTE: Currently occuring in EntityAssembler until the appropriate method in GeometryGenerator is resolved
# For now, smooth entire mesh, then unwrap for lightmap if needed
if not (build_flags & FuncGodotMap.BuildFlags.DISABLE_SMOOTHING) and data.is_smooth_shaded(map_settings.entity_smoothing_property):
mesh_instance.mesh = FuncGodotUtil.smooth_mesh_by_angle(data.mesh, data.get_smoothing_angle(map_settings.entity_smoothing_angle_property))
if data.is_gi_enabled() and (build_flags & FuncGodotMap.BuildFlags.UNWRAP_UV2):
mesh_instance.mesh.lightmap_unwrap(
Transform3D.IDENTITY,
map_settings.uv_unwrap_texel_size * map_settings.scale_factor
)
# Collision generation
if data.shapes.size() and node is CollisionObject3D:
node.collision_layer = definition.collision_layer
@ -131,7 +139,9 @@ func generate_solid_entity_node(node: Node, node_name: String, data: _EntityData
if "position" in node:
if node.position is Vector3:
node.position = FuncGodotUtil.id_to_opengl(data.origin) * map_settings.scale_factor
node.position = FuncGodotUtil.id_to_opengl(data.origin)
elif node.position is Vector2:
node.position = Vector2(data.origin.z, -data.origin.y) * map_settings.inverse_scale_factor
if not data.mesh_metadata.is_empty():
node.set_meta("func_godot_mesh_data", data.mesh_metadata)
@ -152,9 +162,9 @@ func generate_point_entity_node(node: Node, node_name: String, properties: Dicti
node = ClassDB.instantiate(definition.node_class)
else:
var script: Script = get_script_by_class_name(definition.node_class)
if script is GDScript:
node = (script as GDScript).new()
else:
if script is Script:
node = script.new()
if not node:
node = Node3D.new()
node.name = node_name
@ -164,22 +174,28 @@ func generate_point_entity_node(node: Node, node_name: String, properties: Dicti
if "angles" in properties or "mangle" in properties:
var key := "angles" if "angles" in properties else "mangle"
var angles_raw = properties[key]
if not angles_raw is Vector3:
if angles_raw is String:
angles_raw = angles_raw.split_floats(' ')
if angles_raw.size() > 2:
if angles_raw.size() < 3:
push_error("Invalid vector format for \"" + key + "\" in entity \"" + classname + "\"")
angles_raw = null
if angles_raw:
angles = Vector3(-angles_raw[0], angles_raw[1], -angles_raw[2])
if key == "mangle":
if definition.classname.begins_with("light"):
angles = Vector3(angles_raw[1], angles_raw[0], -angles_raw[2])
elif definition.classname == "info_intermission":
angles = Vector3(angles_raw[0], angles_raw[1], -angles_raw[2])
else:
push_error("Invalid vector format for \"" + key + "\" in entity \"" + classname + "\"")
elif "angle" in properties:
var angle = properties["angle"]
if not angle is float:
angle = float(angle)
angles.y += angle
if is_equal_approx(angle, -1):
angles.x = 90
elif is_equal_approx(angle, -2):
angles.x = -90
else:
angles.y += angle
angles.y += 180
node.rotation_degrees = angles
@ -203,11 +219,18 @@ func generate_point_entity_node(node: Node, node_name: String, properties: Dicti
if "origin" in properties:
var origin_vec: Vector3 = Vector3.ZERO
var origin_comps: PackedFloat64Array = properties['origin'].split_floats(' ')
if origin_comps.size() > 2:
origin_vec = Vector3(origin_comps[1], origin_comps[2], origin_comps[0])
var origin_prop = properties['origin']
if origin_prop is Vector3:
origin_vec = Vector3(origin_prop.y, origin_prop.z, origin_prop.x)
elif origin_prop is String:
var origin_comps: PackedFloat64Array = properties['origin'].split_floats(' ')
if origin_comps.size() > 2:
origin_vec = Vector3(origin_comps[1], origin_comps[2], origin_comps[0])
else:
push_error("Invalid vector format for \"origin\" in " + node_name)
else:
push_error("Invalid vector format for \"origin\" in " + node_name)
if "position" in node:
if node.position is Vector3:
node.position = origin_vec * map_settings.scale_factor
@ -220,127 +243,15 @@ func generate_point_entity_node(node: Node, node_name: String, properties: Dicti
## based upon the [FuncGodotFGDEntity]'s class properties, then attempts to send those properties to a [code]func_godot_properties[/code] [Dictionary]
## and an [code]_func_godot_apply_properties(properties: Dictionary)[/code] method on the node. A deferred call to [code]_func_godot_build_complete()[/code] is also made.
func apply_entity_properties(node: Node, data: _EntityData) -> void:
var properties: Dictionary = data.properties
var properties: Dictionary[String, Variant] = data.properties
if data.definition:
var def := data.definition
for property in properties:
var prop_string = properties[property]
if property in def.class_properties:
var prop_default: Variant = def.class_properties[property]
match typeof(prop_default):
TYPE_INT:
properties[property] = prop_string.to_int()
TYPE_FLOAT:
properties[property] = prop_string.to_float()
TYPE_BOOL:
properties[property] = bool(prop_string.to_int())
TYPE_VECTOR3:
var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
if prop_comps.size() > 2:
properties[property] = Vector3(prop_comps[0], prop_comps[1], prop_comps[2])
else:
push_error("Invalid Vector3 format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_default
TYPE_VECTOR3I:
var prop_vec: Vector3i = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 2:
for i in 3:
prop_vec[i] = prop_comps[i].to_int()
else:
push_error("Invalid Vector3i format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_COLOR:
var prop_color: Color = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 2:
prop_color.r8 = prop_comps[0].to_int()
prop_color.g8 = prop_comps[1].to_int()
prop_color.b8 = prop_comps[2].to_int()
prop_color.a = 1.0
else:
push_error("Invalid Color format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_color
TYPE_DICTIONARY:
var prop_desc = def.class_property_descriptions[property]
if prop_desc is Array and prop_desc.size() > 1 and prop_desc[1] is int:
properties[property] = prop_string.to_int()
TYPE_ARRAY:
properties[property] = prop_string.to_int()
TYPE_VECTOR2:
var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
if prop_comps.size() > 1:
properties[property] = Vector2(prop_comps[0], prop_comps[1])
else:
push_error("Invalid Vector2 format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_default
TYPE_VECTOR2I:
var prop_vec: Vector2i = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 1:
for i in 2:
prop_vec[i] = prop_comps[i].to_int()
else:
push_error("Invalid Vector2i format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_VECTOR4:
var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
if prop_comps.size() > 3:
properties[property] = Vector4(prop_comps[0], prop_comps[1], prop_comps[2], prop_comps[3])
else:
push_error("Invalid Vector4 format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_default
TYPE_VECTOR4I:
var prop_vec: Vector4i = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 3:
for i in 4:
prop_vec[i] = prop_comps[i].to_int()
else:
push_error("Invalid Vector4i format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_STRING_NAME:
properties[property] = StringName(prop_string)
TYPE_NODE_PATH:
properties[property] = prop_string
TYPE_OBJECT:
properties[property] = prop_string
# Assign properties not defined with defaults from the entity definition
for property in def.class_properties:
if not property in properties:
var prop_default: Variant = def.class_properties[property]
# Flags
if prop_default is Array:
var prop_flags_sum := 0
for prop_flag in prop_default:
if prop_flag is Array and prop_flag.size() > 2:
if prop_flag[2] and prop_flag[1] is int:
prop_flags_sum += prop_flag[1]
properties[property] = prop_flags_sum
# Choices
elif prop_default is Dictionary:
var prop_desc = def.class_property_descriptions.get(property, "")
if prop_desc is Array and prop_desc.size() > 1 and (prop_desc[1] is int or prop_desc[1] is String):
properties[property] = prop_desc[1]
elif prop_default.size():
properties[property] = prop_default[prop_default.keys().front()]
else:
properties[property] = 0
# Materials, Shaders, and Sounds
elif prop_default is Resource:
properties[property] = prop_default.resource_path
# Target Destination and Target Source
elif prop_default is NodePath or prop_default is Object or prop_default == null:
properties[property] = ""
# Everything else
else:
properties[property] = prop_default
if def.auto_apply_to_matching_node_properties:
for property in properties:
if property == 'scale' and def is FuncGodotFGDPointClass and def.apply_scale_on_map_build:
# scale has already been applied
continue
if property in node:
if typeof(node.get(property)) == typeof(properties[property]):
node.set(property, properties[property])
@ -361,49 +272,44 @@ func apply_entity_properties(node: Node, data: _EntityData) -> void:
func generate_entity_node(entity_data: _EntityData, entity_index: int) -> Node:
var node: Node = null
var node_name: String = "entity_%s" % entity_index
var properties: Dictionary = entity_data.properties
var properties: Dictionary[String, Variant] = entity_data.properties
var entity_def: FuncGodotFGDEntityClass = entity_data.definition
if "classname" in entity_data.properties:
var classname: String = properties["classname"]
node_name += "_" + properties["classname"]
var default_point_def := FuncGodotFGDPointClass.new()
var default_solid_def := FuncGodotFGDSolidClass.new()
default_solid_def.collision_shape_type = FuncGodotFGDSolidClass.CollisionShapeType.NONE
if entity_def:
var name_prop: String
if entity_def.name_property in properties:
name_prop = str(properties[entity_def.name_property])
elif map_settings.entity_name_property in properties:
name_prop = str(properties[map_settings.entity_name_property])
if not name_prop.is_empty():
node_name = "entity_" + name_prop
if entity_def is FuncGodotFGDSolidClass:
node = generate_solid_entity_node(node, node_name, entity_data, entity_def)
elif entity_def is FuncGodotFGDPointClass:
node = generate_point_entity_node(node, node_name, properties, entity_def)
else:
push_error("Invalid entity definition for \"" + node_name + "\". Entity definition must be Solid Class or Point Class.")
node = generate_point_entity_node(node, node_name, properties, default_point_def)
if node and entity_def.script_class:
var name_prop: String
if entity_def.name_property in properties:
name_prop = str(properties[entity_def.name_property])
elif map_settings.entity_name_property in properties:
name_prop = str(properties[map_settings.entity_name_property])
if not name_prop.is_empty():
node_name = "entity_" + name_prop
if entity_def is FuncGodotFGDSolidClass:
node = generate_solid_entity_node(node, node_name, entity_data, entity_def)
elif entity_def is FuncGodotFGDPointClass:
node = generate_point_entity_node(node, node_name, properties, entity_def)
if node:
if entity_def.script_class:
node.set_script(entity_def.script_class)
else:
push_error("No entity definition found for \"" + node_name + "\"")
if entity_data.brushes.size():
node = generate_solid_entity_node(node, node_name, entity_data, default_solid_def)
else:
node = generate_point_entity_node(node, node_name, properties, default_point_def)
var node_groups: Array[String] = map_settings.entity_node_groups.duplicate()
node_groups.append_array(entity_def.node_groups)
for node_group in node_groups:
if node_group.is_empty():
continue
node.add_to_group(node_group, true)
return node
## Main entity assembly process called by [FuncGodotMap]. Generates and sorts group nodes in the [SceneTree] first,
## then generates and assembles [Node]s based upon the provided [FuncGodotData.EntityData] and adds them to the [SceneTree].
func build(map_node: FuncGodotMap, entities: Array[_EntityData], groups: Array[_GroupData]) -> void:
var scene_root := map_node.get_tree().edited_scene_root if map_node.get_tree() else map_node
var scene_root := map_node.get_tree().edited_scene_root if map_node.is_inside_tree() else map_node
build_flags = map_node.build_flags
if map_settings.use_groups_hierarchy:

View file

@ -1,136 +0,0 @@
class_name FuncGodot extends RefCounted
var map_data:= FuncGodotMapData.new()
var map_parser:= FuncGodotMapParser.new(map_data)
var geo_generator = preload("res://addons/func_godot/src/core/func_godot_geo_generator.gd").new(map_data)
var map_settings: FuncGodotMapSettings = null:
set(new):
if not new or new == map_settings: return
surface_gatherer.map_settings = new
map_settings = new
var surface_gatherer:= FuncGodotSurfaceGatherer.new(map_data, map_settings)
func load_map(filename: String, keep_tb_groups: bool) -> void:
map_parser.load_map(filename, keep_tb_groups)
func get_texture_list() -> PackedStringArray:
var g_textures: PackedStringArray
var tex_count: int = map_data.textures.size()
g_textures.resize(tex_count)
for i in range(tex_count):
g_textures.set(i, map_data.textures[i].name)
return g_textures
func set_entity_definitions(entity_defs: Dictionary) -> void:
for i in range(entity_defs.size()):
var classname: String = entity_defs.keys()[i]
var spawn_type: int = entity_defs.values()[i].get("spawn_type", FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY)
var origin_type: int = entity_defs.values()[i].get("origin_type", FuncGodotMapData.FuncGodotEntityOriginType.BOUNDS_CENTER)
var metadata_inclusion_flags: int = entity_defs.values()[i].get("metadata_inclusion_flags", FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags.NONE)
map_data.set_entity_types_by_classname(classname, spawn_type, origin_type, metadata_inclusion_flags)
func get_texture_info(texture_name: String) -> FuncGodotMapData.FuncGodotTextureType:
if texture_name == map_settings.origin_texture:
return FuncGodotMapData.FuncGodotTextureType.ORIGIN
return FuncGodotMapData.FuncGodotTextureType.NORMAL
func generate_geometry(texture_dict: Dictionary) -> void:
var keys: Array = texture_dict.keys()
for key in keys:
var val: Vector2 = texture_dict[key]
map_data.set_texture_info(key, val.x, val.y, get_texture_info(key))
geo_generator.run()
func get_entity_dicts() -> Array:
var ent_dicts: Array
for entity in map_data.entities:
var dict: Dictionary
dict["brush_count"] = entity.brushes.size()
# TODO: This is a horrible remnant of the worldspawn layer system, remove it.
var brush_indices: PackedInt64Array
brush_indices.resize(entity.brushes.size())
for b in range(entity.brushes.size()):
brush_indices[b] = b
dict["brush_indices"] = brush_indices
dict["center"] = Vector3(entity.center.y, entity.center.z, entity.center.x)
dict["properties"] = entity.properties
ent_dicts.append(dict)
return ent_dicts
func gather_texture_surfaces(texture_name: String) -> Dictionary:
var sg: FuncGodotSurfaceGatherer = FuncGodotSurfaceGatherer.new(map_data, map_settings)
sg.reset_params()
sg.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.ENTITY
const MFlags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags
sg.metadata_skip_flags = MFlags.TEXTURES | MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP
sg.set_texture_filter(texture_name)
sg.set_clip_filter_texture(map_settings.clip_texture)
sg.set_skip_filter_texture(map_settings.skip_texture)
sg.set_origin_filter_texture(map_settings.origin_texture)
sg.run()
return {
surfaces = fetch_surfaces(sg),
metadata = sg.out_metadata,
}
func gather_entity_convex_collision_surfaces(entity_idx: int) -> void:
surface_gatherer.reset_params()
surface_gatherer.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.BRUSH
surface_gatherer.entity_filter_idx = entity_idx
surface_gatherer.set_origin_filter_texture(map_settings.origin_texture)
surface_gatherer.run()
func gather_entity_concave_collision_surfaces(entity_idx: int) -> void:
surface_gatherer.reset_params()
surface_gatherer.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.NONE
surface_gatherer.entity_filter_idx = entity_idx
const MFlags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags
surface_gatherer.metadata_skip_flags |= MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP
surface_gatherer.set_skip_filter_texture(map_settings.skip_texture)
surface_gatherer.set_origin_filter_texture(map_settings.origin_texture)
surface_gatherer.run()
func fetch_surfaces(sg: FuncGodotSurfaceGatherer) -> Array:
var surfs: Array[FuncGodotMapData.FuncGodotFaceGeometry] = sg.out_surfaces
var surf_array: Array
for surf in surfs:
if surf == null or surf.vertices.size() == 0:
surf_array.append(null)
continue
var vertices: PackedVector3Array
var normals: PackedVector3Array
var tangents: PackedFloat64Array
var uvs: PackedVector2Array
for v in surf.vertices:
vertices.append(Vector3(v.vertex.y, v.vertex.z, v.vertex.x) * map_settings.scale_factor)
normals.append(Vector3(v.normal.y, v.normal.z, v.normal.x))
tangents.append(v.tangent.y)
tangents.append(v.tangent.z)
tangents.append(v.tangent.x)
tangents.append(v.tangent.w)
uvs.append(Vector2(v.uv.x, v.uv.y))
var indices: PackedInt32Array
if surf.indicies.size() > 0:
indices.append_array(surf.indicies)
var brush_array: Array
brush_array.resize(Mesh.ARRAY_MAX)
brush_array[Mesh.ARRAY_VERTEX] = vertices
brush_array[Mesh.ARRAY_NORMAL] = normals
brush_array[Mesh.ARRAY_TANGENT] = tangents
brush_array[Mesh.ARRAY_TEX_UV] = uvs
brush_array[Mesh.ARRAY_INDEX] = indices
surf_array.append(brush_array)
return surf_array

View file

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

View file

@ -1,381 +0,0 @@
extends RefCounted
# Min distance between two verts in a brush before they're merged. Higher values fix angled brushes near extents.
const CMP_EPSILON:= 0.008
const UP_VECTOR:= Vector3(0.0, 0.0, 1.0)
const RIGHT_VECTOR:= Vector3(0.0, 1.0, 0.0)
const FORWARD_VECTOR:= Vector3(1.0, 0.0, 0.0)
var map_data: FuncGodotMapData
var wind_entity_idx: int = 0
var wind_brush_idx: int = 0
var wind_face_idx: int = 0
var wind_face_center: Vector3
var wind_face_basis: Vector3
var wind_face_normal: Vector3
func _init(in_map_data: FuncGodotMapData) -> void:
map_data = in_map_data
func sort_vertices_by_winding(a: FuncGodotMapData.FuncGodotFaceVertex, b: FuncGodotMapData.FuncGodotFaceVertex) -> bool:
var face: FuncGodotMapData.FuncGodotFace = map_data.entities[wind_entity_idx].brushes[wind_brush_idx].faces[wind_face_idx]
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = map_data.entity_geo[wind_entity_idx].brushes[wind_brush_idx].faces[wind_face_idx]
var u: Vector3 = wind_face_basis.normalized()
var v: Vector3 = u.cross(wind_face_normal).normalized()
var loc_a: Vector3 = a.vertex - wind_face_center
var a_pu: float = loc_a.dot(u)
var a_pv: float = loc_a.dot(v)
var loc_b: Vector3 = b.vertex - wind_face_center
var b_pu: float = loc_b.dot(u)
var b_pv: float = loc_b.dot(v)
var a_angle: float = atan2(a_pv, a_pu)
var b_angle: float = atan2(b_pv, b_pu)
return a_angle < b_angle
# returns null if no intersection, else intersection vertex.
func intersect_face(f0: FuncGodotMapData.FuncGodotFace, f1: FuncGodotMapData.FuncGodotFace, f2: FuncGodotMapData.FuncGodotFace) -> Variant:
var n0:= f0.plane_normal
var n1:= f1.plane_normal
var n2:= f2.plane_normal
var denom: float = n0.cross(n1).dot(n2)
if denom > 0.0:
return (n1.cross(n2) * f0.plane_dist + n2.cross(n0) * f1.plane_dist + n0.cross(n1) * f2.plane_dist) / denom
return null
func vertex_in_hull(faces: Array[FuncGodotMapData.FuncGodotFace], vertex: Vector3) -> bool:
for face in faces:
var proj: float = face.plane_normal.dot(vertex)
if proj > face.plane_dist and absf(face.plane_dist - proj) > CMP_EPSILON:
return false
return true
func get_standard_uv(vertex: Vector3, face: FuncGodotMapData.FuncGodotFace, texture_width: int, texture_height: int) -> Vector2:
var uv_out: Vector2
var du:= absf(face.plane_normal.dot(UP_VECTOR))
var dr:= absf(face.plane_normal.dot(RIGHT_VECTOR))
var df:= absf(face.plane_normal.dot(FORWARD_VECTOR))
if du >= dr and du >= df:
uv_out = Vector2(vertex.x, -vertex.y)
elif dr >= du and dr >= df:
uv_out = Vector2(vertex.x, -vertex.z)
elif df >= du and df >= dr:
uv_out = Vector2(vertex.y, -vertex.z)
var angle: float = deg_to_rad(face.uv_extra.rot)
uv_out = Vector2(
uv_out.x * cos(angle) - uv_out.y * sin(angle),
uv_out.x * sin(angle) + uv_out.y * cos(angle))
uv_out.x /= texture_width
uv_out.y /= texture_height
uv_out.x /= face.uv_extra.scale_x
uv_out.y /= face.uv_extra.scale_y
uv_out.x += face.uv_standard.x / texture_width
uv_out.y += face.uv_standard.y / texture_height
return uv_out
func get_valve_uv(vertex: Vector3, face: FuncGodotMapData.FuncGodotFace, texture_width: int, texture_height: int) -> Vector2:
var uv_out: Vector2
var u_axis:= face.uv_valve.u.axis
var v_axis:= face.uv_valve.v.axis
var u_shift:= face.uv_valve.u.offset
var v_shift:= face.uv_valve.v.offset
uv_out.x = u_axis.dot(vertex);
uv_out.y = v_axis.dot(vertex);
uv_out.x /= texture_width;
uv_out.y /= texture_height;
uv_out.x /= face.uv_extra.scale_x;
uv_out.y /= face.uv_extra.scale_y;
uv_out.x += u_shift / texture_width;
uv_out.y += v_shift / texture_height;
return uv_out
func get_standard_tangent(face: FuncGodotMapData.FuncGodotFace) -> Vector4:
var du:= face.plane_normal.dot(UP_VECTOR)
var dr:= face.plane_normal.dot(RIGHT_VECTOR)
var df:= face.plane_normal.dot(FORWARD_VECTOR)
var dua:= absf(du)
var dra:= absf(dr)
var dfa:= absf(df)
var u_axis: Vector3
var v_sign: float = 0.0
if dua >= dra and dua >= dfa:
u_axis = FORWARD_VECTOR
v_sign = signf(du)
elif dra >= dua and dra >= dfa:
u_axis = FORWARD_VECTOR
v_sign = -signf(dr)
elif dfa >= dua and dfa >= dra:
u_axis = RIGHT_VECTOR
v_sign = signf(df)
v_sign *= signf(face.uv_extra.scale_y);
u_axis = u_axis.rotated(face.plane_normal, deg_to_rad(-face.uv_extra.rot) * v_sign)
return Vector4(u_axis.x, u_axis.y, u_axis.z, v_sign)
func get_valve_tangent(face: FuncGodotMapData.FuncGodotFace) -> Vector4:
var u_axis:= face.uv_valve.u.axis.normalized()
var v_axis:= face.uv_valve.v.axis.normalized()
var v_sign = -signf(face.plane_normal.cross(u_axis).dot(v_axis))
return Vector4(u_axis.x, u_axis.y, u_axis.z, v_sign)
func generate_brush_vertices(entity_idx: int, brush_idx: int) -> void:
var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[entity_idx]
var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[brush_idx]
var face_count: int = brush.faces.size()
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[entity_idx]
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[brush_idx]
var phong: bool = entity.properties.get("_phong", "0") == "1"
var phong_angle_str: String = entity.properties.get("_phong_angle", "89")
var phong_angle: float = float(phong_angle_str) if phong_angle_str.is_valid_float() else 89.0
for f0 in range(face_count):
var face: FuncGodotMapData.FuncGodotFace = brush.faces[f0]
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f0]
var texture: FuncGodotMapData.FuncGodotTextureData = map_data.textures[face.texture_idx]
for f1 in range(face_count):
for f2 in range(face_count):
var vertex = intersect_face(brush.faces[f0], brush.faces[f1], brush.faces[f2])
if not vertex is Vector3:
continue
if not vertex_in_hull(brush.faces, vertex):
continue
var merged: bool = false
for f3 in range(f0):
var other_face_geo : FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f3]
for i in range(len(other_face_geo.vertices)):
if other_face_geo.vertices[i].vertex.distance_to(vertex) < CMP_EPSILON:
vertex = other_face_geo.vertices[i].vertex
merged = true;
break
if merged:
break
var normal: Vector3 = face.plane_normal
if phong:
var threshold:= cos((phong_angle + 0.01) * 0.0174533)
if face.plane_normal.dot(brush.faces[f1].plane_normal) > threshold:
normal += brush.faces[f1].plane_normal
if face.plane_normal.dot(brush.faces[f2].plane_normal) > threshold:
normal += brush.faces[f2].plane_normal
normal = normal.normalized()
var uv: Vector2
var tangent: Vector4
if face.is_valve_uv:
uv = get_valve_uv(vertex, face, texture.width, texture.height)
tangent = get_valve_tangent(face)
else:
uv = get_standard_uv(vertex, face, texture.width, texture.height)
tangent = get_standard_tangent(face)
# Check for a duplicate vertex in the current face.
var duplicate_idx: int = -1
for i in range(face_geo.vertices.size()):
if face_geo.vertices[i].vertex == vertex:
duplicate_idx = i
break
if duplicate_idx < 0:
var new_face_vert:= FuncGodotMapData.FuncGodotFaceVertex.new()
new_face_vert.vertex = vertex
new_face_vert.normal = normal
new_face_vert.tangent = tangent
new_face_vert.uv = uv
face_geo.vertices.append(new_face_vert)
elif phong:
face_geo.vertices[duplicate_idx].normal += normal
# maybe optimisable?
for face_geo in brush_geo.faces:
for i in range(face_geo.vertices.size()):
face_geo.vertices[i].normal = face_geo.vertices[i].normal.normalized()
func run() -> void:
map_data.entity_geo.resize(map_data.entities.size())
for i in range(map_data.entity_geo.size()):
map_data.entity_geo[i] = FuncGodotMapData.FuncGodotEntityGeometry.new()
for e in range(map_data.entities.size()):
var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[e]
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e]
entity_geo.brushes.resize(entity.brushes.size())
for i in range(entity_geo.brushes.size()):
entity_geo.brushes[i] = FuncGodotMapData.FuncGodotBrushGeometry.new()
for b in range(entity.brushes.size()):
var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[b]
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[b]
brush_geo.faces.resize(brush.faces.size())
for i in range(brush_geo.faces.size()):
brush_geo.faces[i] = FuncGodotMapData.FuncGodotFaceGeometry.new()
var generate_vertices_task = func(e):
var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[e]
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e]
var entity_mins: Vector3 = Vector3.INF
var entity_maxs: Vector3 = Vector3.INF
var origin_mins: Vector3 = Vector3.INF
var origin_maxs: Vector3 = -Vector3.INF
entity.center = Vector3.ZERO
for b in range(entity.brushes.size()):
var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[b]
brush.center = Vector3.ZERO
var vert_count: int = 0
# Check if this is a special brush (eg: origin)
var brush_texture_type: FuncGodotMapData.FuncGodotTextureType = FuncGodotMapData.FuncGodotTextureType.NORMAL
if brush.faces.size() > 0:
brush_texture_type = map_data.textures[brush.faces[0].texture_idx].type
# Check that all the faces match the same type
for face_idx in range(1,brush.faces.size()):
if map_data.textures[brush.faces[face_idx].texture_idx].type != brush_texture_type:
brush_texture_type = FuncGodotMapData.FuncGodotTextureType.NORMAL # Reset face type if it doesn't match
break
generate_brush_vertices(e, b)
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = map_data.entity_geo[e].brushes[b]
for face in brush_geo.faces:
for vert in face.vertices:
if entity_mins != Vector3.INF:
entity_mins = entity_mins.min(vert.vertex)
else:
entity_mins = vert.vertex
if entity_maxs != Vector3.INF:
entity_maxs = entity_maxs.max(vert.vertex)
else:
entity_maxs = vert.vertex
if brush_texture_type == FuncGodotMapData.FuncGodotTextureType.ORIGIN:
if origin_mins != Vector3.INF:
origin_mins = origin_mins.min(vert.vertex)
else:
origin_mins = vert.vertex
if origin_maxs != Vector3.INF:
origin_maxs = origin_maxs.max(vert.vertex)
else:
origin_maxs = vert.vertex
brush.center += vert.vertex
vert_count += 1
if vert_count > 0:
brush.center /= float(vert_count)
# Default origin type is BOUNDS_CENTER
if entity_maxs != Vector3.INF and entity_mins != Vector3.INF:
entity.center = entity_maxs - ((entity_maxs - entity_mins) * 0.5)
if entity.origin_type != FuncGodotMapData.FuncGodotEntityOriginType.BOUNDS_CENTER and entity.brushes.size() > 0:
match entity.origin_type:
FuncGodotMapData.FuncGodotEntityOriginType.ABSOLUTE, FuncGodotMapData.FuncGodotEntityOriginType.RELATIVE:
if 'origin' in entity.properties:
var origin_comps: PackedFloat64Array = entity.properties['origin'].split_floats(' ')
if origin_comps.size() > 2:
if entity.origin_type == FuncGodotMapData.FuncGodotEntityOriginType.ABSOLUTE:
entity.center = Vector3(origin_comps[0], origin_comps[1], origin_comps[2])
else: # OriginType.RELATIVE
entity.center += Vector3(origin_comps[0], origin_comps[1], origin_comps[2])
FuncGodotMapData.FuncGodotEntityOriginType.BRUSH:
if origin_mins != Vector3.INF:
entity.center = origin_maxs - ((origin_maxs - origin_mins) * 0.5)
FuncGodotMapData.FuncGodotEntityOriginType.BOUNDS_MINS:
entity.center = entity_mins
FuncGodotMapData.FuncGodotEntityOriginType.BOUNDS_MAXS:
entity.center = entity_maxs
FuncGodotMapData.FuncGodotEntityOriginType.AVERAGED:
entity.center = Vector3.ZERO
for b in range(entity.brushes.size()):
entity.center += entity.brushes[b].center
entity.center /= float(entity.brushes.size())
var generate_vertices_task_id:= WorkerThreadPool.add_group_task(generate_vertices_task, map_data.entities.size(), 4, true)
WorkerThreadPool.wait_for_group_task_completion(generate_vertices_task_id)
# wind face vertices
for e in range(map_data.entities.size()):
var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[e]
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e]
for b in range(entity.brushes.size()):
var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[b]
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[b]
for f in range(brush.faces.size()):
var face: FuncGodotMapData.FuncGodotFace = brush.faces[f]
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f]
if face_geo.vertices.size() < 3:
continue
wind_entity_idx = e
wind_brush_idx = b
wind_face_idx = f
wind_face_basis = face_geo.vertices[1].vertex - face_geo.vertices[0].vertex
wind_face_center = Vector3.ZERO
wind_face_normal = face.plane_normal
for v in face_geo.vertices:
wind_face_center += v.vertex
wind_face_center /= face_geo.vertices.size()
face_geo.vertices.sort_custom(sort_vertices_by_winding)
wind_entity_idx = 0
# index face vertices
var index_faces_task:= func(e):
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e]
for b in range(entity_geo.brushes.size()):
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[b]
for f in range(brush_geo.faces.size()):
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f]
if face_geo.vertices.size() < 3:
continue
var i_count: int = 0
face_geo.indicies.resize((face_geo.vertices.size() - 2) * 3)
for i in range(face_geo.vertices.size() - 2):
face_geo.indicies[i_count] = 0
face_geo.indicies[i_count + 1] = i + 1
face_geo.indicies[i_count + 2] = i + 2
i_count += 3
var index_faces_task_id:= WorkerThreadPool.add_group_task(index_faces_task, map_data.entities.size(), 4, true)
WorkerThreadPool.wait_for_group_task_completion(index_faces_task_id)

View file

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

View file

@ -1,158 +0,0 @@
class_name FuncGodotMapData extends RefCounted
var entities: Array[FuncGodotMapData.FuncGodotEntity]
var entity_geo: Array[FuncGodotMapData.FuncGodotEntityGeometry]
var textures: Array[FuncGodotMapData.FuncGodotTextureData]
func register_texture(name: String) -> int:
for i in range(textures.size()):
if textures[i].name == name:
return i
textures.append(FuncGodotTextureData.new(name))
return textures.size() - 1
func set_texture_info(name: String, width: int, height: int, type: FuncGodotTextureType) -> void:
for i in range(textures.size()):
if textures[i].name == name:
textures[i].width = width
textures[i].height = height
textures[i].type = type
return
func find_texture(texture_name: String) -> int:
for i in range(textures.size()):
if textures[i].name == texture_name:
return i
return -1
func set_entity_types_by_classname(classname: String, spawn_type: int, origin_type: int, meta_flags: int) -> void:
for entity in entities:
if entity.properties.has("classname") and entity.properties["classname"] == classname:
entity.metadata_inclusion_flags = meta_flags as FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags
entity.spawn_type = spawn_type as FuncGodotMapData.FuncGodotEntitySpawnType
if entity.spawn_type == FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY:
entity.origin_type = origin_type as FuncGodotMapData.FuncGodotEntityOriginType
else:
entity.origin_type = FuncGodotMapData.FuncGodotEntityOriginType.AVERAGED
func clear() -> void:
entities.clear()
entity_geo.clear()
textures.clear()
# --------------------------------------------------------------------------------------------------
# Nested Types
# --------------------------------------------------------------------------------------------------
enum FuncGodotEntitySpawnType {
WORLDSPAWN = 0,
MERGE_WORLDSPAWN = 1,
ENTITY = 2
}
enum FuncGodotEntityOriginType {
AVERAGED = 0,
ABSOLUTE = 1,
RELATIVE = 2,
BRUSH = 3,
BOUNDS_CENTER = 4,
BOUNDS_MINS = 5,
BOUNDS_MAXS = 6,
}
enum FuncGodotEntityMetadataInclusionFlags {
NONE = 0,
ENTITY_INDEX_RANGES = 1,
TEXTURES = 2,
VERTEX = 4,
FACE_POSITION = 8,
FACE_NORMAL = 16,
COLLISION_SHAPE_TO_FACE_RANGE_MAP = 32,
}
enum FuncGodotTextureType {
NORMAL = 0,
ORIGIN = 1
}
class FuncGodotFacePoints:
var v0: Vector3
var v1: Vector3
var v2: Vector3
class FuncGodotValveTextureAxis:
var axis: Vector3
var offset: float
class FuncGodotValveUV:
var u: FuncGodotValveTextureAxis
var v: FuncGodotValveTextureAxis
func _init() -> void:
u = FuncGodotValveTextureAxis.new()
v = FuncGodotValveTextureAxis.new()
class FuncGodotFaceUVExtra:
var rot: float
var scale_x: float
var scale_y: float
class FuncGodotFace:
var plane_points: FuncGodotFacePoints
var plane_normal: Vector3
var plane_dist: float
var texture_idx: int
var is_valve_uv: bool
var uv_standard: Vector2
var uv_valve: FuncGodotValveUV
var uv_extra: FuncGodotFaceUVExtra
func _init() -> void:
plane_points = FuncGodotFacePoints.new()
uv_valve = FuncGodotValveUV.new()
uv_extra = FuncGodotFaceUVExtra.new()
class FuncGodotBrush:
var faces: Array[FuncGodotFace]
var center: Vector3
class FuncGodotEntity:
var properties: Dictionary
var brushes: Array[FuncGodotBrush]
var center: Vector3
var spawn_type: FuncGodotEntitySpawnType
var origin_type: FuncGodotEntityOriginType
var metadata_inclusion_flags: FuncGodotEntityMetadataInclusionFlags
class FuncGodotFaceVertex:
var vertex: Vector3
var normal: Vector3
var uv: Vector2
var tangent: Vector4
func duplicate() -> FuncGodotFaceVertex:
var new_vert := FuncGodotFaceVertex.new()
new_vert.vertex = vertex
new_vert.normal = normal
new_vert.uv = uv
new_vert.tangent = tangent
return new_vert
class FuncGodotFaceGeometry:
var vertices: Array[FuncGodotFaceVertex]
var indicies: Array[int]
class FuncGodotBrushGeometry:
var faces: Array[FuncGodotFaceGeometry]
class FuncGodotEntityGeometry:
var brushes: Array[FuncGodotBrushGeometry]
class FuncGodotTextureData:
var name: String
var width: int
var height: int
var type: FuncGodotTextureType
func _init(in_name: String):
name = in_name

View file

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

View file

@ -1,326 +0,0 @@
class_name FuncGodotMapParser extends RefCounted
var scope:= FuncGodotMapParser.ParseScope.FILE
var comment: bool = false
var entity_idx: int = -1
var brush_idx: int = -1
var face_idx: int = -1
var component_idx: int = 0
var prop_key: String = ""
var current_property: String = ""
var valve_uvs: bool = false
var current_face: FuncGodotMapData.FuncGodotFace
var current_brush: FuncGodotMapData.FuncGodotBrush
var current_entity: FuncGodotMapData.FuncGodotEntity
var map_data: FuncGodotMapData
var _keep_tb_groups: bool = false
func _init(in_map_data: FuncGodotMapData) -> void:
map_data = in_map_data
func load_map(map_file: String, keep_tb_groups: bool) -> bool:
current_face = FuncGodotMapData.FuncGodotFace.new()
current_brush = FuncGodotMapData.FuncGodotBrush.new()
current_entity = FuncGodotMapData.FuncGodotEntity.new()
scope = FuncGodotMapParser.ParseScope.FILE
comment = false
entity_idx = -1
brush_idx = -1
face_idx = -1
component_idx = 0
valve_uvs = false
_keep_tb_groups = keep_tb_groups
var lines: PackedStringArray = []
var map: FileAccess = FileAccess.open(map_file, FileAccess.READ)
if map == null:
printerr("Error: Failed to open map file (" + map_file + ")")
return false
if map_file.ends_with(".import"):
while not map.eof_reached():
var line: String = map.get_line()
if line.begins_with("path"):
map.close()
line = line.replace("path=", "");
line = line.replace('"', '')
var map_data: String = (load(line) as QuakeMapFile).map_data
if map_data.is_empty():
printerr("Error: Failed to open map file (" + line + ")")
return false
lines = map_data.split("\n")
break
else:
while not map.eof_reached():
var line: String = map.get_line()
lines.append(line)
for line in lines:
if comment:
comment = false
var tokens := split_string(line, [" ", "\t"], true)
for s in tokens:
token(s)
return true
func split_string(s: String, delimeters: Array[String], allow_empty: bool = true) -> Array[String]:
var parts: Array[String] = []
var start := 0
var i := 0
while i < s.length():
if s[i] in delimeters:
if allow_empty or start < i:
parts.push_back(s.substr(start, i - start))
start = i + 1
i += 1
if allow_empty or start < i:
parts.push_back(s.substr(start, i - start))
return parts
func set_scope(new_scope: FuncGodotMapParser.ParseScope) -> void:
"""
match new_scope:
ParseScope.FILE:
print("Switching to file scope.")
ParseScope.ENTITY:
print("Switching to entity " + str(entity_idx) + "scope")
ParseScope.PROPERTY_VALUE:
print("Switching to property value scope")
ParseScope.BRUSH:
print("Switching to brush " + str(brush_idx) + " scope")
ParseScope.PLANE_0:
print("Switching to face " + str(face_idx) + " plane 0 scope")
ParseScope.PLANE_1:
print("Switching to face " + str(face_idx) + " plane 1 scope")
ParseScope.PLANE_2:
print("Switching to face " + str(face_idx) + " plane 2 scope")
ParseScope.TEXTURE:
print("Switching to texture scope")
ParseScope.U:
print("Switching to U scope")
ParseScope.V:
print("Switching to V scope")
ParseScope.VALVE_U:
print("Switching to Valve U scope")
ParseScope.VALVE_V:
print("Switching to Valve V scope")
ParseScope.ROT:
print("Switching to rotation scope")
ParseScope.U_SCALE:
print("Switching to U scale scope")
ParseScope.V_SCALE:
print("Switching to V scale scope")
"""
scope = new_scope
func token(buf_str: String) -> void:
if comment:
return
elif buf_str == "//":
comment = true
return
match scope:
FuncGodotMapParser.ParseScope.FILE:
if buf_str == "{":
entity_idx += 1
brush_idx = -1
set_scope(FuncGodotMapParser.ParseScope.ENTITY)
FuncGodotMapParser.ParseScope.ENTITY:
if buf_str.begins_with('"'):
prop_key = buf_str.substr(1)
if prop_key.ends_with('"'):
prop_key = prop_key.left(-1)
set_scope(FuncGodotMapParser.ParseScope.PROPERTY_VALUE)
elif buf_str == "{":
brush_idx += 1
face_idx = -1
set_scope(FuncGodotMapParser.ParseScope.BRUSH)
elif buf_str == "}":
commit_entity()
set_scope(FuncGodotMapParser.ParseScope.FILE)
FuncGodotMapParser.ParseScope.PROPERTY_VALUE:
var is_first = buf_str[0] == '"'
var is_last = buf_str.right(1) == '"'
if is_first:
if current_property != "":
current_property = ""
if not is_last:
current_property += buf_str + " "
else:
current_property += buf_str
if is_last:
current_entity.properties[prop_key] = current_property.substr(1, len(current_property) - 2)
set_scope(FuncGodotMapParser.ParseScope.ENTITY)
FuncGodotMapParser.ParseScope.BRUSH:
if buf_str == "(":
face_idx += 1
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.PLANE_0)
elif buf_str == "}":
commit_brush()
set_scope(FuncGodotMapParser.ParseScope.ENTITY)
FuncGodotMapParser.ParseScope.PLANE_0:
if buf_str == ")":
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.PLANE_1)
else:
match component_idx:
0:
current_face.plane_points.v0.x = float(buf_str)
1:
current_face.plane_points.v0.y = float(buf_str)
2:
current_face.plane_points.v0.z = float(buf_str)
component_idx += 1
FuncGodotMapParser.ParseScope.PLANE_1:
if buf_str != "(":
if buf_str == ")":
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.PLANE_2)
else:
match component_idx:
0:
current_face.plane_points.v1.x = float(buf_str)
1:
current_face.plane_points.v1.y = float(buf_str)
2:
current_face.plane_points.v1.z = float(buf_str)
component_idx += 1
FuncGodotMapParser.ParseScope.PLANE_2:
if buf_str != "(":
if buf_str == ")":
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.TEXTURE)
else:
match component_idx:
0:
current_face.plane_points.v2.x = float(buf_str)
1:
current_face.plane_points.v2.y = float(buf_str)
2:
current_face.plane_points.v2.z = float(buf_str)
component_idx += 1
FuncGodotMapParser.ParseScope.TEXTURE:
current_face.texture_idx = map_data.register_texture(buf_str)
set_scope(FuncGodotMapParser.ParseScope.U)
FuncGodotMapParser.ParseScope.U:
if buf_str == "[":
valve_uvs = true
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.VALVE_U)
else:
valve_uvs = false
current_face.uv_standard.x = float(buf_str)
set_scope(FuncGodotMapParser.ParseScope.V)
FuncGodotMapParser.ParseScope.V:
current_face.uv_standard.y = float(buf_str)
set_scope(FuncGodotMapParser.ParseScope.ROT)
FuncGodotMapParser.ParseScope.VALVE_U:
if buf_str == "]":
component_idx = 0
set_scope(FuncGodotMapParser.ParseScope.VALVE_V)
else:
match component_idx:
0:
current_face.uv_valve.u.axis.x = float(buf_str)
1:
current_face.uv_valve.u.axis.y = float(buf_str)
2:
current_face.uv_valve.u.axis.z = float(buf_str)
3:
current_face.uv_valve.u.offset = float(buf_str)
component_idx += 1
FuncGodotMapParser.ParseScope.VALVE_V:
if buf_str != "[":
if buf_str == "]":
set_scope(FuncGodotMapParser.ParseScope.ROT)
else:
match component_idx:
0:
current_face.uv_valve.v.axis.x = float(buf_str)
1:
current_face.uv_valve.v.axis.y = float(buf_str)
2:
current_face.uv_valve.v.axis.z = float(buf_str)
3:
current_face.uv_valve.v.offset = float(buf_str)
component_idx += 1
FuncGodotMapParser.ParseScope.ROT:
current_face.uv_extra.rot = float(buf_str)
set_scope(FuncGodotMapParser.ParseScope.U_SCALE)
FuncGodotMapParser.ParseScope.U_SCALE:
current_face.uv_extra.scale_x = float(buf_str)
set_scope(FuncGodotMapParser.ParseScope.V_SCALE)
FuncGodotMapParser.ParseScope.V_SCALE:
current_face.uv_extra.scale_y = float(buf_str)
commit_face()
set_scope(FuncGodotMapParser.ParseScope.BRUSH)
func commit_entity() -> void:
if current_entity.properties.has('_tb_type') and map_data.entities.size() > 0:
map_data.entities[0].brushes.append_array(current_entity.brushes)
current_entity.brushes.clear()
if !_keep_tb_groups:
current_entity = FuncGodotMapData.FuncGodotEntity.new()
return
var new_entity:= FuncGodotMapData.FuncGodotEntity.new()
new_entity.spawn_type = FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY
new_entity.properties = current_entity.properties
new_entity.brushes = current_entity.brushes
map_data.entities.append(new_entity)
current_entity = FuncGodotMapData.FuncGodotEntity.new()
func commit_brush() -> void:
current_entity.brushes.append(current_brush)
current_brush = FuncGodotMapData.FuncGodotBrush.new()
func commit_face() -> void:
var v0v1: Vector3 = current_face.plane_points.v1 - current_face.plane_points.v0
var v1v2: Vector3 = current_face.plane_points.v2 - current_face.plane_points.v1
current_face.plane_normal = v1v2.cross(v0v1).normalized()
current_face.plane_dist = current_face.plane_normal.dot(current_face.plane_points.v0)
current_face.is_valve_uv = valve_uvs
current_brush.faces.append(current_face)
current_face = FuncGodotMapData.FuncGodotFace.new()
# Nested
enum ParseScope{
FILE,
COMMENT,
ENTITY,
PROPERTY_VALUE,
BRUSH,
PLANE_0,
PLANE_1,
PLANE_2,
TEXTURE,
U,
V,
VALVE_U,
VALVE_V,
ROT,
U_SCALE,
V_SCALE
}

View file

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

View file

@ -1,217 +0,0 @@
class_name FuncGodotSurfaceGatherer extends RefCounted
var map_data: FuncGodotMapData
var map_settings: FuncGodotMapSettings
var split_type: SurfaceSplitType = SurfaceSplitType.NONE
var entity_filter_idx: int = -1
var texture_filter_idx: int = -1
var clip_filter_texture_idx: int
var skip_filter_texture_idx: int
var origin_filter_texture_idx: int
var metadata_skip_flags: int
var out_surfaces: Array[FuncGodotMapData.FuncGodotFaceGeometry]
var out_metadata: Dictionary
func _init(in_map_data: FuncGodotMapData, in_map_settings: FuncGodotMapSettings) -> void:
map_data = in_map_data
map_settings = in_map_settings
func set_texture_filter(texture_name: String) -> void:
texture_filter_idx = map_data.find_texture(texture_name)
func set_clip_filter_texture(texture_name: String) -> void:
clip_filter_texture_idx = map_data.find_texture(texture_name)
func set_skip_filter_texture(texture_name: String) -> void:
skip_filter_texture_idx = map_data.find_texture(texture_name)
func set_origin_filter_texture(texture_name: String) -> void:
origin_filter_texture_idx = map_data.find_texture(texture_name)
func filter_entity(entity_idx: int) -> bool:
if entity_filter_idx != -1 and entity_idx != entity_filter_idx:
return true
return false
func filter_face(entity_idx: int, brush_idx: int, face_idx: int) -> bool:
var face: FuncGodotMapData.FuncGodotFace = map_data.entities[entity_idx].brushes[brush_idx].faces[face_idx]
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = map_data.entity_geo[entity_idx].brushes[brush_idx].faces[face_idx]
if face_geo.vertices.size() < 3:
return true
# Omit faces textured with Clip
if clip_filter_texture_idx != -1 and face.texture_idx == clip_filter_texture_idx:
return true
# Omit faces textured with Skip
if skip_filter_texture_idx != -1 and face.texture_idx == skip_filter_texture_idx:
return true
# Omit faces textured with Origin
if origin_filter_texture_idx != -1 and face.texture_idx == origin_filter_texture_idx:
return true
# Omit filtered texture indices
if texture_filter_idx != -1 and face.texture_idx != texture_filter_idx:
return true
return false
func run() -> void:
out_surfaces.clear()
var texture_names: Array[StringName] = []
var textures: PackedInt32Array = []
var vertices: PackedVector3Array = []
var positions: PackedVector3Array = []
var normals: PackedVector3Array = []
var shape_index_ranges: Array[Vector2i] = []
var entity_index_ranges: Array[Vector2i] = []
var index_offset: int = 0
var entity_face_range: Vector2i = Vector2i.ZERO
const MFlags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags
var build_entity_index_ranges: bool = not metadata_skip_flags & MFlags.ENTITY_INDEX_RANGES
var surf: FuncGodotMapData.FuncGodotFaceGeometry
if split_type == SurfaceSplitType.NONE:
surf = add_surface()
index_offset = len(out_surfaces) - 1
for e in range(map_data.entities.size()):
var entity:= map_data.entities[e]
var entity_geo:= map_data.entity_geo[e]
var shape_face_range := Vector2i.ZERO
var total_entity_tris := 0
var include_normals_metadata: bool = not metadata_skip_flags & MFlags.FACE_NORMAL and entity.metadata_inclusion_flags & MFlags.FACE_NORMAL
var include_vertices_metadata: bool = not metadata_skip_flags & MFlags.VERTEX and entity.metadata_inclusion_flags & MFlags.VERTEX
var include_textures_metadata: bool = not metadata_skip_flags & MFlags.TEXTURES and entity.metadata_inclusion_flags & MFlags.TEXTURES
var include_positions_metadata: bool = not metadata_skip_flags & MFlags.FACE_POSITION and entity.metadata_inclusion_flags & MFlags.FACE_POSITION
var include_shape_range_metadata: bool = not metadata_skip_flags & MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP and entity.metadata_inclusion_flags & MFlags.COLLISION_SHAPE_TO_FACE_RANGE_MAP
if filter_entity(e):
continue
if split_type == SurfaceSplitType.ENTITY:
if entity.spawn_type == FuncGodotMapData.FuncGodotEntitySpawnType.MERGE_WORLDSPAWN:
add_surface()
surf = out_surfaces[0]
index_offset = surf.vertices.size()
else:
surf = add_surface()
index_offset = surf.vertices.size()
for b in range(entity.brushes.size()):
var brush:= entity.brushes[b]
var brush_geo:= entity_geo.brushes[b]
var total_brush_tris:= 0
if split_type == SurfaceSplitType.BRUSH:
index_offset = 0
surf = add_surface()
for f in range(brush.faces.size()):
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f]
var face: FuncGodotMapData.FuncGodotFace = brush.faces[f]
var num_tris = face_geo.vertices.size() - 2
if filter_face(e, b, f):
continue
for v in range(face_geo.vertices.size()):
var vert: FuncGodotMapData.FuncGodotFaceVertex = face_geo.vertices[v].duplicate()
if entity.spawn_type == FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY:
vert.vertex -= entity.center
surf.vertices.append(vert)
if include_normals_metadata:
var normal := Vector3(face.plane_normal.y, face.plane_normal.z, face.plane_normal.x)
for i in num_tris:
normals.append(normal)
if include_shape_range_metadata or build_entity_index_ranges:
total_brush_tris += num_tris
if include_textures_metadata:
var texname := StringName(map_data.textures[face.texture_idx].name)
var index: int
if texture_names.is_empty():
texture_names.append(texname)
index = 0
elif texture_names.back() == texname:
# Common case, faces with textures are next to each other
index = texture_names.size() - 1
else:
var texture_name_index: int = texture_names.find(texname)
if texture_name_index == -1:
index = texture_names.size()
texture_names.append(texname)
else:
index = texture_name_index
# Metadata addresses triangles, so we have to duplicate the info for each tri
for i in num_tris:
textures.append(index)
var avg_vertex_pos := Vector3.ZERO
var avg_vertex_pos_ct: int = 0
for i in range(num_tris * 3):
surf.indicies.append(face_geo.indicies[i] + index_offset)
var vertex: Vector3 = surf.vertices[surf.indicies.back()].vertex
vertex = Vector3(vertex.y, vertex.z, vertex.x) * map_settings.scale_factor
if include_vertices_metadata:
vertices.append(vertex)
if include_positions_metadata:
avg_vertex_pos_ct += 1
avg_vertex_pos += vertex
if avg_vertex_pos_ct == 3:
avg_vertex_pos /= 3
positions.append(avg_vertex_pos)
avg_vertex_pos = Vector3.ZERO
avg_vertex_pos_ct = 0
index_offset += face_geo.vertices.size()
if include_shape_range_metadata:
shape_face_range.x = shape_face_range.y
shape_face_range.y = shape_face_range.x + total_brush_tris
shape_index_ranges.append(shape_face_range)
if build_entity_index_ranges:
total_entity_tris += total_brush_tris
if build_entity_index_ranges:
entity_face_range.x = entity_face_range.y
entity_face_range.y = entity_face_range.x + total_entity_tris
entity_index_ranges.append(entity_face_range)
out_metadata = {
textures = textures,
texture_names = texture_names,
normals = normals,
vertices = vertices,
positions = positions,
shape_index_ranges = shape_index_ranges,
}
if build_entity_index_ranges:
out_metadata["entity_index_ranges"] = entity_index_ranges
func add_surface() -> FuncGodotMapData.FuncGodotFaceGeometry:
var surf:= FuncGodotMapData.FuncGodotFaceGeometry.new()
out_surfaces.append(surf)
return surf
func reset_params() -> void:
split_type = SurfaceSplitType.NONE
entity_filter_idx = -1
texture_filter_idx = -1
clip_filter_texture_idx = -1
skip_filter_texture_idx = -1
metadata_skip_flags = FuncGodotMapData.FuncGodotEntityMetadataInclusionFlags.ENTITY_INDEX_RANGES
# nested
enum SurfaceSplitType{
NONE,
ENTITY,
BRUSH
}

View file

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

View file

@ -8,8 +8,6 @@ const _SIGNATURE: String = "[GEO]"
const _VERTEX_EPSILON := FuncGodotUtil._VERTEX_EPSILON
const _VERTEX_EPSILON2 := _VERTEX_EPSILON * _VERTEX_EPSILON
const _HYPERPLANE_SIZE := 65355.0
const _OriginType := FuncGodotFGDSolidClass.OriginType
const _GroupData := FuncGodotData.GroupData
@ -21,6 +19,7 @@ const _VertexGroupData := FuncGodotData.VertexGroupData
# Class members
var map_settings: FuncGodotMapSettings = null
var hyperplane_size: float = 512.0
var entity_data: Array[_EntityData]
var texture_materials: Dictionary[String, Material]
var texture_sizes: Dictionary[String, Vector2]
@ -30,8 +29,9 @@ var texture_sizes: Dictionary[String, Vector2]
## Emitted when beginning a new step of the generation process.
signal declare_step(step: String)
func _init(settings: FuncGodotMapSettings = null) -> void:
func _init(settings: FuncGodotMapSettings = null, hplane_size: float = 512.0) -> void:
map_settings = settings
hyperplane_size = hplane_size
#region TOOLS
func is_skip(face: _FaceData) -> bool:
@ -99,10 +99,11 @@ func generate_base_winding(plane: Plane) -> PackedVector3Array:
# construct oversized square on the plane to clip against
var winding := PackedVector3Array()
winding.append(centroid + (right * _HYPERPLANE_SIZE) + (forward * _HYPERPLANE_SIZE))
winding.append(centroid + (right * -_HYPERPLANE_SIZE) + (forward * _HYPERPLANE_SIZE))
winding.append(centroid + (right * -_HYPERPLANE_SIZE) + (forward * -_HYPERPLANE_SIZE))
winding.append(centroid + (right * _HYPERPLANE_SIZE) + (forward * -_HYPERPLANE_SIZE))
var h: float = hyperplane_size
winding.append(centroid + (right * h) + (forward * h))
winding.append(centroid + (right * -h) + (forward * h))
winding.append(centroid + (right * -h) + (forward * -h))
winding.append(centroid + (right * h) + (forward * -h))
return winding
func generate_face_vertices(brush: _BrushData, face_index: int, vertex_merge_distance: float = 0.0) -> PackedVector3Array:
@ -121,9 +122,17 @@ func generate_face_vertices(brush: _BrushData, face_index: int, vertex_merge_dis
if winding.is_empty():
break
# Reduce seams between vertices
for i in winding.size():
winding.set(i, winding.get(i).snappedf(vertex_merge_distance))
# Perform rounding and merge adjacent vertices that are equivalent
if vertex_merge_distance > 0:
var merged_winding : PackedVector3Array = PackedVector3Array()
var prev_vtx : Vector3 = winding[0].snappedf(vertex_merge_distance)
merged_winding.append(prev_vtx)
for i in range(1, winding.size()):
var cur_vtx : Vector3 = winding[i].snappedf(vertex_merge_distance)
if prev_vtx != cur_vtx:
merged_winding.append(cur_vtx)
prev_vtx = cur_vtx
winding = merged_winding
return winding
@ -206,9 +215,9 @@ func determine_entity_origins(entity_index: int) -> void:
var origin_comps: PackedFloat64Array = entity.properties["origin"].split_floats(" ")
if origin_comps.size() > 2:
if entity.origin_type == _OriginType.ABSOLUTE:
entity.origin = Vector3(origin_comps[0], origin_comps[1], origin_comps[2])
entity.origin = Vector3(origin_comps[0], origin_comps[1], origin_comps[2]) * map_settings.scale_factor
else: # _OriginType.RELATIVE
entity.origin += Vector3(origin_comps[0], origin_comps[1], origin_comps[2])
entity.origin += Vector3(origin_comps[0], origin_comps[1], origin_comps[2]) * map_settings.scale_factor
_OriginType.BRUSH:
if origin_mins != Vector3.INF:
@ -312,7 +321,7 @@ func generate_entity_surfaces(entity_index: int) -> void:
def = entity.definition
var op_entity_ogl_xf: Callable = func(v: Vector3) -> Vector3:
return (FuncGodotUtil.id_to_opengl(v - entity.origin) * map_settings.scale_factor)
return (FuncGodotUtil.id_to_opengl(v - entity.origin))
# Surface groupings <texture_name, Array[Face]>
var surfaces: Dictionary[String, Array] = {}
@ -372,13 +381,70 @@ func generate_entity_surfaces(entity_index: int) -> void:
# Begin fresh index offset for this subarray
var index_offset: int = 0
for face in faces:
for face: _FaceData in faces:
# FACE SCOPE BEGIN
# Reject invalid faces
if face.vertices.size() < 3 or is_skip(face) or is_origin(face):
continue
#region Reject interior faces only if desired
if entity.properties.get(map_settings.cull_interior_faces_property, false):
var remove_face := false
for face2: _FaceData in faces:
if face == face2:
continue
# Are the planes aligned?
if !face2.plane.has_point(face.plane.get_center()):
continue
# Opposite planes
if !(face.plane.normal*-1.0).is_equal_approx(face2.plane.normal):
continue;
# Check for faces that share all their vertices.
var all_verts_in_face := true
for vert in face.vertices:
if !face2.vertices.has(vert):
all_verts_in_face = false
break;
if all_verts_in_face:
remove_face = true
break
# Check if all vertices of Face1 intersect with any triangle of face 2
# If they do, then Face 1 is entirely overlapped on Face 2 and we can remove Face 1
var all_verts_in_face2 := true
for vert in face.vertices:
var vert_in_any_tri := false
var from := vert - face2.plane.normal*0.001
var to := face2.plane.normal*0.001
# Loop over all triangles in face 2 and see if the vert intersects any of them
for i in ((face2.indices.size()/3)):
var intersect = Geometry3D.ray_intersects_triangle(
from,
to,
face2.vertices[face2.indices[i*3]],
face2.vertices[face2.indices[i*3 + 1]],
face2.vertices[face2.indices[i*3 + 2]]
)
if !intersect:
continue
if intersect:
vert_in_any_tri = true
break;
# This vert didn't show up any triangle, can't remove this face
if !vert_in_any_tri:
all_verts_in_face2 = false
break
# All verts of face 1 are in face 2, so we can safely remove that face
if all_verts_in_face2:
remove_face = true
break;
if remove_face:
continue;
#endregion
# Create trimesh points regardless of texture
if build_concave:
var tris: PackedVector3Array
@ -519,9 +585,11 @@ func generate_entity_surfaces(entity_index: int) -> void:
func unwrap_uv2s(entity_index: int, texel_size: float) -> void:
var entity: _EntityData = entity_data[entity_index]
if entity.mesh:
if (entity.definition as FuncGodotFGDSolidClass).global_illumination_mode:
entity.mesh.lightmap_unwrap(Transform3D.IDENTITY, texel_size)
# NOTE: This skips smoothed meshes as they need to be unwrapped after smoothing.
# Ideally smoothing will be performed here in GeoGen before this process.
# For now, since it occurs in EntityAssembler, skip it.
if entity.mesh and entity.is_gi_enabled() and not entity.is_smooth_shaded(map_settings.entity_smoothing_property):
entity.mesh.lightmap_unwrap(Transform3D.IDENTITY, texel_size)
# Main build process
func build(build_flags: int, entities: Array[_EntityData]) -> Error:

View file

@ -81,8 +81,22 @@ func parse_map_data(map_file: String, map_settings: FuncGodotMapSettings) -> _Pa
var entities_data: Array[_EntityData] = parse_data.entities
var entity_defs: Dictionary[String, FuncGodotFGDEntityClass] = map_settings.entity_fgd.get_entity_definitions()
var missing_defs: PackedStringArray = []
declare_step.emit("Checking entity omission and definition status")
var default_point_class := FuncGodotFGDPointClass.new()
default_point_class.node_class = "Marker3D"
var default_solid_class := FuncGodotFGDSolidClass.new()
default_solid_class.spawn_type = FuncGodotFGDSolidClass.SpawnType.ENTITY
default_solid_class.build_occlusion = false
default_solid_class.collision_shape_type = FuncGodotFGDSolidClass.CollisionShapeType.NONE
default_solid_class.origin_type = FuncGodotFGDSolidClass.OriginType.BRUSH
declare_step.emit("Checking entity omission, definition status, and property types")
# Cache retrieved class property defaults. Format is Dictionary[Classname, Properties].
var prop_defaults_cache: Dictionary[String, Dictionary] = {}
var prop_descriptions_cache: Dictionary[String, Dictionary] = {}
for i in range(entities_data.size() - 1, -1, -1):
var entity: _EntityData = entities_data[i]
@ -98,6 +112,145 @@ func parse_map_data(map_file: String, map_settings: FuncGodotMapSettings) -> _Pa
var classname: String = entity.properties["classname"]
if classname in entity_defs:
entity.definition = entity_defs[classname]
if not entity.definition is FuncGodotFGDSolidClass and not entity.definition is FuncGodotFGDPointClass:
if missing_defs.find(classname) < 0:
push_error("Invalid entity definition for \"" + classname + "\". Entity definition must be Solid Class or Point Class.")
missing_defs.append(classname)
entity.definition = null
elif missing_defs.find(classname) < 0:
push_error("No entity definition found for \"" + classname + "\"")
missing_defs.append(classname)
# Make sure we have a default definition to build entities from
# This will make sure nothing goes wrong in the build processes
if not entity.definition:
if entity.brushes.is_empty():
entity.definition = default_point_class
else:
entity.definition = default_solid_class
# Convert the string values of the entity's properties Dictionary to various
# Variant formats based on the entity definition's class property defaults.
var def := entity.definition
var properties: Dictionary = entity.properties
for property in properties:
var prop_string = entity.properties[property]
if property in def.class_properties:
var prop_default: Variant = def.class_properties[property]
match typeof(prop_default):
TYPE_INT:
properties[property] = prop_string.to_int()
TYPE_FLOAT:
properties[property] = prop_string.to_float()
TYPE_BOOL:
properties[property] = bool(prop_string.to_int())
TYPE_VECTOR3:
var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
if prop_comps.size() > 2:
properties[property] = Vector3(prop_comps[0], prop_comps[1], prop_comps[2])
else:
push_error("Invalid Vector3 format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_default
TYPE_VECTOR3I:
var prop_vec: Vector3i = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 2:
for v in 3:
prop_vec[v] = prop_comps[v].to_int()
else:
push_error("Invalid Vector3i format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_COLOR:
var prop_color: Color = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 2:
prop_color.r8 = prop_comps[0].to_int()
prop_color.g8 = prop_comps[1].to_int()
prop_color.b8 = prop_comps[2].to_int()
prop_color.a = 1.0
else:
push_error("Invalid Color format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_color
TYPE_DICTIONARY:
var prop_desc = def.class_property_descriptions[property]
if prop_desc is Array and prop_desc.size() > 1 and prop_desc[1] is int:
properties[property] = prop_string.to_int()
TYPE_ARRAY:
properties[property] = prop_string.to_int()
TYPE_VECTOR2:
var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
if prop_comps.size() > 1:
properties[property] = Vector2(prop_comps[0], prop_comps[1])
else:
push_error("Invalid Vector2 format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_default
TYPE_VECTOR2I:
var prop_vec: Vector2i = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 1:
for v in 2:
prop_vec[v] = prop_comps[v].to_int()
else:
push_error("Invalid Vector2i format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_VECTOR4:
var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
if prop_comps.size() > 3:
properties[property] = Vector4(prop_comps[0], prop_comps[1], prop_comps[2], prop_comps[3])
else:
push_error("Invalid Vector4 format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_default
TYPE_VECTOR4I:
var prop_vec: Vector4i = prop_default
var prop_comps: PackedStringArray = prop_string.split(" ")
if prop_comps.size() > 3:
for v in 4:
prop_vec[v] = prop_comps[v].to_int()
else:
push_error("Invalid Vector4i format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
properties[property] = prop_vec
TYPE_STRING_NAME:
properties[property] = StringName(prop_string)
TYPE_NODE_PATH:
properties[property] = prop_string
TYPE_OBJECT:
properties[property] = prop_string
# Retrieve default properties.
var def_properties: Dictionary[String, Variant] = prop_defaults_cache.get(def.classname, def.retrieve_all_class_properties())
var def_descriptions: Dictionary[String, Variant] = prop_descriptions_cache.get(def.classname, def.retrieve_all_class_property_descriptions())
# Assign properties not defined with defaults from the entity definition
for property in def_properties:
if not property in properties:
var prop_default: Variant = def_properties[property]
# Flags
if prop_default is Array:
var prop_flags_sum := 0
for prop_flag in prop_default:
if prop_flag is Array and prop_flag.size() > 2:
if prop_flag[2] and prop_flag[1] is int:
prop_flags_sum += prop_flag[1]
properties[property] = prop_flags_sum
# Choices
elif prop_default is Dictionary:
var prop_desc = def_descriptions.get(property, "")
if prop_desc is Array and prop_desc.size() > 1 and (prop_desc[1] is int or prop_desc[1] is String):
properties[property] = prop_desc[1]
elif prop_default.size():
properties[property] = prop_default[prop_default.keys().front()]
else:
properties[property] = 0
# Materials, Shaders, and Sounds
elif prop_default is Resource:
properties[property] = prop_default.resource_path
# Target Destination and Target Source
elif prop_default is NodePath or prop_default is Object or prop_default == null:
properties[property] = ""
# Everything else
else:
properties[property] = prop_default
# Delete omitted groups
declare_step.emit("Removing omitted layers and groups")
@ -203,7 +356,7 @@ func _parse_quake_map(map_data: PackedStringArray, map_settings: FuncGodotMapSet
for i in 3:
tokens[i] = tokens[i].trim_prefix("(")
var pts: PackedFloat64Array = tokens[i].split_floats(" ", false)
var point := Vector3(pts[0], pts[1], pts[2])
var point := Vector3(pts[0], pts[1], pts[2]) * map_settings.scale_factor
points[i] = point
var plane := Plane(points[0], points[1], points[2])
@ -244,8 +397,8 @@ func _parse_quake_map(map_data: PackedStringArray, map_settings: FuncGodotMapSet
coords = tokens[2].split_floats(" ", false)
# UV scale factor stored in basis
face.uv.x = Vector2(coords[1], 0.0)
face.uv.y = Vector2(0.0, coords[2])
face.uv.x = Vector2(coords[1], 0.0) * map_settings.scale_factor
face.uv.y = Vector2(0.0, coords[2]) * map_settings.scale_factor
# Quake Standard: texname offsetX offsetY rotation scaleX scaleY
else:
@ -253,8 +406,8 @@ func _parse_quake_map(map_data: PackedStringArray, map_settings: FuncGodotMapSet
face.uv.origin = Vector2(coords[0], coords[1])
var r: float = deg_to_rad(coords[2])
face.uv.x = Vector2(cos(r), -sin(r)) * coords[3]
face.uv.y = Vector2(sin(r), cos(r)) * coords[4]
face.uv.x = Vector2(cos(r), -sin(r)) * coords[3] * map_settings.scale_factor
face.uv.y = Vector2(sin(r), cos(r)) * coords[4] * map_settings.scale_factor
brush.faces.append(face)
continue
@ -374,7 +527,7 @@ func _parse_vmf(map_data: PackedStringArray, map_settings: FuncGodotMapSettings,
for i in 3:
tokens[i] = tokens[i].trim_prefix("(")
var pts: PackedFloat64Array = tokens[i].split_floats(" ", false)
var point: Vector3 = Vector3(pts[0], pts[1], pts[2])
var point: Vector3 = Vector3(pts[0], pts[1], pts[2]) * map_settings.scale_factor
points[i] = point
brush.planes.append(Plane(points[0], points[1], points[2]))
brush.faces.append(_FaceData.new())
@ -399,10 +552,10 @@ func _parse_vmf(map_data: PackedStringArray, map_settings: FuncGodotMapSettings,
face.uv_axes.append(Vector3(vals[0], vals[1], vals[2]))
if key.begins_with("u"):
face.uv.origin.x = vals[3] # Offset
face.uv.x *= vals[4] # Scale
face.uv.x *= vals[4] * map_settings.scale_factor # Scale
else:
face.uv.origin.y = vals[3] # Offset
face.uv.y *= vals[4] # Scale
face.uv.y *= vals[4] * map_settings.scale_factor # Scale
continue
"rotation":
# Rotation isn't used in Valve 220 mapping and VMFs are 220 exclusive

View file

@ -1,5 +1,5 @@
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
class_name FuncGodotFGDEntityClass extends Resource
@abstract class_name FuncGodotFGDEntityClass extends Resource
## Entity definition template. WARNING! Not to be used directly! Use [FuncGodotFGDBaseClass], [FuncGodotFGDSolidClass], or [FuncGodotFGDPointClass] instead.
##
## Entity definition template. It holds all of the common entity class properties shared between [FuncGodotFGDBaseClass], [FuncGodotFGDSolidClass], or [FuncGodotFGDPointClass].
@ -28,24 +28,24 @@ var prefix: String = ""
## Key value pair properties that will appear in the map editor. After building the [FuncGodotMap] in Godot, these properties will be added to a [Dictionary]
## that gets applied to the generated node, as long as that node is a tool script with an exported `func_godot_properties` Dictionary.
@export var class_properties : Dictionary = {}
@export var class_properties : Dictionary[String, Variant] = {}
## Map editor descriptions for previously defined key value pair properties. Optional but recommended.
@export var class_property_descriptions : Dictionary = {}
@export var class_property_descriptions : Dictionary[String, Variant] = {}
## Automatically applies entity class properties to matching properties in the generated node.
## When using this feature, class properties need to be the correct type or you may run into errors on map build.
@export var auto_apply_to_matching_node_properties : bool = false
## Appearance properties for the map editor. See the Valve Developer Wiki and TrenchBroom documentation for more information.
@export var meta_properties : Dictionary = {
@export var meta_properties : Dictionary[String, Variant] = {
"size": AABB(Vector3(-8, -8, -8), Vector3(8, 8, 8)),
"color": Color(0.8, 0.8, 0.8)
}
@export_group("Node Generation")
## Node to generate on map build. This can be a built-in Godot class, a GDScript class, or a GDExtension class.
## Node to generate on map build. This can be a built-in Godot class, a Script class, or a GDExtension class.
## For Point Class entities that use Scene File instantiation leave this blank.
@export var node_class := ""
@ -54,6 +54,9 @@ var prefix: String = ""
## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior.
@export var name_property := ""
## Optional array of node groups to add the generated node to.
@export var node_groups : Array[String] = []
## Parses the definition and outputs it into the FGD format.
func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
# Class prefix
@ -231,3 +234,17 @@ func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors =
res += "]" + FuncGodotUtil.newline()
return res
func retrieve_all_class_properties(properties: Dictionary[String, Variant] = {}) -> Dictionary[String, Variant]:
for key in class_properties.keys():
properties[key] = class_properties[key]
for b in base_classes:
properties = b.retrieve_all_class_properties(properties)
return properties
func retrieve_all_class_property_descriptions(descriptions: Dictionary[String, Variant] = {}) -> Dictionary[String, Variant]:
for key in class_property_descriptions.keys():
descriptions[key] = class_property_descriptions[key]
for b in base_classes:
descriptions = b.retrieve_all_class_property_descriptions(descriptions)
return descriptions

View file

@ -23,28 +23,25 @@ func export_button() -> void:
do_export_file(target_map_editor)
func do_export_file(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM, fgd_output_folder: String = "") -> void:
if not Engine.is_editor_hint():
return
if fgd_output_folder.is_empty():
fgd_output_folder = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.FGD_OUTPUT_FOLDER) as String
if fgd_output_folder.is_empty():
print("Skipping export: No game config folder")
printerr("Skipping export: No game config folder")
return
if fgd_name == "":
print("Skipping export: Empty FGD name")
printerr("Skipping export: Empty FGD name")
if not DirAccess.dir_exists_absolute(fgd_output_folder):
if DirAccess.make_dir_recursive_absolute(fgd_output_folder) != OK:
print("Skipping export: Failed to create directory")
printerr("Skipping export: Failed to create directory")
return
var fgd_file = fgd_output_folder.path_join(fgd_name + ".fgd")
var file_obj := FileAccess.open(fgd_file, FileAccess.WRITE)
if not file_obj:
print("Failed to open file for writing: ", fgd_file)
printerr("Failed to open file for writing: ", fgd_file)
return
print("Exporting FGD to ", fgd_file)
@ -76,6 +73,9 @@ func do_export_file(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMa
## Array of resources that inherit from [FuncGodotFGDEntityClass]. This array defines the entities that will be added to the exported FGD file and the nodes that will be generated in a [FuncGodotMap].
@export var entity_definitions: Array[Resource] = []
## Toggles whether [FuncGodotFGDModelPointClass] resources will generate models from their [PackedScene] files.
@export var generate_model_point_class_models: bool = true
func build_class_text(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
var res : String = ""
@ -91,6 +91,8 @@ func build_class_text(target_editor: FuncGodotTargetMapEditors = FuncGodotTarget
continue
if ent.func_godot_internal:
continue
if ent is FuncGodotFGDModelPointClass:
ent._model_generation_enabled = generate_model_point_class_models
var ent_text = ent.build_def_text(target_editor)
res += ent_text
@ -127,9 +129,9 @@ func get_entity_definitions() -> Dictionary[String, FuncGodotFGDEntityClass]:
if ent is FuncGodotFGDPointClass or ent is FuncGodotFGDSolidClass:
var entity_def = ent.duplicate()
var meta_properties := {}
var class_properties := {}
var class_property_descriptions := {}
var meta_properties: Dictionary[String, Variant] = {}
var class_properties: Dictionary[String, Variant] = {}
var class_property_descriptions: Dictionary[String, Variant] = {}
for base_class in _generate_base_class_list(entity_def):
for meta_property in base_class.meta_properties:

View file

@ -29,6 +29,8 @@ enum TargetMapEditor {
## Creates a .gdignore file in the model export folder to prevent Godot importing the display models. Only needs to be generated once.
@export_tool_button("Generate GD Ignore File", "FileAccess") var generate_gd_ignore_file : Callable = _generate_gd_ignore_file
var _model_generation_enabled: bool = false
func _generate_gd_ignore_file() -> void:
if Engine.is_editor_hint():
var path: String = _get_game_path().path_join(_get_model_folder())
@ -45,7 +47,9 @@ func _generate_gd_ignore_file() -> void:
## Builds and saves the display model into the specified destination, then parses the definition and outputs it into the FGD format.
func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
_generate_model()
if _model_generation_enabled:
_generate_model()
_model_generation_enabled = false
return super()
func _generate_model() -> void:
@ -65,7 +69,10 @@ func _generate_model() -> void:
if target_map_editor == TargetMapEditor.TRENCHBROOM:
const model_key: String = "model"
if scale_expression.is_empty():
meta_properties[model_key] = '"%s"' % _get_local_path()
meta_properties[model_key] = '{"path": "%s", "scale": %s }' % [
_get_local_path(),
ProjectSettings.get_setting("func_godot/default_inverse_scale_factor", 32.0) as float
]
else:
meta_properties[model_key] = '{"path": "%s", "scale": %s }' % [
_get_local_path(),

View file

@ -3,7 +3,7 @@
class_name FuncGodotFGDPointClass extends FuncGodotFGDEntityClass
## FGD PointClass entity definition.
##
## A resource used to define an FGD PointClass entity. PointClass entities can use either the [member FuncGodotFGDEntityClass.node_class]
## A resource used to define an FGD Point Class entity. PointClass entities can use either the [member FuncGodotFGDEntityClass.node_class]
## or the [member scene_file] property to tell [FuncGodotMap] what to generate on map build.
##
## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
@ -17,15 +17,12 @@ class_name FuncGodotFGDPointClass extends FuncGodotFGDEntityClass
func _init() -> void:
prefix = "@PointClass"
@export_group ("Scene")
## An optional [PackedScene] file to instantiate on map build. Overrides [member FuncGodotFGDEntityClass.node_class] and [member script_class].
@export var scene_file: PackedScene
@export_group ("Scripting")
## An optional [Script] resource to attach to the node generated on map build. Ignored if [member scene_file] is specified.
@export var script_class: Script
@export_group("Build")
## Toggles whether entity will use `angles`, `mangle`, or `angle` to determine rotations on [FuncGodotMap] build, prioritizing the key value pairs in that order.
## Set to [code]false[/code] if you would like to define how the generated node is rotated yourself.
@export var apply_rotation_on_map_build : bool = true
@ -33,3 +30,91 @@ func _init() -> void:
## Toggles whether entity will use `scale` to determine the generated node or scene's scale. This is performed on the top level node.
## The property can be a [float], [Vector3], or [Vector2]. Set to [code]false[/code] if you would like to define how the generated node is scaled yourself.
@export var apply_scale_on_map_build: bool = true
## An optional [Array] of [FuncGodotFGDPointClassDisplayDescriptor] that describes how this Point Entity should appear in the map editor.
## When using multiple display descriptors, only the first element found without [member FuncGodotFGDPointClassDisplayDescriptor.conditional]
## will be used as the default display asset. If no descriptor is found without a condition, the last descriptor will become the default.[br][br]
## Conditional display descriptors will be written to the FGD in the order set in the array.[br][br]
## [color=orange]WARNING:[/color] Multiple descriptors are only supported by TrenchBroom! They will be omitted on export when
## [member FuncGodotFGDFile.target_map_editor] is not set to [enum FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM].
@export var display_descriptors: Array[FuncGodotFGDPointClassDisplayDescriptor] = []
func _build_model_branch_text(descriptor: FuncGodotFGDPointClassDisplayDescriptor) -> String:
if not descriptor:
return ''
var model_string: String = ''
var uses_options: bool = false
if not descriptor.scale.is_empty() or not descriptor.skin.is_empty() or not descriptor.frame.is_empty():
uses_options = true
if not uses_options:
return descriptor.display_asset_path
model_string = '{ \"path\": %s' % descriptor.display_asset_path
if not descriptor.skin.is_empty():
model_string += ', \"skin\": %s' % descriptor.skin
if not descriptor.frame.is_empty():
model_string += ', \"frame\": %s' % descriptor.frame
if not descriptor.scale.is_empty():
model_string += ', \"scale\": %s' % descriptor.scale
model_string += " }"
return model_string
func _build_model_text() -> String:
var model_string: String = ''
if display_descriptors.is_empty():
return model_string
if display_descriptors.size() == 1:
return _build_model_branch_text(display_descriptors[0])
model_string = '{{'
var default_display: FuncGodotFGDPointClassDisplayDescriptor
for i in display_descriptors.size():
var d: FuncGodotFGDPointClassDisplayDescriptor = display_descriptors[i]
# Only set the first discovered descriptor without a condition to the default, which must be the last option in a list.
# If a conditional is not set, skip it.
if d.conditional.is_empty():
if not default_display:
default_display = d
else:
printerr(classname + " has a Point Class Display Descriptor without required conditionals set. Must have only 1 conditionless Display Descriptor!")
continue
model_string += '%s -> %s, ' % [d.conditional, _build_model_branch_text(d)]
if default_display:
model_string += '%s }}' % _build_model_branch_text(default_display)
else:
model_string = model_string.trim_suffix(', ')
model_string += ' }}'
return model_string
func _build_studio_text() -> String:
var display_string = ""
for d in display_descriptors:
if d.display_asset_path.find('\"') != -1:
display_string = d.display_asset_path
else:
printerr(classname + " attempting to set an invalid value to @studio format during FGD export. Only relative file paths encapsulated by quotations are valid.")
return display_string
func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
if not display_descriptors.is_empty():
if target_editor == FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM:
var display_string: String = _build_model_text()
if not display_string.is_empty():
meta_properties["model"] = display_string
else:
var display_string: String = _build_studio_text()
if not display_string.is_empty():
meta_properties["studio"] = display_string
return super(target_editor)

View file

@ -0,0 +1,48 @@
@tool
@icon("res://addons/func_godot/icons/icon_godambler3d.svg")
class_name FuncGodotFGDPointClassDisplayDescriptor extends Resource
## Resource that describes how to display an FGD Point Class entity.
##
## A resource for [FuncGodotFGDPointClass] that describes how to display a point entity in a map editor.
## Values entered into the different options are taken literally: paths should be enclosed within quotation marks,
## while class property keys and integer values should omit them.[br][br]
##
## Most editors only support the [member display_asset] option. Exporting an FGD compatible with these editors will
## automatically omit the unsupported options introduced by TrenchBroom when exporting from their respective game configuration resources
## or setting [member FuncGodotFGDFile.target_map_editor] away from [enum FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM].
##
## The extra options are considered advanced features and are unable to be evaluated by FuncGodot to ensure they were input correctly.
## Exercise caution, care, and patience when attempting to use these, especially the [member conditional] option.
##
## @tutorial(Level Design Book: Display Models for Entities): https://book.leveldesignbook.com/appendix/resources/formats/fgd#display-models-for-entities
## @tutorial(Valve Developer Wiki FGD Article: Entity Description Section): https://developer.valvesoftware.com/wiki/FGD#Entity_Description
## @tutorial(TrenchBroom Manual: Display Models for Entities): https://trenchbroom.github.io/manual/latest/#display-models-for-entities
## @tutorial(TrenchBroom Manual: Expression Language): https://trenchbroom.github.io/manual/latest/#expression_language
## Either a file path to the asset that will be displayed for this point entity, relative to the map editor's game path,
## or a class property key that can contain the path.[br][br]
## For paths, you must surround the path with quotes, e.g: [code]"models/marsfrog.glb"[/code].
## For properties, you must omit the quotes, e.g: [code]display_model_path[/code].[br][br]
## Different editors support different file types: common ones include MDL, GLB, SPR, and PNG.
@export var display_asset_path: String = ""
@export_group("TrenchBroom Options")
## Optional string that determines the scale of the display asset. This can be a number, a class property key, or
## a scale expression in accordance with TrenchBroom's Expression Language. Leave blank to use the game configuration's default scale expression.[br][br]
## [color=orange]WARNING:[/color] Only utilized by TrenchBroom!
@export var scale: String = ""
## Optional string that determines which skin the display asset should use. This can be either a number or a class property key.[br][br]
## [color=orange]WARNING:[/color] Only utilized by TrenchBroom!
@export var skin: String = ""
## Optional string that determines the appearance of a display asset based on its file type. This can be either a number or a class property key.[br][br]
## Traditional Quake MDL files will set the display to that frame of its animations (all animations in a Quake MDL are compiled into a single animation).
## GLBs meanwhile seem to set themselves to the animation assigned to an index that matches the [code]frame[/code] value.[br][br]
## [color=orange]WARNING:[/color] Only utilized by TrenchBroom!
@export var frame: String = ""
## Optional evaluation string that, when true, will force the Point Class to display the asset defined by [member display_asset_path].
## Format should be [code]property == value[/code] or some other valid expression in accordance with TrenchBroom's Expression Language.[br][br]
## [color=orange]WARNING:[/color] Only utilized by TrenchBroom!
@export var conditional: String = ""

View file

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

View file

@ -38,6 +38,6 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
map_resource.revision += 1
else:
map_resource = QuakeMapFile.new()
map_resource.map_data = FileAccess.open(source_file, FileAccess.READ).get_as_text(true)
map_resource.map_data = FileAccess.open(source_file, FileAccess.READ).get_as_text()
return ResourceSaver.save(map_resource, save_path_str)

View file

@ -34,7 +34,7 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
var file = FileAccess.open(source_file, FileAccess.READ)
if file == null:
var err = FileAccess.get_open_error()
print(['Error opening super.lmp file: ', err])
printerr(['Error opening super.lmp file: ', err])
return err
var colors := PackedColorArray()

View file

@ -10,5 +10,5 @@ class_name QuakeWadFile extends Resource
## Collection of [ImageTexture] imported from the WAD file.
@export var textures: Dictionary[String, ImageTexture]
func _init(textures: Dictionary = Dictionary()):
func _init(textures: Dictionary[String, ImageTexture] = {}):
self.textures = textures

View file

@ -70,7 +70,7 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
var file = FileAccess.open(source_file, FileAccess.READ)
if file == null:
var err = FileAccess.get_open_error()
print(['Error opening super.wad file: ', err])
printerr(['Error opening super.wad file: ', err])
return err
# Read WAD header
@ -81,13 +81,13 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
if magic_string == 'WAD3':
wad_format = WadFormat.HalfLife
elif magic_string != 'WAD2':
print('Error: Invalid WAD magic')
printerr('Error: Invalid WAD magic')
return ERR_INVALID_DATA
var palette_path : String = options['palette_file']
var palette_file : QuakePaletteFile = load(palette_path) as QuakePaletteFile
if wad_format == WadFormat.Quake and not palette_file:
print('Error: Invalid Quake palette file')
printerr('Error: Invalid Quake palette file')
file.close()
return ERR_CANT_ACQUIRE_RESOURCE

View file

@ -43,6 +43,12 @@ var _map_file_internal: String = ""
## [enum BuildFlags] that can affect certain aspects of the build process.
@export_flags("Unwrap UV2:1", "Show Profiling Info:2", "Disable Smooth Shading:4") var build_flags: int = 0
## The hyperplane is an initial plane that all geometry faces are cut from, like a large sheet of marble before a sculptor begins chiseling.
## The hyperplane size would need to be able to cover your map's potential total area.
## Smaller values can minimize floating point errors, reducing the effect of gaps between polygon seams.
## Measured in Godot units, not Quake units.
@export_range(256.0, 2048.0, 128.0) var hyperplane_size: float = 512.0
## Map build failure handler. Displays error message and emits [signal build_failed] signal.
func fail_build(reason: String, notify: bool = false) -> void:
push_error(_SIGNATURE, " ", reason)
@ -117,7 +123,7 @@ func build() -> void:
parser = null
# Retrieve geometry
var generator := FuncGodotGeometryGenerator.new(map_settings)
var generator := FuncGodotGeometryGenerator.new(map_settings, hyperplane_size)
if build_flags & BuildFlags.SHOW_PROFILE_INFO:
print("\nGEOMETRY GENERATOR")
generator.declare_step.connect(FuncGodotUtil.print_profile_info.bind(generator._SIGNATURE))

View file

@ -4,7 +4,8 @@ class_name FuncGodotMapSettings extends Resource
## Reusable map settings configuration for [FuncGodotMap] nodes.
#region BUILD
@export_category("Build Settings")
@export_group("Build Settings")
## Set automatically when [member inverse_scale_factor] is changed. Used primarily during the build process.
var scale_factor: float = 0.03125
@ -21,30 +22,51 @@ var scale_factor: float = 0.03125
## [FuncGodotFGDFile] that translates map file classnames into Godot nodes and packed scenes.
@export var entity_fgd: FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
## Default class property to use in naming generated nodes. This setting is overridden by [member FuncGodotFGDEntityClass.name_property].
## Naming occurs before adding to the [SceneTree] and applying properties.
## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior.
@export var entity_name_property: String = ""
## Class property that determines whether the [FuncGodotFGDSolidClass] entity performs mesh smoothing operations.
@export var entity_smoothing_property: String = "_phong"
## Class property that contains the angular threshold that determines when a [FuncGodotFGDSolidClass] entity's mesh vertices are smoothed.
@export var entity_smoothing_angle_property: String = "_phong_angle"
## If true, will organize [SceneTree] using TrenchBroom Layers and Groups or Hammer Visgroups. Groups will be generated as [Node3D] nodes.
## All non-entity structural brushes will be moved out of their groups and merged into the `Worldspawn` entity.
## Any Layers toggled to be omitted from export in TrenchBroom and their child entities and groups will not be built.
@export var use_groups_hierarchy: bool = false
## Class property that contains the snapping epsilon for generated vertices of [FuncGodotFGDSolidClass] entities.
## Utilizing this property can help reduce instances of seams between polygons.
@export var vertex_merge_distance_property: String = "_vertex_merge_distance"
## Texel size for UV2 unwrapping.
## Actual texel size is uv_unwrap_texel_size / [member inverse_scale_factor]. A ratio of 1/16 is usually a good place to start with
## (if inverse_scale_factor is 32, start with a uv_unwrap_texel_size of 2).
## Larger values will produce less detailed lightmaps. To conserve memory and filesize, use the largest value that still looks good.
@export var uv_unwrap_texel_size: float = 2.0
#endregion
#region ENTITY
@export_group("Entity Settings")
## Optional array of node groups to add all generated nodes to.
@export var entity_node_groups: Array[String] = []
@export_subgroup("Entity Property Names")
## Default class property to use in naming generated nodes. This setting is overridden by [member FuncGodotFGDEntityClass.name_property].
## Naming occurs before adding to the [SceneTree] and applying properties.
## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior.
@export var entity_name_property: String = ""
## Entity class property that determines whether the [FuncGodotFGDSolidClass] entity performs mesh smoothing operations.
@export var entity_smoothing_property: String = "_phong"
## Entity class property that contains the angular threshold that determines when a [FuncGodotFGDSolidClass] entity's mesh vertices are smoothed.
@export var entity_smoothing_angle_property: String = "_phong_angle"
## Entity class property that contains the snapping epsilon for generated vertices of [FuncGodotFGDSolidClass] entities.
## Utilizing this property can help reduce instances of seams between polygons.
@export var vertex_merge_distance_property: String = "_vertex_merge_distance"
## Entity class property that tells whether interior faces should be culled for that brush entity.
## Interior faces are faces with matching vertices or are flush within a larger face.
## Note that this has a performance impact that scales with how many brushes are in the entity.
@export var cull_interior_faces_property: String = "_cull_interior_faces"
@export_subgroup("")
#endregion
#region TEXTURES
@export_category("Textures")
@export_group("Textures")
## Base directory for textures. When building materials, FuncGodot will search this directory for texture files with matching names to the textures assigned to map brush faces.
@export_dir var base_texture_dir: String = "res://textures"
@ -52,25 +74,27 @@ var scale_factor: float = 0.03125
## File extensions to search for texture data.
@export var texture_file_extensions: Array[String] = ["png", "jpg", "jpeg", "bmp", "tga", "webp"]
@export_subgroup("Hint Textures")
## Optional path for the clip texture, relative to [member base_texture_dir].
## Brush faces textured with the clip texture will have those faces removed from the generated [Mesh] but not the generated [Shape3D].
@export var clip_texture: String = "special/clip":
@export var clip_texture: String = "clip":
set(tex):
clip_texture = tex.to_lower()
## Optional path for the skip texture, relative to [member base_texture_dir].
## Brush faces textured with the skip texture will have those faces removed from the generated [Mesh].
## If [member FuncGodotFGDSolidClass.collision_shape_type] is set to concave then it will also remove collision from those faces in the generated [Shape3D].
@export var skip_texture: String = "special/skip":
@export var skip_texture: String = "skip":
set(tex):
skip_texture = tex.to_lower()
## Optional path for the origin texture, relative to [member base_texture_dir].
## Brush faces textured with the origin texture will have those faces removed from the generated [Mesh] and [Shape3D].
## The bounds of these faces will be used to calculate the origin point of the entity.
@export var origin_texture: String = "special/origin":
@export var origin_texture: String = "origin":
set(tex):
origin_texture = tex.to_lower()
@export_subgroup("")
## Optional [QuakeWadFile] resources to apply textures from. See the [Quake Wiki](https://quakewiki.org/wiki/Texture_Wad) for more information on Quake Texture WADs.
@export var texture_wads: Array[QuakeWadFile] = []
@ -78,7 +102,7 @@ var scale_factor: float = 0.03125
#endregion
#region MATERIALS
@export_category("Materials")
@export_group("Materials")
## Base directory for loading and saving materials. When building materials, FuncGodot will search this directory for material resources
## with matching names to the textures assigned to map brush faces. If not found, will fall back to [member base_texture_dir].
@ -93,33 +117,33 @@ var scale_factor: float = 0.03125
## Sampler2D uniform that supplies the Albedo in a custom shader when [member default_material] is a [ShaderMaterial].
@export var default_material_albedo_uniform: String = ""
## Automatic [ShaderMaterial] generation mapping patterns. Only used when [member default_material] is a ShaderMaterial.
## Keys should be the names of the shader uniforms while the values should be the suffixes for the texture maps.
## Patterns only use one replacement String: the texture name, ex: [code]"%s_normal"[/code].
@export var shader_material_uniform_map_patterns: Dictionary[String, String] = {}
@export_subgroup("BaseMaterial3D Map Patterns")
## Automatic PBR material generation albedo map pattern.
@export var albedo_map_pattern: String = "%s_albedo.%s"
@export var albedo_map_pattern: String = "%s_albedo"
## Automatic PBR material generation normal map pattern.
@export var normal_map_pattern: String = "%s_normal.%s"
@export var normal_map_pattern: String = "%s_normal"
## Automatic PBR material generation metallic map pattern
@export var metallic_map_pattern: String = "%s_metallic.%s"
@export var metallic_map_pattern: String = "%s_metallic"
## Automatic PBR material generation roughness map pattern
@export var roughness_map_pattern: String = "%s_roughness.%s"
@export var roughness_map_pattern: String = "%s_roughness"
## Automatic PBR material generation emission map pattern
@export var emission_map_pattern: String = "%s_emission.%s"
@export var emission_map_pattern: String = "%s_emission"
## Automatic PBR material generation ambient occlusion map pattern
@export var ao_map_pattern: String = "%s_ao.%s"
@export var ao_map_pattern: String = "%s_ao"
## Automatic PBR material generation height map pattern
@export var height_map_pattern: String = "%s_height.%s"
@export var height_map_pattern: String = "%s_height"
## Automatic PBR material generation ORM map pattern
@export var orm_map_pattern: String = "%s_orm.%s"
@export var orm_map_pattern: String = "%s_orm"
@export_subgroup("")
## Save automatically generated materials to disk, allowing reuse across [FuncGodotMap] nodes.
## [i]NOTE: Materials do not use the [member default_material] settings after saving.[/i]
@export var save_generated_materials: bool = true
@export_group("")
#endregion
@export_category("UV Unwrap")
## Texel size for UV2 unwrapping.
## Actual texel size is uv_unwrap_texel_size / [member inverse_scale_factor]. A ratio of 1/16 is usually a good place to start with
## (if inverse_scale_factor is 32, start with a uv_unwrap_texel_size of 2).
## Larger values will produce less detailed lightmaps. To conserve memory and filesize, use the largest value that still looks good.
@export var uv_unwrap_texel_size: float = 2.0

View file

@ -27,6 +27,9 @@ enum NetRadiantCustomMapType {
## this should be the master FGD that contains them in [member FuncGodotFGDFile.base_fgd_files].
@export var fgd_file : FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
## Toggles whether [FuncGodotFGDModelPointClass] resources will generate models from their [PackedScene] files.
@export var generate_model_point_class_models: bool = true
## Collection of [NetRadiantCustomShader] resources for shader file generation.
@export var netradiant_custom_shaders : Array[Resource] = [
preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres"),
@ -34,28 +37,30 @@ enum NetRadiantCustomMapType {
preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_origin.tres")
]
## Supported texture file types.
@export var texture_types : PackedStringArray = ["png", "jpg", "jpeg", "bmp", "tga"]
## Supported model file types.
@export var model_types : PackedStringArray = ["glb", "gltf", "obj"]
## Supported audio file types.
@export var sound_types : PackedStringArray = ["wav", "ogg"]
## Quake map type NetRadiant will filter the map for, determining whether PatchDef entries are saved.
## [color=red][b]WARNING![/b][/color] Toggling this option may be destructive!
@export var map_type: NetRadiantCustomMapType = NetRadiantCustomMapType.QUAKE_3
@export_group("Textures")
## Supported texture file types.
@export var texture_types : PackedStringArray = ["png", "jpg", "jpeg", "bmp", "tga"]
## Default scale of textures in NetRadiant Custom.
@export var default_scale : String = "1.0"
## Clip texture path that gets applied to [i]weapclip[/i] and [i]nodraw[/i] shaders.
@export var clip_texture: String = "textures/special/clip"
@export var clip_texture: String = "textures/clip"
## Skip texture path that gets applied to [i]caulk[/i] and [i]nodrawnonsolid[/i] shaders.
@export var skip_texture: String = "textures/special/skip"
## Quake map type NetRadiant will filter the map for, determining whether PatchDef entries are saved.
## [color=red][b]WARNING![/b][/color] Toggling this option may be destructive!
@export var map_type: NetRadiantCustomMapType = NetRadiantCustomMapType.QUAKE_1
@export var skip_texture: String = "textures/skip"
@export_group("Build Menu")
## Variables to include in the exported gamepack's [code]default_build_menu.xml[/code].[br][br]
## Each [String] key defines a variable name, and its corresponding [String] value as the literal command-line string
## to execute in place of this variable identifier[br][br]
@ -312,5 +317,6 @@ func export_file() -> void:
# FGD
var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
export_fgd.generate_model_point_class_models = generate_model_point_class_models
export_fgd.do_export_file(FuncGodotFGDFile.FuncGodotTargetMapEditors.NET_RADIANT_CUSTOM, gamepacks_folder + "/" + gamepack_name + ".game/" + base_game_path)
print("NetRadiant Custom Gamepack export complete\n")

View file

@ -31,7 +31,7 @@ enum GameConfigVersion {
{ "format": "Quake3" }
]
@export_category("Textures")
@export_group("Textures")
## Path to top level textures folder relative to the game path. Also referred to as materials in the latest versions of TrenchBroom.
@export var textures_root_folder: String = "textures"
@ -42,7 +42,7 @@ enum GameConfigVersion {
## Palette path relative to your Game Path. Only needed for Quake WAD2 files. Half-Life WAD3 files contain the palettes within the texture information.
@export var palette_path: String = "textures/palette.lmp"
@export_category("Entities")
@export_group("Entities")
## [FuncGodotFGDFile] resource to include with this game. If using multiple FGD File resources,
## this should be the master FGD File that contains them in [member FuncGodotFGDFile.base_fgd_files].
@ -52,8 +52,11 @@ enum GameConfigVersion {
## See [url="https://trenchbroom.github.io/manual/latest/#game_configuration_files_entities"]TrenchBroom Manual Entity Configuration Information[/url] for more information.
@export var entity_scale: String = "32"
## Toggles whether [FuncGodotFGDModelPointClass] resources will generate models from their [PackedScene] files.
@export var generate_model_point_class_models: bool = true
## Arrays containing the [TrenchbroomTag] resource type.
@export_category("Tags")
@export_group("Tags")
## [TrenchbroomTag] resources that apply to brush entities.
@export var brush_tags : Array[Resource] = []
@ -65,12 +68,12 @@ enum GameConfigVersion {
preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_origin.tres")
]
@export_category("Face Attributes")
@export_group("Face Attributes")
## Default scale of textures on new brushes and when UV scale is reset.
@export var default_uv_scale : Vector2 = Vector2(1, 1)
@export_category("Compatibility")
@export_group("Compatibility")
## Game configuration format compatible with the version of TrenchBroom being used.
@export var game_config_version: GameConfigVersion = GameConfigVersion.Latest
@ -229,6 +232,7 @@ func export_file() -> void:
# FGD
var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
export_fgd.generate_model_point_class_models = generate_model_point_class_models
export_fgd.do_export_file(FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM, config_folder)
print("TrenchBroom Game Config export complete\n")

View file

@ -55,18 +55,6 @@ 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
#}
]
var _settings_dict: Dictionary
@ -117,9 +105,13 @@ func _get_config_property(name: StringName) -> Variant:
## Reload this system's configuration settings into the Local Config resource.
func reload_func_godot_settings() -> void:
_loaded = true
var path = _get_path()
var path = "user://func_godot_config.json"
if not FileAccess.file_exists(path):
return
var application_name: String = ProjectSettings.get('application/config/name')
application_name = application_name.replace(" ", "_")
path = "user://" + application_name + "_FuncGodotConfig.json"
if not FileAccess.file_exists(path):
return
var settings = FileAccess.get_file_as_string(path)
_settings_dict = {}
if not settings or settings.is_empty():
@ -137,14 +129,9 @@ func _try_loading() -> void:
func export_func_godot_settings() -> void:
if _settings_dict.size() == 0:
return
var path = _get_path()
var path = "user://func_godot_config.json"
var file = FileAccess.open(path, FileAccess.WRITE)
var json = JSON.stringify(_settings_dict)
file.store_line(json)
_loaded = false
print("Saved settings to ", path)
func _get_path() -> String:
var application_name: String = ProjectSettings.get('application/config/name')
application_name = application_name.replace(" ", "_")
return 'user://' + application_name + '_FuncGodotConfig.json'
print("Saved settings to ", file.get_path_absolute())

View file

@ -1,188 +0,0 @@
class_name FuncGodotTextureLoader
enum PBRSuffix {
ALBEDO,
NORMAL,
METALLIC,
ROUGHNESS,
EMISSION,
AO,
HEIGHT,
ORM
}
# 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',
PBRSuffix.ORM: 'orm'
}
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',
PBRSuffix.ORM: '%s_orm.%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,
PBRSuffix.ORM: ORMMaterial3D.TEXTURE_ORM
}
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") or ResourceLoader.exists(texture_path + ".import", "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) or FileAccess.file_exists(material_path + ".remap")):
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 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)
elif material is ORMMaterial3D:
material.set_texture(ORMMaterial3D.TEXTURE_ALBEDO, 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_name != map_settings.origin_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://c0r8ajf4k061i

View file

@ -80,16 +80,33 @@ const _pbr_textures: PackedInt32Array = [
StandardMaterial3D.TEXTURE_EMISSION,
StandardMaterial3D.TEXTURE_AMBIENT_OCCLUSION,
StandardMaterial3D.TEXTURE_HEIGHTMAP,
ORMMaterial3D.TEXTURE_ORM
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):
return load(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:
@ -131,8 +148,8 @@ static func filter_face(texture: String, map_settings: FuncGodotMapSettings) ->
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)
if DirAccess.open(path):
path = path.path_join(texture)
var pbr_suffixes: PackedStringArray = [
map_settings.albedo_map_pattern,
@ -145,12 +162,33 @@ static func build_base_material(map_settings: FuncGodotMapSettings, material: Ba
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))
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,
@ -202,7 +240,7 @@ static func build_texture_map(entity_data: Array[FuncGodotData.EntityData], map_
# Material generation
elif map_settings.default_material:
var material = map_settings.default_material.duplicate(true)
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()
@ -211,12 +249,29 @@ static func build_texture_map(entity_data: Array[FuncGodotData.EntityData], map_
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
@ -232,30 +287,30 @@ static func build_texture_map(entity_data: Array[FuncGodotData.EntityData], map_
## 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
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_basis := Transform2D.IDENTITY, texture_size := Vector2.ONE) -> Vector2:
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))
var uv: Vector2
if ny >= nx and ny >= nz:
uv = Vector2(vertex.x, -vertex.z)
uv_out = Vector2(vertex.x, -vertex.z)
elif nx >= ny and nx >= nz:
uv = Vector2(vertex.y, -vertex.z)
uv_out = Vector2(vertex.y, -vertex.z)
else:
uv = Vector2(vertex.x, vertex.y)
uv_out = 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 = 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
@ -322,10 +377,10 @@ static func get_face_tangent(face: FuncGodotData.FaceData) -> PackedFloat32Array
#region MESH
static func smooth_mesh_by_angle(mesh: ArrayMesh, angle_deg: float = 89.0) -> 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
return null
var angle: float = deg_to_rad(clampf(angle_deg, 0.0, 360.0))

View file

@ -3,21 +3,23 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://dhmu0toe1itnr"
path="res://.godot/imported/clip.png-508a86fa3876d8467d5c9af6188a34df.ctex"
path="res://.godot/imported/clip.png-747f19bfc6fe499e254d63eeccc3bbe4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/func_godot/textures/special/clip.png"
dest_files=["res://.godot/imported/clip.png-508a86fa3876d8467d5c9af6188a34df.ctex"]
source_file="res://addons/func_godot/textures/clip.png"
dest_files=["res://.godot/imported/clip.png-747f19bfc6fe499e254d63eeccc3bbe4.ctex"]
[params]
compress/mode=3
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
@ -25,6 +27,10 @@ mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false

View file

@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/default_texture.png-145fbd5fef7f63ace60797fec
compress/mode=3
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
@ -25,6 +27,10 @@ mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false

View file

@ -3,21 +3,23 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://dutip72dl002r"
path="res://.godot/imported/origin.png-85b62dd151467f05fa8f98ed6d2927d0.ctex"
path="res://.godot/imported/origin.png-501c9242087fbbeb03d876a682f0832c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/func_godot/textures/special/origin.png"
dest_files=["res://.godot/imported/origin.png-85b62dd151467f05fa8f98ed6d2927d0.ctex"]
source_file="res://addons/func_godot/textures/origin.png"
dest_files=["res://.godot/imported/origin.png-501c9242087fbbeb03d876a682f0832c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
@ -25,6 +27,10 @@ mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false

View file

@ -3,21 +3,23 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://bk5oo263y3u7w"
path="res://.godot/imported/skip.png-d741e3eb75a5e289907774cb73d93931.ctex"
path="res://.godot/imported/skip.png-b8ce29eeb9f4be76c300a81fd41322fa.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/func_godot/textures/special/skip.png"
dest_files=["res://.godot/imported/skip.png-d741e3eb75a5e289907774cb73d93931.ctex"]
source_file="res://addons/func_godot/textures/skip.png"
dest_files=["res://.godot/imported/skip.png-b8ce29eeb9f4be76c300a81fd41322fa.ctex"]
[params]
compress/mode=3
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
@ -25,6 +27,10 @@ mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false