Updated to Godot 4.5 and func_godot 2025.9. Added grid_white and grid_taupe textures. Unfucked func_godot and navmesh settings. Began blocking out motel layout in map.
This commit is contained in:
@@ -1,28 +1,37 @@
|
|||||||
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 format=3 uid="uid://cxy7jnh6d7msn"]
|
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=4 format=3 uid="uid://cxy7jnh6d7msn"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://cmoudtn1s5egw" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="1_0fsmp"]
|
[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://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"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_0fsmp")
|
script = ExtResource("1_0fsmp")
|
||||||
spawn_type = 2
|
spawn_type = 2
|
||||||
origin_type = 0
|
origin_type = 3
|
||||||
build_visuals = true
|
build_visuals = true
|
||||||
|
global_illumination_mode = 1
|
||||||
use_in_baked_light = true
|
use_in_baked_light = true
|
||||||
shadow_casting_setting = 0
|
shadow_casting_setting = 1
|
||||||
build_occlusion = false
|
build_occlusion = false
|
||||||
render_layers = 1
|
render_layers = 1
|
||||||
collision_shape_type = 2
|
collision_shape_type = 2
|
||||||
collision_layer = 1
|
collision_layer = 1
|
||||||
collision_mask = 1
|
collision_mask = 0
|
||||||
collision_priority = 1.0
|
collision_priority = 1.0
|
||||||
collision_shape_margin = 0.04
|
collision_shape_margin = 0.04
|
||||||
classname = "func_transparent"
|
add_textures_metadata = false
|
||||||
description = "Static collidable geometry. Builds a StaticBody3D with a MeshInstance3D and a single convex CollisionShape3D. Does not occlude other VisualInstance3D nodes."
|
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
|
func_godot_internal = false
|
||||||
base_classes = Array[Resource]([ExtResource("1_c3bns")])
|
base_classes = Array[Resource]([ExtResource("1_c3bns"), ExtResource("2_c03gr")])
|
||||||
class_properties = {}
|
class_properties = {}
|
||||||
class_property_descriptions = {}
|
class_property_descriptions = {}
|
||||||
|
auto_apply_to_matching_node_properties = false
|
||||||
meta_properties = {
|
meta_properties = {
|
||||||
"color": Color(0.8, 0.8, 0.8, 1)
|
"color": Color(0.8, 0.8, 0.8, 1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 format=3 uid="uid://ch3e0dix85uhb"]
|
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=4 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://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_ar63x"]
|
||||||
[ext_resource type="Script" uid="uid://cmoudtn1s5egw" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_lhb87"]
|
[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"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("2_lhb87")
|
script = ExtResource("2_lhb87")
|
||||||
spawn_type = 2
|
spawn_type = 2
|
||||||
origin_type = 0
|
origin_type = 3
|
||||||
build_visuals = true
|
build_visuals = true
|
||||||
|
global_illumination_mode = 1
|
||||||
use_in_baked_light = true
|
use_in_baked_light = true
|
||||||
shadow_casting_setting = 1
|
shadow_casting_setting = 1
|
||||||
build_occlusion = false
|
build_occlusion = false
|
||||||
@@ -17,12 +19,19 @@ collision_layer = 1
|
|||||||
collision_mask = 1
|
collision_mask = 1
|
||||||
collision_priority = 1.0
|
collision_priority = 1.0
|
||||||
collision_shape_margin = 0.04
|
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"
|
classname = "func_detail_illusionary"
|
||||||
description = "Static geometry with no collision. Builds a Node3D with a MeshInstance3D. Does not occlude other VisualInstance3D nodes."
|
description = "Static geometry with no collision. Builds a Node3D with a MeshInstance3D. Does not occlude other VisualInstance3D nodes."
|
||||||
func_godot_internal = false
|
func_godot_internal = false
|
||||||
base_classes = Array[Resource]([ExtResource("1_ar63x")])
|
base_classes = Array[Resource]([ExtResource("1_ar63x"), ExtResource("2_j7vgq")])
|
||||||
class_properties = {}
|
class_properties = {}
|
||||||
class_property_descriptions = {}
|
class_property_descriptions = {}
|
||||||
|
auto_apply_to_matching_node_properties = false
|
||||||
meta_properties = {
|
meta_properties = {
|
||||||
"color": Color(0.8, 0.8, 0.8, 1)
|
"color": Color(0.8, 0.8, 0.8, 1)
|
||||||
}
|
}
|
||||||
|
|||||||
39
addons/func_godot/fgd/func_geo.tres
Normal file
39
addons/func_godot/fgd/func_geo.tres
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=4 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"]
|
||||||
|
|
||||||
|
[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 = {
|
||||||
|
"color": Color(0.8, 0.8, 0.8, 1)
|
||||||
|
}
|
||||||
|
node_class = "StaticBody3D"
|
||||||
|
name_property = ""
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
[gd_resource type="Resource" script_class="FuncGodotFGDFile" load_steps=7 format=3 uid="uid://crgpdahjaj"]
|
[gd_resource type="Resource" script_class="FuncGodotFGDFile" load_steps=9 format=3 uid="uid://crgpdahjaj"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://ckisq2tcxg062" path="res://addons/func_godot/src/fgd/func_godot_fgd_file.gd" id="1_axt3h"]
|
[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"]
|
[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_ehab8"]
|
||||||
|
[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="2_7jebp"]
|
||||||
[ext_resource type="Resource" uid="uid://bdji3873bg32h" path="res://addons/func_godot/fgd/worldspawn.tres" id="2_ri2rx"]
|
[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://cxy7jnh6d7msn" path="res://addons/func_godot/fgd/func_detail.tres" id="3_fqfww"]
|
||||||
[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://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"]
|
[ext_resource type="Resource" uid="uid://ch3e0dix85uhb" path="res://addons/func_godot/fgd/func_detail_illusionary.tres" id="5_b2q3p"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_axt3h")
|
script = ExtResource("1_axt3h")
|
||||||
export_file = false
|
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")])
|
||||||
model_key_word_supported = true
|
|
||||||
fgd_name = "FuncGodot"
|
|
||||||
base_fgd_files = Array[Resource]([])
|
|
||||||
entity_definitions = Array[Resource]([ExtResource("1_ehab8"), ExtResource("2_ri2rx"), ExtResource("3_fqfww"), ExtResource("5_b2q3p"), ExtResource("4_c4ucw"), ExtResource("3_fqfww")])
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 format=3 uid="uid://dg5x44cc7flew"]
|
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=4 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://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_kv0mq"]
|
||||||
[ext_resource type="Script" uid="uid://cmoudtn1s5egw" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_uffhi"]
|
[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"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("2_uffhi")
|
script = ExtResource("2_uffhi")
|
||||||
spawn_type = 2
|
spawn_type = 2
|
||||||
origin_type = 0
|
origin_type = 3
|
||||||
build_visuals = true
|
build_visuals = true
|
||||||
|
global_illumination_mode = 1
|
||||||
use_in_baked_light = true
|
use_in_baked_light = true
|
||||||
shadow_casting_setting = 1
|
shadow_casting_setting = 1
|
||||||
build_occlusion = true
|
build_occlusion = true
|
||||||
@@ -17,12 +19,19 @@ collision_layer = 1
|
|||||||
collision_mask = 1
|
collision_mask = 1
|
||||||
collision_priority = 1.0
|
collision_priority = 1.0
|
||||||
collision_shape_margin = 0.04
|
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"
|
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."
|
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
|
func_godot_internal = false
|
||||||
base_classes = Array[Resource]([ExtResource("1_kv0mq")])
|
base_classes = Array[Resource]([ExtResource("1_kv0mq"), ExtResource("2_hovr4")])
|
||||||
class_properties = {}
|
class_properties = {}
|
||||||
class_property_descriptions = {}
|
class_property_descriptions = {}
|
||||||
|
auto_apply_to_matching_node_properties = false
|
||||||
meta_properties = {
|
meta_properties = {
|
||||||
"color": Color(0.8, 0.8, 0.8, 1)
|
"color": Color(0.8, 0.8, 0.8, 1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[gd_resource type="Resource" script_class="FuncGodotFGDBaseClass" load_steps=2 format=3 uid="uid://nayxb8n7see2"]
|
[gd_resource type="Resource" script_class="FuncGodotFGDBaseClass" load_steps=2 format=3 uid="uid://nayxb8n7see2"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://dyv4g30betqgm" path="res://addons/func_godot/src/fgd/func_godot_fgd_base_class.gd" id="1_04y3n"]
|
[ext_resource type="Script" uid="uid://ck575aqs1sbrb" path="res://addons/func_godot/src/fgd/func_godot_fgd_base_class.gd" id="1_04y3n"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_04y3n")
|
script = ExtResource("1_04y3n")
|
||||||
@@ -19,6 +19,7 @@ class_property_descriptions = {
|
|||||||
"_phong": ["Phong shading", 0],
|
"_phong": ["Phong shading", 0],
|
||||||
"_phong_angle": "Phong smoothing angle"
|
"_phong_angle": "Phong smoothing angle"
|
||||||
}
|
}
|
||||||
|
auto_apply_to_matching_node_properties = false
|
||||||
meta_properties = {
|
meta_properties = {
|
||||||
"color": Color(0.8, 0.8, 0.8, 1),
|
"color": Color(0.8, 0.8, 0.8, 1),
|
||||||
"size": AABB(-8, -8, -8, 8, 8, 8)
|
"size": AABB(-8, -8, -8, 8, 8, 8)
|
||||||
|
|||||||
24
addons/func_godot/fgd/vertex_merge_distance_base.tres
Normal file
24
addons/func_godot/fgd/vertex_merge_distance_base.tres
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[gd_resource type="Resource" script_class="FuncGodotFGDBaseClass" load_steps=2 format=3 uid="uid://doo4ly322b4jc"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://ck575aqs1sbrb" path="res://addons/func_godot/src/fgd/func_godot_fgd_base_class.gd" id="1_h3atm"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
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 = {
|
||||||
|
"_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"
|
||||||
@@ -1,29 +1,17 @@
|
|||||||
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=2 format=3 uid="uid://bdji3873bg32h"]
|
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 format=3 uid="uid://bdji3873bg32h"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://cmoudtn1s5egw" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="1_62t8m"]
|
[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"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_62t8m")
|
script = ExtResource("1_62t8m")
|
||||||
spawn_type = 0
|
spawn_type = 0
|
||||||
origin_type = 0
|
origin_type = 1
|
||||||
build_visuals = true
|
|
||||||
use_in_baked_light = true
|
|
||||||
shadow_casting_setting = 1
|
|
||||||
build_occlusion = true
|
|
||||||
render_layers = 1
|
|
||||||
collision_shape_type = 1
|
|
||||||
collision_layer = 3
|
collision_layer = 3
|
||||||
collision_mask = 1
|
|
||||||
collision_priority = 1.0
|
|
||||||
collision_shape_margin = 0.04
|
|
||||||
classname = "worldspawn"
|
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."
|
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")])
|
||||||
base_classes = Array[Resource]([])
|
|
||||||
class_properties = {}
|
|
||||||
class_property_descriptions = {}
|
|
||||||
meta_properties = {
|
meta_properties = {
|
||||||
"color": Color(0.8, 0.8, 0.8, 1)
|
"color": Color(0.8, 0.8, 0.8, 1)
|
||||||
}
|
}
|
||||||
node_class = "StaticBody3D"
|
node_class = "StaticBody3D"
|
||||||
name_property = ""
|
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
[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="Material" uid="uid://cvex6toty8yn7" path="res://addons/func_godot/textures/default_material.tres" id="1_8l5wm"]
|
||||||
[ext_resource type="Script" uid="uid://dhlc0pfo61428" path="res://addons/func_godot/src/map/func_godot_map_settings.gd" id="1_dlf23"]
|
[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="1_hd7se"]
|
[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]
|
[resource]
|
||||||
script = ExtResource("1_dlf23")
|
script = ExtResource("1_dlf23")
|
||||||
inverse_scale_factor = 32.0
|
inverse_scale_factor = 32.0
|
||||||
entity_fgd = ExtResource("1_hd7se")
|
entity_fgd = ExtResource("2_hf4oi")
|
||||||
entity_name_property = ""
|
entity_name_property = ""
|
||||||
|
entity_smoothing_property = "_phong"
|
||||||
|
entity_smoothing_angle_property = "_phong_angle"
|
||||||
|
use_groups_hierarchy = false
|
||||||
base_texture_dir = "res://textures"
|
base_texture_dir = "res://textures"
|
||||||
texture_file_extensions = Array[String](["png", "jpg", "jpeg", "bmp", "tga", "webp"])
|
texture_file_extensions = Array[String](["png", "jpg", "jpeg", "bmp", "tga", "webp"])
|
||||||
clip_texture = "clip"
|
clip_texture = "special/clip"
|
||||||
skip_texture = "skip"
|
skip_texture = "special/skip"
|
||||||
texture_wads = Array[Resource]([])
|
origin_texture = "special/origin"
|
||||||
|
texture_wads = Array[ExtResource("4_576s4")]([])
|
||||||
|
base_material_dir = ""
|
||||||
material_file_extension = "tres"
|
material_file_extension = "tres"
|
||||||
default_material = ExtResource("1_8l5wm")
|
default_material = ExtResource("1_8l5wm")
|
||||||
default_material_albedo_uniform = ""
|
default_material_albedo_uniform = ""
|
||||||
@@ -24,7 +30,6 @@ roughness_map_pattern = "%s_roughness.%s"
|
|||||||
emission_map_pattern = "%s_emission.%s"
|
emission_map_pattern = "%s_emission.%s"
|
||||||
ao_map_pattern = "%s_ao.%s"
|
ao_map_pattern = "%s_ao.%s"
|
||||||
height_map_pattern = "%s_height.%s"
|
height_map_pattern = "%s_height.%s"
|
||||||
unshaded = false
|
orm_map_pattern = "%s_orm.%s"
|
||||||
save_generated_materials = false
|
save_generated_materials = true
|
||||||
uv_unwrap_texel_size = 1.0
|
uv_unwrap_texel_size = 2.0
|
||||||
use_trenchbroom_groups_hierarchy = false
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
[gd_resource type="Resource" script_class="FuncGodotLocalConfig" load_steps=2 format=3 uid="uid://bqjt7nyekxgog"]
|
[gd_resource type="Resource" script_class="FuncGodotLocalConfig" load_steps=2 format=3 uid="uid://bqjt7nyekxgog"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://bjtqjywscfgdy" path="res://addons/func_godot/src/util/func_godot_local_config.gd" id="1_g8kqj"]
|
[ext_resource type="Script" uid="uid://xsjnhahhyein" path="res://addons/func_godot/src/util/func_godot_local_config.gd" id="1_g8kqj"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_g8kqj")
|
script = ExtResource("1_g8kqj")
|
||||||
export_func_godot_settings = false
|
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
[gd_resource type="Resource" script_class="NetRadiantCustomGamePackConfig" load_steps=5 format=3 uid="uid://cv1k2e85fo2ax"]
|
[gd_resource type="Resource" script_class="NetRadiantCustomGamePackConfig" load_steps=6 format=3 uid="uid://cv1k2e85fo2ax"]
|
||||||
|
|
||||||
[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_gct4v"]
|
[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_gct4v"]
|
||||||
[ext_resource type="Script" uid="uid://ckvnkpgsci7hg" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd" id="2_en8ro"]
|
[ext_resource type="Script" uid="uid://dfhj3me2g5j0l" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd" id="2_en8ro"]
|
||||||
[ext_resource type="Resource" uid="uid://f5erfnvbg6b7" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres" id="2_w7psh"]
|
[ext_resource type="Resource" uid="uid://f5erfnvbg6b7" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres" id="2_w7psh"]
|
||||||
[ext_resource type="Resource" uid="uid://cfhg30jclb4lw" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres" id="3_6gpk8"]
|
[ext_resource type="Resource" uid="uid://cfhg30jclb4lw" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres" id="3_6gpk8"]
|
||||||
|
[ext_resource type="Resource" uid="uid://bpnj14oaufdpt" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_origin.tres" id="4_8rl60"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("2_en8ro")
|
script = ExtResource("2_en8ro")
|
||||||
export_file = false
|
|
||||||
gamepack_name = "func_godot"
|
gamepack_name = "func_godot"
|
||||||
game_name = "FuncGodot"
|
game_name = "FuncGodot"
|
||||||
base_game_path = ""
|
base_game_path = ""
|
||||||
fgd_file = ExtResource("1_gct4v")
|
fgd_file = ExtResource("1_gct4v")
|
||||||
netradiant_custom_shaders = Array[Resource]([ExtResource("2_w7psh"), ExtResource("3_6gpk8")])
|
netradiant_custom_shaders = Array[Resource]([ExtResource("2_w7psh"), ExtResource("3_6gpk8"), ExtResource("4_8rl60")])
|
||||||
texture_types = PackedStringArray("png", "jpg", "jpeg", "bmp", "tga")
|
texture_types = PackedStringArray("png", "jpg", "jpeg", "bmp", "tga")
|
||||||
model_types = PackedStringArray("glb", "gltf", "obj")
|
model_types = PackedStringArray("glb", "gltf", "obj")
|
||||||
sound_types = PackedStringArray("wav", "ogg")
|
sound_types = PackedStringArray("wav", "ogg")
|
||||||
default_scale = "1.0"
|
default_scale = "1.0"
|
||||||
clip_texture = "textures/special/clip"
|
clip_texture = "textures/special/clip"
|
||||||
skip_texture = "textures/special/skip"
|
skip_texture = "textures/special/skip"
|
||||||
|
map_type = 1
|
||||||
|
default_build_menu_variables = {}
|
||||||
|
default_build_menu_commands = {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[gd_resource type="Resource" script_class="NetRadiantCustomShader" load_steps=2 format=3 uid="uid://f5erfnvbg6b7"]
|
[gd_resource type="Resource" script_class="NetRadiantCustomShader" load_steps=2 format=3 uid="uid://f5erfnvbg6b7"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://dtmnasjcec1cl" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_cuylw"]
|
[ext_resource type="Script" uid="uid://dn86acprv4e86" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_cuylw"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_cuylw")
|
script = ExtResource("1_cuylw")
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[gd_resource type="Resource" script_class="NetRadiantCustomShader" load_steps=2 format=3 uid="uid://bpnj14oaufdpt"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://dn86acprv4e86" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_ah2cp"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_ah2cp")
|
||||||
|
texture_path = "textures/special/origin"
|
||||||
|
shader_attributes = Array[String](["qer_trans 0.4"])
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[gd_resource type="Resource" script_class="NetRadiantCustomShader" load_steps=2 format=3 uid="uid://cfhg30jclb4lw"]
|
[gd_resource type="Resource" script_class="NetRadiantCustomShader" load_steps=2 format=3 uid="uid://cfhg30jclb4lw"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://dtmnasjcec1cl" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_4ja6h"]
|
[ext_resource type="Script" uid="uid://dn86acprv4e86" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_4ja6h"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_4ja6h")
|
script = ExtResource("1_4ja6h")
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
[gd_resource type="Resource" script_class="TrenchBroomGameConfig" load_steps=8 format=3 uid="uid://b44ah5b2000wa"]
|
[gd_resource type="Resource" script_class="TrenchBroomGameConfig" load_steps=7 format=3 uid="uid://b44ah5b2000wa"]
|
||||||
|
|
||||||
[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_8u1vq"]
|
[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_8u1vq"]
|
||||||
[ext_resource type="Resource" uid="uid://b4xhdj0e16lop" path="res://addons/func_godot/game_config/trenchbroom/tb_face_tag_clip.tres" id="1_rsp20"]
|
[ext_resource type="Resource" uid="uid://b4xhdj0e16lop" path="res://addons/func_godot/game_config/trenchbroom/tb_face_tag_clip.tres" id="1_rsp20"]
|
||||||
[ext_resource type="Resource" uid="uid://ca7377sfgj074" path="res://addons/func_godot/game_config/trenchbroom/tb_face_tag_skip.tres" id="2_166i2"]
|
[ext_resource type="Resource" uid="uid://ca7377sfgj074" path="res://addons/func_godot/game_config/trenchbroom/tb_face_tag_skip.tres" id="2_166i2"]
|
||||||
[ext_resource type="Script" uid="uid://c51cy33iaei4s" path="res://addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd" id="2_ns6ah"]
|
[ext_resource type="Script" uid="uid://cx44c4vnq8bt5" path="res://addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd" id="2_ns6ah"]
|
||||||
[ext_resource type="Resource" uid="uid://37iduqf7tpxq" path="res://addons/func_godot/game_config/trenchbroom/tb_brush_tag_func.tres" id="3_mbuv7"]
|
[ext_resource type="Resource" uid="uid://bkjxc54mmdhbo" path="res://addons/func_godot/game_config/trenchbroom/tb_face_tag_origin.tres" id="3_stisi"]
|
||||||
[ext_resource type="Resource" uid="uid://co2sb1ng7cw4i" path="res://addons/func_godot/game_config/trenchbroom/tb_brush_tag_trigger.tres" id="4_y0tjm"]
|
|
||||||
[ext_resource type="Texture2D" uid="uid://decwujsyhj0qy" path="res://addons/func_godot/icon32.png" id="6_tex5j"]
|
[ext_resource type="Texture2D" uid="uid://decwujsyhj0qy" path="res://addons/func_godot/icon32.png" id="6_tex5j"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("2_ns6ah")
|
script = ExtResource("2_ns6ah")
|
||||||
export_file = false
|
game_name = "FuncGodot"
|
||||||
game_name = "maskmaker"
|
|
||||||
icon = ExtResource("6_tex5j")
|
icon = ExtResource("6_tex5j")
|
||||||
map_formats = Array[Dictionary]([{
|
map_formats = Array[Dictionary]([{
|
||||||
"format": "Valve",
|
"format": "Valve",
|
||||||
@@ -25,9 +23,12 @@ map_formats = Array[Dictionary]([{
|
|||||||
}, {
|
}, {
|
||||||
"format": "Quake3"
|
"format": "Quake3"
|
||||||
}])
|
}])
|
||||||
|
textures_root_folder = "textures"
|
||||||
texture_exclusion_patterns = Array[String](["*_albedo", "*_ao", "*_emission", "*_height", "*_metallic", "*_normal", "*_orm", "*_roughness", "*_sss"])
|
texture_exclusion_patterns = Array[String](["*_albedo", "*_ao", "*_emission", "*_height", "*_metallic", "*_normal", "*_orm", "*_roughness", "*_sss"])
|
||||||
|
palette_path = "textures/palette.lmp"
|
||||||
fgd_file = ExtResource("1_8u1vq")
|
fgd_file = ExtResource("1_8u1vq")
|
||||||
entity_scale = "32"
|
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)
|
default_uv_scale = Vector2(1, 1)
|
||||||
brush_tags = Array[Resource]([ExtResource("3_mbuv7"), ExtResource("4_y0tjm")])
|
game_config_version = 0
|
||||||
brushface_tags = Array[Resource]([ExtResource("1_rsp20"), ExtResource("2_166i2")])
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://37iduqf7tpxq"]
|
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://37iduqf7tpxq"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://5eacgso7tkh6" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_rn13a"]
|
[ext_resource type="Script" uid="uid://b66qdknwqpfup" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_rn13a"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_rn13a")
|
script = ExtResource("1_rn13a")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://co2sb1ng7cw4i"]
|
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://co2sb1ng7cw4i"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://5eacgso7tkh6" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_msqpk"]
|
[ext_resource type="Script" uid="uid://b66qdknwqpfup" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_msqpk"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_msqpk")
|
script = ExtResource("1_msqpk")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://b4xhdj0e16lop"]
|
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://b4xhdj0e16lop"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://5eacgso7tkh6" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_7td58"]
|
[ext_resource type="Script" uid="uid://b66qdknwqpfup" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_7td58"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_7td58")
|
script = ExtResource("1_7td58")
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://bkjxc54mmdhbo"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://b66qdknwqpfup" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_enkfc"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_enkfc")
|
||||||
|
tag_name = "Origin"
|
||||||
|
tag_attributes = Array[String](["transparent"])
|
||||||
|
tag_match_type = 0
|
||||||
|
tag_pattern = "origin"
|
||||||
|
texture_name = ""
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://ca7377sfgj074"]
|
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://ca7377sfgj074"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://5eacgso7tkh6" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_2teqe"]
|
[ext_resource type="Script" uid="uid://b66qdknwqpfup" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_2teqe"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_2teqe")
|
script = ExtResource("1_2teqe")
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon.svg-99f2c56e0c1ce867c819715c68d9c120.cte
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
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/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon_godambler.svg-a6dbba375ab2a45be046a1875b
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
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/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon_godambler3d.svg-f7df9bfe58320474198644aa
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
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/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon_godot_ranger.svg-8572582518f54de6403b767
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
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/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon_godot_ranger3d.svg-a9a2c9bcf2e8b1e07a0a9
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
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/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon_quake_file.svg-1718b9a2b5e0b124f6d72bb4c
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
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/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
13
addons/func_godot/icons/icon_slipgate.svg
Normal file
13
addons/func_godot/icons/icon_slipgate.svg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<!-- Created using Krita: https://krita.org -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:krita="http://krita.org/namespaces/svg/krita"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
width="23.04pt"
|
||||||
|
height="23.04pt"
|
||||||
|
viewBox="0 0 23.04 23.04">
|
||||||
|
<defs/>
|
||||||
|
<path id="shape0" transform="translate(3.32859367052032, 1.70718625021606)" fill="#ffffff" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0 2.17125L5.76 4.3425L11.52 2.17125L5.76 0Z" sodipodi:nodetypes="ccccc"/><path id="shape1" transform="translate(3.49734362007209, 4.23843624987037)" fill="#ffffff" fill-rule="evenodd" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0 0L0 12.96L5.4 15.12L5.49 2.16Z" sodipodi:nodetypes="ccccc"/><path id="shape01" transform="translate(9.22078101093241, 4.42406124416546)" fill="#ffffff" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0 1.8L5.0175 3.78563L10.4906 1.98563L5.085 0Z" sodipodi:nodetypes="ccccc"/><path id="shape11" transform="translate(9.38953096048418, 6.58406124416546)" fill="#ffffff" fill-rule="evenodd" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0 0L0.03375 1.97438L4.69125 3.94875L4.6575 1.97438Z" sodipodi:nodetypes="ccccc"/><path id="shape011" transform="matrix(-1 0 0 1 19.705781131254 6.76968624399261)" fill="#ffffff" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0.03375 0L0 1.97438L5.39438 3.76313L5.42813 1.78875Z" sodipodi:nodetypes="ccccc"/><path id="shape02" transform="translate(8.855151086652, 15.2240587499568)" fill="#ffffff" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0 1.8L5.0175 3.78563L10.4906 1.98563L5.085 0Z" sodipodi:nodetypes="ccccc"/><path id="shape12" transform="translate(9.02390103620378, 17.3840587499568)" fill="#ffffff" fill-rule="evenodd" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0 0L0.03375 1.97438L4.69125 3.94875L4.6575 1.97438Z" sodipodi:nodetypes="ccccc"/><path id="shape012" transform="matrix(-1 0 0 1 19.3401512069735 17.5696837497839)" fill="#ffffff" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0.03375 0L0 1.97438L5.39438 3.76313L5.42813 1.78875Z" sodipodi:nodetypes="ccccc"/><path id="shape2" transform="translate(9.05484388076874, 8.91843624987036)" fill="#ffffff" fill-rule="evenodd" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M0 7.92L0 0L5.90625 2.16L5.805 5.76Z" sodipodi:nodetypes="ccccc"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
37
addons/func_godot/icons/icon_slipgate.svg.import
Normal file
37
addons/func_godot/icons/icon_slipgate.svg.import
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bw74kacajcaxb"
|
||||||
|
path="res://.godot/imported/icon_slipgate.svg-f42668b28b92f93c031f56d95dfcf5a6.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://addons/func_godot/icons/icon_slipgate.svg"
|
||||||
|
dest_files=["res://.godot/imported/icon_slipgate.svg-f42668b28b92f93c031f56d95dfcf5a6.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon_slipgate3d.svg-f125bef6ff5aa79b5fe3f232a
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
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/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[plugin]
|
[plugin]
|
||||||
|
|
||||||
name="FuncGodot"
|
name="FuncGodot"
|
||||||
description="Quake .map file support for Godot."
|
description="Quake .map and Half-Life .vmf file support for Godot."
|
||||||
author="Shifty, Hannah Crawford, Emberlynn Bland, Tim Maccabe"
|
author="Josh Palmer, Hannah Crawford, Emberlynn Bland, Tim Maccabe, Vera Lux"
|
||||||
version="2024.1"
|
version="2025.9"
|
||||||
script="src/func_godot_plugin.gd"
|
script="src/func_godot_plugin.gd"
|
||||||
190
addons/func_godot/src/core/data.gd
Normal file
190
addons/func_godot/src/core/data.gd
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
||||||
|
class_name FuncGodotData
|
||||||
|
## Container that holds various data structs to be used in the [FuncGodotMap] build process.
|
||||||
|
##
|
||||||
|
## FuncGodot utilizes multiple custom data structs to hold information parsed from the map file
|
||||||
|
## and read and modified by the other core build classes.
|
||||||
|
## All data structs extend from [RefCounted], therefore all data is passed by reference.
|
||||||
|
## [br][br]
|
||||||
|
## [FuncGodotData.FaceData][br]
|
||||||
|
## [FuncGodotData.BrushData][br]
|
||||||
|
## [FuncGodotData.PatchData][br]
|
||||||
|
## [FuncGodotData.GroupData][br]
|
||||||
|
## [FuncGodotData.EntityData][br]
|
||||||
|
|
||||||
|
## Data struct representing both a single map plane and a mesh face. Generated during parsing by plane definitions in the map file,
|
||||||
|
## it is further modified and utilized during the geo generation stage to create the final entity meshes.
|
||||||
|
class FaceData extends RefCounted:
|
||||||
|
## Vertex array for the face. Only populated in combination with other faces, as a result of planar intersections.
|
||||||
|
var vertices: PackedVector3Array = []
|
||||||
|
## Index array for the face. Used in ArrayMesh creation.
|
||||||
|
var indices: PackedInt32Array = []
|
||||||
|
## Vertex normal array for the face.
|
||||||
|
## By default, set to the planar normal, which results in flat shading. May be modified to adjust shading.
|
||||||
|
var normals: PackedVector3Array = []
|
||||||
|
## Tangent data for the face.
|
||||||
|
var tangents: PackedFloat32Array = []
|
||||||
|
## Local path to the texture without the extension, relative to the FuncGodotMap node's settings' base texture directory.
|
||||||
|
var texture: String
|
||||||
|
## UV transform data generated during the parsing stage. Used for both Standard and Valve 220 UV formats,
|
||||||
|
## though rotation is not applied to the transform when using Valve 220.
|
||||||
|
var uv: Transform2D
|
||||||
|
## Raw vector data provided by the Valve 220 format during parsing. It is used to calculate rotations.
|
||||||
|
## The presence of this data determines how face UVs and tangents are calculated.
|
||||||
|
var uv_axes: PackedVector3Array = []
|
||||||
|
## Raw plane data parsed from the map file using the id Tech coordinate system.
|
||||||
|
var plane: Plane
|
||||||
|
|
||||||
|
## Returns the average position of all vertices in the face. Only valid when the face has at least one vertex.
|
||||||
|
func get_centroid() -> Vector3:
|
||||||
|
return FuncGodotUtil.op_vec3_avg(vertices)
|
||||||
|
|
||||||
|
## Returns an arbitrary coplanar direction to use for winding the face.
|
||||||
|
## Only valid when the face has at least two vertices.
|
||||||
|
func get_basis() -> Vector3:
|
||||||
|
if vertices.size() < 2:
|
||||||
|
push_error("Cannot get winding basis without at least 2 vertices!")
|
||||||
|
return Vector3.ZERO
|
||||||
|
return (vertices[1] - vertices[0]).normalized()
|
||||||
|
|
||||||
|
## Prepares the face for OpenGL triangle winding order.
|
||||||
|
## Sorts the vertex array in-place by angle from the centroid.
|
||||||
|
func wind() -> void:
|
||||||
|
var centroid: Vector3 = get_centroid()
|
||||||
|
var u_axis: Vector3 = get_basis()
|
||||||
|
var v_axis: Vector3 = u_axis.cross(plane.normal).normalized()
|
||||||
|
var cmp_winding_angle: Callable = (
|
||||||
|
func(a: Vector3, b: Vector3) -> bool:
|
||||||
|
var dir_a: Vector3 = a - centroid
|
||||||
|
var dir_b: Vector3 = b - centroid
|
||||||
|
var angle_a: float = atan2(dir_a.dot(v_axis), dir_a.dot(u_axis))
|
||||||
|
var angle_b: float = atan2(dir_b.dot(v_axis), dir_b.dot(u_axis))
|
||||||
|
return angle_a < angle_b
|
||||||
|
)
|
||||||
|
|
||||||
|
var _vertices: Array[Vector3]
|
||||||
|
_vertices.assign(vertices)
|
||||||
|
_vertices.sort_custom(cmp_winding_angle)
|
||||||
|
vertices = _vertices
|
||||||
|
|
||||||
|
## Repopulate the [member indices] array to create a triangle fan.
|
||||||
|
## The face must be properly wound for the resulting indices to be valid.
|
||||||
|
func index_vertices() -> void:
|
||||||
|
var tri_count: int = vertices.size() - 2
|
||||||
|
indices.resize(tri_count * 3)
|
||||||
|
var index: int = 0
|
||||||
|
for i in tri_count:
|
||||||
|
indices[index] = 0
|
||||||
|
indices[index + 1] = i + 1
|
||||||
|
indices[index + 2] = i + 2
|
||||||
|
index += 3
|
||||||
|
|
||||||
|
## Data struct representing a single map format brush. It is largely meant as a container for [FuncGodotData.FaceData] data.
|
||||||
|
class BrushData extends RefCounted:
|
||||||
|
## Raw plane data parsed from the map file using the id Tech coordinate system.
|
||||||
|
var planes: Array[Plane]
|
||||||
|
## Collection of [FuncGodotData.FaceData].
|
||||||
|
var faces: Array[FaceData]
|
||||||
|
## [code]true[/code] if this brush is completely covered in the [i]Origin[/i] texture defined in [FuncGodotMapSettings].
|
||||||
|
## Determined during [FuncGodotParser] and utilized during [FuncGodotGeometryGenerator].
|
||||||
|
var origin: bool = false
|
||||||
|
|
||||||
|
## Data struct representing a patch def entity.
|
||||||
|
class PatchData extends RefCounted:
|
||||||
|
## Local path to the texture without the extension, relative to the FuncGodotMap node's settings' base texture directory.
|
||||||
|
var texture: String
|
||||||
|
var size: PackedInt32Array
|
||||||
|
var points: PackedVector3Array
|
||||||
|
var uvs: PackedVector2Array
|
||||||
|
|
||||||
|
## Data struct representing a TrenchBroom Group, TrenchBroom Layer, or Valve VisGroup.
|
||||||
|
## Generated during the parsing stage and utilized during both parsing and entity assembly stages.
|
||||||
|
class GroupData extends RefCounted:
|
||||||
|
enum GroupType { GROUP, LAYER, }
|
||||||
|
## Defines whether the group is a Group or a Layer. Currently only determines the name of the group.
|
||||||
|
var type: GroupType = GroupType.GROUP
|
||||||
|
## Group ID retrieved from the map file. Utilized during the parsing and entity assembly stages to determine
|
||||||
|
## which entities belong to which groups as well as which groups are children of other groups.
|
||||||
|
var id: int
|
||||||
|
## Generated during the parsing stage using the format of type_id_name, eg: group_2_Arkham.
|
||||||
|
var name: String
|
||||||
|
## ID of the parent group data, used to determine which group data is this group's parent.
|
||||||
|
var parent_id: int = -1
|
||||||
|
## Pointer to another group data that this group is a child of.
|
||||||
|
var parent: GroupData = null
|
||||||
|
## Pointer to generated Node3D representing this group in the SceneTree.
|
||||||
|
var node: Node3D = null
|
||||||
|
## If true, erases all entities assigned to this group and then the group itself at the end of the parsing stage, preventing those entities from being generated into nodes.
|
||||||
|
## Can be set in TrenchBroom on layers using the "omit layer" option.
|
||||||
|
var omit: bool = false
|
||||||
|
|
||||||
|
## Data struct representing a map format entity.
|
||||||
|
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 = {}
|
||||||
|
## 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] = []
|
||||||
|
## The entity's patch def data collected during the parsing stage. If the entity's FGD resource cannot be found,
|
||||||
|
## the presence of a single patch def determines this entity to be a Solid Entity.
|
||||||
|
var patches: Array[PatchData] = []
|
||||||
|
## Pointer to the group data this entity belongs to.
|
||||||
|
var group: GroupData = null
|
||||||
|
## The entity's FGD resource, determined by matching the classname properties of each.
|
||||||
|
## This can only be a [FuncGodotFGDSolidClass], [FuncGodotFGDPointClass], or [FuncGodotFGDModelPointClass].
|
||||||
|
var definition: FuncGodotFGDEntityClass = null
|
||||||
|
## Mesh resource generated during the geometry generation stage and applied during the entity assembly stage.
|
||||||
|
var mesh: ArrayMesh = null
|
||||||
|
## MeshInstance3D node generated during the entity assembly stage.
|
||||||
|
var mesh_instance: MeshInstance3D = null
|
||||||
|
## Optional mesh metadata compiled during the geometry generation stage, used to determine face information from collision.
|
||||||
|
var mesh_metadata: Dictionary = {}
|
||||||
|
## A collection of collision shape resources generated during the geometry generation stage and applied during the entity assembly stage.
|
||||||
|
var shapes: Array[Shape3D] = []
|
||||||
|
## A collection of [CollisionShape3D] nodes generated during the entity assembly stage. Each node corresponds to a shape in the [member shapes] array.
|
||||||
|
var collision_shapes: Array[CollisionShape3D] = []
|
||||||
|
## [OccluderInstance3D] node generated during the entity assembly stage using the [member mesh] resource.
|
||||||
|
var occluder_instance: OccluderInstance3D = null
|
||||||
|
## True global position of the entity's generated node that the mesh's vertices are offset by during the geometry generation stage.
|
||||||
|
var origin: Vector3 = Vector3.ZERO
|
||||||
|
|
||||||
|
## Checks the entity's FGD resource definition, returning whether the Solid Class has a [MeshInstance3D] built for it.
|
||||||
|
func is_visual() -> bool:
|
||||||
|
return (definition
|
||||||
|
and definition is FuncGodotFGDSolidClass
|
||||||
|
and definition.build_visuals)
|
||||||
|
|
||||||
|
## Checks the entity's FGD resource definition, returning whether the Solid Class CollisionShapeType is set to Convex.
|
||||||
|
func is_collision_convex() -> bool:
|
||||||
|
return (definition
|
||||||
|
and definition is FuncGodotFGDSolidClass
|
||||||
|
and definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.CONVEX
|
||||||
|
)
|
||||||
|
|
||||||
|
## Checks the entity's FGD resource definition, returning whether the Solid Class CollisionShapeType is set to Concave.
|
||||||
|
func is_collision_concave() -> bool:
|
||||||
|
return (definition
|
||||||
|
and definition is FuncGodotFGDSolidClass
|
||||||
|
and definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.CONCAVE
|
||||||
|
)
|
||||||
|
|
||||||
|
## 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()
|
||||||
|
|
||||||
|
## 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()
|
||||||
|
|
||||||
|
class VertexGroupData:
|
||||||
|
## Faces this vertex appears in.
|
||||||
|
var faces: Array[FaceData]
|
||||||
|
## Index within the associated face for this vertex.
|
||||||
|
var face_indices: PackedInt32Array
|
||||||
|
|
||||||
|
class ParseData:
|
||||||
|
var entities: Array[EntityData] = []
|
||||||
|
var groups: Array[GroupData] = []
|
||||||
1
addons/func_godot/src/core/data.gd.uid
Normal file
1
addons/func_godot/src/core/data.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cqye8dehq4c7q
|
||||||
451
addons/func_godot/src/core/entity_assembler.gd
Normal file
451
addons/func_godot/src/core/entity_assembler.gd
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
||||||
|
class_name FuncGodotEntityAssembler extends RefCounted
|
||||||
|
## Entity assembly class that is instantiated by a [FuncGodotMap] node.
|
||||||
|
|
||||||
|
const _SIGNATURE: String = "[ENT]"
|
||||||
|
|
||||||
|
# Namespacing
|
||||||
|
const _GroupData := FuncGodotData.GroupData
|
||||||
|
const _EntityData := FuncGodotData.EntityData
|
||||||
|
|
||||||
|
# Class members
|
||||||
|
## [FuncGodotMapSettings] provided by the [FuncGodotMap] during the build process.
|
||||||
|
var map_settings: FuncGodotMapSettings = null
|
||||||
|
## [enum FuncGodotMap.BuildFlags] that may affect the build process provided by the [FuncGodotMap].
|
||||||
|
var build_flags: int = 0
|
||||||
|
|
||||||
|
# Signals
|
||||||
|
## Emitted when a step in the entity assembly process is completed.
|
||||||
|
## It is connected to [method FuncGodotUtil.print_profile_info] method if [member FuncGodotMap.build_flags] SHOW_PROFILE_INFO flag is set.
|
||||||
|
signal declare_step(step: String)
|
||||||
|
|
||||||
|
func _init(settings: FuncGodotMapSettings) -> void:
|
||||||
|
map_settings = settings
|
||||||
|
|
||||||
|
## Attempts to retrieve a [Script] via class name, to allow for [GDScript] class instantiation.
|
||||||
|
static func get_script_by_class_name(name_of_class: String) -> Script:
|
||||||
|
if ResourceLoader.exists(name_of_class, "Script"):
|
||||||
|
return load(name_of_class) as Script
|
||||||
|
for global_class in ProjectSettings.get_global_class_list():
|
||||||
|
var found_name_of_class : String = global_class["class"]
|
||||||
|
var found_path : String = global_class["path"]
|
||||||
|
if found_name_of_class == name_of_class:
|
||||||
|
return load(found_path) as Script
|
||||||
|
return null
|
||||||
|
|
||||||
|
## Generates a [Node3D] for a group's [SceneTree] representation and links the new [Node3D] to that group.
|
||||||
|
func generate_group_node(group_data: _GroupData) -> Node3D:
|
||||||
|
var group_node := Node3D.new()
|
||||||
|
group_node.name = group_data.name
|
||||||
|
group_data.node = group_node
|
||||||
|
return group_node
|
||||||
|
|
||||||
|
## Generates and assembles a new [Node] based upon processed [FuncGodotData.EntityData]. Depending upon provided data,
|
||||||
|
## additional [MeshInstance3D], [CollisionShape3D], and [OccluderInstance3D] nodes may also be generated.
|
||||||
|
func generate_solid_entity_node(node: Node, node_name: String, data: _EntityData, definition: FuncGodotFGDSolidClass) -> Node:
|
||||||
|
if definition.spawn_type == FuncGodotFGDSolidClass.SpawnType.MERGE_WORLDSPAWN:
|
||||||
|
return null
|
||||||
|
|
||||||
|
if definition.node_class != "":
|
||||||
|
if ClassDB.class_exists(definition.node_class):
|
||||||
|
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:
|
||||||
|
node = Node3D.new()
|
||||||
|
|
||||||
|
node.name = node_name
|
||||||
|
node_name = node_name.trim_suffix(definition.classname).trim_suffix("_")
|
||||||
|
var properties: Dictionary = data.properties
|
||||||
|
|
||||||
|
# Mesh Instance generation
|
||||||
|
if data.mesh:
|
||||||
|
var mesh_instance := MeshInstance3D.new()
|
||||||
|
mesh_instance.name = node_name + "_mesh_instance"
|
||||||
|
mesh_instance.mesh = data.mesh
|
||||||
|
mesh_instance.gi_mode = GeometryInstance3D.GI_MODE_DISABLED
|
||||||
|
if definition.global_illumination_mode:
|
||||||
|
mesh_instance.gi_mode = definition.global_illumination_mode
|
||||||
|
mesh_instance.cast_shadow = definition.shadow_casting_setting
|
||||||
|
mesh_instance.layers = definition.render_layers
|
||||||
|
node.add_child(mesh_instance)
|
||||||
|
data.mesh_instance = mesh_instance
|
||||||
|
|
||||||
|
# Occluder generation
|
||||||
|
if definition.build_occlusion and data.mesh:
|
||||||
|
var verts: PackedVector3Array = []
|
||||||
|
var indices: PackedInt32Array = []
|
||||||
|
var index: int = 0
|
||||||
|
for surf_idx in range(data.mesh.get_surface_count()):
|
||||||
|
var vert_count: int = verts.size()
|
||||||
|
var surf_array: Array = data.mesh.surface_get_arrays(surf_idx)
|
||||||
|
verts.append_array(surf_array[Mesh.ARRAY_VERTEX])
|
||||||
|
indices.resize(indices.size() + surf_array[Mesh.ARRAY_INDEX].size())
|
||||||
|
for new_index in surf_array[Mesh.ARRAY_INDEX]:
|
||||||
|
indices[index] = (new_index + vert_count)
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
var occluder := ArrayOccluder3D.new()
|
||||||
|
occluder.set_arrays(verts, indices)
|
||||||
|
var occluder_instance := OccluderInstance3D.new()
|
||||||
|
occluder_instance.name = node_name + "_occluder_instance"
|
||||||
|
occluder_instance.occluder = occluder
|
||||||
|
node.add_child(occluder_instance)
|
||||||
|
data.occluder_instance = occluder_instance
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
# Collision generation
|
||||||
|
if data.shapes.size() and node is CollisionObject3D:
|
||||||
|
node.collision_layer = definition.collision_layer
|
||||||
|
node.collision_mask = definition.collision_mask
|
||||||
|
node.collision_priority = definition.collision_priority
|
||||||
|
|
||||||
|
var shape_to_face_array : Array[PackedInt32Array] = []
|
||||||
|
if data.mesh_metadata.has('shape_to_face_array'):
|
||||||
|
shape_to_face_array = data.mesh_metadata['shape_to_face_array']
|
||||||
|
data.mesh_metadata.erase('shape_to_face_array')
|
||||||
|
|
||||||
|
# Generate CollisionShape3D nodes and apply shapes
|
||||||
|
var face_index_metadata : Dictionary[String, PackedInt32Array] = {}
|
||||||
|
for i in data.shapes.size():
|
||||||
|
var shape := data.shapes[i]
|
||||||
|
var collision_shape := CollisionShape3D.new()
|
||||||
|
if definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.CONCAVE:
|
||||||
|
collision_shape.name = node_name + "_collision_shape"
|
||||||
|
else:
|
||||||
|
collision_shape.name = node_name + "_brush_%s_collision_shape" % i
|
||||||
|
collision_shape.shape = shape
|
||||||
|
collision_shape.shape.margin = definition.collision_shape_margin
|
||||||
|
collision_shape.owner = node.owner
|
||||||
|
node.add_child(collision_shape)
|
||||||
|
data.collision_shapes.append(collision_shape)
|
||||||
|
if shape_to_face_array.size() > i:
|
||||||
|
face_index_metadata[collision_shape.name] = shape_to_face_array[i]
|
||||||
|
|
||||||
|
if definition.add_collision_shape_to_face_indices_metadata:
|
||||||
|
data.mesh_metadata['collision_shape_to_face_indices_map'] = face_index_metadata
|
||||||
|
|
||||||
|
if "position" in node:
|
||||||
|
if node.position is Vector3:
|
||||||
|
node.position = FuncGodotUtil.id_to_opengl(data.origin) * map_settings.scale_factor
|
||||||
|
|
||||||
|
if not data.mesh_metadata.is_empty():
|
||||||
|
node.set_meta("func_godot_mesh_data", data.mesh_metadata)
|
||||||
|
|
||||||
|
return node
|
||||||
|
|
||||||
|
## Generates and assembles a new [Node] or [PackedScene] based upon processed [FuncGodotData.EntityData].
|
||||||
|
func generate_point_entity_node(node: Node, node_name: String, properties: Dictionary, definition: FuncGodotFGDPointClass) -> Node:
|
||||||
|
var classname: String = properties["classname"]
|
||||||
|
|
||||||
|
if definition.scene_file:
|
||||||
|
var flag: PackedScene.GenEditState = PackedScene.GEN_EDIT_STATE_DISABLED
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
flag = PackedScene.GEN_EDIT_STATE_INSTANCE
|
||||||
|
node = definition.scene_file.instantiate(flag)
|
||||||
|
elif definition.node_class != "":
|
||||||
|
if ClassDB.class_exists(definition.node_class):
|
||||||
|
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:
|
||||||
|
node = Node3D.new()
|
||||||
|
|
||||||
|
node.name = node_name
|
||||||
|
|
||||||
|
if "rotation_degrees" in node and definition.apply_rotation_on_map_build:
|
||||||
|
var angles := Vector3.ZERO
|
||||||
|
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:
|
||||||
|
angles_raw = angles_raw.split_floats(' ')
|
||||||
|
if angles_raw.size() > 2:
|
||||||
|
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
|
||||||
|
angles.y += 180
|
||||||
|
node.rotation_degrees = angles
|
||||||
|
|
||||||
|
if "scale" in node and definition.apply_scale_on_map_build:
|
||||||
|
if "scale" in properties:
|
||||||
|
var scale_prop: Variant = properties["scale"]
|
||||||
|
if typeof(scale_prop) == TYPE_STRING:
|
||||||
|
var scale_arr: PackedStringArray = (scale_prop as String).split(" ")
|
||||||
|
match scale_arr.size():
|
||||||
|
1: scale_prop = scale_arr[0].to_float()
|
||||||
|
3: scale_prop = Vector3(scale_arr[1].to_float(), scale_arr[2].to_float(), scale_arr[0].to_float())
|
||||||
|
2: scale_prop = Vector2(scale_arr[0].to_float(), scale_arr[0].to_float())
|
||||||
|
if typeof(scale_prop) == TYPE_FLOAT or typeof(scale_prop) == TYPE_INT:
|
||||||
|
node.scale *= scale_prop as float
|
||||||
|
elif node.scale is Vector3:
|
||||||
|
if typeof(scale_prop) == TYPE_VECTOR3 or typeof(scale_prop) == TYPE_VECTOR3I:
|
||||||
|
node.scale *= scale_prop as Vector3
|
||||||
|
elif node.scale is Vector2:
|
||||||
|
if typeof(scale_prop) == TYPE_VECTOR2 or typeof(scale_prop) == TYPE_VECTOR2I:
|
||||||
|
node.scale *= scale_prop as Vector2
|
||||||
|
|
||||||
|
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])
|
||||||
|
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
|
||||||
|
elif node.position is Vector2:
|
||||||
|
node.position = Vector2(origin_vec.z, -origin_vec.y)
|
||||||
|
|
||||||
|
return node
|
||||||
|
|
||||||
|
## Converts the [String] values of the entity data's [code]properties[/code] [Dictionary] to various [Variant] formats
|
||||||
|
## 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
|
||||||
|
|
||||||
|
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 in node:
|
||||||
|
if typeof(node.get(property)) == typeof(properties[property]):
|
||||||
|
node.set(property, properties[property])
|
||||||
|
else:
|
||||||
|
push_error("Entity %s property \'%s\' type mismatch with matching generated node property." % [node.name, property])
|
||||||
|
|
||||||
|
if "func_godot_properties" in node:
|
||||||
|
node.func_godot_properties = properties
|
||||||
|
|
||||||
|
if node.has_method("_func_godot_apply_properties"):
|
||||||
|
node.call("_func_godot_apply_properties", properties)
|
||||||
|
|
||||||
|
if node.has_method("_func_godot_build_complete"):
|
||||||
|
node.call_deferred("_func_godot_build_complete")
|
||||||
|
|
||||||
|
## Generate a [Node] from [FuncGodotData.EntityData]. The returned node value can be [code]null[/code],
|
||||||
|
## in the case of [FuncGodotFGDSolidClass] entities with no [FuncGodotData.BrushData] entries.
|
||||||
|
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 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:
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
build_flags = map_node.build_flags
|
||||||
|
|
||||||
|
if map_settings.use_groups_hierarchy:
|
||||||
|
declare_step.emit("Generating %s groups" % groups.size())
|
||||||
|
# Generate group nodes
|
||||||
|
for group in groups:
|
||||||
|
group.node = generate_group_node(group)
|
||||||
|
# Sort hierarchy and add them to the map
|
||||||
|
for group in groups:
|
||||||
|
if group.parent_id < 0:
|
||||||
|
map_node.add_child(group.node)
|
||||||
|
group.node.owner = scene_root
|
||||||
|
else:
|
||||||
|
for parent in groups:
|
||||||
|
if group.parent_id == parent.id:
|
||||||
|
parent.node.add_child(group.node)
|
||||||
|
group.node.owner = scene_root
|
||||||
|
declare_step.emit("Groups generation and sorting complete")
|
||||||
|
|
||||||
|
declare_step.emit("Assembling %s entities" % entities.size())
|
||||||
|
var entity_node: Node = null
|
||||||
|
for entity_index in entities.size():
|
||||||
|
var entity_data : _EntityData = entities[entity_index]
|
||||||
|
entity_node = generate_entity_node(entity_data, entity_index)
|
||||||
|
if entity_node:
|
||||||
|
if not map_settings.use_groups_hierarchy or not entity_data.group:
|
||||||
|
map_node.add_child(entity_node)
|
||||||
|
if entity_index == 0:
|
||||||
|
map_node.move_child(entity_node, 0)
|
||||||
|
elif map_settings.use_groups_hierarchy:
|
||||||
|
for group in groups:
|
||||||
|
if entity_data.group.id == group.id:
|
||||||
|
group.node.add_child(entity_node)
|
||||||
|
|
||||||
|
entity_node.owner = scene_root
|
||||||
|
if entity_data.mesh_instance:
|
||||||
|
entity_data.mesh_instance.owner = scene_root
|
||||||
|
for shape in entity_data.collision_shapes:
|
||||||
|
if shape:
|
||||||
|
shape.owner = scene_root
|
||||||
|
if entity_data.occluder_instance:
|
||||||
|
entity_data.occluder_instance.owner = scene_root
|
||||||
|
|
||||||
|
apply_entity_properties(entity_node, entity_data)
|
||||||
|
declare_step.emit("Entity assembly and property application complete")
|
||||||
1
addons/func_godot/src/core/entity_assembler.gd.uid
Normal file
1
addons/func_godot/src/core/entity_assembler.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dh73tfvwp7kr6
|
||||||
@@ -1,139 +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 surface_gatherer:= FuncGodotSurfaceGatherer.new(map_data)
|
|
||||||
|
|
||||||
func load_map(filename: String, keep_tb_groups: bool) -> void:
|
|
||||||
map_parser.load(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.IGNORE)
|
|
||||||
map_data.set_entity_types_by_classname(classname, spawn_type, origin_type)
|
|
||||||
|
|
||||||
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_size(key, val.x, val.y)
|
|
||||||
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_mt(texture_name: String, clip_filter_texture: String, skip_filter_texture: String, inverse_scale_factor: float) -> Array:
|
|
||||||
var sg: FuncGodotSurfaceGatherer = FuncGodotSurfaceGatherer.new(map_data)
|
|
||||||
sg.reset_params()
|
|
||||||
sg.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.ENTITY
|
|
||||||
sg.set_texture_filter(texture_name)
|
|
||||||
sg.set_clip_filter_texture(clip_filter_texture)
|
|
||||||
sg.set_skip_filter_texture(skip_filter_texture)
|
|
||||||
sg.run()
|
|
||||||
return _fetch_surfaces_internal(sg, inverse_scale_factor)
|
|
||||||
|
|
||||||
func gather_worldspawn_layer_surfaces(texture_name: String, clip_filter_texture: String, skip_filter_texture: String) -> void:
|
|
||||||
_gather_texture_surfaces_internal(texture_name, clip_filter_texture, skip_filter_texture)
|
|
||||||
|
|
||||||
func gather_entity_convex_collision_surfaces(entity_idx: int) -> void:
|
|
||||||
_gather_convex_collision_surfaces(entity_idx)
|
|
||||||
|
|
||||||
func gather_entity_concave_collision_surfaces(entity_idx: int, skip_filter_texture: String) -> void:
|
|
||||||
_gather_concave_collision_surfaces(entity_idx, skip_filter_texture)
|
|
||||||
|
|
||||||
func gather_worldspawn_layer_collision_surfaces(entity_idx: int) -> void:
|
|
||||||
_gather_convex_collision_surfaces(entity_idx)
|
|
||||||
|
|
||||||
func fetch_surfaces(inverse_scale_factor: float) -> Array:
|
|
||||||
return _fetch_surfaces_internal(surface_gatherer, inverse_scale_factor)
|
|
||||||
|
|
||||||
func _fetch_surfaces_internal(surf_gatherer: FuncGodotSurfaceGatherer, inverse_scale_factor: float) -> Array:
|
|
||||||
var surfs: Array[FuncGodotMapData.FuncGodotFaceGeometry] = surf_gatherer.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) / inverse_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
|
|
||||||
|
|
||||||
# internal
|
|
||||||
func _gather_texture_surfaces_internal(texture_name: String, clip_filter_texture: String, skip_filter_texture: String) -> void:
|
|
||||||
surface_gatherer.reset_params()
|
|
||||||
surface_gatherer.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.ENTITY
|
|
||||||
surface_gatherer.set_texture_filter(texture_name)
|
|
||||||
surface_gatherer.set_clip_filter_texture(clip_filter_texture)
|
|
||||||
surface_gatherer.set_skip_filter_texture(skip_filter_texture)
|
|
||||||
surface_gatherer.run()
|
|
||||||
|
|
||||||
func _gather_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.run()
|
|
||||||
|
|
||||||
func _gather_concave_collision_surfaces(entity_idx: int, skip_filter_texture: String) -> void:
|
|
||||||
surface_gatherer.reset_params()
|
|
||||||
surface_gatherer.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.NONE
|
|
||||||
surface_gatherer.entity_filter_idx = entity_idx
|
|
||||||
surface_gatherer.set_skip_filter_texture(skip_filter_texture)
|
|
||||||
surface_gatherer.run()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://b13x4qa2xgrkb
|
|
||||||
@@ -1,352 +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, b) -> bool:
|
|
||||||
var face:= map_data.entities[wind_entity_idx].brushes[wind_brush_idx].faces[wind_face_idx]
|
|
||||||
var face_geo:= map_data.entity_geo[wind_entity_idx].brushes[wind_brush_idx].faces[wind_face_idx]
|
|
||||||
|
|
||||||
var u:= wind_face_basis.normalized()
|
|
||||||
var v:= u.cross(wind_face_normal).normalized()
|
|
||||||
|
|
||||||
var loc_a = a.vertex - wind_face_center
|
|
||||||
var a_pu: float = loc_a.dot(u)
|
|
||||||
var a_pv: float = loc_a.dot(v)
|
|
||||||
|
|
||||||
var loc_b = b.vertex - wind_face_center
|
|
||||||
var b_pu: float = loc_b.dot(u)
|
|
||||||
var b_pv: float = loc_b.dot(v)
|
|
||||||
|
|
||||||
var a_angle:= atan2(a_pv, a_pu)
|
|
||||||
var b_angle:= atan2(b_pv, b_pu)
|
|
||||||
|
|
||||||
return a_angle < b_angle
|
|
||||||
|
|
||||||
func run() -> void:
|
|
||||||
# resize arrays
|
|
||||||
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:= map_data.entities[e]
|
|
||||||
var entity_geo:= 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:= entity.brushes[b]
|
|
||||||
var brush_geo:= 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:= map_data.entities[e]
|
|
||||||
var entity_geo:= map_data.entity_geo[e]
|
|
||||||
entity.center = Vector3.ZERO
|
|
||||||
|
|
||||||
for b in range(entity.brushes.size()):
|
|
||||||
var brush:= entity.brushes[b]
|
|
||||||
brush.center = Vector3.ZERO
|
|
||||||
var vert_count: int = 0
|
|
||||||
|
|
||||||
generate_brush_vertices(e, b)
|
|
||||||
|
|
||||||
var brush_geo:= map_data.entity_geo[e].brushes[b]
|
|
||||||
for face in brush_geo.faces:
|
|
||||||
for vert in face.vertices:
|
|
||||||
brush.center += vert.vertex
|
|
||||||
vert_count += 1
|
|
||||||
|
|
||||||
if vert_count > 0:
|
|
||||||
brush.center /= float(vert_count)
|
|
||||||
|
|
||||||
entity.center += brush.center
|
|
||||||
|
|
||||||
if entity.brushes.size() > 0:
|
|
||||||
entity.center /= float(entity.brushes.size())
|
|
||||||
if entity.origin_type != FuncGodotMapData.FuncGodotEntityOriginType.IGNORE and '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])
|
|
||||||
elif entity.origin_type == FuncGodotMapData.FuncGodotEntityOriginType.RELATIVE:
|
|
||||||
entity.center += Vector3(origin_comps[0], origin_comps[1], origin_comps[2])
|
|
||||||
|
|
||||||
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:= map_data.entities[e]
|
|
||||||
var entity_geo:= map_data.entity_geo[e]
|
|
||||||
|
|
||||||
for b in range(entity.brushes.size()):
|
|
||||||
var brush:= entity.brushes[b]
|
|
||||||
var brush_geo:= entity_geo.brushes[b]
|
|
||||||
|
|
||||||
for f in range(brush.faces.size()):
|
|
||||||
var face:= brush.faces[f]
|
|
||||||
var face_geo:= 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:= map_data.entity_geo[e]
|
|
||||||
|
|
||||||
for b in range(entity_geo.brushes.size()):
|
|
||||||
var brush_geo:= entity_geo.brushes[b]
|
|
||||||
|
|
||||||
for f in range(brush_geo.faces.size()):
|
|
||||||
var face_geo:= 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)
|
|
||||||
|
|
||||||
func generate_brush_vertices(entity_idx: int, brush_idx: int) -> void:
|
|
||||||
var entity:= map_data.entities[entity_idx]
|
|
||||||
var brush:= entity.brushes[brush_idx]
|
|
||||||
var face_count: int = brush.faces.size()
|
|
||||||
|
|
||||||
var entity_geo:= map_data.entity_geo[entity_idx]
|
|
||||||
var brush_geo:= 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:= brush.faces[f0]
|
|
||||||
var face_geo:= brush_geo.faces[f0]
|
|
||||||
var texture:= 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 := 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
|
|
||||||
if phong:
|
|
||||||
var threshold:= cos((phong_angle + 0.01) * 0.0174533)
|
|
||||||
normal = face.plane_normal
|
|
||||||
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()
|
|
||||||
else:
|
|
||||||
normal = face.plane_normal
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
# returns null if no intersection, else intersection vertex.
|
|
||||||
func intersect_face(f0: FuncGodotMapData.FuncGodotFace, f1: FuncGodotMapData.FuncGodotFace, f2: FuncGodotMapData.FuncGodotFace):
|
|
||||||
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 < CMP_EPSILON:
|
|
||||||
return null
|
|
||||||
|
|
||||||
return (n1.cross(n2) * f0.plane_dist + n2.cross(n0) * f1.plane_dist + n0.cross(n1) * f2.plane_dist) / denom
|
|
||||||
|
|
||||||
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 get_entities() -> Array[FuncGodotMapData.FuncGodotEntityGeometry]:
|
|
||||||
return map_data.entity_geo
|
|
||||||
|
|
||||||
func get_brush_vertex_count(entity_idx: int, brush_idx: int) -> int:
|
|
||||||
var vertex_count: int = 0
|
|
||||||
var brush_geo:= map_data.entity_geo[entity_idx].brushes[brush_idx]
|
|
||||||
for face in brush_geo.faces:
|
|
||||||
vertex_count += face.vertices.size()
|
|
||||||
return vertex_count
|
|
||||||
|
|
||||||
func get_brush_index_count(entity_idx: int, brush_idx: int) -> int:
|
|
||||||
var index_count: int = 0
|
|
||||||
var brush_geo:= map_data.entity_geo[entity_idx].brushes[brush_idx]
|
|
||||||
for face in brush_geo.faces:
|
|
||||||
index_count += face.indicies.size()
|
|
||||||
return index_count
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://bc8yx7le80s04
|
|
||||||
@@ -1,135 +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_size(name: String, width: int, height: int) -> void:
|
|
||||||
for i in range(textures.size()):
|
|
||||||
if textures[i].name == name:
|
|
||||||
textures[i].width = width
|
|
||||||
textures[i].height = height
|
|
||||||
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) -> void:
|
|
||||||
for entity in entities:
|
|
||||||
if entity.properties.has("classname") and entity.properties["classname"] == classname:
|
|
||||||
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.IGNORE
|
|
||||||
|
|
||||||
func clear() -> void:
|
|
||||||
entities.clear()
|
|
||||||
entity_geo.clear()
|
|
||||||
textures.clear()
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------------------------------
|
|
||||||
# Nested Types
|
|
||||||
# --------------------------------------------------------------------------------------------------
|
|
||||||
enum FuncGodotEntitySpawnType {
|
|
||||||
WORLDSPAWN = 0,
|
|
||||||
MERGE_WORLDSPAWN = 1,
|
|
||||||
ENTITY = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
enum FuncGodotEntityOriginType {
|
|
||||||
IGNORE = 0,
|
|
||||||
ABSOLUTE = 1,
|
|
||||||
RELATIVE = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
func _init(in_name: String):
|
|
||||||
name = in_name
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://ct2p43hd44co8
|
|
||||||
@@ -1,307 +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_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 map: FileAccess = FileAccess.open(map_file, FileAccess.READ)
|
|
||||||
if map == null:
|
|
||||||
printerr("Error: Failed to open map file (" + map_file + ")")
|
|
||||||
return false
|
|
||||||
|
|
||||||
while not map.eof_reached():
|
|
||||||
var line: String = map.get_line()
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://baybsken3c4pi
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
class_name FuncGodotSurfaceGatherer extends RefCounted
|
|
||||||
|
|
||||||
var map_data: FuncGodotMapData
|
|
||||||
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 out_surfaces: Array[FuncGodotMapData.FuncGodotFaceGeometry]
|
|
||||||
|
|
||||||
func _init(in_map_data: FuncGodotMapData) -> void:
|
|
||||||
map_data = in_map_data
|
|
||||||
|
|
||||||
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 filter_entity(entity_idx: int) -> bool:
|
|
||||||
if entity_filter_idx != -1 and entity_idx != entity_filter_idx:
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
func filter_brush(entity_idx: int, brush_idx: int) -> bool:
|
|
||||||
var entity:= map_data.entities[entity_idx]
|
|
||||||
var brush:= entity.brushes[brush_idx]
|
|
||||||
|
|
||||||
# omit brushes that are fully-textured with clip
|
|
||||||
if clip_filter_texture_idx != -1:
|
|
||||||
var fully_textured: bool = true
|
|
||||||
for face in brush.faces:
|
|
||||||
if face.texture_idx != clip_filter_texture_idx:
|
|
||||||
fully_textured = false
|
|
||||||
break
|
|
||||||
|
|
||||||
if fully_textured:
|
|
||||||
return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
|
|
||||||
func filter_face(entity_idx: int, brush_idx: int, face_idx: int) -> bool:
|
|
||||||
var face:= map_data.entities[entity_idx].brushes[brush_idx].faces[face_idx]
|
|
||||||
var face_geo:= map_data.entity_geo[entity_idx].brushes[brush_idx].faces[face_idx]
|
|
||||||
|
|
||||||
if face_geo.vertices.size() < 3:
|
|
||||||
return true
|
|
||||||
|
|
||||||
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 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 index_offset: int = 0
|
|
||||||
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]
|
|
||||||
|
|
||||||
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()):
|
|
||||||
if filter_brush(e, b):
|
|
||||||
continue
|
|
||||||
|
|
||||||
var brush:= entity.brushes[b]
|
|
||||||
var brush_geo:= entity_geo.brushes[b]
|
|
||||||
|
|
||||||
if split_type == SurfaceSplitType.BRUSH:
|
|
||||||
index_offset = 0
|
|
||||||
surf = add_surface()
|
|
||||||
|
|
||||||
for f in range(brush.faces.size()):
|
|
||||||
var face_geo:= brush_geo.faces[f]
|
|
||||||
|
|
||||||
if filter_face(e, b, f):
|
|
||||||
continue
|
|
||||||
|
|
||||||
for v in range(face_geo.vertices.size()):
|
|
||||||
var vert:= face_geo.vertices[v].duplicate()
|
|
||||||
|
|
||||||
if entity.spawn_type == FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY:
|
|
||||||
vert.vertex -= entity.center
|
|
||||||
|
|
||||||
surf.vertices.append(vert)
|
|
||||||
|
|
||||||
for i in range((face_geo.vertices.size() - 2) * 3):
|
|
||||||
surf.indicies.append(face_geo.indicies[i] + index_offset)
|
|
||||||
|
|
||||||
index_offset += face_geo.vertices.size()
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# nested
|
|
||||||
enum SurfaceSplitType{
|
|
||||||
NONE,
|
|
||||||
ENTITY,
|
|
||||||
BRUSH
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://d3n8qk8adx3yp
|
|
||||||
567
addons/func_godot/src/core/geometry_generator.gd
Normal file
567
addons/func_godot/src/core/geometry_generator.gd
Normal file
@@ -0,0 +1,567 @@
|
|||||||
|
@icon("res://addons/func_godot/icons/icon_slipgate.svg")
|
||||||
|
class_name FuncGodotGeometryGenerator extends RefCounted
|
||||||
|
## Geometry generation class that is instantiated by a [FuncGodotMap] node.
|
||||||
|
|
||||||
|
const _SIGNATURE: String = "[GEO]"
|
||||||
|
|
||||||
|
# Namespacing
|
||||||
|
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
|
||||||
|
const _EntityData := FuncGodotData.EntityData
|
||||||
|
const _BrushData := FuncGodotData.BrushData
|
||||||
|
const _PatchData := FuncGodotData.PatchData
|
||||||
|
const _FaceData := FuncGodotData.FaceData
|
||||||
|
const _VertexGroupData := FuncGodotData.VertexGroupData
|
||||||
|
|
||||||
|
# Class members
|
||||||
|
var map_settings: FuncGodotMapSettings = null
|
||||||
|
var entity_data: Array[_EntityData]
|
||||||
|
var texture_materials: Dictionary[String, Material]
|
||||||
|
var texture_sizes: Dictionary[String, Vector2]
|
||||||
|
|
||||||
|
# Signals
|
||||||
|
|
||||||
|
## Emitted when beginning a new step of the generation process.
|
||||||
|
signal declare_step(step: String)
|
||||||
|
|
||||||
|
func _init(settings: FuncGodotMapSettings = null) -> void:
|
||||||
|
map_settings = settings
|
||||||
|
|
||||||
|
#region TOOLS
|
||||||
|
func is_skip(face: _FaceData) -> bool:
|
||||||
|
return FuncGodotUtil.is_skip(face.texture, map_settings)
|
||||||
|
|
||||||
|
func is_clip(face: _FaceData) -> bool:
|
||||||
|
return FuncGodotUtil.is_clip(face.texture, map_settings)
|
||||||
|
|
||||||
|
func is_origin(face: _FaceData) -> bool:
|
||||||
|
return FuncGodotUtil.is_origin(face.texture, map_settings)
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region PATCHES
|
||||||
|
func sample_bezier_curve(controls: Array[Vector3], t: float) -> Vector3:
|
||||||
|
var points: Array[Vector3] = controls.duplicate()
|
||||||
|
for i in controls.size():
|
||||||
|
for j in controls.size() - 1 - i:
|
||||||
|
points[j] = points[j].lerp(points[j + 1], t)
|
||||||
|
return points[0]
|
||||||
|
|
||||||
|
func sample_bezier_surface(controls: Array[Vector3], width: int, height: int, u: float, v: float) -> Vector3:
|
||||||
|
var curve: Array[Vector3] = []
|
||||||
|
for x in range(width):
|
||||||
|
var col: Array[Vector3] = []
|
||||||
|
for y in range(height):
|
||||||
|
var idx := y * width + x
|
||||||
|
col.append(controls[idx])
|
||||||
|
curve.append(sample_bezier_curve(col, v))
|
||||||
|
return sample_bezier_curve(curve, u)
|
||||||
|
|
||||||
|
# Generate patch triangle indices
|
||||||
|
func get_triangle_indices(width: int, height: int) -> Array[int]:
|
||||||
|
var indices: Array[int] = []
|
||||||
|
if width < 2 or height < 2:
|
||||||
|
return indices
|
||||||
|
|
||||||
|
for row in range(height - 1):
|
||||||
|
for col in range(width - 1):
|
||||||
|
## First triangle of the square; top left, top right, bottom left
|
||||||
|
indices.append(col + row * width)
|
||||||
|
indices.append((col + 1) + row * width)
|
||||||
|
indices.append(col + (row + 1) * width)
|
||||||
|
|
||||||
|
## Second triangle of the square; top right, bottom right, bottom left
|
||||||
|
indices.append((col + 1) + row * width)
|
||||||
|
indices.append((col + 1) + (row + 1) * width)
|
||||||
|
indices.append(col + (row + 1) * width)
|
||||||
|
return indices
|
||||||
|
|
||||||
|
func create_patch_mesh(data: Array[_PatchData], mesh: Mesh):
|
||||||
|
return
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region BRUSHES
|
||||||
|
func generate_base_winding(plane: Plane) -> PackedVector3Array:
|
||||||
|
var up := Vector3.UP
|
||||||
|
if abs(plane.normal.dot(up)) > 0.9:
|
||||||
|
up = Vector3.RIGHT
|
||||||
|
|
||||||
|
var right: Vector3 = plane.normal.cross(up).normalized()
|
||||||
|
var forward: Vector3 = right.cross(plane.normal).normalized()
|
||||||
|
var centroid: Vector3 = plane.get_center()
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
return winding
|
||||||
|
|
||||||
|
func generate_face_vertices(brush: _BrushData, face_index: int, vertex_merge_distance: float = 0.0) -> PackedVector3Array:
|
||||||
|
var plane: Plane = brush.faces[face_index].plane
|
||||||
|
|
||||||
|
# Generate initial square polygon to clip other planes against
|
||||||
|
var winding: PackedVector3Array = generate_base_winding(plane)
|
||||||
|
|
||||||
|
for other_face_index in brush.faces.size():
|
||||||
|
if other_face_index == face_index:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# NOTE: This may need to be recentered to the origin, then moved back to the correct face position
|
||||||
|
# This problem may arise from floating point inaccuracy, given a large enough initial brush
|
||||||
|
winding = Geometry3D.clip_polygon(winding, brush.faces[other_face_index].plane)
|
||||||
|
if winding.is_empty():
|
||||||
|
break
|
||||||
|
|
||||||
|
# Reduce seams between vertices
|
||||||
|
for i in winding.size():
|
||||||
|
winding.set(i, winding.get(i).snappedf(vertex_merge_distance))
|
||||||
|
|
||||||
|
return winding
|
||||||
|
|
||||||
|
func generate_brush_vertices(entity_index: int, brush_index: int) -> void:
|
||||||
|
var entity: _EntityData = entity_data[entity_index]
|
||||||
|
var brush: _BrushData = entity.brushes[brush_index]
|
||||||
|
var vertex_merge_distance: float = entity.properties.get(map_settings.vertex_merge_distance_property, 0.0) as float
|
||||||
|
|
||||||
|
for face_index in brush.faces.size():
|
||||||
|
var face: _FaceData = brush.faces[face_index]
|
||||||
|
face.vertices = generate_face_vertices(brush, face_index, vertex_merge_distance)
|
||||||
|
|
||||||
|
face.normals.resize(face.vertices.size())
|
||||||
|
face.normals.fill(face.plane.normal)
|
||||||
|
|
||||||
|
var tangent: PackedFloat32Array = FuncGodotUtil.get_face_tangent(face)
|
||||||
|
|
||||||
|
# convert into OpenGL coordinates
|
||||||
|
for i in face.vertices.size():
|
||||||
|
face.tangents.append(tangent[1]) # Y
|
||||||
|
face.tangents.append(tangent[2]) # Z
|
||||||
|
face.tangents.append(tangent[0]) # X
|
||||||
|
face.tangents.append(tangent[3]) # W
|
||||||
|
return
|
||||||
|
|
||||||
|
func generate_entity_vertices(entity_index: int) -> void:
|
||||||
|
var entity: _EntityData = entity_data[entity_index]
|
||||||
|
for brush_index in entity.brushes.size():
|
||||||
|
generate_brush_vertices(entity_index, brush_index)
|
||||||
|
|
||||||
|
func determine_entity_origins(entity_index: int) -> void:
|
||||||
|
var entity: _EntityData = entity_data[entity_index]
|
||||||
|
var origin_type := _OriginType.BRUSH
|
||||||
|
|
||||||
|
if entity.definition is not FuncGodotFGDSolidClass:
|
||||||
|
if entity.brushes.is_empty():
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
origin_type = entity.definition.origin_type
|
||||||
|
|
||||||
|
if entity_index == 0:
|
||||||
|
entity.origin = Vector3.ZERO
|
||||||
|
return
|
||||||
|
|
||||||
|
var entity_mins: Vector3 = Vector3.INF
|
||||||
|
var entity_maxs: Vector3 = Vector3.INF
|
||||||
|
var origin_mins: Vector3 = Vector3.INF
|
||||||
|
var origin_maxs: Vector3 = -Vector3.INF
|
||||||
|
|
||||||
|
for brush in entity.brushes:
|
||||||
|
for face in brush.faces:
|
||||||
|
for vertex in face.vertices:
|
||||||
|
if entity_mins != Vector3.INF:
|
||||||
|
entity_mins = entity_mins.min(vertex)
|
||||||
|
else:
|
||||||
|
entity_mins = vertex
|
||||||
|
if entity_maxs != Vector3.INF:
|
||||||
|
entity_maxs = entity_maxs.max(vertex)
|
||||||
|
else:
|
||||||
|
entity_maxs = vertex
|
||||||
|
|
||||||
|
if brush.origin:
|
||||||
|
if origin_mins != Vector3.INF:
|
||||||
|
origin_mins = origin_mins.min(vertex)
|
||||||
|
else:
|
||||||
|
origin_mins = vertex
|
||||||
|
if origin_maxs != Vector3.INF:
|
||||||
|
origin_maxs = origin_maxs.max(vertex)
|
||||||
|
else:
|
||||||
|
origin_maxs = vertex
|
||||||
|
|
||||||
|
# Default origin type is BOUNDS_CENTER
|
||||||
|
if entity_maxs != Vector3.INF and entity_mins != Vector3.INF:
|
||||||
|
entity.origin = entity_maxs - ((entity_maxs - entity_mins) * 0.5)
|
||||||
|
|
||||||
|
if origin_type != _OriginType.BOUNDS_CENTER and entity.brushes.size() > 0:
|
||||||
|
match origin_type:
|
||||||
|
_OriginType.ABSOLUTE, _OriginType.RELATIVE:
|
||||||
|
if "origin" in entity.properties:
|
||||||
|
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])
|
||||||
|
else: # _OriginType.RELATIVE
|
||||||
|
entity.origin += Vector3(origin_comps[0], origin_comps[1], origin_comps[2])
|
||||||
|
|
||||||
|
_OriginType.BRUSH:
|
||||||
|
if origin_mins != Vector3.INF:
|
||||||
|
entity.origin = origin_maxs - ((origin_maxs - origin_mins) * 0.5)
|
||||||
|
|
||||||
|
_OriginType.BOUNDS_MINS:
|
||||||
|
entity.origin = entity_mins
|
||||||
|
|
||||||
|
_OriginType.BOUNDS_MAXS:
|
||||||
|
entity.origin = entity_maxs
|
||||||
|
|
||||||
|
_OriginType.AVERAGED:
|
||||||
|
entity.origin = Vector3.ZERO
|
||||||
|
var vertices: PackedVector3Array
|
||||||
|
for brush in entity.brushes:
|
||||||
|
for face in brush.faces:
|
||||||
|
vertices.append_array(face.vertices)
|
||||||
|
entity.origin = FuncGodotUtil.op_vec3_avg(vertices)
|
||||||
|
|
||||||
|
func wind_entity_faces(entity_index: int) -> void:
|
||||||
|
var entity: _EntityData = entity_data[entity_index]
|
||||||
|
for brush in entity.brushes:
|
||||||
|
for face in brush.faces:
|
||||||
|
# Faces should already be wound from the new generation process, but this should be tested further first.
|
||||||
|
face.wind()
|
||||||
|
face.index_vertices()
|
||||||
|
|
||||||
|
func smooth_entity_vertices(entity_index: int) -> void:
|
||||||
|
var entity: _EntityData = entity_data[entity_index]
|
||||||
|
if not entity.is_smooth_shaded(map_settings.entity_smoothing_property):
|
||||||
|
return
|
||||||
|
|
||||||
|
var smoothing_angle: float = deg_to_rad(entity.get_smoothing_angle(map_settings.entity_smoothing_angle_property))
|
||||||
|
var vertex_map: Dictionary[Vector3, _VertexGroupData] = {}
|
||||||
|
|
||||||
|
# Group vertices by position and build map. NOTE: Vector3 keys can suffer from floating point precision.
|
||||||
|
# However, the vertex position should have already been snapped to _VERTEX_EPSILON.
|
||||||
|
for brush in entity.brushes:
|
||||||
|
for face in brush.faces:
|
||||||
|
for i in face.vertices.size():
|
||||||
|
var pos := face.vertices[i].snappedf(_VERTEX_EPSILON)
|
||||||
|
|
||||||
|
if not vertex_map.has(pos):
|
||||||
|
vertex_map[pos] = _VertexGroupData.new()
|
||||||
|
|
||||||
|
var data := vertex_map[pos]
|
||||||
|
data.faces.append(face)
|
||||||
|
data.face_indices.append(i)
|
||||||
|
|
||||||
|
var smoothed_normals: PackedVector3Array
|
||||||
|
|
||||||
|
for vertex_group in vertex_map.values():
|
||||||
|
if vertex_group.faces.size() <= 1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Collect final normals in a temporary arrays
|
||||||
|
# These cannot be applied until all original normals have been checked.
|
||||||
|
smoothed_normals = []
|
||||||
|
|
||||||
|
for i in vertex_group.faces.size():
|
||||||
|
var this_face: _FaceData = vertex_group.faces[i]
|
||||||
|
var this_index: int = vertex_group.face_indices[i]
|
||||||
|
var this_normal: Vector3 = this_face.normals[this_index]
|
||||||
|
var average_normal: Vector3 = this_normal
|
||||||
|
|
||||||
|
for j in vertex_group.faces.size():
|
||||||
|
# Skip this face
|
||||||
|
if i == j:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var other_face: _FaceData = vertex_group.faces[j]
|
||||||
|
var other_index: int = vertex_group.face_indices[j]
|
||||||
|
var other_normal: Vector3 = other_face.normals[other_index]
|
||||||
|
|
||||||
|
if this_normal.angle_to(other_normal) <= smoothing_angle:
|
||||||
|
average_normal += other_normal
|
||||||
|
|
||||||
|
# Store the averaged normal
|
||||||
|
smoothed_normals.append(average_normal.normalized())
|
||||||
|
|
||||||
|
# Apply smoothed normals back to face data
|
||||||
|
for i in vertex_group.faces.size():
|
||||||
|
var face: _FaceData = vertex_group.faces[i]
|
||||||
|
var index: int = vertex_group.face_indices[i]
|
||||||
|
face.normals[index] = smoothed_normals[i]
|
||||||
|
return
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
func generate_entity_surfaces(entity_index: int) -> void:
|
||||||
|
var entity: _EntityData = entity_data[entity_index]
|
||||||
|
|
||||||
|
# Don't build for non-solid classes or solids without any brushes.
|
||||||
|
if not entity or entity.brushes.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var def: FuncGodotFGDSolidClass
|
||||||
|
if entity.definition is not FuncGodotFGDSolidClass:
|
||||||
|
def = FuncGodotFGDSolidClass.new()
|
||||||
|
else:
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Surface groupings <texture_name, Array[Face]>
|
||||||
|
var surfaces: Dictionary[String, Array] = {}
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
var current_metadata_index: int = 0
|
||||||
|
var texture_names_metadata: Array[StringName] = []
|
||||||
|
var textures_metadata: PackedInt32Array = []
|
||||||
|
var vertices_metadata: PackedVector3Array = []
|
||||||
|
var normals_metadata: PackedVector3Array = []
|
||||||
|
var positions_metadata: PackedVector3Array = []
|
||||||
|
var shape_to_face_metadata: Array[PackedInt32Array] = []
|
||||||
|
var face_index_metadata_map: Dictionary[_FaceData, PackedInt32Array] = {}
|
||||||
|
|
||||||
|
# Arrange faces by surface texture
|
||||||
|
for brush in entity.brushes:
|
||||||
|
for face in brush.faces:
|
||||||
|
if is_skip(face) or is_origin(face):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not surfaces.has(face.texture):
|
||||||
|
surfaces[face.texture] = []
|
||||||
|
surfaces[face.texture].append(face)
|
||||||
|
|
||||||
|
# Cache order for consistency when rebuilding
|
||||||
|
var textures: Array[String] = surfaces.keys()
|
||||||
|
|
||||||
|
# Output mesh data
|
||||||
|
var mesh := ArrayMesh.new()
|
||||||
|
var mesh_arrays: Array[Array] = []
|
||||||
|
var build_concave: bool = entity.is_collision_concave()
|
||||||
|
var concave_vertices: PackedVector3Array
|
||||||
|
|
||||||
|
# Iteration variables
|
||||||
|
var arrays: Array
|
||||||
|
var faces: Array
|
||||||
|
|
||||||
|
# MULTISURFACE SCOPE BEGIN
|
||||||
|
for texture_name in textures:
|
||||||
|
# SURFACE SCOPE BEGIN
|
||||||
|
faces = surfaces[texture_name]
|
||||||
|
|
||||||
|
# Get texture index for metadata
|
||||||
|
var tex_index: int = texture_names_metadata.size()
|
||||||
|
if def.add_textures_metadata:
|
||||||
|
texture_names_metadata.append(texture_name)
|
||||||
|
|
||||||
|
# Prepare new array
|
||||||
|
arrays = Array()
|
||||||
|
arrays.resize(ArrayMesh.ARRAY_MAX)
|
||||||
|
arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array()
|
||||||
|
arrays[Mesh.ARRAY_NORMAL] = PackedVector3Array()
|
||||||
|
arrays[Mesh.ARRAY_TANGENT] = PackedFloat32Array()
|
||||||
|
arrays[Mesh.ARRAY_TEX_UV] = PackedVector2Array()
|
||||||
|
arrays[Mesh.ARRAY_INDEX] = PackedInt32Array()
|
||||||
|
|
||||||
|
# Begin fresh index offset for this subarray
|
||||||
|
var index_offset: int = 0
|
||||||
|
|
||||||
|
for face in faces:
|
||||||
|
# FACE SCOPE BEGIN
|
||||||
|
|
||||||
|
# Reject invalid faces
|
||||||
|
if face.vertices.size() < 3 or is_skip(face) or is_origin(face):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Create trimesh points regardless of texture
|
||||||
|
if build_concave:
|
||||||
|
var tris: PackedVector3Array
|
||||||
|
tris.resize(face.indices.size())
|
||||||
|
|
||||||
|
# Add triangles from face indices directly
|
||||||
|
# TODO: This can possibly be merged with the below loop in a clever way
|
||||||
|
for i in face.indices.size():
|
||||||
|
tris[i] = op_entity_ogl_xf.call(face.vertices[face.indices[i]])
|
||||||
|
|
||||||
|
concave_vertices.append_array(tris)
|
||||||
|
|
||||||
|
# Do not generate visuals for clip textures
|
||||||
|
if is_clip(face):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle metadata for this face
|
||||||
|
# Add metadata per triangle rather than per face to keep consistent metadata
|
||||||
|
var num_tris = face.indices.size() / 3
|
||||||
|
if def.add_textures_metadata:
|
||||||
|
var tex_array: Array[int] = []
|
||||||
|
tex_array.resize(num_tris)
|
||||||
|
tex_array.fill(tex_index)
|
||||||
|
textures_metadata.append_array(tex_array)
|
||||||
|
if def.add_face_normal_metadata:
|
||||||
|
var normal_array: Array[Vector3] = []
|
||||||
|
normal_array.resize(num_tris)
|
||||||
|
normal_array.fill(FuncGodotUtil.id_to_opengl(face.plane.normal))
|
||||||
|
normals_metadata.append_array(normal_array)
|
||||||
|
if def.add_face_position_metadata:
|
||||||
|
for i in num_tris:
|
||||||
|
var triangle_indices: Array[int] = []
|
||||||
|
var triangle_vertices: Array[Vector3] = []
|
||||||
|
triangle_indices.assign(face.indices.slice(i * 3, i * 3 + 3))
|
||||||
|
triangle_vertices.assign(triangle_indices.map(func(idx : int) -> Vector3: return face.vertices[idx]))
|
||||||
|
var position := FuncGodotUtil.op_vec3_avg(triangle_vertices)
|
||||||
|
positions_metadata.append(op_entity_ogl_xf.call(position))
|
||||||
|
if def.add_vertex_metadata:
|
||||||
|
for i in face.indices:
|
||||||
|
vertices_metadata.append(op_entity_ogl_xf.call(face.vertices[i]))
|
||||||
|
if def.add_collision_shape_to_face_indices_metadata:
|
||||||
|
face_index_metadata_map[face] = PackedInt32Array(range(current_metadata_index, current_metadata_index + num_tris))
|
||||||
|
current_metadata_index += num_tris
|
||||||
|
|
||||||
|
# Append face data to surface array
|
||||||
|
for i in face.vertices.size():
|
||||||
|
# TODO: Mesh metadata may be generated here.
|
||||||
|
var v: Vector3 = face.vertices[i]
|
||||||
|
arrays[ArrayMesh.ARRAY_VERTEX].append(op_entity_ogl_xf.call(v))
|
||||||
|
arrays[ArrayMesh.ARRAY_NORMAL].append(FuncGodotUtil.id_to_opengl(face.normals[i]))
|
||||||
|
var tx_sz: Vector2 = texture_sizes.get(face.texture, Vector2.ONE * map_settings.inverse_scale_factor)
|
||||||
|
arrays[ArrayMesh.ARRAY_TEX_UV].append(FuncGodotUtil.get_face_vertex_uv(v, face, tx_sz))
|
||||||
|
|
||||||
|
for j in 4:
|
||||||
|
arrays[ArrayMesh.ARRAY_TANGENT].append(face.tangents[(i * 4) + j])
|
||||||
|
|
||||||
|
# Create offset indices for the visual mesh
|
||||||
|
var op_shift_index: Callable = (func(a: int) -> int: return a + index_offset)
|
||||||
|
arrays[ArrayMesh.ARRAY_INDEX].append_array(Array(face.indices).map(op_shift_index))
|
||||||
|
|
||||||
|
index_offset += face.vertices.size()
|
||||||
|
|
||||||
|
# FACE SCOPE END
|
||||||
|
|
||||||
|
if FuncGodotUtil.filter_face(texture_name, map_settings):
|
||||||
|
continue
|
||||||
|
|
||||||
|
mesh_arrays.append(arrays)
|
||||||
|
|
||||||
|
# SURFACE SCOPE END
|
||||||
|
|
||||||
|
# MULTISURFACE SCOPE END
|
||||||
|
textures.erase(map_settings.clip_texture)
|
||||||
|
|
||||||
|
if def.build_visuals:
|
||||||
|
# Build mesh
|
||||||
|
for array_index in mesh_arrays.size():
|
||||||
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, mesh_arrays[array_index])
|
||||||
|
mesh.surface_set_name(array_index, textures[array_index])
|
||||||
|
mesh.surface_set_material(array_index, texture_materials[textures[array_index]])
|
||||||
|
|
||||||
|
# Apply mesh metadata
|
||||||
|
if def.add_textures_metadata:
|
||||||
|
entity.mesh_metadata["texture_names"] = texture_names_metadata
|
||||||
|
entity.mesh_metadata["textures"] = textures_metadata
|
||||||
|
if def.add_vertex_metadata:
|
||||||
|
entity.mesh_metadata["vertices"] = vertices_metadata
|
||||||
|
if def.add_face_normal_metadata:
|
||||||
|
entity.mesh_metadata["normals"] = normals_metadata
|
||||||
|
if def.add_face_position_metadata:
|
||||||
|
entity.mesh_metadata["positions"] = positions_metadata
|
||||||
|
|
||||||
|
entity.mesh = mesh
|
||||||
|
|
||||||
|
# Clear up unusued memory
|
||||||
|
arrays = []
|
||||||
|
surfaces = {}
|
||||||
|
|
||||||
|
if entity.is_collision_convex():
|
||||||
|
var sh: ConvexPolygonShape3D
|
||||||
|
for b in entity.brushes:
|
||||||
|
if b.planes.is_empty() or b.origin:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var points := Array(Geometry3D.compute_convex_mesh_points(b.planes)).map(op_entity_ogl_xf)
|
||||||
|
if points.is_empty():
|
||||||
|
continue
|
||||||
|
|
||||||
|
sh = ConvexPolygonShape3D.new()
|
||||||
|
sh.points = points
|
||||||
|
entity.shapes.append(sh)
|
||||||
|
|
||||||
|
if def.add_collision_shape_to_face_indices_metadata:
|
||||||
|
# convex collision has one shape per brush, so collect the
|
||||||
|
# indices for this brush's faces
|
||||||
|
var face_indices_array : PackedInt32Array = []
|
||||||
|
for face in b.faces:
|
||||||
|
if face_index_metadata_map.has(face):
|
||||||
|
face_indices_array.append_array(face_index_metadata_map[face])
|
||||||
|
shape_to_face_metadata.append(face_indices_array)
|
||||||
|
|
||||||
|
elif build_concave and concave_vertices.size():
|
||||||
|
var sh := ConcavePolygonShape3D.new()
|
||||||
|
sh.set_faces(concave_vertices)
|
||||||
|
entity.shapes.append(sh)
|
||||||
|
|
||||||
|
if def.add_collision_shape_to_face_indices_metadata:
|
||||||
|
# for concave collision the shape will always represent every face
|
||||||
|
# in the entity, so just add every face here
|
||||||
|
var face_indices_array : PackedInt32Array = []
|
||||||
|
for fm in face_index_metadata_map.values():
|
||||||
|
face_indices_array.append_array(fm)
|
||||||
|
shape_to_face_metadata.append(face_indices_array)
|
||||||
|
|
||||||
|
if def.add_collision_shape_to_face_indices_metadata:
|
||||||
|
# this metadata will be mapped to the actual shape node names during entity assembly
|
||||||
|
entity.mesh_metadata["shape_to_face_array"] = shape_to_face_metadata
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Main build process
|
||||||
|
func build(build_flags: int, entities: Array[_EntityData]) -> Error:
|
||||||
|
var entity_count: int = entities.size()
|
||||||
|
declare_step.emit("Preparing %s %s" % [entity_count, "entity" if entity_count == 1 else "entities"])
|
||||||
|
entity_data = entities
|
||||||
|
|
||||||
|
declare_step.emit("Gathering materials")
|
||||||
|
var texture_map: Array[Dictionary] = FuncGodotUtil.build_texture_map(entity_data, map_settings)
|
||||||
|
texture_materials = texture_map[0]
|
||||||
|
texture_sizes = texture_map[1]
|
||||||
|
|
||||||
|
var task_id: int
|
||||||
|
declare_step.emit("Generating brush vertices")
|
||||||
|
task_id = WorkerThreadPool.add_group_task(generate_entity_vertices, entity_count, -1, false, "Generate Brush Vertices")
|
||||||
|
WorkerThreadPool.wait_for_group_task_completion(task_id)
|
||||||
|
|
||||||
|
declare_step.emit("Determining solid entity origins")
|
||||||
|
task_id = WorkerThreadPool.add_group_task(determine_entity_origins, entity_count, -1, false, "Determine Entity Origins")
|
||||||
|
WorkerThreadPool.wait_for_group_task_completion(task_id)
|
||||||
|
|
||||||
|
declare_step.emit("Winding faces")
|
||||||
|
task_id = WorkerThreadPool.add_group_task(wind_entity_faces, entity_count, -1, false, "Wind Brush Faces")
|
||||||
|
WorkerThreadPool.wait_for_group_task_completion(task_id)
|
||||||
|
|
||||||
|
# TODO: Reimplement after solving issues
|
||||||
|
#if not (build_flags & FuncGodotMap.BuildFlags.DISABLE_SMOOTHING):
|
||||||
|
# declare_step.emit("Smoothing entity faces")
|
||||||
|
# task_id = WorkerThreadPool.add_group_task(smooth_entity_vertices, entity_count, -1, false, "Smooth Entities")
|
||||||
|
# WorkerThreadPool.wait_for_group_task_completion(task_id)
|
||||||
|
|
||||||
|
declare_step.emit("Generating surfaces")
|
||||||
|
task_id = WorkerThreadPool.add_group_task(generate_entity_surfaces, entity_count, -1, false, "Generate Surfaces")
|
||||||
|
WorkerThreadPool.wait_for_group_task_completion(task_id)
|
||||||
|
|
||||||
|
if build_flags & FuncGodotMap.BuildFlags.UNWRAP_UV2:
|
||||||
|
declare_step.emit("Unwrapping UV2s")
|
||||||
|
var texel_size: float = map_settings.uv_unwrap_texel_size * map_settings.scale_factor
|
||||||
|
for entity_index in entity_count:
|
||||||
|
unwrap_uv2s(entity_index, texel_size)
|
||||||
|
|
||||||
|
declare_step.emit("Geometry generation complete")
|
||||||
|
return OK
|
||||||
1
addons/func_godot/src/core/geometry_generator.gd.uid
Normal file
1
addons/func_godot/src/core/geometry_generator.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b1yg28xbyno7v
|
||||||
439
addons/func_godot/src/core/parser.gd
Normal file
439
addons/func_godot/src/core/parser.gd
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
@icon("res://addons/func_godot/icons/icon_godambler.svg")
|
||||||
|
class_name FuncGodotParser extends RefCounted
|
||||||
|
## MAP and VMF parser class that is instantiated by a [FuncGodotMap] node during the build process.
|
||||||
|
##
|
||||||
|
## @tutorial(Quake Wiki Map Format Article): https://quakewiki.org/wiki/Quake_Map_Format
|
||||||
|
## @tutorial(Valve Developer Wiki VMF Article): https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)
|
||||||
|
|
||||||
|
const _SIGNATURE: String = "[PRS]"
|
||||||
|
|
||||||
|
const _GroupData := FuncGodotData.GroupData
|
||||||
|
const _EntityData := FuncGodotData.EntityData
|
||||||
|
const _BrushData := FuncGodotData.BrushData
|
||||||
|
const _PatchData := FuncGodotData.PatchData
|
||||||
|
const _FaceData := FuncGodotData.FaceData
|
||||||
|
const _ParseData := FuncGodotData.ParseData
|
||||||
|
|
||||||
|
## Emitted when a step in the parsing process is completed.
|
||||||
|
## It is connected to [method FuncGodotUtil.print_profile_info] method if [member FuncGodotMap.build_flags] SHOW_PROFILE_INFO flag is set.
|
||||||
|
signal declare_step(step: String)
|
||||||
|
|
||||||
|
## Parses the map file, generating entity and group data and sub-data, then returns the generated data as an array of arrays.
|
||||||
|
## The first array is Array[FuncGodotData.EntityData], while the second array is Array[FuncGodotData.GroupData].
|
||||||
|
func parse_map_data(map_file: String, map_settings: FuncGodotMapSettings) -> _ParseData:
|
||||||
|
var map_data: PackedStringArray = []
|
||||||
|
var parse_data := _ParseData.new()
|
||||||
|
declare_step.emit("Loading map file %s" % map_file)
|
||||||
|
|
||||||
|
# Retrieve real path if needed
|
||||||
|
if map_file.begins_with("uid://"):
|
||||||
|
var uid := ResourceUID.text_to_id(map_file)
|
||||||
|
if not ResourceUID.has_id(uid):
|
||||||
|
printerr("Error: failed to retrieve path for UID (%s)" % map_file)
|
||||||
|
return parse_data
|
||||||
|
map_file = ResourceUID.get_id_path(uid)
|
||||||
|
|
||||||
|
# Open the map file
|
||||||
|
var file: FileAccess = FileAccess.open(map_file, FileAccess.READ)
|
||||||
|
if not file:
|
||||||
|
file = FileAccess.open(map_file + ".import", FileAccess.READ)
|
||||||
|
if file:
|
||||||
|
map_file += ".import"
|
||||||
|
else:
|
||||||
|
printerr("Error: Failed to open map file (" + map_file + ")")
|
||||||
|
return parse_data
|
||||||
|
|
||||||
|
# Packed map file resources need to be accessed differently in exported projects.
|
||||||
|
if map_file.ends_with(".import"):
|
||||||
|
while not file.eof_reached():
|
||||||
|
var line: String = file.get_line()
|
||||||
|
if line.begins_with("path"):
|
||||||
|
file.close()
|
||||||
|
line = line.replace("path=", "")
|
||||||
|
line = line.replace('"', '')
|
||||||
|
var data: String = (load(line) as QuakeMapFile).map_data
|
||||||
|
if data.is_empty():
|
||||||
|
printerr("Error: Failed to open map file (" + line + ")")
|
||||||
|
return parse_data
|
||||||
|
map_data = data.split("\n")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
while not file.eof_reached():
|
||||||
|
map_data.append(file.get_line())
|
||||||
|
|
||||||
|
# Determine map type and parse data
|
||||||
|
if map_file.to_lower().contains(".map"):
|
||||||
|
declare_step.emit("Parsing as Quake MAP")
|
||||||
|
parse_data = _parse_quake_map(map_data, map_settings, parse_data)
|
||||||
|
elif map_file.to_lower().contains(".vmf"):
|
||||||
|
declare_step.emit("Parsing as Source VMF")
|
||||||
|
parse_data = _parse_vmf(map_data, map_settings, parse_data)
|
||||||
|
|
||||||
|
# Determine group hierarchy
|
||||||
|
declare_step.emit("Determining groups hierarchy")
|
||||||
|
var groups_data: Array[_GroupData] = parse_data.groups
|
||||||
|
for g in groups_data:
|
||||||
|
if g.parent_id != -1:
|
||||||
|
for p in groups_data:
|
||||||
|
if p.id == g.parent_id:
|
||||||
|
g.parent = p
|
||||||
|
break
|
||||||
|
|
||||||
|
var entities_data: Array[_EntityData] = parse_data.entities
|
||||||
|
var entity_defs: Dictionary[String, FuncGodotFGDEntityClass] = map_settings.entity_fgd.get_entity_definitions()
|
||||||
|
|
||||||
|
declare_step.emit("Checking entity omission and definition status")
|
||||||
|
|
||||||
|
for i in range(entities_data.size() - 1, -1, -1):
|
||||||
|
var entity: _EntityData = entities_data[i]
|
||||||
|
|
||||||
|
# Delete entities from omitted groups
|
||||||
|
if entity.group != null and entity.group.omit == true:
|
||||||
|
entities_data.remove_at(i)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Provide entity definition to entity data. This gets used in both
|
||||||
|
# geo generation and entity assembly.
|
||||||
|
if "classname" in entity.properties:
|
||||||
|
var classname: String = entity.properties["classname"]
|
||||||
|
if classname in entity_defs:
|
||||||
|
entity.definition = entity_defs[classname]
|
||||||
|
|
||||||
|
# Delete omitted groups
|
||||||
|
declare_step.emit("Removing omitted layers and groups")
|
||||||
|
for i in range(groups_data.size() - 1, -1, -1):
|
||||||
|
if groups_data[i].omit == true:
|
||||||
|
groups_data.remove_at(i)
|
||||||
|
|
||||||
|
declare_step.emit("Map parsing complete")
|
||||||
|
return parse_data
|
||||||
|
|
||||||
|
## Parser subroutine called by [method parse_map_data], specializing in the Quake MAP format.
|
||||||
|
func _parse_quake_map(map_data: PackedStringArray, map_settings: FuncGodotMapSettings, parse_data: _ParseData) -> _ParseData:
|
||||||
|
var entities_data: Array[_EntityData] = parse_data.entities
|
||||||
|
var groups_data: Array[_GroupData] = parse_data.groups
|
||||||
|
var ent: _EntityData = null
|
||||||
|
var brush: _BrushData = null
|
||||||
|
var patch: _PatchData = null
|
||||||
|
var scope: int = 0 # Scope level, to keep track of where we are in PatchDef parsing
|
||||||
|
|
||||||
|
for line in map_data:
|
||||||
|
line = line.replace("\t", "")
|
||||||
|
|
||||||
|
#region START DATA
|
||||||
|
# Start entity, brush, or patchdef
|
||||||
|
if line.begins_with("{"):
|
||||||
|
if not ent:
|
||||||
|
ent = _EntityData.new()
|
||||||
|
else:
|
||||||
|
if not patch:
|
||||||
|
brush = _BrushData.new()
|
||||||
|
else:
|
||||||
|
scope += 1
|
||||||
|
continue
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region COMMIT DATA
|
||||||
|
# Commit entity or brush
|
||||||
|
if line.begins_with("}"):
|
||||||
|
if brush:
|
||||||
|
ent.brushes.append(brush)
|
||||||
|
brush = null
|
||||||
|
elif patch:
|
||||||
|
if scope:
|
||||||
|
scope -= 1
|
||||||
|
else:
|
||||||
|
ent.patches.append(patch)
|
||||||
|
patch = null
|
||||||
|
else:
|
||||||
|
# TrenchBroom layers and groups
|
||||||
|
if ent.properties["classname"] == "func_group" and ent.properties.has("_tb_type"):
|
||||||
|
# Merge TB Group / Layer structural brushes with worldspawn
|
||||||
|
if entities_data.size():
|
||||||
|
entities_data[0].brushes.append_array(ent.brushes)
|
||||||
|
|
||||||
|
# Create group data
|
||||||
|
var group: _GroupData = _GroupData.new()
|
||||||
|
var props: Dictionary = ent.properties
|
||||||
|
group.id = props["_tb_id"] as int
|
||||||
|
if props["_tb_type"] == "_tb_layer":
|
||||||
|
group.type = _GroupData.GroupType.GROUP
|
||||||
|
group.name = "layer_"
|
||||||
|
else:
|
||||||
|
group.name = "group_"
|
||||||
|
group.name = group.name + str(group.id)
|
||||||
|
if props["_tb_name"] != "Unnamed":
|
||||||
|
group.name = group.name + "_" + (props["_tb_name"] as String).replace(" ", "_")
|
||||||
|
if props.has("_tb_layer"):
|
||||||
|
group.parent_id = props["_tb_layer"] as int
|
||||||
|
if props.has("_tb_group"):
|
||||||
|
group.parent_id = props["_tb_group"] as int
|
||||||
|
if props.has("_tb_layer_omit_from_export"):
|
||||||
|
group.omit = true
|
||||||
|
|
||||||
|
# Commit group
|
||||||
|
groups_data.append(group)
|
||||||
|
|
||||||
|
# Commit entity
|
||||||
|
else:
|
||||||
|
entities_data.append(ent)
|
||||||
|
ent = null
|
||||||
|
continue
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region PROPERTY DATA
|
||||||
|
# Retrieve key value pairs
|
||||||
|
if line.begins_with("\""):
|
||||||
|
var tokens: PackedStringArray = line.split("\" \"")
|
||||||
|
if tokens.size() < 2:
|
||||||
|
tokens = line.split("\"\"")
|
||||||
|
var key: String = tokens[0].trim_prefix("\"")
|
||||||
|
var value: String = tokens[1].trim_suffix("\"")
|
||||||
|
ent.properties[key] = value
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region BRUSH DATA
|
||||||
|
if brush and line.begins_with("("):
|
||||||
|
line = line.replace("(","")
|
||||||
|
var tokens: PackedStringArray = line.split(" ) ")
|
||||||
|
|
||||||
|
# Retrieve plane data
|
||||||
|
var points: PackedVector3Array
|
||||||
|
points.resize(3)
|
||||||
|
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])
|
||||||
|
points[i] = point
|
||||||
|
|
||||||
|
var plane := Plane(points[0], points[1], points[2])
|
||||||
|
brush.planes.append(plane)
|
||||||
|
|
||||||
|
var face: _FaceData = _FaceData.new()
|
||||||
|
face.plane = plane
|
||||||
|
|
||||||
|
# Retrieve texture data
|
||||||
|
var tex: String = String()
|
||||||
|
if tokens[3].begins_with("\""): # textures with spaces get surrounded by double quotes
|
||||||
|
var last_quote := tokens[3].rfind("\"")
|
||||||
|
tex = tokens[3].substr(1, last_quote - 1)
|
||||||
|
tokens = tokens[3].substr(last_quote + 2).split(" ] ")
|
||||||
|
else:
|
||||||
|
tex = tokens[3].get_slice(" ", 0)
|
||||||
|
tokens = tokens[3].trim_prefix(tex + " ").split(" ] ")
|
||||||
|
face.texture = tex
|
||||||
|
|
||||||
|
# Check for origin brushes. Brushes must be completely textured with origin to be valid.
|
||||||
|
if brush.faces.is_empty():
|
||||||
|
if tex == map_settings.origin_texture:
|
||||||
|
brush.origin = true
|
||||||
|
elif brush.origin == true:
|
||||||
|
if tex != map_settings.origin_texture:
|
||||||
|
brush.origin = false
|
||||||
|
|
||||||
|
# Retrieve UV data
|
||||||
|
var uv: Transform2D = Transform2D.IDENTITY
|
||||||
|
|
||||||
|
# Valve 220: texname [ ux uy ux offsetX ] [vx vy vz offsetY] rotation scaleX scaleY
|
||||||
|
if tokens.size() > 1:
|
||||||
|
var coords: PackedFloat64Array
|
||||||
|
for i in 2:
|
||||||
|
coords = tokens[i].trim_prefix("[ ").split_floats(" ", false)
|
||||||
|
face.uv_axes.append(Vector3(coords[0], coords[1], coords[2])) # Save axis vectors separately
|
||||||
|
face.uv.origin[i] = coords[3] # UV offset stored as transform origin
|
||||||
|
|
||||||
|
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])
|
||||||
|
|
||||||
|
# Quake Standard: texname offsetX offsetY rotation scaleX scaleY
|
||||||
|
else:
|
||||||
|
var coords: PackedFloat64Array = tokens[0].split_floats(" ", false)
|
||||||
|
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]
|
||||||
|
|
||||||
|
brush.faces.append(face)
|
||||||
|
continue
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region PATCH DATA
|
||||||
|
if patch:
|
||||||
|
if line.begins_with("("):
|
||||||
|
line = line.replace("( ","")
|
||||||
|
# Retrieve patch control points
|
||||||
|
if patch.size:
|
||||||
|
var tokens: PackedStringArray = line.replace("(", "").split(" )", false)
|
||||||
|
for i in tokens.size():
|
||||||
|
var subtokens: PackedFloat64Array = tokens[i].split_floats(" ", false)
|
||||||
|
patch.points.append(Vector3(subtokens[0], subtokens[1], subtokens[2]))
|
||||||
|
patch.uvs.append(Vector2(subtokens[3], subtokens[4]))
|
||||||
|
# Retrieve patch size
|
||||||
|
else:
|
||||||
|
var tokens: PackedStringArray = line.replace(")","").split(" ", false)
|
||||||
|
patch.size.resize(tokens.size())
|
||||||
|
for i in tokens.size():
|
||||||
|
patch.size[i] = tokens[i].to_int()
|
||||||
|
# Retrieve patch texture
|
||||||
|
elif not line.begins_with(")"):
|
||||||
|
patch.texture = line.replace("\"","")
|
||||||
|
|
||||||
|
if line.begins_with("patchDef"):
|
||||||
|
brush = null
|
||||||
|
patch = _PatchData.new()
|
||||||
|
continue
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ASSIGN GROUPS
|
||||||
|
for e in entities_data:
|
||||||
|
var group_id: int = -1
|
||||||
|
if e.properties.has("_tb_layer"):
|
||||||
|
group_id = e.properties["_tb_layer"] as int
|
||||||
|
elif e.properties.has("_tb_group"):
|
||||||
|
group_id = e.properties["_tb_group"] as int
|
||||||
|
if group_id != -1:
|
||||||
|
for g in groups_data:
|
||||||
|
if g.id == group_id:
|
||||||
|
e.group = g
|
||||||
|
break
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
return parse_data
|
||||||
|
|
||||||
|
## Parser subroutine called by [method parse_map_data], specializing in the Valve Map Format used by Hammer based editors.
|
||||||
|
func _parse_vmf(map_data: PackedStringArray, map_settings: FuncGodotMapSettings, parse_data: _ParseData) -> _ParseData:
|
||||||
|
var entities_data: Array[_EntityData] = parse_data.entities
|
||||||
|
var groups_data: Array[_GroupData] = parse_data.groups
|
||||||
|
var ent: _EntityData = null
|
||||||
|
var brush: _BrushData = null
|
||||||
|
var group: _GroupData = null
|
||||||
|
var group_parent_hierarchy: Array[_GroupData] = []
|
||||||
|
var scope: int = 0
|
||||||
|
|
||||||
|
for line in map_data:
|
||||||
|
line = line.replace("\t", "")
|
||||||
|
|
||||||
|
#region START DATA
|
||||||
|
if line.begins_with("entity") or line.begins_with("world"):
|
||||||
|
ent = _EntityData.new()
|
||||||
|
continue
|
||||||
|
if line.begins_with("solid"):
|
||||||
|
brush = _BrushData.new()
|
||||||
|
continue
|
||||||
|
if brush and line.begins_with("{"):
|
||||||
|
scope += 1
|
||||||
|
continue
|
||||||
|
if line == "visgroup":
|
||||||
|
if group != null:
|
||||||
|
groups_data.append(group)
|
||||||
|
group_parent_hierarchy.append(group)
|
||||||
|
group = _GroupData.new()
|
||||||
|
if group_parent_hierarchy.size():
|
||||||
|
group.parent = group_parent_hierarchy.back()
|
||||||
|
group.parent_id = group.parent.id
|
||||||
|
continue
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region COMMIT DATA
|
||||||
|
if line.begins_with("}"):
|
||||||
|
if scope > 0:
|
||||||
|
scope -= 1
|
||||||
|
if not scope:
|
||||||
|
if brush:
|
||||||
|
if brush.faces.size():
|
||||||
|
ent.brushes.append(brush)
|
||||||
|
brush = null
|
||||||
|
elif ent:
|
||||||
|
entities_data.append(ent)
|
||||||
|
ent = null
|
||||||
|
elif group:
|
||||||
|
groups_data.append(group)
|
||||||
|
group = null
|
||||||
|
elif group_parent_hierarchy.size():
|
||||||
|
group_parent_hierarchy.pop_back()
|
||||||
|
continue
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
# Retrieve key value pairs
|
||||||
|
if (ent or group) and line.begins_with("\""):
|
||||||
|
var tokens: PackedStringArray = line.split("\" \"")
|
||||||
|
var key: String = tokens[0].trim_prefix("\"")
|
||||||
|
var value: String = tokens[1].trim_suffix("\"")
|
||||||
|
|
||||||
|
#region BRUSH DATA
|
||||||
|
if brush:
|
||||||
|
if scope > 1:
|
||||||
|
match key:
|
||||||
|
"plane":
|
||||||
|
tokens = value.replace("(", "").split(")", false)
|
||||||
|
var points: PackedVector3Array
|
||||||
|
points.resize(3)
|
||||||
|
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])
|
||||||
|
points[i] = point
|
||||||
|
brush.planes.append(Plane(points[0], points[1], points[2]))
|
||||||
|
brush.faces.append(_FaceData.new())
|
||||||
|
brush.faces[-1].plane = brush.planes[-1]
|
||||||
|
continue
|
||||||
|
"material":
|
||||||
|
if brush.faces.size():
|
||||||
|
brush.faces[-1].texture = value
|
||||||
|
# Origin brush needs to be completely set to origin, otherwise it's invalid
|
||||||
|
if brush.faces.size() < 2:
|
||||||
|
if value == map_settings.origin_texture:
|
||||||
|
brush.origin = true
|
||||||
|
elif brush.origin == true:
|
||||||
|
if value != map_settings.origin_texture:
|
||||||
|
brush.origin = false
|
||||||
|
continue
|
||||||
|
"uaxis", "vaxis":
|
||||||
|
if brush.faces.size():
|
||||||
|
value = value.replace("[", "")
|
||||||
|
var vals: PackedFloat64Array = value.replace("]", "").split_floats(" ", false)
|
||||||
|
var face: _FaceData = brush.faces[-1]
|
||||||
|
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
|
||||||
|
else:
|
||||||
|
face.uv.origin.y = vals[3] # Offset
|
||||||
|
face.uv.y *= vals[4] # Scale
|
||||||
|
continue
|
||||||
|
"rotation":
|
||||||
|
# Rotation isn't used in Valve 220 mapping and VMFs are 220 exclusive
|
||||||
|
continue
|
||||||
|
"visgroupid":
|
||||||
|
# Don't put worldspawn into a group
|
||||||
|
if entities_data.size():
|
||||||
|
# Only nodes can be organized into groups in the SceneTree, so only use the first brush's group
|
||||||
|
if not ent.properties.has(key):
|
||||||
|
ent.properties[key] = value
|
||||||
|
#endregion
|
||||||
|
elif ent:
|
||||||
|
ent.properties[key] = value
|
||||||
|
continue
|
||||||
|
elif group:
|
||||||
|
if key == "name":
|
||||||
|
group.name = "group_%s_" + value
|
||||||
|
elif key == "visgroupid":
|
||||||
|
group.id = value.to_int()
|
||||||
|
group.name = group.name % value
|
||||||
|
group.name = group.name.replace(" ", "_")
|
||||||
|
continue
|
||||||
|
|
||||||
|
#region ASSIGN GROUPS
|
||||||
|
for e in entities_data:
|
||||||
|
if e.properties.has("visgroupid"):
|
||||||
|
var group_id: int = e.properties["visgroupid"] as int
|
||||||
|
for g in groups_data:
|
||||||
|
if g.id == group_id:
|
||||||
|
e.group = g
|
||||||
|
break
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
return parse_data
|
||||||
1
addons/func_godot/src/core/parser.gd.uid
Normal file
1
addons/func_godot/src/core/parser.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dflet6p5hbqts
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
@tool
|
@tool
|
||||||
## Special inheritance class for [FuncGodotFGDSolidClass] and [FuncGodotFGDPointClass] entity definitions. Useful for adding shared or common properties and descriptions.
|
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
||||||
class_name FuncGodotFGDBaseClass
|
class_name FuncGodotFGDBaseClass extends FuncGodotFGDEntityClass
|
||||||
extends FuncGodotFGDEntityClass
|
## Special inheritance class for [FuncGodotFGDSolidClass] and [FuncGodotFGDPointClass] entity definitions.
|
||||||
|
##
|
||||||
|
## Inheritance class for [FuncGodotFGDSolidClass] and [FuncGodotFGDPointClass] entities,
|
||||||
|
## used to shared or common properties and descriptions across different definitions.
|
||||||
|
##
|
||||||
|
## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
|
||||||
|
## @tutorial(Level Design Book: Entity Types and Settings): https://book.leveldesignbook.com/appendix/resources/formats/fgd#entity-types-and-settings-basic
|
||||||
|
## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD#Class_Types_and_Properties
|
||||||
|
|
||||||
func _init() -> void:
|
func _init() -> void:
|
||||||
prefix = "@BaseClass"
|
prefix = "@BaseClass"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://dyv4g30betqgm
|
uid://ck575aqs1sbrb
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
||||||
## Base entity definition class. Not to be used directly, use [FuncGodotFGDBaseClass], [FuncGodotFGDSolidClass], or [FuncGodotFGDPointClass] instead.
|
class_name FuncGodotFGDEntityClass extends Resource
|
||||||
class_name FuncGodotFGDEntityClass
|
## Entity definition template. WARNING! Not to be used directly! Use [FuncGodotFGDBaseClass], [FuncGodotFGDSolidClass], or [FuncGodotFGDPointClass] instead.
|
||||||
extends Resource
|
##
|
||||||
|
## Entity definition template. It holds all of the common entity class properties shared between [FuncGodotFGDBaseClass], [FuncGodotFGDSolidClass], or [FuncGodotFGDPointClass].
|
||||||
|
## Not to be used directly, use one of the aforementioned FGD class types instead.
|
||||||
|
##
|
||||||
|
## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
|
||||||
|
## @tutorial(Level Design Book: Entity Types and Settings): https://book.leveldesignbook.com/appendix/resources/formats/fgd#entity-types-and-settings-basic
|
||||||
|
## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD#Class_Types_and_Properties
|
||||||
|
## @tutorial(Valve Developer Wiki Entity Descriptions): https://developer.valvesoftware.com/wiki/FGD#Entity_Description
|
||||||
|
|
||||||
var prefix: String = ""
|
var prefix: String = ""
|
||||||
|
|
||||||
@export_group("Entity Definition")
|
@export_group("Entity Definition")
|
||||||
|
|
||||||
## Entity classname. This is a required field in all entity types as it is parsed by both the map editor and by FuncGodot on map build.
|
## Entity classname. [b][i]This is a required field in all entity types[/i][/b] as it is parsed by both the map editor and by FuncGodot on map build.
|
||||||
@export var classname : String = ""
|
@export var classname : String = ""
|
||||||
|
|
||||||
## Entity description that appears in the map editor. Not required.
|
## Entity description that appears in the map editor. Not required.
|
||||||
@@ -16,16 +23,21 @@ var prefix: String = ""
|
|||||||
## Entity does not get written to the exported FGD. Entity is only used for [FuncGodotMap] build process.
|
## Entity does not get written to the exported FGD. Entity is only used for [FuncGodotMap] build process.
|
||||||
@export var func_godot_internal : bool = false
|
@export var func_godot_internal : bool = false
|
||||||
|
|
||||||
## FuncGodotFGDBaseClass resources to inherit [member class_properties] and [member class_descriptions] from.
|
## [FuncGodotFGDBaseClass] resources to inherit [member class_properties] and [member class_descriptions] from.
|
||||||
@export var base_classes: Array[Resource] = []
|
@export var base_classes: Array[Resource] = []
|
||||||
|
|
||||||
## 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.
|
## 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 = {}
|
||||||
|
|
||||||
## Descriptions for previously defined key value pair properties.
|
## 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 = {}
|
||||||
|
|
||||||
## Appearance properties for the map editor. See the [**Valve FGD**](https://developer.valvesoftware.com/wiki/FGD#Entity_Description) and [**TrenchBroom**](https://trenchbroom.github.io/manual/latest/#display-models-for-entities) documentation for more information.
|
## 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 = {
|
||||||
"size": AABB(Vector3(-8, -8, -8), Vector3(8, 8, 8)),
|
"size": AABB(Vector3(-8, -8, -8), Vector3(8, 8, 8)),
|
||||||
"color": Color(0.8, 0.8, 0.8)
|
"color": Color(0.8, 0.8, 0.8)
|
||||||
@@ -33,15 +45,17 @@ var prefix: String = ""
|
|||||||
|
|
||||||
@export_group("Node Generation")
|
@export_group("Node Generation")
|
||||||
|
|
||||||
## Node to generate on map build. This can be a built-in Godot class or a GDExtension class. For Point Class entities that use Scene File instantiation leave this blank.
|
## Node to generate on map build. This can be a built-in Godot class, a GDScript class, or a GDExtension class.
|
||||||
|
## For Point Class entities that use Scene File instantiation leave this blank.
|
||||||
@export var node_class := ""
|
@export var node_class := ""
|
||||||
|
|
||||||
## Class property to use in naming the generated node. Overrides `name_property` in [FuncGodotMapSettings].
|
## Optional class property to use in naming the generated node. Overrides [member FuncGodotMapSettings.name_property].
|
||||||
## Naming occurs before adding to the [SceneTree] and applying properties.
|
## 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.
|
## 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 := ""
|
@export var name_property := ""
|
||||||
|
|
||||||
func build_def_text(model_key_supported: bool = true) -> 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
|
# Class prefix
|
||||||
var res : String = prefix
|
var res : String = prefix
|
||||||
|
|
||||||
@@ -66,7 +80,7 @@ func build_def_text(model_key_supported: bool = true) -> String:
|
|||||||
if prop == "size" or prop == "model":
|
if prop == "size" or prop == "model":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if prop == 'model' and not model_key_supported:
|
if prop == 'model' and target_editor != FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
var value = meta_props[prop]
|
var value = meta_props[prop]
|
||||||
@@ -89,13 +103,15 @@ func build_def_text(model_key_supported: bool = true) -> String:
|
|||||||
]
|
]
|
||||||
elif value is String:
|
elif value is String:
|
||||||
res += value
|
res += value
|
||||||
|
elif value is Dictionary and target_editor == FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM:
|
||||||
|
res += JSON.stringify(value)
|
||||||
|
|
||||||
res += ")"
|
res += ")"
|
||||||
|
|
||||||
res += " = " + classname
|
res += " = " + classname
|
||||||
|
|
||||||
if prefix != "@BaseClass": # having a description in BaseClasses crashes some editors
|
if prefix != "@BaseClass": # having a description in BaseClasses crashes some editors
|
||||||
var normalized_description = description.replace("\n", " ").strip_edges() if prefix != "@BaseClass" else ""
|
var normalized_description = description.replace("\"", "\'")
|
||||||
if normalized_description != "":
|
if normalized_description != "":
|
||||||
res += " : \"%s\" " % [normalized_description]
|
res += " : \"%s\" " % [normalized_description]
|
||||||
else: # Having no description crashes some editors
|
else: # Having no description crashes some editors
|
||||||
@@ -117,7 +133,8 @@ func build_def_text(model_key_supported: bool = true) -> String:
|
|||||||
if value is Dictionary and class_property_descriptions[prop] is Array:
|
if value is Dictionary and class_property_descriptions[prop] is Array:
|
||||||
var prop_arr: Array = class_property_descriptions[prop]
|
var prop_arr: Array = class_property_descriptions[prop]
|
||||||
if prop_arr.size() > 1 and (prop_arr[1] is int or prop_arr[1] is String):
|
if prop_arr.size() > 1 and (prop_arr[1] is int or prop_arr[1] is String):
|
||||||
prop_description = "\"" + prop_arr[0] + "\" : " + str(prop_arr[1])
|
var value_str : String = str(prop_arr[1]) if prop_arr[1] is int else "\"" + prop_arr[1] + "\""
|
||||||
|
prop_description = "\"" + prop_arr[0] + "\" : " + value_str
|
||||||
else:
|
else:
|
||||||
prop_description = "\"\" : 0"
|
prop_description = "\"\" : 0"
|
||||||
printerr(str(prop) + " has incorrect description format. Should be [String description, int / String default value].")
|
printerr(str(prop) + " has incorrect description format. Should be [String description, int / String default value].")
|
||||||
@@ -159,6 +176,9 @@ func build_def_text(model_key_supported: bool = true) -> String:
|
|||||||
prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline()
|
prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline()
|
||||||
for choice in value:
|
for choice in value:
|
||||||
var choice_val = value[choice]
|
var choice_val = value[choice]
|
||||||
|
if typeof(choice_val) == TYPE_STRING:
|
||||||
|
if not (choice_val as String).begins_with("\""):
|
||||||
|
choice_val = "\"" + choice_val + "\""
|
||||||
prop_val += "\t\t" + str(choice_val) + " : \"" + choice + "\"" + FuncGodotUtil.newline()
|
prop_val += "\t\t" + str(choice_val) + " : \"" + choice + "\"" + FuncGodotUtil.newline()
|
||||||
prop_val += "\t]"
|
prop_val += "\t]"
|
||||||
TYPE_ARRAY:
|
TYPE_ARRAY:
|
||||||
@@ -169,8 +189,22 @@ func build_def_text(model_key_supported: bool = true) -> String:
|
|||||||
prop_val += "\t]"
|
prop_val += "\t]"
|
||||||
TYPE_NODE_PATH:
|
TYPE_NODE_PATH:
|
||||||
prop_type = "target_destination"
|
prop_type = "target_destination"
|
||||||
|
prop_val = "\"\""
|
||||||
TYPE_OBJECT:
|
TYPE_OBJECT:
|
||||||
prop_type = "target_source"
|
if value is Resource:
|
||||||
|
prop_val = "\"" + value.resource_path + "\""
|
||||||
|
if value is Material:
|
||||||
|
if target_editor != FuncGodotFGDFile.FuncGodotTargetMapEditors.JACK:
|
||||||
|
prop_type = "material"
|
||||||
|
else:
|
||||||
|
prop_type = "shader"
|
||||||
|
elif value is Texture2D:
|
||||||
|
prop_type = "decal"
|
||||||
|
elif value is AudioStream:
|
||||||
|
prop_type = "sound"
|
||||||
|
else:
|
||||||
|
prop_type = "target_source"
|
||||||
|
prop_val = "\"\""
|
||||||
|
|
||||||
if prop_val:
|
if prop_val:
|
||||||
res += "\t"
|
res += "\t"
|
||||||
@@ -184,7 +218,9 @@ func build_def_text(model_key_supported: bool = true) -> String:
|
|||||||
res += " : "
|
res += " : "
|
||||||
res += prop_description
|
res += prop_description
|
||||||
|
|
||||||
if value is bool or value is Dictionary or value is Array:
|
if value is bool:
|
||||||
|
res += " : 1 = " if value else " : 0 = "
|
||||||
|
elif value is Dictionary or value is Array:
|
||||||
res += " = "
|
res += " = "
|
||||||
else:
|
else:
|
||||||
res += " : "
|
res += " : "
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://b6vs8fw3on4e2
|
uid://cgkrrgcimlr8y
|
||||||
|
|||||||
@@ -1,19 +1,28 @@
|
|||||||
@tool
|
@tool
|
||||||
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
||||||
## [Resource] file used to express a set of [FuncGodotFGDEntity] definitions. Can be exported as an FGD file for use with a Quake map editor. Used in conjunction with a [FuncGodotMapSetting] resource to generate nodes in a [FuncGodotMap] node.
|
class_name FuncGodotFGDFile extends Resource
|
||||||
class_name FuncGodotFGDFile
|
## [Resource] file used to express a set of [FuncGodotFGDEntity] definitions.
|
||||||
extends Resource
|
##
|
||||||
|
## Can be exported as an FGD file for use with a Quake or Hammer-based map editor. Used in conjunction with [FuncGodotMapSetting] to generate nodes in a [FuncGodotMap] node.
|
||||||
|
##
|
||||||
|
## @tutorial(Level Design Book FGD Chapter): https://book.leveldesignbook.com/appendix/resources/formats/fgd
|
||||||
|
## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD
|
||||||
|
|
||||||
|
## Supported map editors enum, used in conjunction with [member target_map_editor].
|
||||||
|
enum FuncGodotTargetMapEditors {
|
||||||
|
OTHER,
|
||||||
|
TRENCHBROOM,
|
||||||
|
JACK,
|
||||||
|
NET_RADIANT_CUSTOM,
|
||||||
|
}
|
||||||
|
|
||||||
## Builds and exports the FGD file.
|
## Builds and exports the FGD file.
|
||||||
@export var export_file: bool:
|
@export_tool_button("Export FGD") var export_file := export_button
|
||||||
get:
|
|
||||||
return export_file # TODO Converter40 Non existent get function
|
|
||||||
set(new_export_file):
|
|
||||||
if new_export_file != export_file:
|
|
||||||
do_export_file(model_key_word_supported)
|
|
||||||
|
|
||||||
func do_export_file(model_key_supported: bool = true, fgd_output_folder: String = "") -> void:
|
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():
|
if not Engine.is_editor_hint():
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -26,19 +35,35 @@ func do_export_file(model_key_supported: bool = true, fgd_output_folder: String
|
|||||||
if fgd_name == "":
|
if fgd_name == "":
|
||||||
print("Skipping export: Empty FGD name")
|
print("Skipping export: Empty FGD name")
|
||||||
|
|
||||||
var fgd_file = fgd_output_folder + "/" + fgd_name + ".fgd"
|
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")
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
|
||||||
print("Exporting FGD to ", fgd_file)
|
print("Exporting FGD to ", fgd_file)
|
||||||
var file_obj := FileAccess.open(fgd_file, FileAccess.WRITE)
|
file_obj.store_string(build_class_text(target_editor))
|
||||||
file_obj.store_string(build_class_text(model_key_supported))
|
|
||||||
file_obj.close()
|
file_obj.close()
|
||||||
|
|
||||||
@export_group("Map Editor")
|
@export_group("Map Editor")
|
||||||
|
|
||||||
## Some map editors do not support the "model" key word and require the "studio" key word instead.
|
## Some map editors do not support the features found in others
|
||||||
## If you get errors in your map editor, try changing this setting.
|
## (ex: TrenchBroom supports the "model" key word while others require "studio",
|
||||||
|
## J.A.C.K. uses the "shader" key word while others use "material", etc...).
|
||||||
|
## If you get errors in your map editor, try changing this setting and re-exporting.
|
||||||
## This setting is overridden when the FGD is built via the Game Config resource.
|
## This setting is overridden when the FGD is built via the Game Config resource.
|
||||||
@export var model_key_word_supported: bool = true
|
@export var target_map_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM
|
||||||
|
|
||||||
|
# Some map editors do not support the "model" key word and require the "studio" key word instead.
|
||||||
|
# If you get errors in your map editor, try changing this setting.
|
||||||
|
# This setting is overridden when the FGD is built via the Game Config resource.
|
||||||
|
#@export var model_key_word_supported: bool = true
|
||||||
|
|
||||||
@export_group("FGD")
|
@export_group("FGD")
|
||||||
|
|
||||||
@@ -51,12 +76,14 @@ func do_export_file(model_key_supported: bool = true, fgd_output_folder: String
|
|||||||
## 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].
|
## 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] = []
|
@export var entity_definitions: Array[Resource] = []
|
||||||
|
|
||||||
func build_class_text(model_key_supported: bool = true) -> String:
|
func build_class_text(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
|
||||||
var res : String = ""
|
var res : String = ""
|
||||||
|
|
||||||
for base_fgd in base_fgd_files:
|
for base_fgd in base_fgd_files:
|
||||||
if base_fgd is FuncGodotFGDFile:
|
if base_fgd is FuncGodotFGDFile:
|
||||||
res += base_fgd.build_class_text(model_key_supported)
|
res += base_fgd.build_class_text(target_editor)
|
||||||
|
else:
|
||||||
|
printerr("Base Fgd Files contains incorrect resource type! Should only be type FuncGodotFGDFile.")
|
||||||
|
|
||||||
var entities = get_fgd_classes()
|
var entities = get_fgd_classes()
|
||||||
for ent in entities:
|
for ent in entities:
|
||||||
@@ -65,7 +92,7 @@ func build_class_text(model_key_supported: bool = true) -> String:
|
|||||||
if ent.func_godot_internal:
|
if ent.func_godot_internal:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
var ent_text = ent.build_def_text(model_key_supported)
|
var ent_text = ent.build_def_text(target_editor)
|
||||||
res += ent_text
|
res += ent_text
|
||||||
if ent != entities[-1]:
|
if ent != entities[-1]:
|
||||||
res += "\n"
|
res += "\n"
|
||||||
@@ -84,8 +111,8 @@ func get_fgd_classes() -> Array:
|
|||||||
res.append(cur_ent_def)
|
res.append(cur_ent_def)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
func get_entity_definitions() -> Dictionary:
|
func get_entity_definitions() -> Dictionary[String, FuncGodotFGDEntityClass]:
|
||||||
var res : Dictionary = {}
|
var res: Dictionary[String, FuncGodotFGDEntityClass] = {}
|
||||||
|
|
||||||
for base_fgd in base_fgd_files:
|
for base_fgd in base_fgd_files:
|
||||||
var fgd_res = base_fgd.get_entity_definitions()
|
var fgd_res = base_fgd.get_entity_definitions()
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://ckisq2tcxg062
|
uid://drlmgulwbjwqu
|
||||||
|
|||||||
@@ -1,25 +1,36 @@
|
|||||||
@tool
|
@tool
|
||||||
## A special type of [FuncGodotFGDPointClass] entity that can automatically generate a special simplified GLB model file for the map editor display.
|
@icon("res://addons/func_godot/icons/icon_godambler3d.svg")
|
||||||
|
class_name FuncGodotFGDModelPointClass extends FuncGodotFGDPointClass
|
||||||
|
## A special type of [FuncGodotFGDPointClass] entity that automatically generates a special simplified GLB model file for the map editor display.
|
||||||
## Only supported in map editors that support GLTF or GLB.
|
## Only supported in map editors that support GLTF or GLB.
|
||||||
class_name FuncGodotFGDModelPointClass
|
##
|
||||||
extends FuncGodotFGDPointClass
|
## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
|
||||||
|
## @tutorial(Level Design Book: Entity Types and Settings): https://book.leveldesignbook.com/appendix/resources/formats/fgd#entity-types-and-settings-basic
|
||||||
|
## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD#Class_Types_and_Properties
|
||||||
|
## @tutorial(dumptruck_ds' Quake Mapping Entities Tutorial): https://www.youtube.com/watch?v=gtL9f6_N2WM
|
||||||
|
## @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
|
||||||
|
|
||||||
enum TargetMapEditor {
|
enum TargetMapEditor {
|
||||||
GENERIC,
|
GENERIC, ## Entity definition uses the [b]@studio[/b] key word. [member scale_expression] is ignored. Supported by all map editors.
|
||||||
TRENCHBROOM
|
TRENCHBROOM ## Entity definition uses the [b]@model[/b] key word. [member scale_expression] is applied if set.
|
||||||
}
|
}
|
||||||
|
|
||||||
## Determines how model interprets [member scale_expression].
|
|
||||||
@export var target_map_editor: TargetMapEditor = TargetMapEditor.GENERIC
|
@export var target_map_editor: TargetMapEditor = TargetMapEditor.GENERIC
|
||||||
## Display model export folder relative to the model folder set by [FuncGodotLocalConfig].
|
## Display model export folder relative to [member ProjectSettings.func_godot/model_point_class_save_path].
|
||||||
@export var models_sub_folder : String = ""
|
@export var models_sub_folder : String = ""
|
||||||
## Scale expression applied to model. See the [TrenchBroom Documentation](https://trenchbroom.github.io/manual/latest/#display-models-for-entities) for more information.
|
## Scale expression applied to model. Only used by TrenchBroom. If left empty, uses [member ProjectSettings.func_godot/default_inverse_scale_factor]. [br][br]Read the TrenchBroom Manual for more information on the "scale expression" feature.
|
||||||
@export var scale_expression : String = ""
|
@export var scale_expression : String = ""
|
||||||
## Model Point Class can override the 'size' meta property by auto-generating a value from the meshes' [AABB]. Proper generation requires 'scale_expression' set to a float or [Vector3]. **WARNING:** Generated size property unlikely to align cleanly to grid!
|
## Model Point Class can override the 'size' meta property by auto-generating a value from the meshes' [AABB]. Proper generation requires [member scale_expression] set to a float or vector. [br][br][color=orange]WARNING:[/color] Generated size property unlikely to align cleanly to grid!
|
||||||
@export var generate_size_property : bool = false
|
@export var generate_size_property : bool = false
|
||||||
|
## Degrees to rotate model prior to export. Different editors may handle GLTF transformations differently. If your model isn't oriented correctly, try modifying this property.
|
||||||
|
@export var rotation_offset: Vector3 = Vector3(0.0, 0.0, 0.0)
|
||||||
## Creates a .gdignore file in the model export folder to prevent Godot importing the display models. Only needs to be generated once.
|
## Creates a .gdignore file in the model export folder to prevent Godot importing the display models. Only needs to be generated once.
|
||||||
@export var generate_gd_ignore_file : bool = false :
|
@export_tool_button("Generate GD Ignore File", "FileAccess") var generate_gd_ignore_file : Callable = _generate_gd_ignore_file
|
||||||
set(ignore):
|
|
||||||
|
func _generate_gd_ignore_file() -> void:
|
||||||
|
if Engine.is_editor_hint():
|
||||||
var path: String = _get_game_path().path_join(_get_model_folder())
|
var path: String = _get_game_path().path_join(_get_model_folder())
|
||||||
var error: Error = DirAccess.make_dir_recursive_absolute(path)
|
var error: Error = DirAccess.make_dir_recursive_absolute(path)
|
||||||
if error != Error.OK:
|
if error != Error.OK:
|
||||||
@@ -32,7 +43,8 @@ enum TargetMapEditor {
|
|||||||
file.store_string('')
|
file.store_string('')
|
||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
func build_def_text(model_key_supported: bool = true) -> String:
|
## 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()
|
_generate_model()
|
||||||
return super()
|
return super()
|
||||||
|
|
||||||
@@ -41,13 +53,15 @@ func _generate_model() -> void:
|
|||||||
return
|
return
|
||||||
|
|
||||||
var gltf_state := GLTFState.new()
|
var gltf_state := GLTFState.new()
|
||||||
var path = _get_export_dir()
|
var path: String = _get_export_dir()
|
||||||
var node = _get_node()
|
var node: Node3D = _get_node()
|
||||||
if node == null: return
|
if not node:
|
||||||
|
return
|
||||||
if not _create_gltf_file(gltf_state, path, node):
|
if not _create_gltf_file(gltf_state, path, node):
|
||||||
printerr("could not create gltf file")
|
printerr("could not create gltf file")
|
||||||
return
|
return
|
||||||
node.queue_free()
|
node.queue_free()
|
||||||
|
|
||||||
if target_map_editor == TargetMapEditor.TRENCHBROOM:
|
if target_map_editor == TargetMapEditor.TRENCHBROOM:
|
||||||
const model_key: String = "model"
|
const model_key: String = "model"
|
||||||
if scale_expression.is_empty():
|
if scale_expression.is_empty():
|
||||||
@@ -61,7 +75,7 @@ func _generate_model() -> void:
|
|||||||
meta_properties["studio"] = '"%s"' % _get_local_path()
|
meta_properties["studio"] = '"%s"' % _get_local_path()
|
||||||
|
|
||||||
if generate_size_property:
|
if generate_size_property:
|
||||||
meta_properties["size"] = _generate_size_from_aabb(gltf_state.meshes)
|
meta_properties["size"] = _generate_size_from_aabb(gltf_state.meshes, gltf_state.get_nodes())
|
||||||
|
|
||||||
func _get_node() -> Node3D:
|
func _get_node() -> Node3D:
|
||||||
var node := scene_file.instantiate()
|
var node := scene_file.instantiate()
|
||||||
@@ -80,7 +94,7 @@ func _get_local_path() -> String:
|
|||||||
return _get_model_folder().path_join('%s.glb' % classname)
|
return _get_model_folder().path_join('%s.glb' % classname)
|
||||||
|
|
||||||
func _get_model_folder() -> String:
|
func _get_model_folder() -> String:
|
||||||
var model_dir: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.GAME_PATH_MODELS_FOLDER) as String
|
var model_dir: String = ProjectSettings.get_setting("func_godot/model_point_class_save_path", "") as String
|
||||||
if not models_sub_folder.is_empty():
|
if not models_sub_folder.is_empty():
|
||||||
model_dir = model_dir.path_join(models_sub_folder)
|
model_dir = model_dir.path_join(models_sub_folder)
|
||||||
return model_dir
|
return model_dir
|
||||||
@@ -93,13 +107,15 @@ func _create_gltf_file(gltf_state: GLTFState, path: String, node: Node3D) -> boo
|
|||||||
var gltf_document := GLTFDocument.new()
|
var gltf_document := GLTFDocument.new()
|
||||||
gltf_state.create_animations = false
|
gltf_state.create_animations = false
|
||||||
|
|
||||||
node.rotate_y(deg_to_rad(-90))
|
node.rotate_x(deg_to_rad(rotation_offset.x))
|
||||||
|
node.rotate_y(deg_to_rad(rotation_offset.y))
|
||||||
|
node.rotate_z(deg_to_rad(rotation_offset.z))
|
||||||
|
|
||||||
# With TrenchBroom we can specify a scale expression, but for other editors we need to scale our models manually.
|
# With TrenchBroom we can specify a scale expression, but for other editors we need to scale our models manually.
|
||||||
if target_map_editor != TargetMapEditor.TRENCHBROOM:
|
if target_map_editor != TargetMapEditor.TRENCHBROOM:
|
||||||
var scale_factor: Vector3 = Vector3.ONE
|
var scale_factor: Vector3 = Vector3.ONE
|
||||||
if scale_expression.is_empty():
|
if scale_expression.is_empty():
|
||||||
scale_factor *= FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.DEFAULT_INVERSE_SCALE) as float
|
scale_factor *= ProjectSettings.get_setting("func_godot/default_inverse_scale_factor", 32.0) as float
|
||||||
else:
|
else:
|
||||||
if scale_expression.begins_with('\''):
|
if scale_expression.begins_with('\''):
|
||||||
var scale_arr := scale_expression.split_floats(' ', false)
|
var scale_arr := scale_expression.split_floats(' ', false)
|
||||||
@@ -126,15 +142,24 @@ func _save_to_file_system(gltf_document: GLTFDocument, gltf_state: GLTFState, pa
|
|||||||
return
|
return
|
||||||
|
|
||||||
error = gltf_document.write_to_filesystem(gltf_state, path)
|
error = gltf_document.write_to_filesystem(gltf_state, path)
|
||||||
if error != OK:
|
if error != Error.OK:
|
||||||
printerr("Failed writing to file system", error)
|
printerr("Failed writing to file system", error)
|
||||||
return
|
return
|
||||||
print('Exported model to ', path)
|
print('Exported model to ', path)
|
||||||
|
|
||||||
func _generate_size_from_aabb(meshes: Array[GLTFMesh]) -> AABB:
|
func _generate_size_from_aabb(meshes: Array[GLTFMesh], nodes: Array[GLTFNode]) -> AABB:
|
||||||
var aabb := AABB()
|
var aabb := AABB()
|
||||||
for mesh in meshes:
|
for mesh in meshes:
|
||||||
aabb = aabb.merge(mesh.mesh.get_mesh().get_aabb())
|
aabb = aabb.merge(mesh.mesh.get_mesh().get_aabb())
|
||||||
|
var pos_ofs := Vector3.ZERO
|
||||||
|
if not nodes.is_empty():
|
||||||
|
var ct: int = 0
|
||||||
|
for node in nodes:
|
||||||
|
if node.parent == 0:
|
||||||
|
pos_ofs += node.position
|
||||||
|
ct += 1
|
||||||
|
pos_ofs /= maxi(ct, 1)
|
||||||
|
aabb.position += pos_ofs
|
||||||
|
|
||||||
# Reorient the AABB so it matches TrenchBroom's coordinate system
|
# Reorient the AABB so it matches TrenchBroom's coordinate system
|
||||||
var size_prop := AABB()
|
var size_prop := AABB()
|
||||||
@@ -145,12 +170,15 @@ func _generate_size_from_aabb(meshes: Array[GLTFMesh]) -> AABB:
|
|||||||
# Scale factor will need to be set if we decide to auto-generate our bounds
|
# Scale factor will need to be set if we decide to auto-generate our bounds
|
||||||
var scale_factor: Vector3 = Vector3.ONE
|
var scale_factor: Vector3 = Vector3.ONE
|
||||||
if target_map_editor == TargetMapEditor.TRENCHBROOM:
|
if target_map_editor == TargetMapEditor.TRENCHBROOM:
|
||||||
if scale_expression.begins_with('\''):
|
if scale_expression.is_empty():
|
||||||
var scale_arr := scale_expression.split_floats(' ', false)
|
scale_factor *= ProjectSettings.get_setting("func_godot/default_inverse_scale_factor", 32.0) as float
|
||||||
if scale_arr.size() == 3:
|
else:
|
||||||
scale_factor *= Vector3(scale_arr[0], scale_arr[1], scale_arr[2])
|
if scale_expression.begins_with('\''):
|
||||||
elif scale_expression.to_float() > 0:
|
var scale_arr := scale_expression.split_floats(' ', false)
|
||||||
scale_factor *= scale_expression.to_float()
|
if scale_arr.size() == 3:
|
||||||
|
scale_factor *= Vector3(scale_arr[0], scale_arr[1], scale_arr[2])
|
||||||
|
elif scale_expression.to_float() > 0:
|
||||||
|
scale_factor *= scale_expression.to_float()
|
||||||
|
|
||||||
size_prop.position *= scale_factor
|
size_prop.position *= scale_factor
|
||||||
size_prop.size *= scale_factor
|
size_prop.size *= scale_factor
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://ieokw1i153l2
|
uid://ldfqjtq0br35
|
||||||
|
|||||||
@@ -1,20 +1,35 @@
|
|||||||
@tool
|
@tool
|
||||||
## FGD PointClass entity definition, used to define point entities.
|
@icon("res://addons/func_godot/icons/icon_godambler3d.svg")
|
||||||
## PointClass entities can use either the `node_class` or the `scene_file` property to tell [FuncGodotMap] what to generate on map build.
|
class_name FuncGodotFGDPointClass extends FuncGodotFGDEntityClass
|
||||||
class_name FuncGodotFGDPointClass
|
## FGD PointClass entity definition.
|
||||||
extends FuncGodotFGDEntityClass
|
##
|
||||||
|
## A resource used to define an FGD PointClass 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
|
||||||
|
## @tutorial(Level Design Book: Entity Types and Settings): https://book.leveldesignbook.com/appendix/resources/formats/fgd#entity-types-and-settings-basic
|
||||||
|
## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD#Class_Types_and_Properties
|
||||||
|
## @tutorial(dumptruck_ds' Quake Mapping Entities Tutorial): https://www.youtube.com/watch?v=gtL9f6_N2WM
|
||||||
|
## @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
|
||||||
|
|
||||||
func _init() -> void:
|
func _init() -> void:
|
||||||
prefix = "@PointClass"
|
prefix = "@PointClass"
|
||||||
|
|
||||||
@export_group ("Scene")
|
@export_group ("Scene")
|
||||||
## An optional scene file to instantiate on map build. Overrides `node_class` and `script_class`.
|
## An optional [PackedScene] file to instantiate on map build. Overrides [member FuncGodotFGDEntityClass.node_class] and [member script_class].
|
||||||
@export var scene_file: PackedScene
|
@export var scene_file: PackedScene
|
||||||
|
|
||||||
## An optional script file to attach to the node generated on map build. Ignored if `scene_file` is specified.
|
|
||||||
@export_group ("Scripting")
|
@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 var script_class: Script
|
||||||
|
|
||||||
@export_group("Build")
|
@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 `false` if you would like to define how the generated node is rotated yourself.
|
## 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
|
@export var apply_rotation_on_map_build : bool = true
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://dq8gw52aflgyo
|
uid://cxsqwtsqd8w33
|
||||||
|
|||||||
@@ -1,19 +1,31 @@
|
|||||||
@tool
|
@tool
|
||||||
## FGD SolidClass entity definition, used to define brush entities.
|
@icon("res://addons/func_godot/icons/icon_slipgate3d.svg")
|
||||||
## A [MeshInstance3D] will be generated by FuncGodotMap according to this definition's Visual Build settings. If FuncGodotFGDSolidClass [member node_class] inherits [CollisionObject3D] then one or more [CollisionShape3D] nodes will be generated according to Collision Build settings.
|
class_name FuncGodotFGDSolidClass extends FuncGodotFGDEntityClass
|
||||||
class_name FuncGodotFGDSolidClass
|
## FGD SolidClass entity definition that generates a mesh from [FuncGodotData.BrushData].
|
||||||
extends FuncGodotFGDEntityClass
|
##
|
||||||
|
## A [MeshInstance3D] will be generated by [FuncGodotMap] according to this definition's Visual Build settings.
|
||||||
|
## If [member FuncGodotFGDEntityClass.node_class] inherits [CollisionObject3D]
|
||||||
|
## then one or more [CollisionShape3D] nodes will be generated according to Collision Build settings.
|
||||||
|
##
|
||||||
|
## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
|
||||||
|
## @tutorial(Level Design Book: Entity Types and Settings): https://book.leveldesignbook.com/appendix/resources/formats/fgd#entity-types-and-settings-basic
|
||||||
|
## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD#Class_Types_and_Properties
|
||||||
|
## @tutorial(dumptruck_ds' Quake Mapping Entities Tutorial): https://www.youtube.com/watch?v=gtL9f6_N2WM
|
||||||
|
|
||||||
enum SpawnType {
|
enum SpawnType {
|
||||||
WORLDSPAWN = 0, ## Is worldspawn
|
WORLDSPAWN = 0, ## Builds the geometry of this entity relative to the FuncGodotMap position.
|
||||||
MERGE_WORLDSPAWN = 1, ## Should be combined with worldspawn
|
MERGE_WORLDSPAWN = 1, ## This entity's geometry is merged with the [b]worldspawn[/b] entity and this entity is removed. Behavior mimics [b]func_group[/b] in modern Quake compilers.
|
||||||
ENTITY = 2, ## Is its own separate entity
|
ENTITY = 2, ## This entity is built as its own object. It finds the origin of the entity based on [member origin_type].
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OriginType {
|
enum OriginType {
|
||||||
IGNORE = 0, ## Ignore origin property and only use averaged brush vertices for positioning. Standard Quake 1 / Half-Life behavior.
|
AVERAGED = 0, ## Use averaged brush vertices for center position. This is the old Qodot behavior.
|
||||||
ABSOLUTE = 1, ## Use origin property for position center, ignoring brush vertice positions.
|
ABSOLUTE = 1, ## Use [code]origin[/code] class property in global coordinates as the center position.
|
||||||
RELATIVE = 2 ## Use origin relative to averaged brush vertice positions. Use this setting if brush entity vertices have coordinates local to the origin.
|
RELATIVE = 2, ## Calculate center position using [code]origin[/code] class property as an offset to the entity's bounding box center.
|
||||||
|
BRUSH = 3, ## Calculate center position based on the bounding box center of all brushes using the 'origin' texture specified in the [FuncGodotMapSettings]. If no Origin Brush is found, fall back to BOUNDS_CENTER. This is the default option and recommended for most entities.
|
||||||
|
BOUNDS_CENTER = 4, ## Use the center of the entity's bounding box for center position.
|
||||||
|
BOUNDS_MINS = 5, ## Use the lowest bounding box coordinates for center position. This is standard Quake and Half-Life brush entity behavior.
|
||||||
|
BOUNDS_MAXS = 6, ## Use the highest bounding box coordinates for center position.
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CollisionShapeType {
|
enum CollisionShapeType {
|
||||||
@@ -24,13 +36,15 @@ enum CollisionShapeType {
|
|||||||
|
|
||||||
## Controls whether this Solid Class is the worldspawn, is combined with the worldspawn, or is spawned as its own free-standing entity.
|
## Controls whether this Solid Class is the worldspawn, is combined with the worldspawn, or is spawned as its own free-standing entity.
|
||||||
@export var spawn_type: SpawnType = SpawnType.ENTITY
|
@export var spawn_type: SpawnType = SpawnType.ENTITY
|
||||||
## Controls how this Solid Class utilizes the `origin` key value pair to find its position.
|
## Controls how this Solid Class determines its center position. Only valid if [member spawn_type] is set to ENTITY.
|
||||||
@export var origin_type: OriginType = OriginType.IGNORE
|
@export var origin_type: OriginType = OriginType.BRUSH
|
||||||
|
|
||||||
@export_group("Visual Build")
|
@export_group("Visual Build")
|
||||||
## Controls whether a [MeshInstance3D] is built for this Solid Class.
|
## Controls whether a [MeshInstance3D] is built for this Solid Class.
|
||||||
@export var build_visuals : bool = true
|
@export var build_visuals : bool = true
|
||||||
## Sets generated [MeshInstance3D] to be available for UV2 unwrapping after [FuncGodotMap] build. Utilized in baked lightmapping.
|
## Global illumination mode for the generated [MeshInstance3D]. Setting to [b]GI_MODE_STATIC[/b] will unwrap the mesh's UV2 during build.
|
||||||
|
@export var global_illumination_mode : GeometryInstance3D.GIMode = GeometryInstance3D.GI_MODE_STATIC
|
||||||
|
## @deprecated: Use [member global_illumination_mode] instead. [br]Sets generated [MeshInstance3D] to be available for UV2 unwrapping after [FuncGodotMap] build. Utilized in baked lightmapping.
|
||||||
@export var use_in_baked_light : bool = true
|
@export var use_in_baked_light : bool = true
|
||||||
## Shadow casting setting allows for further lightmapping customization.
|
## Shadow casting setting allows for further lightmapping customization.
|
||||||
@export var shadow_casting_setting : GeometryInstance3D.ShadowCastingSetting = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
@export var shadow_casting_setting : GeometryInstance3D.ShadowCastingSetting = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||||
@@ -51,8 +65,41 @@ enum CollisionShapeType {
|
|||||||
## The collision margin for the Solid Class' collision shapes. Not used in Godot Physics. See [Shape3D] for details.
|
## The collision margin for the Solid Class' collision shapes. Not used in Godot Physics. See [Shape3D] for details.
|
||||||
@export var collision_shape_margin: float = 0.04
|
@export var collision_shape_margin: float = 0.04
|
||||||
|
|
||||||
|
## The following properties tell FuncGodot to add a [i]"func_godot_mesh_data"[/i] Dictionary to the metadata of the generated node upon build.
|
||||||
|
## This data is parallelized, so that each element of the array is ordered to reference the same face in the mesh.
|
||||||
|
@export_group("Mesh Metadata")
|
||||||
|
## Add a texture lookup table to the generated node's metadata on build.[br][br]
|
||||||
|
## The data is split between an [Array] of [StringName] called [i]"texture_names"[/i] containing all currently used texture materials
|
||||||
|
## and a [PackedInt32Array] called [i]"textures"[/i] where each element is an index corresponding to the [i]"texture_names"[/i] entries.
|
||||||
|
@export var add_textures_metadata: bool = false
|
||||||
|
## Add a [PackedVector3Array] called [i]"vertices"[/i] to the generated node's metadata on build.[br][br]
|
||||||
|
## This is a list of every vertex in the generated node's [MeshInstance3D]. Every 3 vertices represent a single face.
|
||||||
|
@export var add_vertex_metadata: bool = false
|
||||||
|
## Add a [PackedVector3Array] called [i]"positions"[/i] to the generated node's metadata on build.[br][br]
|
||||||
|
## This is a list of positions for each face, local to the generated node, calculated by averaging the vertices to find the face's center.
|
||||||
|
@export var add_face_position_metadata: bool = false
|
||||||
|
## Add a [PackedVector3Array] called [i]"normals"[/i] to the generated node's metadata on build.[br][br]
|
||||||
|
## Contains a list of each face's normal.
|
||||||
|
@export var add_face_normal_metadata: bool = false
|
||||||
|
## Add a [Dictionary] called [i]"collision_shape_to_face_indices_map"[/i] in the generated node's metadata on build.[br][br]
|
||||||
|
## Contains keys of strings, which are the names of child [CollisionShape3D] nodes, and values of
|
||||||
|
## [PackedInt32Array], containing indices of that child's faces.[br][br]
|
||||||
|
## For example, an element of [br][br][code]{ "entity_1_brush_0_collision_shape" : [0, 1, 3] }[/code][br][br]
|
||||||
|
## shows that this solid class has been generated with one child collision shape named
|
||||||
|
## [i]entity_1_brush_0_collision_shape[/i] which handles 3 faces of the mesh with collision, at indices 0, 1, and 3.
|
||||||
|
@export var add_collision_shape_to_face_indices_metadata : bool = false
|
||||||
|
## [s]Add a [Dictionary] called [i]"collision_shape_to_face_range_map"[/i] in the generated node's metadata on build.[br][br]
|
||||||
|
## Contains keys of strings, which are the names of child [CollisionShape3D] nodes, and values of
|
||||||
|
## [Vector2i], where [i]X[/i] represents the starting index of that child's faces and [i]Y[/i] represents the
|
||||||
|
## ending index.[br][br]
|
||||||
|
## For example, an element of [br][br][code]{ "entity_1_brush_0_collision_shape" : Vector2i(0, 15) }[/code][br][br]
|
||||||
|
## shows that this solid class has been generated with one child collision shape named
|
||||||
|
## [i]entity_1_brush_0_collision_shape[/i] which handles the first 15 faces of the parts of the mesh with collision.[/s]
|
||||||
|
## @deprecated: No longer supported or planned as of 2025.7, but retained in case a contributor provides an appropriate solution in the future.
|
||||||
|
@export var add_collision_shape_face_range_metadata: bool = false
|
||||||
|
|
||||||
@export_group("Scripting")
|
@export_group("Scripting")
|
||||||
## An optional script file to attach to the node generated on map build.
|
## An optional [Script] file to attach to the node generated on map build.
|
||||||
@export var script_class: Script
|
@export var script_class: Script
|
||||||
|
|
||||||
func _init():
|
func _init():
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://cmoudtn1s5egw
|
uid://5cow84q03m6a
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
@tool
|
@tool
|
||||||
class_name FuncGodotPlugin
|
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
||||||
extends EditorPlugin
|
class_name FuncGodotPlugin extends EditorPlugin
|
||||||
|
|
||||||
var map_import_plugin : QuakeMapImportPlugin = null
|
var map_import_plugin : QuakeMapImportPlugin = null
|
||||||
var palette_import_plugin : QuakePaletteImportPlugin = null
|
var palette_import_plugin : QuakePaletteImportPlugin = null
|
||||||
var wad_import_plugin: QuakeWadImportPlugin = null
|
var wad_import_plugin: QuakeWadImportPlugin = null
|
||||||
|
|
||||||
var func_godot_map_control: Control = null
|
#var func_godot_map_progress_bar: Control = null
|
||||||
var func_godot_map_progress_bar: Control = null
|
|
||||||
var edited_object_ref: WeakRef = weakref(null)
|
var edited_object_ref: WeakRef = weakref(null)
|
||||||
|
|
||||||
func _get_plugin_name() -> String:
|
func _get_plugin_name() -> String:
|
||||||
@@ -19,12 +18,9 @@ func _handles(object: Object) -> bool:
|
|||||||
func _edit(object: Object) -> void:
|
func _edit(object: Object) -> void:
|
||||||
edited_object_ref = weakref(object)
|
edited_object_ref = weakref(object)
|
||||||
|
|
||||||
func _make_visible(visible: bool) -> void:
|
#func _make_visible(visible: bool) -> void:
|
||||||
if func_godot_map_control:
|
#if func_godot_map_progress_bar:
|
||||||
func_godot_map_control.set_visible(visible)
|
#func_godot_map_progress_bar.set_visible(visible)
|
||||||
|
|
||||||
if func_godot_map_progress_bar:
|
|
||||||
func_godot_map_progress_bar.set_visible(visible)
|
|
||||||
|
|
||||||
func _enter_tree() -> void:
|
func _enter_tree() -> void:
|
||||||
# Import plugins
|
# Import plugins
|
||||||
@@ -36,17 +32,47 @@ func _enter_tree() -> void:
|
|||||||
add_import_plugin(palette_import_plugin)
|
add_import_plugin(palette_import_plugin)
|
||||||
add_import_plugin(wad_import_plugin)
|
add_import_plugin(wad_import_plugin)
|
||||||
|
|
||||||
# FuncGodotMap button
|
#func_godot_map_progress_bar = create_func_godot_map_progress_bar()
|
||||||
func_godot_map_control = create_func_godot_map_control()
|
#func_godot_map_progress_bar.set_visible(false)
|
||||||
func_godot_map_control.set_visible(false)
|
#add_control_to_container(EditorPlugin.CONTAINER_INSPECTOR_BOTTOM, func_godot_map_progress_bar)
|
||||||
add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, func_godot_map_control)
|
|
||||||
|
|
||||||
func_godot_map_progress_bar = create_func_godot_map_progress_bar()
|
|
||||||
func_godot_map_progress_bar.set_visible(false)
|
|
||||||
add_control_to_container(EditorPlugin.CONTAINER_INSPECTOR_BOTTOM, func_godot_map_progress_bar)
|
|
||||||
|
|
||||||
add_custom_type("FuncGodotMap", "Node3D", preload("res://addons/func_godot/src/map/func_godot_map.gd"), null)
|
add_custom_type("FuncGodotMap", "Node3D", preload("res://addons/func_godot/src/map/func_godot_map.gd"), null)
|
||||||
|
|
||||||
|
# Default Map Settings
|
||||||
|
if not ProjectSettings.has_setting("func_godot/default_map_settings"):
|
||||||
|
ProjectSettings.set_setting("func_godot/default_map_settings", "res://addons/func_godot/func_godot_default_map_settings.tres")
|
||||||
|
var property_info = {
|
||||||
|
"name": "func_godot/default_map_settings",
|
||||||
|
"type": TYPE_STRING,
|
||||||
|
"hint": PROPERTY_HINT_FILE,
|
||||||
|
"hint_string": "*.tres"
|
||||||
|
}
|
||||||
|
ProjectSettings.add_property_info(property_info)
|
||||||
|
ProjectSettings.set_as_basic("func_godot/default_map_settings", true)
|
||||||
|
ProjectSettings.set_initial_value("func_godot/default_map_settings", "res://addons/func_godot/func_godot_default_map_settings.tres")
|
||||||
|
|
||||||
|
# Default Inverse Scale Factor
|
||||||
|
if not ProjectSettings.has_setting("func_godot/default_inverse_scale_factor"):
|
||||||
|
ProjectSettings.set_setting("func_godot/default_inverse_scale_factor", 32.0)
|
||||||
|
var property_info = {
|
||||||
|
"name": "func_godot/default_inverse_scale_factor",
|
||||||
|
"type": TYPE_FLOAT
|
||||||
|
}
|
||||||
|
ProjectSettings.add_property_info(property_info)
|
||||||
|
ProjectSettings.set_as_basic("func_godot/default_inverse_scale_factor", true)
|
||||||
|
ProjectSettings.set_initial_value("func_godot/default_inverse_scale_factor", 32.0)
|
||||||
|
|
||||||
|
# Model Point Class Default Path
|
||||||
|
if not ProjectSettings.has_setting("func_godot/model_point_class_save_path"):
|
||||||
|
ProjectSettings.set_setting("func_godot/model_point_class_save_path", "")
|
||||||
|
var property_info = {
|
||||||
|
"name": "func_godot/model_point_class_save_path",
|
||||||
|
"type": TYPE_STRING
|
||||||
|
}
|
||||||
|
ProjectSettings.add_property_info(property_info)
|
||||||
|
ProjectSettings.set_as_basic("func_godot/model_point_class_save_path", true)
|
||||||
|
ProjectSettings.set_initial_value("func_godot/model_point_class_save_path", "")
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
remove_custom_type("FuncGodotMap")
|
remove_custom_type("FuncGodotMap")
|
||||||
remove_import_plugin(map_import_plugin)
|
remove_import_plugin(map_import_plugin)
|
||||||
@@ -58,115 +84,45 @@ func _exit_tree() -> void:
|
|||||||
palette_import_plugin = null
|
palette_import_plugin = null
|
||||||
wad_import_plugin = null
|
wad_import_plugin = null
|
||||||
|
|
||||||
if func_godot_map_control:
|
#if func_godot_map_progress_bar:
|
||||||
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, func_godot_map_control)
|
#remove_control_from_container(EditorPlugin.CONTAINER_INSPECTOR_BOTTOM, func_godot_map_progress_bar)
|
||||||
func_godot_map_control.queue_free()
|
#func_godot_map_progress_bar.queue_free()
|
||||||
func_godot_map_control = null
|
#func_godot_map_progress_bar = null
|
||||||
|
|
||||||
if func_godot_map_progress_bar:
|
# Create a progress bar for building a [FuncGodotMap]
|
||||||
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, func_godot_map_progress_bar)
|
#func create_func_godot_map_progress_bar() -> Control:
|
||||||
func_godot_map_progress_bar.queue_free()
|
#var progress_label = Label.new()
|
||||||
func_godot_map_progress_bar = null
|
#progress_label.name = "ProgressLabel"
|
||||||
|
#progress_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
#progress_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
#
|
||||||
|
#var progress_bar := ProgressBar.new()
|
||||||
|
#progress_bar.name = "ProgressBar"
|
||||||
|
#progress_bar.show_percentage = false
|
||||||
|
#progress_bar.min_value = 0.0
|
||||||
|
#progress_bar.max_value = 1.0
|
||||||
|
#progress_bar.custom_minimum_size.y = 30
|
||||||
|
#progress_bar.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE)
|
||||||
|
#progress_bar.add_child(progress_label)
|
||||||
|
#progress_label.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE)
|
||||||
|
#progress_label.offset_top = -9
|
||||||
|
#progress_label.offset_left = 3
|
||||||
|
#
|
||||||
|
#return progress_bar
|
||||||
|
|
||||||
## Create the toolbar controls for [FuncGodotMap] instances in the editor
|
# Update the build progress bar (see: [method create_func_godot_map_progress_bar]) to display the current step and progress (0-1)
|
||||||
func create_func_godot_map_control() -> Control:
|
#func func_godot_map_build_progress(step: String, progress: float) -> void:
|
||||||
var separator = VSeparator.new()
|
#var progress_label = func_godot_map_progress_bar.get_node("ProgressLabel")
|
||||||
|
#func_godot_map_progress_bar.value = progress
|
||||||
var icon = TextureRect.new()
|
#progress_label.text = step.capitalize()
|
||||||
icon.texture = preload("res://addons/func_godot/icons/icon_slipgate3d.svg")
|
|
||||||
icon.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
|
||||||
|
|
||||||
var build_button = Button.new()
|
|
||||||
build_button.text = "Build"
|
|
||||||
build_button.connect("pressed",Callable(self,"func_godot_map_build"))
|
|
||||||
|
|
||||||
var unwrap_uv2_button = Button.new()
|
|
||||||
unwrap_uv2_button.text = "Unwrap UV2"
|
|
||||||
unwrap_uv2_button.connect("pressed",Callable(self,"func_godot_map_unwrap_uv2"))
|
|
||||||
|
|
||||||
var control = HBoxContainer.new()
|
|
||||||
control.add_child(separator)
|
|
||||||
control.add_child(icon)
|
|
||||||
control.add_child(build_button)
|
|
||||||
control.add_child(unwrap_uv2_button)
|
|
||||||
|
|
||||||
return control
|
|
||||||
|
|
||||||
## Create a progress bar for building a [FuncGodotMap]
|
|
||||||
func create_func_godot_map_progress_bar() -> Control:
|
|
||||||
var progress_label = Label.new()
|
|
||||||
progress_label.name = "ProgressLabel"
|
|
||||||
progress_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
||||||
progress_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
||||||
|
|
||||||
var progress_bar := ProgressBar.new()
|
|
||||||
progress_bar.name = "ProgressBar"
|
|
||||||
progress_bar.show_percentage = false
|
|
||||||
progress_bar.min_value = 0.0
|
|
||||||
progress_bar.max_value = 1.0
|
|
||||||
progress_bar.custom_minimum_size.y = 30
|
|
||||||
progress_bar.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE)
|
|
||||||
progress_bar.add_child(progress_label)
|
|
||||||
progress_label.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE)
|
|
||||||
progress_label.offset_top = -9
|
|
||||||
progress_label.offset_left = 3
|
|
||||||
|
|
||||||
return progress_bar
|
|
||||||
|
|
||||||
## Create the "Build" button for [FuncGodotMap]s in the editor
|
|
||||||
func func_godot_map_build() -> void:
|
|
||||||
var edited_object : FuncGodotMap = edited_object_ref.get_ref()
|
|
||||||
if not edited_object:
|
|
||||||
return
|
|
||||||
|
|
||||||
edited_object.should_add_children = true
|
|
||||||
edited_object.should_set_owners = true
|
|
||||||
|
|
||||||
set_func_godot_map_control_disabled(true)
|
|
||||||
edited_object.build_progress.connect(func_godot_map_build_progress)
|
|
||||||
edited_object.build_complete.connect(func_godot_map_build_complete.bind(edited_object))
|
|
||||||
edited_object.build_failed.connect(func_godot_map_build_complete.bind(edited_object))
|
|
||||||
|
|
||||||
edited_object.verify_and_build()
|
|
||||||
|
|
||||||
## Create the "Unwrap UV2" button for [FuncGodotMap]s in the editor
|
|
||||||
func func_godot_map_unwrap_uv2() -> void:
|
|
||||||
var edited_object = edited_object_ref.get_ref()
|
|
||||||
if not edited_object:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not edited_object is FuncGodotMap:
|
|
||||||
return
|
|
||||||
|
|
||||||
set_func_godot_map_control_disabled(true)
|
|
||||||
edited_object.connect("unwrap_uv2_complete", func_godot_map_build_complete.bind(edited_object))
|
|
||||||
|
|
||||||
edited_object.unwrap_uv2()
|
|
||||||
|
|
||||||
## Enable or disable the control for [FuncGodotMap]s in the editor
|
|
||||||
func set_func_godot_map_control_disabled(disabled: bool) -> void:
|
|
||||||
if not func_godot_map_control:
|
|
||||||
return
|
|
||||||
|
|
||||||
for child in func_godot_map_control.get_children():
|
|
||||||
if child is Button:
|
|
||||||
child.set_disabled(disabled)
|
|
||||||
|
|
||||||
## Update the build progress bar (see: [method create_func_godot_map_progress_bar]) to display the current step and progress (0-1)
|
|
||||||
func func_godot_map_build_progress(step: String, progress: float) -> void:
|
|
||||||
var progress_label = func_godot_map_progress_bar.get_node("ProgressLabel")
|
|
||||||
func_godot_map_progress_bar.value = progress
|
|
||||||
progress_label.text = step.capitalize()
|
|
||||||
|
|
||||||
## Callback for when the build process for a [FuncGodotMap] is finished.
|
## Callback for when the build process for a [FuncGodotMap] is finished.
|
||||||
func func_godot_map_build_complete(func_godot_map: FuncGodotMap) -> void:
|
func func_godot_map_build_complete(func_godot_map: FuncGodotMap) -> void:
|
||||||
var progress_label = func_godot_map_progress_bar.get_node("ProgressLabel")
|
#var progress_label = func_godot_map_progress_bar.get_node("ProgressLabel")
|
||||||
progress_label.text = "Build Complete"
|
#progress_label.text = "Build Complete"
|
||||||
|
|
||||||
set_func_godot_map_control_disabled(false)
|
#if func_godot_map.is_connected("build_progress",Callable(self,"func_godot_map_build_progress")):
|
||||||
|
#func_godot_map.disconnect("build_progress",Callable(self,"func_godot_map_build_progress"))
|
||||||
if func_godot_map.is_connected("build_progress",Callable(self,"func_godot_map_build_progress")):
|
|
||||||
func_godot_map.disconnect("build_progress",Callable(self,"func_godot_map_build_progress"))
|
|
||||||
|
|
||||||
if func_godot_map.is_connected("build_complete",Callable(self,"func_godot_map_build_complete")):
|
if func_godot_map.is_connected("build_complete",Callable(self,"func_godot_map_build_complete")):
|
||||||
func_godot_map.disconnect("build_complete",Callable(self,"func_godot_map_build_complete"))
|
func_godot_map.disconnect("build_complete",Callable(self,"func_godot_map_build_complete"))
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://di8rxxgk8bt6b
|
uid://bqy3tr83l7di
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
@icon("res://addons/func_godot/icons/icon_quake_file.svg")
|
@icon("res://addons/func_godot/icons/icon_quake_file.svg")
|
||||||
class_name QuakeMapFile
|
class_name QuakeMapFile extends Resource
|
||||||
extends Resource
|
## Map file that can be built by [FuncGodotMap].
|
||||||
|
##
|
||||||
|
## Map file that can be built by a [FuncGodotMap]. Supports the Quake and Valve map formats.
|
||||||
|
##
|
||||||
|
## @tutorial(Quake Wiki Map Format Article): https://quakewiki.org/wiki/Quake_Map_Format
|
||||||
|
## @tutorial(Valve Developer Wiki VMF Article): https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)
|
||||||
|
|
||||||
|
## Number of times this map file has been imported.
|
||||||
@export var revision: int = 0
|
@export var revision: int = 0
|
||||||
|
|
||||||
|
## Raw map data.
|
||||||
|
@export_multiline var map_data: String = ""
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://cumjyudn5ug5t
|
uid://cxvwf50mehesf
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
@tool
|
@tool
|
||||||
class_name QuakeMapImportPlugin
|
class_name QuakeMapImportPlugin extends EditorImportPlugin
|
||||||
extends EditorImportPlugin
|
|
||||||
|
|
||||||
# Quake super.map import plugin
|
|
||||||
|
|
||||||
func _get_importer_name() -> String:
|
func _get_importer_name() -> String:
|
||||||
return 'func_godot.map'
|
return 'func_godot.map'
|
||||||
@@ -14,7 +11,7 @@ func _get_resource_type() -> String:
|
|||||||
return 'Resource'
|
return 'Resource'
|
||||||
|
|
||||||
func _get_recognized_extensions() -> PackedStringArray:
|
func _get_recognized_extensions() -> PackedStringArray:
|
||||||
return PackedStringArray(['map'])
|
return PackedStringArray(['map','vmf'])
|
||||||
|
|
||||||
func _get_priority():
|
func _get_priority():
|
||||||
return 1.0
|
return 1.0
|
||||||
@@ -36,11 +33,11 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
|
|||||||
|
|
||||||
var map_resource : QuakeMapFile = null
|
var map_resource : QuakeMapFile = null
|
||||||
|
|
||||||
var existing_resource := load(save_path_str) as QuakeMapFile
|
if ResourceLoader.exists(save_path_str):
|
||||||
if(existing_resource != null):
|
map_resource = load(save_path_str) as QuakeMapFile
|
||||||
map_resource = existing_resource
|
|
||||||
map_resource.revision += 1
|
map_resource.revision += 1
|
||||||
else:
|
else:
|
||||||
map_resource = QuakeMapFile.new()
|
map_resource = QuakeMapFile.new()
|
||||||
|
map_resource.map_data = FileAccess.open(source_file, FileAccess.READ).get_as_text(true)
|
||||||
|
|
||||||
return ResourceSaver.save(map_resource, save_path_str)
|
return ResourceSaver.save(map_resource, save_path_str)
|
||||||
@@ -1 +1 @@
|
|||||||
uid://chmlk6gbseg5k
|
uid://dnsj08ot32vpc
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
@icon("res://addons/func_godot/icons/icon_quake_file.svg")
|
@icon("res://addons/func_godot/icons/icon_quake_file.svg")
|
||||||
class_name QuakePaletteFile
|
class_name QuakePaletteFile extends Resource
|
||||||
extends Resource
|
## Quake LMP palette format file used with [QuakeWadFile].
|
||||||
|
##
|
||||||
|
## Quake LMP palette format file used in conjunction with a Quake WAD2 format [QuakeWadFile].
|
||||||
|
## Not required for the Valve WAD3 format.
|
||||||
|
##
|
||||||
|
## @tutorial(Quake Wiki Palette Article): https://quakewiki.org/wiki/Quake_palette#palette.lmp
|
||||||
|
|
||||||
|
## Collection of [Color]s retrieved from the LMP palette file.
|
||||||
@export var colors: PackedColorArray
|
@export var colors: PackedColorArray
|
||||||
|
|
||||||
func _init(colors):
|
func _init(colors):
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://bsoyy4aub0hwo
|
uid://dqhjx7jjbif5d
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
@tool
|
@tool
|
||||||
class_name QuakePaletteImportPlugin
|
class_name QuakePaletteImportPlugin extends EditorImportPlugin
|
||||||
extends EditorImportPlugin
|
|
||||||
|
|
||||||
# Quake super.map import plugin
|
|
||||||
|
|
||||||
func _get_importer_name() -> String:
|
func _get_importer_name() -> String:
|
||||||
return 'func_godot.palette'
|
return 'func_godot.palette'
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://djehnbm5nyrwu
|
uid://c6k7hftart3u3
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
@icon("res://addons/func_godot/icons/icon_quake_file.svg")
|
@icon("res://addons/func_godot/icons/icon_quake_file.svg")
|
||||||
class_name QuakeWadFile
|
class_name QuakeWadFile extends Resource
|
||||||
extends Resource
|
## Texture container in the WAD2 or WAD3 format.
|
||||||
|
##
|
||||||
|
## Texture container in the Quake WAD2 or Valve WAD3 format.
|
||||||
|
##
|
||||||
|
## @tutorial(Quake Wiki WAD Article): https://quakewiki.org/wiki/Texture_Wad
|
||||||
|
## @tutorial(Valve Developer Wiki WAD3 Article): https://developer.valvesoftware.com/wiki/WAD
|
||||||
|
|
||||||
@export var textures: Dictionary
|
## Collection of [ImageTexture] imported from the WAD file.
|
||||||
|
@export var textures: Dictionary[String, ImageTexture]
|
||||||
|
|
||||||
func _init(textures: Dictionary):
|
func _init(textures: Dictionary = Dictionary()):
|
||||||
self.textures = textures
|
self.textures = textures
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://clpdmkhontvmb
|
uid://cij36hpqc46c
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
@tool
|
@tool
|
||||||
class_name QuakeWadImportPlugin extends EditorImportPlugin
|
class_name QuakeWadImportPlugin extends EditorImportPlugin
|
||||||
|
|
||||||
enum WadEntryType {
|
enum WadFormat {
|
||||||
|
Quake,
|
||||||
|
HalfLife
|
||||||
|
}
|
||||||
|
|
||||||
|
enum QuakeWadEntryType {
|
||||||
Palette = 0x40,
|
Palette = 0x40,
|
||||||
SBarPic = 0x42,
|
SBarPic = 0x42,
|
||||||
MipsTexture = 0x44,
|
MipsTexture = 0x44,
|
||||||
ConsolePic = 0x45
|
ConsolePic = 0x45
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum HalfLifeWadEntryType {
|
||||||
|
QPic = 0x42,
|
||||||
|
MipsTexture = 0x43,
|
||||||
|
FixedFont = 0x45
|
||||||
|
}
|
||||||
|
|
||||||
const TEXTURE_NAME_LENGTH := 16
|
const TEXTURE_NAME_LENGTH := 16
|
||||||
const MAX_MIP_LEVELS := 4
|
const MAX_MIP_LEVELS := 4
|
||||||
|
|
||||||
@@ -15,7 +26,7 @@ func _get_importer_name() -> String:
|
|||||||
return 'func_godot.wad'
|
return 'func_godot.wad'
|
||||||
|
|
||||||
func _get_visible_name() -> String:
|
func _get_visible_name() -> String:
|
||||||
return 'Quake Texture2D WAD'
|
return 'Quake WAD'
|
||||||
|
|
||||||
func _get_resource_type() -> String:
|
func _get_resource_type() -> String:
|
||||||
return 'Resource'
|
return 'Resource'
|
||||||
@@ -62,20 +73,24 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
|
|||||||
print(['Error opening super.wad file: ', err])
|
print(['Error opening super.wad file: ', err])
|
||||||
return err
|
return err
|
||||||
|
|
||||||
var palette_path : String = options['palette_file']
|
|
||||||
var palette_file : QuakePaletteFile = load(palette_path) as QuakePaletteFile
|
|
||||||
if not palette_file:
|
|
||||||
print('Error: Invalid palette file')
|
|
||||||
return ERR_CANT_ACQUIRE_RESOURCE
|
|
||||||
|
|
||||||
# Read WAD header
|
# Read WAD header
|
||||||
var magic : PackedByteArray = file.get_buffer(4)
|
var magic : PackedByteArray = file.get_buffer(4)
|
||||||
var magic_string : String = magic.get_string_from_ascii()
|
var magic_string : String = magic.get_string_from_ascii()
|
||||||
|
var wad_format: int = WadFormat.Quake
|
||||||
|
|
||||||
if(magic_string != 'WAD2'):
|
if magic_string == 'WAD3':
|
||||||
|
wad_format = WadFormat.HalfLife
|
||||||
|
elif magic_string != 'WAD2':
|
||||||
print('Error: Invalid WAD magic')
|
print('Error: Invalid WAD magic')
|
||||||
return ERR_INVALID_DATA
|
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')
|
||||||
|
file.close()
|
||||||
|
return ERR_CANT_ACQUIRE_RESOURCE
|
||||||
|
|
||||||
var num_entries : int = file.get_32()
|
var num_entries : int = file.get_32()
|
||||||
var dir_offset : int = file.get_32()
|
var dir_offset : int = file.get_32()
|
||||||
|
|
||||||
@@ -95,7 +110,8 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
|
|||||||
var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH)
|
var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH)
|
||||||
var name_string : String = name.get_string_from_ascii()
|
var name_string : String = name.get_string_from_ascii()
|
||||||
|
|
||||||
if type == int(WadEntryType.MipsTexture):
|
if (wad_format == WadFormat.Quake and type == int(QuakeWadEntryType.MipsTexture)) or (
|
||||||
|
wad_format == WadFormat.HalfLife and type == int(HalfLifeWadEntryType.MipsTexture)):
|
||||||
entries.append([
|
entries.append([
|
||||||
offset,
|
offset,
|
||||||
in_wad_size,
|
in_wad_size,
|
||||||
@@ -109,7 +125,6 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
|
|||||||
var texture_data_array: Array = []
|
var texture_data_array: Array = []
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
var offset : int = entry[0]
|
var offset : int = entry[0]
|
||||||
file.seek(0)
|
|
||||||
file.seek(offset)
|
file.seek(offset)
|
||||||
|
|
||||||
var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH)
|
var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH)
|
||||||
@@ -123,10 +138,29 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
|
|||||||
mip_offsets.append(file.get_32())
|
mip_offsets.append(file.get_32())
|
||||||
|
|
||||||
var num_pixels : int = width * height
|
var num_pixels : int = width * height
|
||||||
texture_data_array.append([name_string, width, height, file.get_buffer(num_pixels)])
|
var pixels : PackedByteArray = file.get_buffer(num_pixels)
|
||||||
|
|
||||||
|
if wad_format == WadFormat.Quake:
|
||||||
|
texture_data_array.append([name_string, width, height, pixels])
|
||||||
|
continue
|
||||||
|
# Half-Life WADs have a 256 color palette embedded in each texture
|
||||||
|
elif wad_format == WadFormat.HalfLife:
|
||||||
|
# Find the end of the mipmap data
|
||||||
|
file.seek(offset + mip_offsets[-1] + (width / 8) * (height / 8))
|
||||||
|
file.get_16()
|
||||||
|
|
||||||
|
var palette_colors := PackedColorArray()
|
||||||
|
for idx in 256:
|
||||||
|
var red : int = file.get_8()
|
||||||
|
var green : int = file.get_8()
|
||||||
|
var blue : int = file.get_8()
|
||||||
|
var color := Color(red / 255.0, green / 255.0, blue / 255.0)
|
||||||
|
palette_colors.append(color)
|
||||||
|
|
||||||
|
texture_data_array.append([name_string, width, height, pixels, palette_colors])
|
||||||
|
|
||||||
# Create texture resources
|
# Create texture resources
|
||||||
var textures : Dictionary = {}
|
var textures : Dictionary[String, ImageTexture] = {}
|
||||||
|
|
||||||
for texture_data in texture_data_array:
|
for texture_data in texture_data_array:
|
||||||
var name : String = texture_data[0]
|
var name : String = texture_data[0]
|
||||||
@@ -134,20 +168,41 @@ func _import(source_file, save_path, options, r_platform_variants, r_gen_files)
|
|||||||
var height : int = texture_data[2]
|
var height : int = texture_data[2]
|
||||||
var pixels : PackedByteArray = texture_data[3]
|
var pixels : PackedByteArray = texture_data[3]
|
||||||
|
|
||||||
|
var texture_image : Image
|
||||||
var pixels_rgb := PackedByteArray()
|
var pixels_rgb := PackedByteArray()
|
||||||
for palette_color in pixels:
|
|
||||||
var rgb_color := palette_file.colors[palette_color] as Color
|
|
||||||
pixels_rgb.append(rgb_color.r8)
|
|
||||||
pixels_rgb.append(rgb_color.g8)
|
|
||||||
pixels_rgb.append(rgb_color.b8)
|
|
||||||
|
|
||||||
var texture_image := Image.create_from_data(width, height, false, Image.FORMAT_RGB8, pixels_rgb)
|
if wad_format == WadFormat.HalfLife:
|
||||||
|
var colors : PackedColorArray = texture_data[4]
|
||||||
|
for palette_color in pixels:
|
||||||
|
var rgb_color : Color = colors[palette_color]
|
||||||
|
pixels_rgb.append(rgb_color.r8)
|
||||||
|
pixels_rgb.append(rgb_color.g8)
|
||||||
|
pixels_rgb.append(rgb_color.b8)
|
||||||
|
# Color(0, 0, 255) is used for transparency in Half-Life
|
||||||
|
if rgb_color.b == 1 and rgb_color.r == 0 and rgb_color.b == 0:
|
||||||
|
pixels_rgb.append(0)
|
||||||
|
else:
|
||||||
|
pixels_rgb.append(255)
|
||||||
|
texture_image = Image.create_from_data(width, height, false, Image.FORMAT_RGBA8, pixels_rgb)
|
||||||
|
|
||||||
|
else: # WadFormat.Quake
|
||||||
|
for palette_color in pixels:
|
||||||
|
var rgb_color : Color = palette_file.colors[palette_color]
|
||||||
|
pixels_rgb.append(rgb_color.r8)
|
||||||
|
pixels_rgb.append(rgb_color.g8)
|
||||||
|
pixels_rgb.append(rgb_color.b8)
|
||||||
|
# Palette index 255 is used for transparency
|
||||||
|
if palette_color != 255:
|
||||||
|
pixels_rgb.append(255)
|
||||||
|
else:
|
||||||
|
pixels_rgb.append(0)
|
||||||
|
texture_image = Image.create_from_data(width, height, false, Image.FORMAT_RGBA8, pixels_rgb)
|
||||||
|
|
||||||
if options["generate_mipmaps"] == true:
|
if options["generate_mipmaps"] == true:
|
||||||
texture_image.generate_mipmaps()
|
texture_image.generate_mipmaps()
|
||||||
|
|
||||||
var texture := ImageTexture.create_from_image(texture_image) #,Texture2D.FLAG_MIPMAPS | Texture2D.FLAG_REPEAT | Texture2D.FLAG_ANISOTROPIC_FILTER
|
var texture := ImageTexture.create_from_image(texture_image) #,Texture2D.FLAG_MIPMAPS | Texture2D.FLAG_REPEAT | Texture2D.FLAG_ANISOTROPIC_FILTER
|
||||||
|
textures[name.to_lower()] = texture
|
||||||
textures[name] = texture
|
|
||||||
|
|
||||||
# Save WAD resource
|
# Save WAD resource
|
||||||
var wad_resource := QuakeWadFile.new(textures)
|
var wad_resource := QuakeWadFile.new(textures)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://cv4ixyaqrjisb
|
uid://ridgf32rxg6s
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
|||||||
uid://d0x6kb7gh8xb4
|
uid://cwu5cf7a0awcd
|
||||||
|
|||||||
@@ -1,19 +1,49 @@
|
|||||||
|
@tool
|
||||||
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
||||||
|
class_name FuncGodotMapSettings extends Resource
|
||||||
## Reusable map settings configuration for [FuncGodotMap] nodes.
|
## Reusable map settings configuration for [FuncGodotMap] nodes.
|
||||||
class_name FuncGodotMapSettings
|
|
||||||
extends Resource
|
|
||||||
|
|
||||||
## Ratio between map editor units and Godot units. FuncGodot will divide brush coordinates by this number when building. This does not affect entity properties unless scripted to do so.
|
#region BUILD
|
||||||
@export var inverse_scale_factor: float = 32.0
|
@export_category("Build Settings")
|
||||||
|
## Set automatically when [member inverse_scale_factor] is changed. Used primarily during the build process.
|
||||||
|
var scale_factor: float = 0.03125
|
||||||
|
|
||||||
|
## Ratio between map editor units and Godot units. FuncGodot will divide brush coordinates by this number and save the results to [member scale_factor].
|
||||||
|
## This does not affect entity properties unless scripted to do so.
|
||||||
|
@export var inverse_scale_factor: float = 32.0 :
|
||||||
|
set(value):
|
||||||
|
if value == 0.0:
|
||||||
|
printerr("Error: Cannot set Inverse Scale Factor to Zero")
|
||||||
|
return
|
||||||
|
inverse_scale_factor = value
|
||||||
|
scale_factor = 1.0 / value
|
||||||
|
|
||||||
## [FuncGodotFGDFile] that translates map file classnames into Godot nodes and packed scenes.
|
## [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")
|
@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 `name_property` in [FuncGodotFGDEntityClass].
|
## 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.
|
## 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.
|
## 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 = ""
|
@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"
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region TEXTURES
|
||||||
@export_category("Textures")
|
@export_category("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.
|
## 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.
|
||||||
@@ -22,17 +52,38 @@ extends Resource
|
|||||||
## File extensions to search for texture data.
|
## File extensions to search for texture data.
|
||||||
@export var texture_file_extensions: Array[String] = ["png", "jpg", "jpeg", "bmp", "tga", "webp"]
|
@export var texture_file_extensions: Array[String] = ["png", "jpg", "jpeg", "bmp", "tga", "webp"]
|
||||||
|
|
||||||
## 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 [MeshInstance3D] but not the generated [CollisionShape3D].
|
## Optional path for the clip texture, relative to [member base_texture_dir].
|
||||||
@export var clip_texture: String = "special/clip"
|
## 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":
|
||||||
|
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 [MeshInstance3D]. If the [FuncGodotFGDSolidClass] `collision_shape_type` is set to concave then it will also remove collision from those faces in the generated [CollisionShape3D].
|
## Optional path for the skip texture, relative to [member base_texture_dir].
|
||||||
@export var skip_texture: String = "special/skip"
|
## 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":
|
||||||
|
set(tex):
|
||||||
|
skip_texture = tex.to_lower()
|
||||||
|
|
||||||
## Optional [QuakeWADFile] resources to apply textures from. See the [Quake Wiki](https://quakewiki.org/wiki/Texture_Wad) for more information on Quake Texture WADs.
|
## Optional path for the origin texture, relative to [member base_texture_dir].
|
||||||
@export var texture_wads: Array[Resource] = []
|
## 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":
|
||||||
|
set(tex):
|
||||||
|
origin_texture = tex.to_lower()
|
||||||
|
|
||||||
|
## 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] = []
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region MATERIALS
|
||||||
@export_category("Materials")
|
@export_category("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].
|
||||||
|
@export_dir var base_material_dir: String = ""
|
||||||
|
|
||||||
## File extension to search for [Material] definitions
|
## File extension to search for [Material] definitions
|
||||||
@export var material_file_extension: String = "tres"
|
@export var material_file_extension: String = "tres"
|
||||||
|
|
||||||
@@ -56,21 +107,19 @@ extends Resource
|
|||||||
@export var ao_map_pattern: String = "%s_ao.%s"
|
@export var ao_map_pattern: String = "%s_ao.%s"
|
||||||
## Automatic PBR material generation height map pattern
|
## Automatic PBR material generation height map pattern
|
||||||
@export var height_map_pattern: String = "%s_height.%s"
|
@export var height_map_pattern: String = "%s_height.%s"
|
||||||
|
## Automatic PBR material generation ORM map pattern
|
||||||
|
@export var orm_map_pattern: String = "%s_orm.%s"
|
||||||
|
|
||||||
## If true, all materials will be unshaded, ignoring light. Also known as "fullbright".
|
## Save automatically generated materials to disk, allowing reuse across [FuncGodotMap] nodes.
|
||||||
@export var unshaded: bool = false
|
## [i]NOTE: Materials do not use the [member default_material] settings after saving.[/i]
|
||||||
|
|
||||||
## Save automatically generated materials to disk, allowing reuse across [FuncGodotMap] nodes. [i]NOTE: Materials do not use the Default Material settings after saving.[/i]
|
|
||||||
@export var save_generated_materials: bool = true
|
@export var save_generated_materials: bool = true
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
@export_category("UV Unwrap")
|
@export_category("UV Unwrap")
|
||||||
|
|
||||||
## Texel size for UV2 unwrapping.
|
## Texel size for UV2 unwrapping.
|
||||||
## A texel size of 1 will lead to a 1:1 correspondence between texture texels and lightmap texels. Larger values will produce less detailed lightmaps. To conserve memory and filesize, use the largest value that still looks good.
|
## 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
|
||||||
@export var uv_unwrap_texel_size: float = 1.0
|
## (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_category("TrenchBroom")
|
@export var uv_unwrap_texel_size: float = 2.0
|
||||||
|
|
||||||
## If true, will organize Scene Tree using Trenchbroom Layers and Groups. Layers and Groups will be generated as [Node3D] nodes.
|
|
||||||
## All structural brushes will be moved out of the Layers and Groups and merged into the Worldspawn entity.
|
|
||||||
@export var use_trenchbroom_groups_hierarchy: bool = false
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://dhlc0pfo61428
|
uid://38q6k0ctahjn
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
@tool
|
@tool
|
||||||
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
||||||
|
class_name NetRadiantCustomGamePackConfig extends Resource
|
||||||
## Builds a gamepack for NetRadiant Custom.
|
## Builds a gamepack for NetRadiant Custom.
|
||||||
class_name NetRadiantCustomGamePackConfig
|
##
|
||||||
extends Resource
|
## Resource that builds a gamepack configuration for NetRadiant Custom.
|
||||||
|
|
||||||
## Button to export / update this gamepack's configuration in the NetRadiant Custom Gamepacks Folder.
|
enum NetRadiantCustomMapType {
|
||||||
@export var export_file: bool:
|
QUAKE_1, ## Removes PatchDef entries from the map file.
|
||||||
get:
|
QUAKE_3 ## Allows the saving of PatchDef entries in the map file.
|
||||||
return export_file
|
}
|
||||||
set(new_export_file):
|
|
||||||
if new_export_file != export_file:
|
@export_tool_button("Export Gamepack") var _export_file: Callable = export_file
|
||||||
if Engine.is_editor_hint():
|
|
||||||
do_export_file()
|
|
||||||
|
|
||||||
## Gamepack folder and file name. Must be lower case and must not contain special characters.
|
## Gamepack folder and file name. Must be lower case and must not contain special characters.
|
||||||
@export var gamepack_name : String = "func_godot"
|
@export var gamepack_name : String = "func_godot":
|
||||||
|
set(new_name):
|
||||||
|
gamepack_name = new_name.to_lower()
|
||||||
|
|
||||||
## Name of the game in NetRadiant Custom's gamepack list.
|
## Name of the game in NetRadiant Custom's gamepack list.
|
||||||
@export var game_name : String = "FuncGodot"
|
@export var game_name : String = "FuncGodot"
|
||||||
@@ -22,13 +23,15 @@ extends Resource
|
|||||||
## Directory path containing your maps, textures, shaders, etc... relative to your project directory.
|
## Directory path containing your maps, textures, shaders, etc... relative to your project directory.
|
||||||
@export var base_game_path : String = ""
|
@export var base_game_path : String = ""
|
||||||
|
|
||||||
## FGD resource to include with this gamepack. If using multiple FGD resources, this should be the master FGD that contains them in the `base_fgd_files` resource array.
|
## [FuncGodotFGDFile] to include with this gamepack. If using multiple FGD file resources,
|
||||||
|
## 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")
|
@export var fgd_file : FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
|
||||||
|
|
||||||
## [NetRadiantCustomShader] resources for shader file generation.
|
## Collection of [NetRadiantCustomShader] resources for shader file generation.
|
||||||
@export var netradiant_custom_shaders : Array[Resource] = [
|
@export var netradiant_custom_shaders : Array[Resource] = [
|
||||||
preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres"),
|
preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres"),
|
||||||
preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres")
|
preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres"),
|
||||||
|
preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_origin.tres")
|
||||||
]
|
]
|
||||||
|
|
||||||
## Supported texture file types.
|
## Supported texture file types.
|
||||||
@@ -43,14 +46,31 @@ extends Resource
|
|||||||
## Default scale of textures in NetRadiant Custom.
|
## Default scale of textures in NetRadiant Custom.
|
||||||
@export var default_scale : String = "1.0"
|
@export var default_scale : String = "1.0"
|
||||||
|
|
||||||
## Clip texture path that gets applied to weapclip and nodraw shaders.
|
## 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/special/clip"
|
||||||
|
|
||||||
## Skip texture path that gets applied to caulk and nodrawnonsolid shaders.
|
## Skip texture path that gets applied to [i]caulk[/i] and [i]nodrawnonsolid[/i] shaders.
|
||||||
@export var skip_texture: String = "textures/special/skip"
|
@export var skip_texture: String = "textures/special/skip"
|
||||||
|
|
||||||
## Generates completed text for a .shader file.
|
## Quake map type NetRadiant will filter the map for, determining whether PatchDef entries are saved.
|
||||||
func build_shader_text() -> String:
|
## [color=red][b]WARNING![/b][/color] Toggling this option may be destructive!
|
||||||
|
@export var map_type: NetRadiantCustomMapType = NetRadiantCustomMapType.QUAKE_1
|
||||||
|
|
||||||
|
## 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]
|
||||||
|
## Entries may be referred to by key in [member default_build_menu_commands] values.
|
||||||
|
@export var default_build_menu_variables: Dictionary
|
||||||
|
|
||||||
|
## Commands to include in the exported gamepack's [code]default_build_menu.xml[/code].[br][br]
|
||||||
|
## Keys, specified as a [String], define the build option name as you want it to appear in NetRadiant Custom.[br][br]
|
||||||
|
## Values represent commands taken within each option.[br][br]They may be either a [String] or an [Array] of [String] elements
|
||||||
|
## that will be used as the full command-line text issued by each command [i]within[/i] its associated build option key.[br][br]
|
||||||
|
## They may reference entries in [member default_build_menu_variables] by using brackets: [code][variable key name][/code]
|
||||||
|
@export var default_build_menu_commands: Dictionary
|
||||||
|
|
||||||
|
# Generates completed text for a .shader file.
|
||||||
|
func _build_shader_text() -> String:
|
||||||
var shader_text: String = ""
|
var shader_text: String = ""
|
||||||
for shader_res in netradiant_custom_shaders:
|
for shader_res in netradiant_custom_shaders:
|
||||||
shader_text += (shader_res as NetRadiantCustomShader).texture_path + "\n{\n"
|
shader_text += (shader_res as NetRadiantCustomShader).texture_path + "\n{\n"
|
||||||
@@ -59,8 +79,8 @@ func build_shader_text() -> String:
|
|||||||
shader_text += "}\n"
|
shader_text += "}\n"
|
||||||
return shader_text
|
return shader_text
|
||||||
|
|
||||||
## Generates completed text for a .gamepack file.
|
# Generates completed text for a .gamepack file.
|
||||||
func build_gamepack_text() -> String:
|
func _build_gamepack_text() -> String:
|
||||||
var texturetypes_str: String = ""
|
var texturetypes_str: String = ""
|
||||||
for texture_type in texture_types:
|
for texture_type in texture_types:
|
||||||
texturetypes_str += texture_type
|
texturetypes_str += texture_type
|
||||||
@@ -79,6 +99,13 @@ func build_gamepack_text() -> String:
|
|||||||
if sound_type != sound_types[-1]:
|
if sound_type != sound_types[-1]:
|
||||||
soundtypes_str += " "
|
soundtypes_str += " "
|
||||||
|
|
||||||
|
var maptype_str: String
|
||||||
|
|
||||||
|
if map_type == NetRadiantCustomMapType.QUAKE_3:
|
||||||
|
maptype_str = "mapq3"
|
||||||
|
else:
|
||||||
|
maptype_str = "mapq1"
|
||||||
|
|
||||||
var gamepack_text: String = """<?xml version="1.0"?>
|
var gamepack_text: String = """<?xml version="1.0"?>
|
||||||
<game
|
<game
|
||||||
type="q3"
|
type="q3"
|
||||||
@@ -96,9 +123,9 @@ func build_gamepack_text() -> String:
|
|||||||
texturetypes="%s"
|
texturetypes="%s"
|
||||||
modeltypes="%s"
|
modeltypes="%s"
|
||||||
soundtypes="%s"
|
soundtypes="%s"
|
||||||
maptypes="mapq1"
|
maptypes="%s"
|
||||||
shaders="quake3"
|
shaders="quake3"
|
||||||
entityclass="halflife"
|
entityclass="quake3"
|
||||||
entityclasstype="fgd"
|
entityclasstype="fgd"
|
||||||
entities="quake"
|
entities="quake"
|
||||||
brushtypes="quake"
|
brushtypes="quake"
|
||||||
@@ -126,6 +153,7 @@ func build_gamepack_text() -> String:
|
|||||||
texturetypes_str,
|
texturetypes_str,
|
||||||
modeltypes_str,
|
modeltypes_str,
|
||||||
soundtypes_str,
|
soundtypes_str,
|
||||||
|
maptype_str,
|
||||||
default_scale,
|
default_scale,
|
||||||
clip_texture,
|
clip_texture,
|
||||||
skip_texture,
|
skip_texture,
|
||||||
@@ -133,9 +161,10 @@ func build_gamepack_text() -> String:
|
|||||||
skip_texture
|
skip_texture
|
||||||
]
|
]
|
||||||
|
|
||||||
## Exports or updates a folder in the /games directory, with an icon, .cfg, and all accompanying FGDs.
|
## Exports this game's configuration with an icon, .cfg, and all accompanying FGD files in the [FuncGodotLocalConfig] [b]NetRadiant Custom Gamepacks Folder[/b].
|
||||||
func do_export_file() -> void:
|
func export_file() -> void:
|
||||||
if (FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.MAP_EDITOR_GAME_PATH) as String).is_empty():
|
var game_path: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.MAP_EDITOR_GAME_PATH) as String
|
||||||
|
if game_path.is_empty():
|
||||||
printerr("Skipping export: Map Editor Game Path not set in Project Configuration")
|
printerr("Skipping export: Map Editor Game Path not set in Project Configuration")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -158,7 +187,8 @@ func do_export_file() -> void:
|
|||||||
var gamepack_dir_paths: Array = [
|
var gamepack_dir_paths: Array = [
|
||||||
gamepacks_folder + "/" + gamepack_name + ".game",
|
gamepacks_folder + "/" + gamepack_name + ".game",
|
||||||
gamepacks_folder + "/" + gamepack_name + ".game/" + base_game_path,
|
gamepacks_folder + "/" + gamepack_name + ".game/" + base_game_path,
|
||||||
gamepacks_folder + "/" + gamepack_name + ".game/scripts"
|
gamepacks_folder + "/" + gamepack_name + ".game/scripts",
|
||||||
|
game_path + "/scripts"
|
||||||
]
|
]
|
||||||
var err: Error
|
var err: Error
|
||||||
|
|
||||||
@@ -178,24 +208,50 @@ func do_export_file() -> void:
|
|||||||
print("Exporting NetRadiant Custom Gamepack to ", target_file_path)
|
print("Exporting NetRadiant Custom Gamepack to ", target_file_path)
|
||||||
file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
||||||
if file != null:
|
if file != null:
|
||||||
file.store_string(build_gamepack_text())
|
file.store_string(_build_gamepack_text())
|
||||||
file.close()
|
file.close()
|
||||||
else:
|
else:
|
||||||
printerr("Error: Could not modify " + target_file_path)
|
printerr("Error: Could not modify " + target_file_path)
|
||||||
|
|
||||||
# .shader
|
# .shader
|
||||||
|
# NOTE: To work properly, this should go in the game path. For now, I'm leaving the export to NRC as well, so it can easily
|
||||||
|
# be repackaged for distribution. However, I believe in the end, it shouldn't exist there.
|
||||||
|
# We'll need to make a decision for this. - Vera
|
||||||
|
var shader_text: String = _build_shader_text()
|
||||||
|
|
||||||
|
# build to <gamepack path>/scripts/
|
||||||
target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/" + gamepack_name + ".shader"
|
target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/" + gamepack_name + ".shader"
|
||||||
print("Exporting NetRadiant Custom Shader to ", target_file_path)
|
print("Exporting NetRadiant Custom shader definitions to ", target_file_path)
|
||||||
file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
||||||
if file != null:
|
if file != null:
|
||||||
file.store_string(build_shader_text())
|
file.store_string(shader_text)
|
||||||
file.close()
|
file.close()
|
||||||
else:
|
else:
|
||||||
printerr("Error: Could not modify " + target_file_path)
|
printerr("Error: Could not modify " + target_file_path)
|
||||||
|
|
||||||
# shaderlist.txt
|
# build to <game path>/scripts/
|
||||||
|
target_file_path = game_path.path_join("scripts/%s.shader" % gamepack_name)
|
||||||
|
print("Exporting NetRadiant Custom shader definitions to ", target_file_path)
|
||||||
|
file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
||||||
|
if file != null:
|
||||||
|
file.store_string(shader_text)
|
||||||
|
file.close()
|
||||||
|
else:
|
||||||
|
printerr("Error: could not modify " + target_file_path)
|
||||||
|
|
||||||
|
# shaderlist.txt - see above NOTE regarding duplication
|
||||||
target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/shaderlist.txt"
|
target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/shaderlist.txt"
|
||||||
print("Exporting NetRadiant Custom Default Buld Menu to ", target_file_path)
|
print("Exporting NetRadiant Custom shader list to ", target_file_path)
|
||||||
|
file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
||||||
|
if file != null:
|
||||||
|
file.store_string(gamepack_name)
|
||||||
|
file.close()
|
||||||
|
else:
|
||||||
|
printerr("Error: Could not modify " + target_file_path)
|
||||||
|
|
||||||
|
# game path/scripts/shaderlist.txt
|
||||||
|
target_file_path = game_path.path_join("scripts/shaderlist.txt")
|
||||||
|
print("Exporting NetRadiant Custom shader list to ", target_file_path)
|
||||||
file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
||||||
if file != null:
|
if file != null:
|
||||||
file.store_string(gamepack_name)
|
file.store_string(gamepack_name)
|
||||||
@@ -205,15 +261,56 @@ func do_export_file() -> void:
|
|||||||
|
|
||||||
# default_build_menu.xml
|
# default_build_menu.xml
|
||||||
target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/default_build_menu.xml"
|
target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/default_build_menu.xml"
|
||||||
print("Exporting NetRadiant Custom Default Buld Menu to ", target_file_path)
|
print("Exporting NetRadiant Custom default build menu to ", target_file_path)
|
||||||
file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
||||||
|
|
||||||
if file != null:
|
if file != null:
|
||||||
file.store_string("<?xml version=\"1.0\"?><project version=\"2.0\"></project>")
|
file.store_string("<?xml version=\"1.0\"?>\n<project version=\"2.0\">\n")
|
||||||
file.close()
|
|
||||||
else:
|
for key in default_build_menu_variables.keys():
|
||||||
printerr("Error: Could not modify " + target_file_path)
|
if key is String:
|
||||||
|
if default_build_menu_variables[key] is String:
|
||||||
|
file.store_string('\t<var name="%s">%s</var>\n' % [key, default_build_menu_variables[key]])
|
||||||
|
|
||||||
|
else:
|
||||||
|
push_error(
|
||||||
|
"Variable key '%s' value '%s' is invalid type: %s; should be: String" % [
|
||||||
|
key, default_build_menu_variables[key],
|
||||||
|
type_string(typeof(default_build_menu_variables[key]))
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
push_error(
|
||||||
|
"Variable '%s' is an invalid key type: %s; should be: String" % [
|
||||||
|
key, type_string(typeof(key))
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
for key in default_build_menu_commands.keys():
|
||||||
|
if key is String:
|
||||||
|
file.store_string('\t<build name="%s">\n' % key)
|
||||||
|
|
||||||
|
if default_build_menu_commands[key] is String:
|
||||||
|
file.store_string('\t\t<command>%s</command>\n\t</build>\n' % default_build_menu_commands[key])
|
||||||
|
|
||||||
|
elif default_build_menu_commands[key] is Array:
|
||||||
|
for command in default_build_menu_commands[key]:
|
||||||
|
if command is String:
|
||||||
|
file.store_string('\t\t<command>%s</command>\n' % command)
|
||||||
|
else:
|
||||||
|
push_error("Build option '%s' has invalid command: %s with type: %s; should be: String" % [
|
||||||
|
key, command, type_string(typeof(command))
|
||||||
|
])
|
||||||
|
|
||||||
|
file.store_string('\t</build>\n')
|
||||||
|
|
||||||
|
else:
|
||||||
|
push_error("Build option '%s' is an invalid type: %s; should be: String" % [
|
||||||
|
key, type_string(typeof(key))
|
||||||
|
])
|
||||||
|
|
||||||
|
file.store_string("</project>")
|
||||||
|
|
||||||
# FGD
|
# FGD
|
||||||
var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
|
var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
|
||||||
export_fgd.do_export_file(true, gamepacks_folder + "/" + gamepack_name + ".game/" + base_game_path)
|
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")
|
print("NetRadiant Custom Gamepack export complete\n")
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://ckvnkpgsci7hg
|
uid://dfhj3me2g5j0l
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
||||||
## Resource that gets built into a shader file that applies a special effect to a specified texture in NetRadiant Custom.
|
|
||||||
class_name NetRadiantCustomShader
|
class_name NetRadiantCustomShader
|
||||||
extends Resource
|
extends Resource
|
||||||
|
## Shader resource for NetRadiant Custom configurations.
|
||||||
|
##
|
||||||
|
## Resource that gets built into a shader file that applies a special effect to a specified texture in NetRadiant Custom.
|
||||||
|
|
||||||
## Path to texture without extension, eg: `textures/special/clip`.
|
## Path to texture without extension, eg: [i]"textures/special/clip"[/i].
|
||||||
@export var texture_path: String
|
@export var texture_path: String
|
||||||
|
|
||||||
## Array of shader properties to apply to faces using [member texture_path].
|
## Array of shader properties to apply to faces using [member texture_path].
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://dtmnasjcec1cl
|
uid://dn86acprv4e86
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
@tool
|
@tool
|
||||||
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
||||||
## Defines a game in TrenchBroom to express a set of entity definitions and editor behaviors.
|
class_name TrenchBroomGameConfig extends Resource
|
||||||
class_name TrenchBroomGameConfig
|
## Game configuration definition for TrenchBroom.
|
||||||
extends Resource
|
##
|
||||||
|
## Defines a game for TrenchBroom to express a set of entity definitions and editor behaviors.
|
||||||
|
##
|
||||||
|
## @tutorial(TrenchBroom Manual Game Configuration Information): https://trenchbroom.github.io/manual/latest/#game_configuration
|
||||||
|
|
||||||
## Button to export / update this game's configuration and FGD file in the TrenchBroom Games Path.
|
enum GameConfigVersion {
|
||||||
@export var export_file: bool:
|
Latest,
|
||||||
get:
|
Version4,
|
||||||
return export_file
|
Version8,
|
||||||
set(new_export_file):
|
Version9
|
||||||
if new_export_file != export_file:
|
}
|
||||||
if Engine.is_editor_hint():
|
|
||||||
do_export_file()
|
@export_tool_button("Export GameConfig") var _export_file: Callable = export_file
|
||||||
|
|
||||||
## Name of the game in TrenchBroom's game list.
|
## Name of the game in TrenchBroom's game list.
|
||||||
@export var game_name : String = "FuncGodot"
|
@export var game_name : String = "FuncGodot"
|
||||||
@@ -19,7 +22,8 @@ extends Resource
|
|||||||
## Icon for TrenchBroom's game list.
|
## Icon for TrenchBroom's game list.
|
||||||
@export var icon : Texture2D = preload("res://addons/func_godot/icon32.png")
|
@export var icon : Texture2D = preload("res://addons/func_godot/icon32.png")
|
||||||
|
|
||||||
## Available map formats when creating a new map in TrenchBroom. The order of elements in the array is the order TrenchBroom will list the available formats. The `initialmap` key value is optional.
|
## Available map formats when creating a new map in TrenchBroom. The order of elements in the array is the order TrenchBroom will list the available formats.
|
||||||
|
## The [i]"initialmap"[/i] key value is optional.
|
||||||
@export var map_formats: Array[Dictionary] = [
|
@export var map_formats: Array[Dictionary] = [
|
||||||
{ "format": "Valve", "initialmap": "initial_valve.map" },
|
{ "format": "Valve", "initialmap": "initial_valve.map" },
|
||||||
{ "format": "Standard", "initialmap": "initial_standard.map" },
|
{ "format": "Standard", "initialmap": "initial_standard.map" },
|
||||||
@@ -27,43 +31,63 @@ extends Resource
|
|||||||
{ "format": "Quake3" }
|
{ "format": "Quake3" }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@export_category("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"
|
||||||
|
|
||||||
## Textures matching these patterns will be hidden from TrenchBroom.
|
## Textures matching these patterns will be hidden from TrenchBroom.
|
||||||
@export var texture_exclusion_patterns: Array[String] = ["*_albedo", "*_ao", "*_emission", "*_height", "*_metallic", "*_normal", "*_orm", "*_roughness", "*_sss"]
|
@export var texture_exclusion_patterns: Array[String] = ["*_albedo", "*_ao", "*_emission", "*_height", "*_metallic", "*_normal", "*_orm", "*_roughness", "*_sss"]
|
||||||
|
|
||||||
## FGD resource to include with this game. If using multiple FGD resources, this should be the master FGD that contains them in the `base_fgd_files` resource array.
|
## 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")
|
||||||
|
|
||||||
|
## [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].
|
||||||
@export var fgd_file : FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
|
@export var fgd_file : FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
|
||||||
|
|
||||||
## Scale expression that modifies the default display scale of entities in TrenchBroom. See the [**TrenchBroom Documentation**](https://trenchbroom.github.io/manual/latest/#game_configuration_files_entities) for more information.
|
## Scale expression that modifies the default display scale of entities in TrenchBroom.
|
||||||
|
## 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"
|
@export var entity_scale: String = "32"
|
||||||
|
|
||||||
## Scale of textures on new brushes.
|
## Arrays containing the [TrenchbroomTag] resource type.
|
||||||
@export var default_uv_scale : Vector2 = Vector2(1, 1)
|
@export_category("Tags")
|
||||||
|
|
||||||
## Arrays containing the TrenchBroomTag resource type.
|
## [TrenchbroomTag] resources that apply to brush entities.
|
||||||
@export_category("Editor Hint Tags")
|
|
||||||
|
|
||||||
## TrenchBroomTag resources that apply to brush entities.
|
|
||||||
@export var brush_tags : Array[Resource] = []
|
@export var brush_tags : Array[Resource] = []
|
||||||
|
|
||||||
## TrenchBroomTag resources that apply to brush faces.
|
## [TrenchbroomTag] resources that apply to brush faces.
|
||||||
@export var brushface_tags : Array[Resource] = [
|
@export var brushface_tags : Array[Resource] = [
|
||||||
preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_clip.tres"),
|
preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_clip.tres"),
|
||||||
preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_skip.tres")
|
preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_skip.tres"),
|
||||||
|
preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_origin.tres")
|
||||||
]
|
]
|
||||||
|
|
||||||
## Matches tag key enum to the String name used in .cfg
|
@export_category("Face Attributes")
|
||||||
static func get_match_key(tag_match_type: int) -> String:
|
|
||||||
|
## 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")
|
||||||
|
|
||||||
|
## Game configuration format compatible with the version of TrenchBroom being used.
|
||||||
|
@export var game_config_version: GameConfigVersion = GameConfigVersion.Latest
|
||||||
|
|
||||||
|
# Matches tag key enum to the [String] name used in .cfg
|
||||||
|
static func _get_match_key(tag_match_type: int) -> String:
|
||||||
match tag_match_type:
|
match tag_match_type:
|
||||||
TrenchBroomTag.TagMatchType.TEXTURE:
|
TrenchBroomTag.TagMatchType.TEXTURE:
|
||||||
return "texture"
|
return "material"
|
||||||
TrenchBroomTag.TagMatchType.CLASSNAME:
|
TrenchBroomTag.TagMatchType.CLASSNAME:
|
||||||
return "classname"
|
return "classname"
|
||||||
_:
|
_:
|
||||||
push_error("Tag match type %s is not valid" % [tag_match_type])
|
push_error("Tag match type %s is not valid" % [tag_match_type])
|
||||||
return "ERROR"
|
return "ERROR"
|
||||||
|
|
||||||
## Generates completed text for a .cfg file.
|
# Generates completed text for a .cfg file.
|
||||||
func build_class_text() -> String:
|
func _build_class_text() -> String:
|
||||||
var map_formats_str : String = ""
|
var map_formats_str : String = ""
|
||||||
for map_format in map_formats:
|
for map_format in map_formats:
|
||||||
map_formats_str += "{ \"format\": \"" + map_format.format + "\""
|
map_formats_str += "{ \"format\": \"" + map_format.format + "\""
|
||||||
@@ -82,61 +106,47 @@ func build_class_text() -> String:
|
|||||||
|
|
||||||
var fgd_filename_str : String = "\"" + fgd_file.fgd_name + ".fgd\""
|
var fgd_filename_str : String = "\"" + fgd_file.fgd_name + ".fgd\""
|
||||||
|
|
||||||
var brush_tags_str = parse_tags(brush_tags)
|
var brush_tags_str = _parse_tags(brush_tags)
|
||||||
var brushface_tags_str = parse_tags(brushface_tags)
|
var brushface_tags_str = _parse_tags(brushface_tags)
|
||||||
var uv_scale_str = parse_default_uv_scale(default_uv_scale)
|
var uv_scale_str = _parse_default_uv_scale(default_uv_scale)
|
||||||
|
|
||||||
var config_text : String = """{
|
var config_text : String = ""
|
||||||
"version": 8,
|
match game_config_version:
|
||||||
"name": "%s",
|
GameConfigVersion.Latest, GameConfigVersion.Version8, GameConfigVersion.Version9:
|
||||||
"icon": "icon.png",
|
config_text = _get_game_config_v9v8_text() % [
|
||||||
"fileformats": [
|
game_name,
|
||||||
%s
|
map_formats_str,
|
||||||
],
|
textures_root_folder,
|
||||||
"filesystem": {
|
texture_exclusion_patterns_str,
|
||||||
"searchpath": ".",
|
palette_path,
|
||||||
"packageformat": { "extension": ".zip", "format": "zip" }
|
fgd_filename_str,
|
||||||
},
|
entity_scale,
|
||||||
"textures": {
|
brush_tags_str,
|
||||||
"root": "textures",
|
brushface_tags_str,
|
||||||
"extensions": [".bmp", ".exr", ".hdr", ".jpeg", ".jpg", ".png", ".tga", ".webp"],
|
uv_scale_str
|
||||||
"excludes": [ %s ]
|
]
|
||||||
},
|
|
||||||
"entities": {
|
|
||||||
"definitions": [ %s ],
|
|
||||||
"defaultcolor": "0.6 0.6 0.6 1.0",
|
|
||||||
"scale": %s
|
|
||||||
},
|
|
||||||
"tags": {
|
|
||||||
"brush": [
|
|
||||||
%s
|
|
||||||
],
|
|
||||||
"brushface": [
|
|
||||||
%s
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"faceattribs": {
|
|
||||||
"defaults": {
|
|
||||||
%s
|
|
||||||
},
|
|
||||||
"contentflags": [],
|
|
||||||
"surfaceflags": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
return config_text % [
|
|
||||||
game_name,
|
|
||||||
map_formats_str,
|
|
||||||
texture_exclusion_patterns_str,
|
|
||||||
fgd_filename_str,
|
|
||||||
entity_scale,
|
|
||||||
brush_tags_str,
|
|
||||||
brushface_tags_str,
|
|
||||||
uv_scale_str
|
|
||||||
]
|
|
||||||
|
|
||||||
## Converts brush, FuncGodotFace, and attribute tags into a .cfg-usable String.
|
GameConfigVersion.Version4:
|
||||||
func parse_tags(tags: Array) -> String:
|
config_text = _get_game_config_v4_text() % [
|
||||||
|
game_name,
|
||||||
|
map_formats_str,
|
||||||
|
textures_root_folder,
|
||||||
|
texture_exclusion_patterns_str,
|
||||||
|
palette_path,
|
||||||
|
fgd_filename_str,
|
||||||
|
entity_scale,
|
||||||
|
brush_tags_str,
|
||||||
|
brushface_tags_str,
|
||||||
|
uv_scale_str
|
||||||
|
]
|
||||||
|
|
||||||
|
_:
|
||||||
|
push_error("Unsupported Game Config Version!")
|
||||||
|
|
||||||
|
return config_text
|
||||||
|
|
||||||
|
# Converts brush, face, and attribute tags into a .cfg-usable String.
|
||||||
|
func _parse_tags(tags: Array) -> String:
|
||||||
var tags_str := ""
|
var tags_str := ""
|
||||||
for brush_tag in tags:
|
for brush_tag in tags:
|
||||||
if brush_tag.tag_match_type >= TrenchBroomTag.TagMatchType.size():
|
if brush_tag.tag_match_type >= TrenchBroomTag.TagMatchType.size():
|
||||||
@@ -149,19 +159,21 @@ func parse_tags(tags: Array) -> String:
|
|||||||
if brush_tag_attrib != brush_tag.tag_attributes[-1]:
|
if brush_tag_attrib != brush_tag.tag_attributes[-1]:
|
||||||
attribs_str += ", "
|
attribs_str += ", "
|
||||||
tags_str += "\t\t\t\t\"attribs\": [ %s ],\n" % attribs_str
|
tags_str += "\t\t\t\t\"attribs\": [ %s ],\n" % attribs_str
|
||||||
tags_str += "\t\t\t\t\"match\": \"%s\",\n" % get_match_key(brush_tag.tag_match_type)
|
tags_str += "\t\t\t\t\"match\": \"%s\",\n" % _get_match_key(brush_tag.tag_match_type)
|
||||||
tags_str += "\t\t\t\t\"pattern\": \"%s\"" % brush_tag.tag_pattern
|
tags_str += "\t\t\t\t\"pattern\": \"%s\"" % brush_tag.tag_pattern
|
||||||
if brush_tag.texture_name != "":
|
if brush_tag.texture_name != "":
|
||||||
tags_str += ",\n"
|
tags_str += ",\n"
|
||||||
tags_str += "\t\t\t\t\"texture\": \"%s\"" % brush_tag.texture_name
|
tags_str += "\t\t\t\t\"material\": \"%s\"" % brush_tag.texture_name
|
||||||
tags_str += "\n"
|
tags_str += "\n"
|
||||||
tags_str += "\t\t\t}"
|
tags_str += "\t\t\t}"
|
||||||
if brush_tag != tags[-1]:
|
if brush_tag != tags[-1]:
|
||||||
tags_str += ","
|
tags_str += ","
|
||||||
|
if game_config_version > GameConfigVersion.Latest and game_config_version < GameConfigVersion.Version9:
|
||||||
|
tags_str = tags_str.replace("material", "texture")
|
||||||
return tags_str
|
return tags_str
|
||||||
|
|
||||||
## Converts array of flags to .cfg String.
|
# Converts array of flags to .cfg String.
|
||||||
func parse_flags(flags: Array) -> String:
|
func _parse_flags(flags: Array) -> String:
|
||||||
var flags_str := ""
|
var flags_str := ""
|
||||||
for attrib_flag in flags:
|
for attrib_flag in flags:
|
||||||
flags_str += "{\n"
|
flags_str += "{\n"
|
||||||
@@ -172,23 +184,23 @@ func parse_flags(flags: Array) -> String:
|
|||||||
flags_str += ","
|
flags_str += ","
|
||||||
return flags_str
|
return flags_str
|
||||||
|
|
||||||
## Converts default uv scale vector to .cfg String.
|
# Converts default uv scale vector to .cfg String.
|
||||||
func parse_default_uv_scale(texture_scale : Vector2) -> String:
|
func _parse_default_uv_scale(texture_scale : Vector2) -> String:
|
||||||
var entry_str = "\"scale\": [{x}, {y}]"
|
var entry_str = "\"scale\": [{x}, {y}]"
|
||||||
return entry_str.format({
|
return entry_str.format({
|
||||||
"x": texture_scale.x,
|
"x": texture_scale.x,
|
||||||
"y": texture_scale.y
|
"y": texture_scale.y
|
||||||
})
|
})
|
||||||
|
|
||||||
## Exports or updates a folder in the /games directory, with an icon, .cfg, and all accompanying FGDs.
|
## Exports this game's configuration with an icon, .cfg, and all accompanying FGD files in the [FuncGodotLocalConfig] [b]Trenchbroom Game Config Folder[/b].
|
||||||
func do_export_file() -> void:
|
func export_file() -> void:
|
||||||
var config_folder: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.TRENCHBROOM_GAME_CONFIG_FOLDER) as String
|
var config_folder: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.TRENCHBROOM_GAME_CONFIG_FOLDER) as String
|
||||||
if config_folder.is_empty():
|
if config_folder.is_empty():
|
||||||
printerr("Skipping export: No TrenchBroom Game folder")
|
printerr("Skipping export: No TrenchBroom Game folder")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Make sure FGD file is set
|
# Make sure FGD file is set
|
||||||
if !fgd_file:
|
if not fgd_file:
|
||||||
printerr("Skipping export: No FGD file")
|
printerr("Skipping export: No FGD file")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -212,10 +224,106 @@ func do_export_file() -> void:
|
|||||||
var target_file_path: String = config_folder + "/GameConfig.cfg"
|
var target_file_path: String = config_folder + "/GameConfig.cfg"
|
||||||
print("Exporting TrenchBroom Game Config to ", target_file_path)
|
print("Exporting TrenchBroom Game Config to ", target_file_path)
|
||||||
var file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
var file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
||||||
file.store_string(build_class_text())
|
file.store_string(_build_class_text())
|
||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
# FGD
|
# FGD
|
||||||
var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
|
var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
|
||||||
export_fgd.do_export_file(true, config_folder)
|
export_fgd.do_export_file(FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM, config_folder)
|
||||||
print("TrenchBroom Game Config export complete\n")
|
print("TrenchBroom Game Config export complete\n")
|
||||||
|
|
||||||
|
#region GameConfigDeclarations
|
||||||
|
func _get_game_config_v4_text() -> String:
|
||||||
|
return """\
|
||||||
|
{
|
||||||
|
"version": 4,
|
||||||
|
"name": "%s",
|
||||||
|
"icon": "icon.png",
|
||||||
|
"fileformats": [
|
||||||
|
%s
|
||||||
|
],
|
||||||
|
"filesystem": {
|
||||||
|
"searchpath": ".",
|
||||||
|
"packageformat": { "extension": ".zip", "format": "zip" }
|
||||||
|
},
|
||||||
|
"textures": {
|
||||||
|
"package": { "type": "directory", "root": "%s" },
|
||||||
|
"format": { "extensions": ["jpg", "jpeg", "tga", "png", "D", "C"], "format": "image" },
|
||||||
|
"excludes": [ %s ],
|
||||||
|
"palette": "%s",
|
||||||
|
"attribute": ["_tb_textures", "wad"]
|
||||||
|
},
|
||||||
|
"entities": {
|
||||||
|
"definitions": [ %s ],
|
||||||
|
"defaultcolor": "0.6 0.6 0.6 1.0",
|
||||||
|
"modelformats": [ "bsp, mdl, md2" ],
|
||||||
|
"scale": %s
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"brush": [
|
||||||
|
%s
|
||||||
|
],
|
||||||
|
"brushface": [
|
||||||
|
%s
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"faceattribs": {
|
||||||
|
"defaults": {
|
||||||
|
%s
|
||||||
|
},
|
||||||
|
"contentflags": [],
|
||||||
|
"surfaceflags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
func _get_game_config_v9v8_text() -> String:
|
||||||
|
var config_text: String = """\
|
||||||
|
{
|
||||||
|
"version": 9,
|
||||||
|
"name": "%s",
|
||||||
|
"icon": "icon.png",
|
||||||
|
"fileformats": [
|
||||||
|
%s
|
||||||
|
],
|
||||||
|
"filesystem": {
|
||||||
|
"searchpath": ".",
|
||||||
|
"packageformat": { "extension": ".zip", "format": "zip" }
|
||||||
|
},
|
||||||
|
"materials": {
|
||||||
|
"root": "%s",
|
||||||
|
"extensions": [".bmp", ".exr", ".hdr", ".jpeg", ".jpg", ".png", ".tga", ".webp", ".D", ".C"],
|
||||||
|
"excludes": [ %s ],
|
||||||
|
"palette": "%s",
|
||||||
|
"attribute": "wad"
|
||||||
|
},
|
||||||
|
"entities": {
|
||||||
|
"definitions": [ %s ],
|
||||||
|
"defaultcolor": "0.6 0.6 0.6 1.0",
|
||||||
|
"scale": %s
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"brush": [
|
||||||
|
%s
|
||||||
|
],
|
||||||
|
"brushface": [
|
||||||
|
%s
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"faceattribs": {
|
||||||
|
"defaults": {
|
||||||
|
%s
|
||||||
|
},
|
||||||
|
"contentflags": [],
|
||||||
|
"surfaceflags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
if game_config_version == GameConfigVersion.Version8:
|
||||||
|
config_text = config_text.replace(": 9,", ": 8,")
|
||||||
|
config_text = config_text.replace("material", "texture")
|
||||||
|
|
||||||
|
return config_text
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://c51cy33iaei4s
|
uid://cx44c4vnq8bt5
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
||||||
## Pattern matching tags to enable a number of features in TrenchBroom, including display appearance and menu filtering options. This resource gets added to the [TrenchBroomGameConfig] resource. Does not affect appearance or functionality in Godot.
|
class_name TrenchBroomTag extends Resource
|
||||||
## See the TrenchBroom Documentation on [**Tags under the Game Configuration section**](https://trenchbroom.github.io/manual/latest/#game_configuration_files) and [**Special Bruch FuncGodotFace Types**](https://trenchbroom.github.io/manual/latest/#special_brush_face_types) for more information.
|
## Pattern matching tag added to [TrenchbroomGameConfig] for appearance and menu filtering purposes.
|
||||||
class_name TrenchBroomTag
|
##
|
||||||
extends Resource
|
## Pattern matching tags to enable a number of features in TrenchBroom, including display appearance and menu filtering options.
|
||||||
|
## This resource gets added to the [TrenchBroomGameConfig] resource. Does not affect appearance or functionality in Godot.
|
||||||
|
##
|
||||||
|
## @tutorial(TrenchBroom Manual Game Configuration): https://trenchbroom.github.io/manual/latest/#game_configuration_files
|
||||||
|
## @tutorial(TrenchBroom Manual Special Brush Face Types): https://trenchbroom.github.io/manual/latest/#special_brush_face_types
|
||||||
|
|
||||||
enum TagMatchType {
|
enum TagMatchType {
|
||||||
TEXTURE, ## Tag applies to any brush face with a texture matching the texture name.
|
TEXTURE, ## Tag applies to any brush face with a texture matching the texture name.
|
||||||
@@ -19,7 +23,7 @@ enum TagMatchType {
|
|||||||
@export var tag_match_type: TagMatchType
|
@export var tag_match_type: TagMatchType
|
||||||
|
|
||||||
## A string that filters which flag, param, or classname to use. [code]*[/code] can be used as a wildcard to include multiple options.
|
## A string that filters which flag, param, or classname to use. [code]*[/code] can be used as a wildcard to include multiple options.
|
||||||
## [b]Example:[/b] [code]trigger_*[/code] with [constant TagMatchType] [i]Classname[/i] will apply this tag to all brush entities with the [code]trigger_[/code] prefix.
|
## [b]Example:[/b] [code]trigger*[/code] with [constant TagMatchType] [i]Classname[/i] will apply this tag to all brush entities with the [code]trigger[/code] prefix.
|
||||||
@export var tag_pattern: String
|
@export var tag_pattern: String
|
||||||
|
|
||||||
## A string that filters which textures recieve these attributes. Only used with a [constant TagMatchType] of [i]Texture[/i].
|
## A string that filters which textures recieve these attributes. Only used with a [constant TagMatchType] of [i]Texture[/i].
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://5eacgso7tkh6
|
uid://b66qdknwqpfup
|
||||||
|
|||||||
@@ -1,23 +1,32 @@
|
|||||||
@tool
|
@tool
|
||||||
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
||||||
|
class_name FuncGodotLocalConfig extends Resource
|
||||||
|
## Local machine project wide settings. [color=red]WARNING![/color] Do not create your own! Use the resource in [i]addons/func_godot[/i].
|
||||||
|
##
|
||||||
## Local machine project wide settings. Can define global defaults for some FuncGodot properties.
|
## Local machine project wide settings. Can define global defaults for some FuncGodot properties.
|
||||||
## DO NOT CREATE A NEW RESOURCE! This resource works by saving a configuration file to your game's *user://* folder and pulling the properties from that config file rather than this resource.
|
## [color=red][b]DO NOT CREATE A NEW RESOURCE![/b][/color] This resource works by saving a configuration file to your game's [b][i]user://[/i][/b] folder
|
||||||
## Use the premade `addons/func_godot/func_godot_local_config.tres` instead.
|
## and pulling the properties from that config file rather than this resource. Use the premade [b][i]addons/func_godot/func_godot_local_config.tres[/i][/b] instead.
|
||||||
class_name FuncGodotLocalConfig
|
## [br][br]
|
||||||
extends Resource
|
## [b]Fgd Output Folder :[/b] Global directory path that [FuncGodotFGDFile] saves to when exported. Overridden when exported from a game configuration resource like [TrenchBroomGameConfig].[br][br]
|
||||||
|
## [b]Trenchbroom Game Config Folder :[/b] Global directory path where your TrenchBroom game configuration should be saved to. Consult the [url="https://trenchbroom.github.io/manual/latest/#game_configuration_files"]TrenchBroom Manual's Game Configuration documentation[/url] for more information.[br][br]
|
||||||
|
## [b]Netradiant Custom Gamepacks Folder :[/b] Global directory path where your NetRadiant Custom gamepacks are saved. On Windows this is the [i]gamepacks[/i] folder in your NetRadiant Custom installation.[br][br]
|
||||||
|
## [b]Map Editor Game Path :[/b] Global directory path to your mapping folder where all of your mapping assets exist. This is usually either your project folder or a subfolder within it.[br][br]
|
||||||
|
## [b]Game Path Models Folder :[/b] Relative directory path from your Map Editor Game Path to a subfolder containing any display models you might use for your map editor. Currently only used by [FuncGodotFGDModelPointClass].[br][br]
|
||||||
|
## [b]Default Inverse Scale Factor :[/b] Scale factor that affects how [FuncGodotFGDModelPointClass] entities scale their map editor display models. Not used with TrenchBroom, use [member TrenchBroomGameConfig.entity_scale] expression instead.[br][br]
|
||||||
|
|
||||||
enum PROPERTY {
|
enum PROPERTY {
|
||||||
FGD_OUTPUT_FOLDER,
|
FGD_OUTPUT_FOLDER,
|
||||||
TRENCHBROOM_GAME_CONFIG_FOLDER,
|
TRENCHBROOM_GAME_CONFIG_FOLDER,
|
||||||
NETRADIANT_CUSTOM_GAMEPACKS_FOLDER,
|
NETRADIANT_CUSTOM_GAMEPACKS_FOLDER,
|
||||||
MAP_EDITOR_GAME_PATH,
|
MAP_EDITOR_GAME_PATH,
|
||||||
GAME_PATH_MODELS_FOLDER,
|
#GAME_PATH_MODELS_FOLDER,
|
||||||
DEFAULT_INVERSE_SCALE
|
#DEFAULT_INVERSE_SCALE
|
||||||
}
|
}
|
||||||
|
|
||||||
@export var export_func_godot_settings: bool: set = _save_settings
|
@export_tool_button("Export func_godot settings", "Save") var _save_settings = export_func_godot_settings
|
||||||
|
@export_tool_button("Reload func_godot settings", "Reload") var _load_settings = reload_func_godot_settings
|
||||||
|
|
||||||
const CONFIG_PROPERTIES: Array[Dictionary] = [
|
const _CONFIG_PROPERTIES: Array[Dictionary] = [
|
||||||
{
|
{
|
||||||
"name": "fgd_output_folder",
|
"name": "fgd_output_folder",
|
||||||
"usage": PROPERTY_USAGE_EDITOR,
|
"usage": PROPERTY_USAGE_EDITOR,
|
||||||
@@ -46,44 +55,44 @@ const CONFIG_PROPERTIES: Array[Dictionary] = [
|
|||||||
"hint": PROPERTY_HINT_GLOBAL_DIR,
|
"hint": PROPERTY_HINT_GLOBAL_DIR,
|
||||||
"func_godot_type": PROPERTY.MAP_EDITOR_GAME_PATH
|
"func_godot_type": PROPERTY.MAP_EDITOR_GAME_PATH
|
||||||
},
|
},
|
||||||
{
|
#{
|
||||||
"name": "game_path_models_folder",
|
#"name": "game_path_models_folder",
|
||||||
"usage": PROPERTY_USAGE_EDITOR,
|
#"usage": PROPERTY_USAGE_EDITOR,
|
||||||
"type": TYPE_STRING,
|
#"type": TYPE_STRING,
|
||||||
"func_godot_type": PROPERTY.GAME_PATH_MODELS_FOLDER
|
#"func_godot_type": PROPERTY.GAME_PATH_MODELS_FOLDER
|
||||||
},
|
#},
|
||||||
{
|
#{
|
||||||
"name": "default_inverse_scale_factor",
|
#"name": "default_inverse_scale_factor",
|
||||||
"usage": PROPERTY_USAGE_EDITOR,
|
#"usage": PROPERTY_USAGE_EDITOR,
|
||||||
"type": TYPE_FLOAT,
|
#"type": TYPE_FLOAT,
|
||||||
"func_godot_type": PROPERTY.DEFAULT_INVERSE_SCALE
|
#"func_godot_type": PROPERTY.DEFAULT_INVERSE_SCALE
|
||||||
}
|
#}
|
||||||
]
|
]
|
||||||
|
|
||||||
var settings_dict: Dictionary
|
var _settings_dict: Dictionary
|
||||||
var loaded := false
|
var _loaded := false
|
||||||
|
|
||||||
|
## Retrieve a setting from the local configuration.
|
||||||
static func get_setting(name: PROPERTY) -> Variant:
|
static func get_setting(name: PROPERTY) -> Variant:
|
||||||
var settings = load("res://addons/func_godot/func_godot_local_config.tres")
|
var settings: FuncGodotLocalConfig = load("res://addons/func_godot/func_godot_local_config.tres")
|
||||||
if not settings.loaded:
|
settings.reload_func_godot_settings()
|
||||||
settings._load_settings()
|
return settings._settings_dict.get(PROPERTY.keys()[name], '') as Variant
|
||||||
return settings.settings_dict.get(PROPERTY.keys()[name], '') as Variant
|
|
||||||
|
|
||||||
func _get_property_list() -> Array:
|
func _get_property_list() -> Array:
|
||||||
return CONFIG_PROPERTIES.duplicate()
|
return _CONFIG_PROPERTIES.duplicate()
|
||||||
|
|
||||||
func _get(property: StringName) -> Variant:
|
func _get(property: StringName) -> Variant:
|
||||||
var config = _get_config_property(property)
|
var config = _get_config_property(property)
|
||||||
if config == null and not config is Dictionary:
|
if config == null and not config is Dictionary:
|
||||||
return null
|
return null
|
||||||
_try_loading()
|
_try_loading()
|
||||||
return settings_dict.get(PROPERTY.keys()[config['func_godot_type']], _get_default_value(config['type']))
|
return _settings_dict.get(PROPERTY.keys()[config['func_godot_type']], _get_default_value(config['type']))
|
||||||
|
|
||||||
func _set(property: StringName, value: Variant) -> bool:
|
func _set(property: StringName, value: Variant) -> bool:
|
||||||
var config = _get_config_property(property)
|
var config = _get_config_property(property)
|
||||||
if config == null and not config is Dictionary:
|
if config == null and not config is Dictionary:
|
||||||
return false
|
return false
|
||||||
settings_dict[PROPERTY.keys()[config['func_godot_type']]] = value
|
_settings_dict[PROPERTY.keys()[config['func_godot_type']]] = value
|
||||||
return true
|
return true
|
||||||
|
|
||||||
func _get_default_value(type) -> Variant:
|
func _get_default_value(type) -> Variant:
|
||||||
@@ -100,34 +109,39 @@ func _get_default_value(type) -> Variant:
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
func _get_config_property(name: StringName) -> Variant:
|
func _get_config_property(name: StringName) -> Variant:
|
||||||
for config in CONFIG_PROPERTIES:
|
for config in _CONFIG_PROPERTIES:
|
||||||
if config['name'] == name:
|
if config['name'] == name:
|
||||||
return config
|
return config
|
||||||
return null
|
return null
|
||||||
|
|
||||||
func _load_settings() -> void:
|
## Reload this system's configuration settings into the Local Config resource.
|
||||||
loaded = true
|
func reload_func_godot_settings() -> void:
|
||||||
|
_loaded = true
|
||||||
var path = _get_path()
|
var path = _get_path()
|
||||||
if not FileAccess.file_exists(path): return
|
if not FileAccess.file_exists(path):
|
||||||
|
return
|
||||||
var settings = FileAccess.get_file_as_string(path)
|
var settings = FileAccess.get_file_as_string(path)
|
||||||
settings_dict = {}
|
_settings_dict = {}
|
||||||
if not settings or settings.is_empty(): return
|
if not settings or settings.is_empty():
|
||||||
|
return
|
||||||
settings = JSON.parse_string(settings)
|
settings = JSON.parse_string(settings)
|
||||||
for key in settings.keys():
|
for key in settings.keys():
|
||||||
settings_dict[key] = settings[key]
|
_settings_dict[key] = settings[key]
|
||||||
notify_property_list_changed()
|
notify_property_list_changed()
|
||||||
|
|
||||||
func _try_loading() -> void:
|
func _try_loading() -> void:
|
||||||
if not loaded: _load_settings()
|
if not _loaded:
|
||||||
|
reload_func_godot_settings()
|
||||||
|
|
||||||
func _save_settings(_s = null) -> void:
|
## Export the current resource settings to a configuration file in this game's [i]user://[/i] folder.
|
||||||
if settings_dict.size() == 0:
|
func export_func_godot_settings() -> void:
|
||||||
|
if _settings_dict.size() == 0:
|
||||||
return
|
return
|
||||||
var path = _get_path()
|
var path = _get_path()
|
||||||
var file = FileAccess.open(path, FileAccess.WRITE)
|
var file = FileAccess.open(path, FileAccess.WRITE)
|
||||||
var json = JSON.stringify(settings_dict)
|
var json = JSON.stringify(_settings_dict)
|
||||||
file.store_line(json)
|
file.store_line(json)
|
||||||
loaded = false
|
_loaded = false
|
||||||
print("Saved settings to ", path)
|
print("Saved settings to ", path)
|
||||||
|
|
||||||
func _get_path() -> String:
|
func _get_path() -> String:
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://bjtqjywscfgdy
|
uid://xsjnhahhyein
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
class_name FuncGodotTextureLoader
|
|
||||||
|
|
||||||
enum PBRSuffix {
|
|
||||||
ALBEDO,
|
|
||||||
NORMAL,
|
|
||||||
METALLIC,
|
|
||||||
ROUGHNESS,
|
|
||||||
EMISSION,
|
|
||||||
AO,
|
|
||||||
HEIGHT
|
|
||||||
}
|
|
||||||
|
|
||||||
# Suffix string / Godot enum / StandardMaterial3D property
|
|
||||||
const PBR_SUFFIX_NAMES: Dictionary = {
|
|
||||||
PBRSuffix.ALBEDO: 'albedo',
|
|
||||||
PBRSuffix.NORMAL: 'normal',
|
|
||||||
PBRSuffix.METALLIC: 'metallic',
|
|
||||||
PBRSuffix.ROUGHNESS: 'roughness',
|
|
||||||
PBRSuffix.EMISSION: 'emission',
|
|
||||||
PBRSuffix.AO: 'ao',
|
|
||||||
PBRSuffix.HEIGHT: 'height',
|
|
||||||
}
|
|
||||||
|
|
||||||
const PBR_SUFFIX_PATTERNS: Dictionary = {
|
|
||||||
PBRSuffix.ALBEDO: '%s_albedo.%s',
|
|
||||||
PBRSuffix.NORMAL: '%s_normal.%s',
|
|
||||||
PBRSuffix.METALLIC: '%s_metallic.%s',
|
|
||||||
PBRSuffix.ROUGHNESS: '%s_roughness.%s',
|
|
||||||
PBRSuffix.EMISSION: '%s_emission.%s',
|
|
||||||
PBRSuffix.AO: '%s_ao.%s',
|
|
||||||
PBRSuffix.HEIGHT: '%s_height.%s'
|
|
||||||
}
|
|
||||||
|
|
||||||
var PBR_SUFFIX_TEXTURES: Dictionary = {
|
|
||||||
PBRSuffix.ALBEDO: StandardMaterial3D.TEXTURE_ALBEDO,
|
|
||||||
PBRSuffix.NORMAL: StandardMaterial3D.TEXTURE_NORMAL,
|
|
||||||
PBRSuffix.METALLIC: StandardMaterial3D.TEXTURE_METALLIC,
|
|
||||||
PBRSuffix.ROUGHNESS: StandardMaterial3D.TEXTURE_ROUGHNESS,
|
|
||||||
PBRSuffix.EMISSION: StandardMaterial3D.TEXTURE_EMISSION,
|
|
||||||
PBRSuffix.AO: StandardMaterial3D.TEXTURE_AMBIENT_OCCLUSION,
|
|
||||||
PBRSuffix.HEIGHT: StandardMaterial3D.TEXTURE_HEIGHTMAP
|
|
||||||
}
|
|
||||||
|
|
||||||
const PBR_SUFFIX_PROPERTIES: Dictionary = {
|
|
||||||
PBRSuffix.NORMAL: 'normal_enabled',
|
|
||||||
PBRSuffix.EMISSION: 'emission_enabled',
|
|
||||||
PBRSuffix.AO: 'ao_enabled',
|
|
||||||
PBRSuffix.HEIGHT: 'heightmap_enabled',
|
|
||||||
}
|
|
||||||
|
|
||||||
var map_settings: FuncGodotMapSettings = FuncGodotMapSettings.new()
|
|
||||||
var texture_wad_resources: Array = []
|
|
||||||
|
|
||||||
# Overrides
|
|
||||||
func _init(new_map_settings: FuncGodotMapSettings) -> void:
|
|
||||||
map_settings = new_map_settings
|
|
||||||
load_texture_wad_resources()
|
|
||||||
|
|
||||||
# Business Logic
|
|
||||||
func load_texture_wad_resources() -> void:
|
|
||||||
texture_wad_resources.clear()
|
|
||||||
for texture_wad in map_settings.texture_wads:
|
|
||||||
if texture_wad and not texture_wad in texture_wad_resources:
|
|
||||||
texture_wad_resources.append(texture_wad)
|
|
||||||
|
|
||||||
func load_textures(texture_list: Array) -> Dictionary:
|
|
||||||
var texture_dict: Dictionary = {}
|
|
||||||
for texture_name in texture_list:
|
|
||||||
texture_dict[texture_name] = load_texture(texture_name)
|
|
||||||
return texture_dict
|
|
||||||
|
|
||||||
func load_texture(texture_name: String) -> Texture2D:
|
|
||||||
# Load albedo texture if it exists
|
|
||||||
for texture_extension in map_settings.texture_file_extensions:
|
|
||||||
var texture_path: String = "%s/%s.%s" % [map_settings.base_texture_dir, texture_name, texture_extension]
|
|
||||||
if ResourceLoader.exists(texture_path, "Texture2D"):
|
|
||||||
return load(texture_path) as Texture2D
|
|
||||||
|
|
||||||
var texture_name_lower: String = texture_name.to_lower()
|
|
||||||
for texture_wad in texture_wad_resources:
|
|
||||||
if texture_name_lower in texture_wad.textures:
|
|
||||||
return texture_wad.textures[texture_name_lower]
|
|
||||||
|
|
||||||
return load("res://addons/func_godot/textures/default_texture.png") as Texture2D
|
|
||||||
|
|
||||||
func create_materials(texture_list: Array) -> Dictionary:
|
|
||||||
var texture_materials: Dictionary = {}
|
|
||||||
#prints("TEXLI", texture_list)
|
|
||||||
for texture in texture_list:
|
|
||||||
texture_materials[texture] = create_material(texture)
|
|
||||||
return texture_materials
|
|
||||||
|
|
||||||
func create_material(texture_name: String) -> Material:
|
|
||||||
# Autoload material if it exists
|
|
||||||
var material_dict: Dictionary = {}
|
|
||||||
|
|
||||||
var material_path: String = "%s/%s.%s" % [map_settings.base_texture_dir, texture_name, map_settings.material_file_extension]
|
|
||||||
if not material_path in material_dict and FileAccess.file_exists(material_path):
|
|
||||||
var loaded_material: Material = load(material_path)
|
|
||||||
if loaded_material:
|
|
||||||
material_dict[material_path] = loaded_material
|
|
||||||
|
|
||||||
# If material already exists, use it
|
|
||||||
if material_path in material_dict:
|
|
||||||
return material_dict[material_path]
|
|
||||||
|
|
||||||
var material: Material = null
|
|
||||||
|
|
||||||
if map_settings.default_material:
|
|
||||||
material = map_settings.default_material.duplicate()
|
|
||||||
else:
|
|
||||||
material = StandardMaterial3D.new()
|
|
||||||
var texture: Texture2D = load_texture(texture_name)
|
|
||||||
if not texture:
|
|
||||||
return material
|
|
||||||
|
|
||||||
if material is BaseMaterial3D:
|
|
||||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED if map_settings.unshaded else BaseMaterial3D.SHADING_MODE_PER_PIXEL
|
|
||||||
|
|
||||||
if material is StandardMaterial3D:
|
|
||||||
material.set_texture(StandardMaterial3D.TEXTURE_ALBEDO, texture)
|
|
||||||
elif material is ShaderMaterial && map_settings.default_material_albedo_uniform != "":
|
|
||||||
material.set_shader_parameter(map_settings.default_material_albedo_uniform, texture)
|
|
||||||
|
|
||||||
var pbr_textures : Dictionary = get_pbr_textures(texture_name)
|
|
||||||
|
|
||||||
for pbr_suffix in PBRSuffix.values():
|
|
||||||
var suffix: int = pbr_suffix
|
|
||||||
var tex: Texture2D = pbr_textures[suffix]
|
|
||||||
if tex:
|
|
||||||
if material is ShaderMaterial:
|
|
||||||
material = StandardMaterial3D.new()
|
|
||||||
material.set_texture(StandardMaterial3D.TEXTURE_ALBEDO, texture)
|
|
||||||
var enable_prop: String = PBR_SUFFIX_PROPERTIES[suffix] if suffix in PBR_SUFFIX_PROPERTIES else ""
|
|
||||||
if(enable_prop != ""):
|
|
||||||
material.set(enable_prop, true)
|
|
||||||
material.set_texture(PBR_SUFFIX_TEXTURES[suffix], tex)
|
|
||||||
|
|
||||||
material_dict[material_path] = material
|
|
||||||
|
|
||||||
if (map_settings.save_generated_materials and material
|
|
||||||
and texture_name != map_settings.clip_texture
|
|
||||||
and texture_name != map_settings.skip_texture
|
|
||||||
and texture.resource_path != "res://addons/func_godot/textures/default_texture.png"):
|
|
||||||
ResourceSaver.save(material, material_path)
|
|
||||||
|
|
||||||
return material
|
|
||||||
|
|
||||||
# PBR texture fetching
|
|
||||||
func get_pbr_suffix_pattern(suffix: int) -> String:
|
|
||||||
if not suffix in PBR_SUFFIX_NAMES:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
var pattern_setting: String = "%s_map_pattern" % [PBR_SUFFIX_NAMES[suffix]]
|
|
||||||
if pattern_setting in map_settings:
|
|
||||||
return map_settings.get(pattern_setting)
|
|
||||||
|
|
||||||
return PBR_SUFFIX_PATTERNS[suffix]
|
|
||||||
|
|
||||||
func get_pbr_texture(texture: String, suffix: PBRSuffix) -> Texture2D:
|
|
||||||
var texture_comps: PackedStringArray = texture.split('/')
|
|
||||||
if texture_comps.size() == 0:
|
|
||||||
return null
|
|
||||||
|
|
||||||
for texture_extension in map_settings.texture_file_extensions:
|
|
||||||
var path: String = "%s/%s/%s" % [
|
|
||||||
map_settings.base_texture_dir,
|
|
||||||
'/'.join(texture_comps),
|
|
||||||
get_pbr_suffix_pattern(suffix) % [
|
|
||||||
texture_comps[-1],
|
|
||||||
texture_extension
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
if(FileAccess.file_exists(path)):
|
|
||||||
return load(path) as Texture2D
|
|
||||||
|
|
||||||
return null
|
|
||||||
|
|
||||||
func get_pbr_textures(texture_name: String) -> Dictionary:
|
|
||||||
var pbr_textures: Dictionary = {}
|
|
||||||
for pbr_suffix in PBRSuffix.values():
|
|
||||||
pbr_textures[pbr_suffix] = get_pbr_texture(texture_name, pbr_suffix)
|
|
||||||
return pbr_textures
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://pfjqfqn3imye
|
|
||||||
@@ -1,40 +1,408 @@
|
|||||||
## General-purpose utility functions namespaced to FuncGodot for compatibility
|
|
||||||
class_name FuncGodotUtil
|
class_name FuncGodotUtil
|
||||||
|
## Static class with a number of reuseable utility methods that can be called at Editor or Run Time.
|
||||||
|
|
||||||
## Print debug messages. True to print, false to ignore
|
const _VERTEX_EPSILON: float = 0.008
|
||||||
const DEBUG : bool = true
|
|
||||||
|
|
||||||
## Const-predicated print function to avoid excess log spam. Print msg if [constant DEBUG] is `true`.
|
const _VEC3_UP_ID := Vector3(0.0, 0.0, 1.0)
|
||||||
static func debug_print(msg) -> void:
|
const _VEC3_RIGHT_ID := Vector3(0.0, 1.0, 0.0)
|
||||||
if(DEBUG):
|
const _VEC3_FORWARD_ID := Vector3(1.0, 0.0, 0.0)
|
||||||
print(msg)
|
|
||||||
|
|
||||||
## Return a string that corresponds to the current OS's newline control character(s)
|
## Connected by the [FuncGodotMap] node to the build process' sub-components if the
|
||||||
|
## [member FuncGodotMap.build_flags]'s SHOW_PROFILE_INFO flag is set.
|
||||||
|
static func print_profile_info(message: String, signature: String) -> void:
|
||||||
|
prints(signature, message)
|
||||||
|
|
||||||
|
## Return a [String] that corresponds to the current [OS]'s newline control characters.
|
||||||
static func newline() -> String:
|
static func newline() -> String:
|
||||||
if OS.get_name() == "Windows":
|
if OS.get_name() == "Windows":
|
||||||
return "\r\n"
|
return "\r\n"
|
||||||
else:
|
else:
|
||||||
return "\n"
|
return "\n"
|
||||||
|
|
||||||
## Create a dictionary suitable for creating a category with name when overriding [method Object._get_property_list]
|
#region MATH
|
||||||
static func category_dict(name: String) -> Dictionary:
|
|
||||||
return property_dict(name, TYPE_STRING, -1, "", PROPERTY_USAGE_CATEGORY)
|
|
||||||
|
|
||||||
## Creates a property with name and type from [enum @GlobalScope.Variant.Type].
|
static func op_vec3_sum(lhs: Vector3, rhs: Vector3) -> Vector3:
|
||||||
## Optionally, provide hint from [enum @GlobalScope.PropertyHint] and corresponding hint_string, and usage from [enum @GlobalScope.PropertyUsageFlags].
|
return lhs + rhs
|
||||||
static func property_dict(name: String, type: int, hint: int = -1, hint_string: String = "", usage: int = -1) -> Dictionary:
|
|
||||||
var dict := {
|
|
||||||
'name': name,
|
|
||||||
'type': type
|
|
||||||
}
|
|
||||||
|
|
||||||
if hint != -1:
|
static func op_vec3_avg(array: Array[Vector3]) -> Vector3:
|
||||||
dict['hint'] = hint
|
if array.is_empty():
|
||||||
|
push_error("Cannot average empty Vector3 array!")
|
||||||
|
return Vector3()
|
||||||
|
return array.reduce(op_vec3_sum, Vector3()) / array.size()
|
||||||
|
|
||||||
if hint_string != "":
|
## Conversion from id tech coordinate system to Godot, from a top-down perspective.
|
||||||
dict['hint_string'] = hint_string
|
static func id_to_opengl(vec: Vector3) -> Vector3:
|
||||||
|
return Vector3(vec.y, vec.z, vec.x)
|
||||||
|
|
||||||
if usage != -1:
|
## Check if a point is inside a convex hull defined by a series of planes by an epsilon constant.
|
||||||
dict['usage'] = usage
|
static func is_point_in_convex_hull(planes: Array[Plane], vertex: Vector3) -> bool:
|
||||||
|
for plane in planes:
|
||||||
|
var distance: float = plane.normal.dot(vertex) - plane.d
|
||||||
|
if distance > _VERTEX_EPSILON:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
return dict
|
#endregion
|
||||||
|
|
||||||
|
#region PATCH DEF
|
||||||
|
|
||||||
|
## Returns the control points that defines a cubic curve for a equivalent input quadratic curve.
|
||||||
|
static func elevate_quadratic(p0: Vector3, p1: Vector3, p2: Vector3) -> Array[Vector3]:
|
||||||
|
return [p0, p0 + (2.0/3.0) * (p1 - p0), p2 + (2.0/3.0) * (p1 - p2), p2 ]
|
||||||
|
|
||||||
|
## Create a Curve3D and bake points.
|
||||||
|
static func create_curve(start: Vector3, control: Vector3, end: Vector3, bake_interval: float = 0.05) -> Curve3D:
|
||||||
|
var ret := Curve3D.new()
|
||||||
|
ret.bake_interval = bake_interval
|
||||||
|
update_ref_curve(ret, start, control, end, bake_interval)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
## Update a Curve3D given quadratic inputs.
|
||||||
|
static func update_ref_curve(curve: Curve3D, p0: Vector3, p1: Vector3, p2: Vector3, bake_interval: float = 0.05) -> void:
|
||||||
|
curve.clear_points()
|
||||||
|
curve.bake_interval = bake_interval
|
||||||
|
curve.add_point(p0, (p1 - p0) * (2.0 / 3.0))
|
||||||
|
curve.add_point(p1, (p1 - p0) * (1.0 / 3.0), (p2 - p1) * (1.0 / 3.0))
|
||||||
|
curve.add_point(p2, (p2 - p1 * (2.0 / 3.0)))
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region TEXTURES
|
||||||
|
|
||||||
|
## Fallback texture if the one defined in the [QuakeMapFile] cannot be found.
|
||||||
|
const default_texture_path: String = "res://addons/func_godot/textures/default_texture.png"
|
||||||
|
|
||||||
|
const _pbr_textures: PackedInt32Array = [
|
||||||
|
StandardMaterial3D.TEXTURE_ALBEDO,
|
||||||
|
StandardMaterial3D.TEXTURE_NORMAL,
|
||||||
|
StandardMaterial3D.TEXTURE_METALLIC,
|
||||||
|
StandardMaterial3D.TEXTURE_ROUGHNESS,
|
||||||
|
StandardMaterial3D.TEXTURE_EMISSION,
|
||||||
|
StandardMaterial3D.TEXTURE_AMBIENT_OCCLUSION,
|
||||||
|
StandardMaterial3D.TEXTURE_HEIGHTMAP,
|
||||||
|
ORMMaterial3D.TEXTURE_ORM
|
||||||
|
]
|
||||||
|
|
||||||
|
## Searches for a Texture2D within the base texture directory or the WAD files added to map settings.
|
||||||
|
## If not found, a default texture is returned.
|
||||||
|
static func load_texture(texture_name: String, wad_resources: Array[QuakeWadFile], map_settings: FuncGodotMapSettings) -> Texture2D:
|
||||||
|
for texture_file_extension in map_settings.texture_file_extensions:
|
||||||
|
var texture_path: String = map_settings.base_texture_dir.path_join(texture_name + "." + texture_file_extension)
|
||||||
|
if ResourceLoader.exists(texture_path):
|
||||||
|
return load(texture_path)
|
||||||
|
|
||||||
|
var texture_name_lower: String = texture_name.to_lower()
|
||||||
|
for wad in wad_resources:
|
||||||
|
if texture_name_lower in wad.textures:
|
||||||
|
return wad.textures[texture_name_lower]
|
||||||
|
|
||||||
|
return load(default_texture_path)
|
||||||
|
|
||||||
|
## Filters faces textured with Skip during the geometry generation step of the build process.
|
||||||
|
static func is_skip(texture: String, map_settings: FuncGodotMapSettings) -> bool:
|
||||||
|
if map_settings:
|
||||||
|
return texture.to_lower() == map_settings.skip_texture
|
||||||
|
return false
|
||||||
|
|
||||||
|
## Filters faces textured with Clip during the geometry generation step of the build process.
|
||||||
|
static func is_clip(texture: String, map_settings: FuncGodotMapSettings) -> bool:
|
||||||
|
if map_settings:
|
||||||
|
return texture.to_lower() == map_settings.clip_texture
|
||||||
|
return false
|
||||||
|
|
||||||
|
## Filters faces textured with Origin during the parsing and geometry generation steps of the build process.
|
||||||
|
static func is_origin(texture: String, map_settings: FuncGodotMapSettings) -> bool:
|
||||||
|
if map_settings:
|
||||||
|
return texture.to_lower() == map_settings.origin_texture
|
||||||
|
return false
|
||||||
|
|
||||||
|
## Filters faces textured with any of the tool textures during the geometry generation step of the build process.
|
||||||
|
static func filter_face(texture: String, map_settings: FuncGodotMapSettings) -> bool:
|
||||||
|
if map_settings:
|
||||||
|
texture = texture.to_lower()
|
||||||
|
if (texture == map_settings.skip_texture
|
||||||
|
or texture == map_settings.clip_texture
|
||||||
|
or texture == map_settings.origin_texture
|
||||||
|
):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
## Adds PBR textures to an existing [BaseMaterial3D].
|
||||||
|
static func build_base_material(map_settings: FuncGodotMapSettings, material: BaseMaterial3D, texture: String) -> void:
|
||||||
|
var path: String = map_settings.base_texture_dir.path_join(texture)
|
||||||
|
# Check if there is a subfolder with our PBR textures
|
||||||
|
if DirAccess.open(path.path_join(path)):
|
||||||
|
path = path.path_join(path)
|
||||||
|
|
||||||
|
var pbr_suffixes: PackedStringArray = [
|
||||||
|
map_settings.albedo_map_pattern,
|
||||||
|
map_settings.normal_map_pattern,
|
||||||
|
map_settings.metallic_map_pattern,
|
||||||
|
map_settings.roughness_map_pattern,
|
||||||
|
map_settings.emission_map_pattern,
|
||||||
|
map_settings.ao_map_pattern,
|
||||||
|
map_settings.height_map_pattern,
|
||||||
|
map_settings.orm_map_pattern,
|
||||||
|
]
|
||||||
|
|
||||||
|
for texture_file_extension in map_settings.texture_file_extensions:
|
||||||
|
for i in pbr_suffixes.size():
|
||||||
|
if not pbr_suffixes[i].is_empty():
|
||||||
|
var pbr: String = pbr_suffixes[i] % [path, texture_file_extension]
|
||||||
|
if ResourceLoader.exists(pbr):
|
||||||
|
material.set_texture(_pbr_textures[i], load(pbr))
|
||||||
|
|
||||||
|
## Builds both materials and sizes dictionaries for use in the geometry generation step of the build process.
|
||||||
|
## Both dictionaries use texture names as keys. The materials dictionary uses [Material] as values,
|
||||||
|
## while the sizes dictionary saves the albedo texture sizes to aid in UV mapping.
|
||||||
|
static func build_texture_map(entity_data: Array[FuncGodotData.EntityData], map_settings: FuncGodotMapSettings) -> Array[Dictionary]:
|
||||||
|
var texture_materials: Dictionary[String, Material] = {}
|
||||||
|
var texture_sizes: Dictionary[String, Vector2] = {}
|
||||||
|
|
||||||
|
# Prepare WAD files
|
||||||
|
var wad_resources: Array[QuakeWadFile] = []
|
||||||
|
for wad in map_settings.texture_wads:
|
||||||
|
if wad and not wad in wad_resources:
|
||||||
|
wad_resources.append(wad)
|
||||||
|
|
||||||
|
for entity in entity_data:
|
||||||
|
if not entity.is_visual():
|
||||||
|
continue
|
||||||
|
|
||||||
|
for brush in entity.brushes:
|
||||||
|
for face in brush.faces:
|
||||||
|
var texture_name: String = face.texture
|
||||||
|
|
||||||
|
if filter_face(texture_name, map_settings):
|
||||||
|
continue
|
||||||
|
if texture_materials.has(texture_name):
|
||||||
|
continue
|
||||||
|
|
||||||
|
var material_path: String = map_settings.base_material_dir if not map_settings.base_material_dir.is_empty() else map_settings.base_texture_dir
|
||||||
|
material_path = material_path.path_join(texture_name) + "." + map_settings.material_file_extension
|
||||||
|
material_path = material_path.replace("*", "")
|
||||||
|
|
||||||
|
if ResourceLoader.exists(material_path):
|
||||||
|
var material: Material = load(material_path)
|
||||||
|
texture_materials[texture_name] = material
|
||||||
|
if material is BaseMaterial3D:
|
||||||
|
var albedo = material.albedo_texture
|
||||||
|
if albedo is Texture2D:
|
||||||
|
texture_sizes[texture_name] = material.albedo_texture.get_size()
|
||||||
|
elif material is ShaderMaterial:
|
||||||
|
var albedo = material.get_shader_parameter(map_settings.default_material_albedo_uniform)
|
||||||
|
if albedo is Texture2D:
|
||||||
|
texture_sizes[texture_name] = albedo.get_size()
|
||||||
|
if not texture_sizes.has(texture_name):
|
||||||
|
var texture: Texture2D = load_texture(texture_name, wad_resources, map_settings)
|
||||||
|
if texture:
|
||||||
|
texture_sizes[texture_name] = texture.get_size()
|
||||||
|
if not texture_sizes.has(texture_name):
|
||||||
|
texture_sizes[texture_name] = Vector2.ONE * map_settings.inverse_scale_factor
|
||||||
|
|
||||||
|
# Material generation
|
||||||
|
elif map_settings.default_material:
|
||||||
|
var material = map_settings.default_material.duplicate(true)
|
||||||
|
var texture: Texture2D = load_texture(texture_name, wad_resources, map_settings)
|
||||||
|
texture_sizes[texture_name] = texture.get_size()
|
||||||
|
|
||||||
|
if material is BaseMaterial3D:
|
||||||
|
material.albedo_texture = texture
|
||||||
|
build_base_material(map_settings, material, texture_name)
|
||||||
|
elif material is ShaderMaterial:
|
||||||
|
material.set_shader_parameter(map_settings.default_material_albedo_uniform, texture)
|
||||||
|
|
||||||
|
if (map_settings.save_generated_materials and material
|
||||||
|
and texture_name != map_settings.clip_texture
|
||||||
|
and texture_name != map_settings.skip_texture
|
||||||
|
and texture_name != map_settings.origin_texture
|
||||||
|
and texture.resource_path != default_texture_path):
|
||||||
|
ResourceSaver.save(material, material_path)
|
||||||
|
|
||||||
|
texture_materials[texture_name] = material
|
||||||
|
else: # No default material exists
|
||||||
|
printerr("Error: No default material found in map settings")
|
||||||
|
|
||||||
|
return [texture_materials, texture_sizes]
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region UV MAPPING
|
||||||
|
|
||||||
|
## Returns UV coordinate calculated from the Valve 220 UV format.
|
||||||
|
static func get_valve_uv(vertex: Vector3, u_axis: Vector3, v_axis: Vector3, uv_basis := Transform2D.IDENTITY, texture_size := Vector2.ONE) -> Vector2:
|
||||||
|
var uv := Vector2(u_axis.dot(vertex), v_axis.dot(vertex))
|
||||||
|
uv += (uv_basis.origin * uv_basis.get_scale())
|
||||||
|
uv.x /= uv_basis.x.x
|
||||||
|
uv.y /= uv_basis.y.y
|
||||||
|
uv.x /= texture_size.x
|
||||||
|
uv.y /= texture_size.y
|
||||||
|
return uv
|
||||||
|
|
||||||
|
## Returns UV coordinate calculated from the original id Standard UV format.
|
||||||
|
static func get_quake_uv(vertex: Vector3, normal: Vector3, uv_basis := Transform2D.IDENTITY, texture_size := Vector2.ONE) -> Vector2:
|
||||||
|
var nx := absf(normal.dot(Vector3.RIGHT))
|
||||||
|
var ny := absf(normal.dot(Vector3.UP))
|
||||||
|
var nz := absf(normal.dot(Vector3.FORWARD))
|
||||||
|
var uv: Vector2
|
||||||
|
|
||||||
|
if ny >= nx and ny >= nz:
|
||||||
|
uv = Vector2(vertex.x, -vertex.z)
|
||||||
|
elif nx >= ny and nx >= nz:
|
||||||
|
uv = Vector2(vertex.y, -vertex.z)
|
||||||
|
else:
|
||||||
|
uv = Vector2(vertex.x, vertex.y)
|
||||||
|
|
||||||
|
var uv_out := uv.rotated(uv_basis.get_rotation())
|
||||||
|
uv_out /= uv_basis.get_scale()
|
||||||
|
uv_out += uv_basis.origin
|
||||||
|
uv_out /= texture_size
|
||||||
|
return uv_out
|
||||||
|
|
||||||
|
## Determines which UV format is being used and returns the UV coordinate.
|
||||||
|
static func get_face_vertex_uv(vertex: Vector3, face: FuncGodotData.FaceData, texture_size: Vector2) -> Vector2:
|
||||||
|
if face.uv_axes.size() >= 2:
|
||||||
|
return get_valve_uv(vertex, face.uv_axes[0], face.uv_axes[1], face.uv, texture_size)
|
||||||
|
else:
|
||||||
|
return get_quake_uv(vertex, face.plane.normal, face.uv, texture_size)
|
||||||
|
|
||||||
|
## Returns the tangent calculated from the Valve 220 UV format.
|
||||||
|
static func get_valve_tangent(u: Vector3, v: Vector3, normal: Vector3) -> PackedFloat32Array:
|
||||||
|
var u_axis: Vector3 = u.normalized()
|
||||||
|
var v_axis: Vector3 = v.normalized()
|
||||||
|
var v_sign: float = -signf(normal.cross(u_axis).dot(v_axis))
|
||||||
|
return [u_axis.x, u_axis.y, u_axis.z, v_sign]
|
||||||
|
|
||||||
|
# NOTE: we may still need to orthonormalize tangents. Just in case, here's a rough outline.
|
||||||
|
#var tangent: Vector3 = u.normalized()
|
||||||
|
#tangent = (tangent - normal * normal.dot(tangent)).normalized()
|
||||||
|
#
|
||||||
|
## in the case of parallel U or V axes to planar normal, reconstruct the tangent
|
||||||
|
#if tangent.length_squared() < 0.01:
|
||||||
|
# if absf(normal.y) < 0.9:
|
||||||
|
# tangent = Vector3.UP.cross(normal)
|
||||||
|
# else:
|
||||||
|
# tangent = Vector3.RIGHT.cross(normal)
|
||||||
|
#
|
||||||
|
#tangent = tangent.normalized()
|
||||||
|
#return [tangent.x, tangent.y, tangent.z, -signf(normal.cross(tangent).dot(v.normalized))]
|
||||||
|
|
||||||
|
## Returns the tangent calculated from the original id Standard UV format.
|
||||||
|
static func get_quake_tangent(normal: Vector3, uv_y_scale: float, uv_rotation: float) -> PackedFloat32Array:
|
||||||
|
var dx := normal.dot(_VEC3_RIGHT_ID)
|
||||||
|
var dy := normal.dot(_VEC3_UP_ID)
|
||||||
|
var dz := normal.dot(_VEC3_FORWARD_ID)
|
||||||
|
var dxa := absf(dx)
|
||||||
|
var dya := absf(dy)
|
||||||
|
var dza := absf(dz)
|
||||||
|
var u_axis: Vector3
|
||||||
|
var v_sign: float = 0.0
|
||||||
|
|
||||||
|
if dya >= dxa and dya >= dza:
|
||||||
|
u_axis = _VEC3_FORWARD_ID
|
||||||
|
v_sign = signf(dy)
|
||||||
|
elif dxa >= dya and dxa >= dza:
|
||||||
|
u_axis = _VEC3_FORWARD_ID
|
||||||
|
v_sign = -signf(dx)
|
||||||
|
elif dza >= dya and dza >= dxa:
|
||||||
|
u_axis = _VEC3_RIGHT_ID
|
||||||
|
v_sign = signf(dz)
|
||||||
|
|
||||||
|
v_sign *= signf(uv_y_scale)
|
||||||
|
u_axis = u_axis.rotated(normal, deg_to_rad(-uv_rotation) * v_sign)
|
||||||
|
return [u_axis.x, u_axis.y, u_axis.z, v_sign]
|
||||||
|
|
||||||
|
static func get_face_tangent(face: FuncGodotData.FaceData) -> PackedFloat32Array:
|
||||||
|
if face.uv_axes.size() >= 2:
|
||||||
|
return get_valve_tangent(face.uv_axes[0], face.uv_axes[1], face.plane.normal)
|
||||||
|
else:
|
||||||
|
return get_quake_tangent(face.plane.normal, face.uv.get_scale().y, face.uv.get_rotation())
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region MESH
|
||||||
|
|
||||||
|
static func smooth_mesh_by_angle(mesh: ArrayMesh, angle_deg: float = 89.0) -> Mesh:
|
||||||
|
if not mesh:
|
||||||
|
push_error("Need a source mesh to smooth")
|
||||||
|
return
|
||||||
|
|
||||||
|
var angle: float = deg_to_rad(clampf(angle_deg, 0.0, 360.0))
|
||||||
|
|
||||||
|
var mesh_vertices: Array[Vector3] = []
|
||||||
|
var mesh_normals: Array[Vector3] = []
|
||||||
|
var surface_data: Array[Dictionary] = []
|
||||||
|
var mdt: MeshDataTool
|
||||||
|
var st := SurfaceTool.new()
|
||||||
|
|
||||||
|
# Collect surface information
|
||||||
|
for surface_index in mesh.get_surface_count():
|
||||||
|
mdt = MeshDataTool.new()
|
||||||
|
|
||||||
|
if mdt.create_from_surface(mesh, surface_index) != OK:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var info: Dictionary = {
|
||||||
|
"mdt": mdt,
|
||||||
|
"ofs": mesh_vertices.size(),
|
||||||
|
"mat": mesh.surface_get_material(surface_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
surface_data.append(info)
|
||||||
|
|
||||||
|
for i in mdt.get_vertex_count():
|
||||||
|
mesh_vertices.append(mdt.get_vertex(i))
|
||||||
|
mesh_normals.append(mdt.get_vertex_normal(i))
|
||||||
|
|
||||||
|
var groups: Dictionary = {}
|
||||||
|
|
||||||
|
# Group vertices by position
|
||||||
|
for i in mesh_vertices.size():
|
||||||
|
var pos := mesh_vertices[i]
|
||||||
|
|
||||||
|
# this is likely already snapped from the map building process
|
||||||
|
var key := pos.snappedf(_VERTEX_EPSILON)
|
||||||
|
|
||||||
|
if not groups.has(key):
|
||||||
|
groups[key] = [i]
|
||||||
|
else:
|
||||||
|
groups[key].append(i)
|
||||||
|
|
||||||
|
# Collect normals. Likely optimizable.
|
||||||
|
for group in groups.values():
|
||||||
|
for i in group:
|
||||||
|
var this := mesh_normals[i]
|
||||||
|
var normal_out := Vector3()
|
||||||
|
for j in group:
|
||||||
|
var other := mesh_normals[j]
|
||||||
|
if this.angle_to(other) <= angle:
|
||||||
|
normal_out += other
|
||||||
|
|
||||||
|
mesh_normals[i] = normal_out.normalized()
|
||||||
|
|
||||||
|
var smoothed_mesh := ArrayMesh.new()
|
||||||
|
|
||||||
|
# Construct smoothed output mesh
|
||||||
|
for dict in surface_data:
|
||||||
|
mdt = dict["mdt"]
|
||||||
|
var offset: int = dict["ofs"]
|
||||||
|
for i in mdt.get_vertex_count():
|
||||||
|
mdt.set_vertex_normal(i, mesh_normals[offset + i])
|
||||||
|
|
||||||
|
st = SurfaceTool.new()
|
||||||
|
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||||
|
st.set_material(dict["mat"])
|
||||||
|
|
||||||
|
for i in mdt.get_face_count():
|
||||||
|
for j in 3:
|
||||||
|
var index := mdt.get_face_vertex(i, j)
|
||||||
|
st.set_normal(mdt.get_vertex_normal(index))
|
||||||
|
st.set_uv(mdt.get_vertex_uv(index))
|
||||||
|
st.set_tangent(mdt.get_vertex_tangent(index))
|
||||||
|
st.add_vertex(mdt.get_vertex(index))
|
||||||
|
|
||||||
|
smoothed_mesh = st.commit(smoothed_mesh)
|
||||||
|
|
||||||
|
return smoothed_mesh
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
uid://dy80wnu2py4dk
|
uid://bursmx2g1betd
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
[resource]
|
[resource]
|
||||||
albedo_texture = ExtResource("1_ncj77")
|
albedo_texture = ExtResource("1_ncj77")
|
||||||
metallic_specular = 0.0
|
metallic_specular = 0.0
|
||||||
texture_filter = 4
|
texture_filter = 2
|
||||||
|
|||||||
BIN
addons/func_godot/textures/special/origin.png
Normal file
BIN
addons/func_godot/textures/special/origin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 239 B |
34
addons/func_godot/textures/special/origin.png.import
Normal file
34
addons/func_godot/textures/special/origin.png.import
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dutip72dl002r"
|
||||||
|
path="res://.godot/imported/origin.png-85b62dd151467f05fa8f98ed6d2927d0.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"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -12,21 +12,21 @@ font_size = 96
|
|||||||
|
|
||||||
[sub_resource type="LabelSettings" id="LabelSettings_jqn4v"]
|
[sub_resource type="LabelSettings" id="LabelSettings_jqn4v"]
|
||||||
font = ExtResource("3_mcea6")
|
font = ExtResource("3_mcea6")
|
||||||
font_size = 68
|
font_size = 48
|
||||||
font_color = Color(1, 1, 1, 0.501961)
|
font_color = Color(1, 1, 1, 0.501961)
|
||||||
outline_size = 16
|
outline_size = 16
|
||||||
outline_color = Color(0, 0, 0, 0.501961)
|
outline_color = Color(0, 0, 0, 0.501961)
|
||||||
|
|
||||||
[sub_resource type="LabelSettings" id="LabelSettings_fpuge"]
|
[sub_resource type="LabelSettings" id="LabelSettings_fpuge"]
|
||||||
font = ExtResource("3_mcea6")
|
font = ExtResource("3_mcea6")
|
||||||
font_size = 80
|
font_size = 60
|
||||||
font_color = Color(1, 1, 1, 0.627451)
|
font_color = Color(1, 1, 1, 0.627451)
|
||||||
outline_size = 16
|
outline_size = 16
|
||||||
outline_color = Color(0, 0, 0, 0.627451)
|
outline_color = Color(0, 0, 0, 0.627451)
|
||||||
|
|
||||||
[sub_resource type="LabelSettings" id="LabelSettings_npr4g"]
|
[sub_resource type="LabelSettings" id="LabelSettings_npr4g"]
|
||||||
font = ExtResource("3_mcea6")
|
font = ExtResource("3_mcea6")
|
||||||
font_size = 96
|
font_size = 72
|
||||||
outline_size = 16
|
outline_size = 16
|
||||||
outline_color = Color(0, 0, 0, 1)
|
outline_color = Color(0, 0, 0, 1)
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ theme_override_constants/separation = -8
|
|||||||
[node name="prompt_overflow" type="Label" parent="hud_bottom/promptbox"]
|
[node name="prompt_overflow" type="Label" parent="hud_bottom/promptbox"]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "..."
|
text = ". . ."
|
||||||
label_settings = SubResource("LabelSettings_jqn4v")
|
label_settings = SubResource("LabelSettings_jqn4v")
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
vertical_alignment = 1
|
vertical_alignment = 1
|
||||||
@@ -97,6 +97,23 @@ label_settings = SubResource("LabelSettings_npr4g")
|
|||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
vertical_alignment = 1
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="subtitle" type="MarginContainer" parent="."]
|
||||||
|
visible = false
|
||||||
|
anchors_preset = 12
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_top = -300.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="ColorRect" type="ColorRect" parent="subtitle"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
color = Color(0.15, 0.15, 0.15, 0.784314)
|
||||||
|
|
||||||
[node name="icon_eye" type="TextureRect" parent="."]
|
[node name="icon_eye" type="TextureRect" parent="."]
|
||||||
visible = false
|
visible = false
|
||||||
offset_right = 192.0
|
offset_right = 192.0
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ var on_route : bool = false
|
|||||||
|
|
||||||
@export var behavior_type : String = "guard"
|
@export var behavior_type : String = "guard"
|
||||||
var state : String = "default"
|
var state : String = "default"
|
||||||
var current_waypoint : int = -1
|
var current_waypoint : int = 0
|
||||||
var paused : bool = false
|
var paused : bool = false
|
||||||
var pauseanim : String
|
var pauseanim : String
|
||||||
var pauseturn : bool
|
var pauseturn : bool
|
||||||
@@ -226,7 +226,7 @@ func movement_process(delta):
|
|||||||
if !paused:
|
if !paused:
|
||||||
walk_toward(current_path_point)
|
walk_toward(current_path_point)
|
||||||
|
|
||||||
if global_transform.origin.distance_to(current_path_point) <= 0.5:
|
if global_transform.origin.distance_to(current_path_point) <= 0.25:
|
||||||
current_path_index += 1
|
current_path_index += 1
|
||||||
if current_path_index >= current_path.size():
|
if current_path_index >= current_path.size():
|
||||||
if on_route:
|
if on_route:
|
||||||
@@ -268,6 +268,7 @@ func walk_toward(point: Vector3):
|
|||||||
var direction = global_transform.origin.direction_to(point)
|
var direction = global_transform.origin.direction_to(point)
|
||||||
velocity.x = direction.x * walk_speed
|
velocity.x = direction.x * walk_speed
|
||||||
velocity.z = direction.z * walk_speed
|
velocity.z = direction.z * walk_speed
|
||||||
|
print(direction)
|
||||||
|
|
||||||
lerp_toward(atan2(velocity.x, velocity.z))
|
lerp_toward(atan2(velocity.x, velocity.z))
|
||||||
|
|
||||||
@@ -289,7 +290,8 @@ func setpath(target_position):
|
|||||||
target_position,
|
target_position,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
path_end_point = current_path[len(current_path)-1]
|
if len(current_path) > 0:
|
||||||
|
path_end_point = current_path[len(current_path)-1]
|
||||||
|
|
||||||
func clearpath():
|
func clearpath():
|
||||||
current_path = []
|
current_path = []
|
||||||
|
|||||||
1
maps/autosave/.gdignore
Normal file
1
maps/autosave/.gdignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="func_godot.map"
|
|
||||||
type="Resource"
|
|
||||||
uid="uid://bmjn0u44o5mvf"
|
|
||||||
path="res://.godot/imported/cafemotel.1.map-47307151d410699b7cf7c2fe6d296bb7.tres"
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://maps/autosave/cafemotel.1.map"
|
|
||||||
dest_files=["res://.godot/imported/cafemotel.1.map-47307151d410699b7cf7c2fe6d296bb7.tres"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="func_godot.map"
|
|
||||||
type="Resource"
|
|
||||||
uid="uid://dnemivow04qpk"
|
|
||||||
path="res://.godot/imported/cafemotel.10.map-d2df9a014684f3492d3e6763eb7f1da0.tres"
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
|
|
||||||
source_file="res://maps/autosave/cafemotel.10.map"
|
|
||||||
dest_files=["res://.godot/imported/cafemotel.10.map-d2df9a014684f3492d3e6763eb7f1da0.tres"]
|
|
||||||
|
|
||||||
[params]
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user