Initial commit
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Normalize EOL for all files that Git considers text files.
|
||||
* text=auto eol=lf
|
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/android/
|
||||
|
||||
# Trenchbroom ignores
|
||||
/**/autosave
|
30
addons/func_godot/fgd/func_detail.tres
Normal file
@ -0,0 +1,30 @@
|
||||
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 format=3 uid="uid://cxy7jnh6d7msn"]
|
||||
|
||||
[ext_resource type="Script" 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"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_0fsmp")
|
||||
spawn_type = 2
|
||||
origin_type = 0
|
||||
build_visuals = true
|
||||
use_in_baked_light = true
|
||||
shadow_casting_setting = 1
|
||||
build_occlusion = false
|
||||
render_layers = 1
|
||||
collision_shape_type = 2
|
||||
collision_layer = 1
|
||||
collision_mask = 0
|
||||
collision_priority = 1.0
|
||||
collision_shape_margin = 0.04
|
||||
classname = "func_detail"
|
||||
description = "Static collidable geometry. Builds a StaticBody3D with a MeshInstance3D and a single concave CollisionShape3D. Does not occlude other VisualInstance3D nodes."
|
||||
func_godot_internal = false
|
||||
base_classes = Array[Resource]([ExtResource("1_c3bns")])
|
||||
class_properties = {}
|
||||
class_property_descriptions = {}
|
||||
meta_properties = {
|
||||
"color": Color(0.8, 0.8, 0.8, 1)
|
||||
}
|
||||
node_class = "StaticBody3D"
|
||||
name_property = ""
|
30
addons/func_godot/fgd/func_detail_illusionary.tres
Normal file
@ -0,0 +1,30 @@
|
||||
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 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="Script" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_lhb87"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("2_lhb87")
|
||||
spawn_type = 2
|
||||
origin_type = 0
|
||||
build_visuals = true
|
||||
use_in_baked_light = true
|
||||
shadow_casting_setting = 1
|
||||
build_occlusion = false
|
||||
render_layers = 1
|
||||
collision_shape_type = 0
|
||||
collision_layer = 1
|
||||
collision_mask = 1
|
||||
collision_priority = 1.0
|
||||
collision_shape_margin = 0.04
|
||||
classname = "func_detail_illusionary"
|
||||
description = "Static geometry with no collision. Builds a Node3D with a MeshInstance3D. Does not occlude other VisualInstance3D nodes."
|
||||
func_godot_internal = false
|
||||
base_classes = Array[Resource]([ExtResource("1_ar63x")])
|
||||
class_properties = {}
|
||||
class_property_descriptions = {}
|
||||
meta_properties = {
|
||||
"color": Color(0.8, 0.8, 0.8, 1)
|
||||
}
|
||||
node_class = "Node3D"
|
||||
name_property = ""
|
30
addons/func_godot/fgd/func_geo.tres
Normal file
@ -0,0 +1,30 @@
|
||||
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 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" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_8o081"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("2_8o081")
|
||||
spawn_type = 2
|
||||
origin_type = 0
|
||||
build_visuals = true
|
||||
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
|
||||
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")])
|
||||
class_properties = {}
|
||||
class_property_descriptions = {}
|
||||
meta_properties = {
|
||||
"color": Color(0.8, 0.8, 0.8, 1)
|
||||
}
|
||||
node_class = "StaticBody3D"
|
||||
name_property = ""
|
17
addons/func_godot/fgd/func_godot_fgd.tres
Normal file
@ -0,0 +1,17 @@
|
||||
[gd_resource type="Resource" script_class="FuncGodotFGDFile" load_steps=8 format=3 uid="uid://crgpdahjaj"]
|
||||
|
||||
[ext_resource type="Script" 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://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://dg5x44cc7flew" path="res://addons/func_godot/fgd/func_illusionary.tres" id="4_c4ucw"]
|
||||
[ext_resource type="Resource" uid="uid://ch3e0dix85uhb" path="res://addons/func_godot/fgd/func_detail_illusionary.tres" id="5_b2q3p"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_axt3h")
|
||||
export_file = false
|
||||
target_map_editor = 1
|
||||
fgd_name = "FuncGodot"
|
||||
base_fgd_files = Array[Resource]([])
|
||||
entity_definitions = Array[Resource]([ExtResource("1_ehab8"), ExtResource("2_ri2rx"), ExtResource("3_7jigp"), ExtResource("3_fqfww"), ExtResource("5_b2q3p"), ExtResource("4_c4ucw")])
|
30
addons/func_godot/fgd/func_illusionary.tres
Normal file
@ -0,0 +1,30 @@
|
||||
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=3 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="Script" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_uffhi"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("2_uffhi")
|
||||
spawn_type = 2
|
||||
origin_type = 0
|
||||
build_visuals = true
|
||||
use_in_baked_light = true
|
||||
shadow_casting_setting = 1
|
||||
build_occlusion = true
|
||||
render_layers = 1
|
||||
collision_shape_type = 0
|
||||
collision_layer = 1
|
||||
collision_mask = 1
|
||||
collision_priority = 1.0
|
||||
collision_shape_margin = 0.04
|
||||
classname = "func_illusionary"
|
||||
description = "Static geometry with no collision. Builds a Node3D with a MeshInstance3D and an Occluder3D to aid in render culling of other VisualInstance3D nodes."
|
||||
func_godot_internal = false
|
||||
base_classes = Array[Resource]([ExtResource("1_kv0mq")])
|
||||
class_properties = {}
|
||||
class_property_descriptions = {}
|
||||
meta_properties = {
|
||||
"color": Color(0.8, 0.8, 0.8, 1)
|
||||
}
|
||||
node_class = "Node3D"
|
||||
name_property = ""
|
27
addons/func_godot/fgd/phong_base.tres
Normal file
@ -0,0 +1,27 @@
|
||||
[gd_resource type="Resource" script_class="FuncGodotFGDBaseClass" load_steps=2 format=3 uid="uid://nayxb8n7see2"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/func_godot/src/fgd/func_godot_fgd_base_class.gd" id="1_04y3n"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_04y3n")
|
||||
classname = "Phong"
|
||||
description = "Phong shading options for SolidClass geometry."
|
||||
func_godot_internal = false
|
||||
base_classes = Array[Resource]([])
|
||||
class_properties = {
|
||||
"_phong": {
|
||||
"Disabled": 0,
|
||||
"Smooth shading": 1
|
||||
},
|
||||
"_phong_angle": 89.0
|
||||
}
|
||||
class_property_descriptions = {
|
||||
"_phong": ["Phong shading", 0],
|
||||
"_phong_angle": "Phong smoothing angle"
|
||||
}
|
||||
meta_properties = {
|
||||
"color": Color(0.8, 0.8, 0.8, 1),
|
||||
"size": AABB(-8, -8, -8, 8, 8, 8)
|
||||
}
|
||||
node_class = ""
|
||||
name_property = ""
|
29
addons/func_godot/fgd/worldspawn.tres
Normal file
@ -0,0 +1,29 @@
|
||||
[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=2 format=3 uid="uid://bdji3873bg32h"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="1_62t8m"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_62t8m")
|
||||
spawn_type = 0
|
||||
origin_type = 0
|
||||
build_visuals = true
|
||||
use_in_baked_light = true
|
||||
shadow_casting_setting = 1
|
||||
build_occlusion = true
|
||||
render_layers = 1
|
||||
collision_shape_type = 1
|
||||
collision_layer = 1
|
||||
collision_mask = 0
|
||||
collision_priority = 1.0
|
||||
collision_shape_margin = 0.04
|
||||
classname = "worldspawn"
|
||||
description = "Default static world geometry. Builds a StaticBody3D with a single MeshInstance3D and a single convex CollisionShape3D shape. Also builds Occluder3D to aid in render culling of other VisualInstance3D nodes."
|
||||
func_godot_internal = false
|
||||
base_classes = Array[Resource]([])
|
||||
class_properties = {}
|
||||
class_property_descriptions = {}
|
||||
meta_properties = {
|
||||
"color": Color(0.8, 0.8, 0.8, 1)
|
||||
}
|
||||
node_class = "StaticBody3D"
|
||||
name_property = ""
|
30
addons/func_godot/func_godot_default_map_settings.tres
Normal file
@ -0,0 +1,30 @@
|
||||
[gd_resource type="Resource" script_class="FuncGodotMapSettings" load_steps=4 format=3 uid="uid://bkhxcqsquw1yg"]
|
||||
|
||||
[ext_resource type="Material" uid="uid://cvex6toty8yn7" path="res://addons/func_godot/textures/default_material.tres" id="1_8l5wm"]
|
||||
[ext_resource type="Script" 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"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_dlf23")
|
||||
inverse_scale_factor = 32.0
|
||||
entity_fgd = ExtResource("1_hd7se")
|
||||
entity_name_property = ""
|
||||
base_texture_dir = "res://textures"
|
||||
texture_file_extensions = Array[String](["png", "jpg", "jpeg", "bmp", "tga", "webp"])
|
||||
clip_texture = "special/clip"
|
||||
skip_texture = "special/skip"
|
||||
texture_wads = Array[Resource]([])
|
||||
material_file_extension = "tres"
|
||||
unshaded = false
|
||||
default_material = ExtResource("1_8l5wm")
|
||||
default_material_albedo_uniform = ""
|
||||
albedo_map_pattern = "%s_albedo.%s"
|
||||
normal_map_pattern = "%s_normal.%s"
|
||||
metallic_map_pattern = "%s_metallic.%s"
|
||||
roughness_map_pattern = "%s_roughness.%s"
|
||||
emission_map_pattern = "%s_emission.%s"
|
||||
ao_map_pattern = "%s_ao.%s"
|
||||
height_map_pattern = "%s_height.%s"
|
||||
save_generated_materials = true
|
||||
uv_unwrap_texel_size = 2.0
|
||||
use_trenchbroom_groups_hierarchy = false
|
8
addons/func_godot/func_godot_local_config.tres
Normal file
@ -0,0 +1,8 @@
|
||||
[gd_resource type="Resource" script_class="FuncGodotLocalConfig" load_steps=2 format=3 uid="uid://bqjt7nyekxgog"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/func_godot/src/util/func_godot_local_config.gd" id="1_g8kqj"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_g8kqj")
|
||||
export_func_godot_settings = false
|
||||
reload_func_godot_settings = false
|
@ -0,0 +1,21 @@
|
||||
[gd_resource type="Resource" script_class="NetRadiantCustomGamePackConfig" load_steps=5 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="Script" 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://cfhg30jclb4lw" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres" id="3_6gpk8"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("2_en8ro")
|
||||
export_file = false
|
||||
gamepack_name = "func_godot"
|
||||
game_name = "FuncGodot"
|
||||
base_game_path = ""
|
||||
fgd_file = ExtResource("1_gct4v")
|
||||
netradiant_custom_shaders = Array[Resource]([ExtResource("2_w7psh"), ExtResource("3_6gpk8")])
|
||||
texture_types = PackedStringArray("png", "jpg", "jpeg", "bmp", "tga")
|
||||
model_types = PackedStringArray("glb", "gltf", "obj")
|
||||
sound_types = PackedStringArray("wav", "ogg")
|
||||
default_scale = "1.0"
|
||||
clip_texture = "textures/special/clip"
|
||||
skip_texture = "textures/special/skip"
|
@ -0,0 +1,8 @@
|
||||
[gd_resource type="Resource" script_class="NetRadiantCustomShader" load_steps=2 format=3 uid="uid://f5erfnvbg6b7"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_cuylw"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_cuylw")
|
||||
texture_path = "textures/special/clip"
|
||||
shader_attributes = Array[String](["qer_trans 0.4"])
|
@ -0,0 +1,8 @@
|
||||
[gd_resource type="Resource" script_class="NetRadiantCustomShader" load_steps=2 format=3 uid="uid://cfhg30jclb4lw"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_4ja6h"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_4ja6h")
|
||||
texture_path = "textures/special/skip"
|
||||
shader_attributes = Array[String](["qer_trans 0.4"])
|
@ -0,0 +1,33 @@
|
||||
[gd_resource type="Resource" script_class="TrenchBroomGameConfig" load_steps=8 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://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="Script" 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://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"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("2_ns6ah")
|
||||
export_file = false
|
||||
game_name = "FuncGodot"
|
||||
icon = ExtResource("6_tex5j")
|
||||
map_formats = Array[Dictionary]([{
|
||||
"format": "Valve",
|
||||
"initialmap": "initial_valve.map"
|
||||
}, {
|
||||
"format": "Standard",
|
||||
"initialmap": "initial_standard.map"
|
||||
}, {
|
||||
"format": "Quake2",
|
||||
"initialmap": "initial_quake2.map"
|
||||
}, {
|
||||
"format": "Quake3"
|
||||
}])
|
||||
texture_exclusion_patterns = Array[String](["*_albedo", "*_ao", "*_emission", "*_height", "*_metallic", "*_normal", "*_orm", "*_roughness", "*_sss"])
|
||||
fgd_file = ExtResource("1_8u1vq")
|
||||
entity_scale = "32"
|
||||
default_uv_scale = Vector2(1, 1)
|
||||
brush_tags = Array[Resource]([ExtResource("3_mbuv7"), ExtResource("4_y0tjm")])
|
||||
brushface_tags = Array[Resource]([ExtResource("1_rsp20"), ExtResource("2_166i2")])
|
@ -0,0 +1,11 @@
|
||||
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://37iduqf7tpxq"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_rn13a"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_rn13a")
|
||||
tag_name = "Func"
|
||||
tag_attributes = Array[String]([])
|
||||
tag_match_type = 1
|
||||
tag_pattern = "func*"
|
||||
texture_name = ""
|
@ -0,0 +1,11 @@
|
||||
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://co2sb1ng7cw4i"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_msqpk"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_msqpk")
|
||||
tag_name = "Trigger"
|
||||
tag_attributes = Array[String](["transparent"])
|
||||
tag_match_type = 1
|
||||
tag_pattern = "trigger*"
|
||||
texture_name = "special/trigger"
|
@ -0,0 +1,11 @@
|
||||
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://b4xhdj0e16lop"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_7td58"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_7td58")
|
||||
tag_name = "Clip"
|
||||
tag_attributes = Array[String](["transparent"])
|
||||
tag_match_type = 0
|
||||
tag_pattern = "clip"
|
||||
texture_name = ""
|
@ -0,0 +1,11 @@
|
||||
[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://ca7377sfgj074"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_2teqe"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_2teqe")
|
||||
tag_name = "Skip"
|
||||
tag_attributes = Array[String](["transparent"])
|
||||
tag_match_type = 0
|
||||
tag_pattern = "skip"
|
||||
texture_name = ""
|
BIN
addons/func_godot/icon.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
34
addons/func_godot/icon.png.import
Normal file
@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cp2as6ujvknyu"
|
||||
path="res://.godot/imported/icon.png-6db43b6a52df1ce3744a82f15cbdbbea.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/func_godot/icon.png"
|
||||
dest_files=["res://.godot/imported/icon.png-6db43b6a52df1ce3744a82f15cbdbbea.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
|
13
addons/func_godot/icon.svg
Normal file
After Width: | Height: | Size: 10 KiB |
37
addons/func_godot/icon.svg.import
Normal file
@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bx5buvf1ydm7q"
|
||||
path="res://.godot/imported/icon.svg-99f2c56e0c1ce867c819715c68d9c120.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/func_godot/icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-99f2c56e0c1ce867c819715c68d9c120.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
|
BIN
addons/func_godot/icon32.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
34
addons/func_godot/icon32.png.import
Normal file
@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://decwujsyhj0qy"
|
||||
path="res://.godot/imported/icon32.png-7025e2d95a64a3066b7947e1900b4daf.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/func_godot/icon32.png"
|
||||
dest_files=["res://.godot/imported/icon32.png-7025e2d95a64a3066b7947e1900b4daf.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
|
13
addons/func_godot/icons/icon_godambler.svg
Normal file
After Width: | Height: | Size: 9.4 KiB |
37
addons/func_godot/icons/icon_godambler.svg.import
Normal file
@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bm2kwpq18quv0"
|
||||
path="res://.godot/imported/icon_godambler.svg-a6dbba375ab2a45be046a1875b8d41e6.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/func_godot/icons/icon_godambler.svg"
|
||||
dest_files=["res://.godot/imported/icon_godambler.svg-a6dbba375ab2a45be046a1875b8d41e6.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
|
34
addons/func_godot/icons/icon_godambler3d.svg
Normal file
After Width: | Height: | Size: 11 KiB |
37
addons/func_godot/icons/icon_godambler3d.svg.import
Normal file
@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dieefivfbkovw"
|
||||
path="res://.godot/imported/icon_godambler3d.svg-f7df9bfe58320474198644aa06a8f3f6.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/func_godot/icons/icon_godambler3d.svg"
|
||||
dest_files=["res://.godot/imported/icon_godambler3d.svg-f7df9bfe58320474198644aa06a8f3f6.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
|
13
addons/func_godot/icons/icon_godot_ranger.svg
Normal file
After Width: | Height: | Size: 19 KiB |
37
addons/func_godot/icons/icon_godot_ranger.svg.import
Normal file
@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cfxlhjsefleff"
|
||||
path="res://.godot/imported/icon_godot_ranger.svg-8572582518f54de6403b767a923b5a92.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/func_godot/icons/icon_godot_ranger.svg"
|
||||
dest_files=["res://.godot/imported/icon_godot_ranger.svg-8572582518f54de6403b767a923b5a92.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
|
13
addons/func_godot/icons/icon_godot_ranger3d.svg
Normal file
After Width: | Height: | Size: 19 KiB |
37
addons/func_godot/icons/icon_godot_ranger3d.svg.import
Normal file
@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://brm515f5ivx8m"
|
||||
path="res://.godot/imported/icon_godot_ranger3d.svg-a9a2c9bcf2e8b1e07a0a941a16264e98.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/func_godot/icons/icon_godot_ranger3d.svg"
|
||||
dest_files=["res://.godot/imported/icon_godot_ranger3d.svg-a9a2c9bcf2e8b1e07a0a941a16264e98.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
|
85
addons/func_godot/icons/icon_quake_file.svg
Normal file
@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
||||
inkscape:export-filename="/home/djrm/Projects/godot/tools/editor/icons/icon_node.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90"
|
||||
sodipodi:docname="icon_qodot_node.svg">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="31.999999"
|
||||
inkscape:cx="9.2742768"
|
||||
inkscape:cy="5.9794586"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:snap-bbox-midpoints="true"
|
||||
inkscape:snap-object-midpoints="true"
|
||||
inkscape:snap-center="true"
|
||||
inkscape:window-width="1849"
|
||||
inkscape:window-height="942"
|
||||
inkscape:window-x="2365"
|
||||
inkscape:window-y="478"
|
||||
inkscape:window-maximized="0">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid3336"
|
||||
empspacing="4" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-1036.3622)">
|
||||
<g
|
||||
transform="matrix(0.00898883,0,0,0.00898883,2.84873,1036.9616)"
|
||||
id="g8"
|
||||
style="fill:#e0e0e0;fill-opacity:0.99607843">
|
||||
<path
|
||||
sodipodi:type="inkscape:offset"
|
||||
inkscape:radius="32.247818"
|
||||
inkscape:original="M 805.49219 48.865234 L 805.49219 52 L 805.49219 70.470703 C 908.63676 125.4219 990.75756 219.02053 1030.5371 328.76562 C 1072.5343 442.84389 1069.0608 572.88678 1019.4355 684.0293 C 962.84256 813.15825 847.75434 914.65851 712.625 954.68555 C 687.95498 962.05941 662.7661 967.4194 637.26562 970.68359 C 637.37347 905.40191 636.93661 840.04476 637.35156 774.81055 C 644.27173 753.89399 667.02241 744.70589 686.79883 740.29102 C 696.25394 738.22707 705.90322 736.98982 715.58203 736.76953 C 714.41755 731.86565 718.02354 722.1913 713.58203 720.56445 L 439 720.56445 C 440.15426 725.49354 436.59893 734.98734 440.9375 736.83008 C 464.02438 738.2287 488.57642 742.59448 506.58789 758.30469 C 513.72262 764.95503 519.23908 774.34645 517.41992 784.43945 L 517.41992 970.64062 C 392.2186 952.28136 280.08237 877.51602 203.18555 778.49805 C 146.20758 705.03793 105.92922 616.34944 98.074219 523.1582 C 90.131888 410.66597 124.62307 296.10332 192.38281 205.89258 C 233.9121 150.23193 287.72076 103.61704 348.8125 70.462891 C 348.8205 63.264976 348.82683 56.067058 348.83203 48.869141 C 226.43763 103.66249 125.42782 204.32365 70.367188 326.61133 C 10.634796 457.1454 4.9961598 611.01761 54.242188 745.77539 C 100.18371 873.54309 194.5137 982.99051 313.99414 1047.4492 C 376.81007 1081.5901 446.33117 1103.3409 517.41992 1110.9785 C 517.56059 1169.4421 517.13902 1227.9564 517.62891 1286.3887 C 536.50698 1390.7196 555.38596 1495.0499 574.26367 1599.3809 C 595.24637 1494.6025 616.32947 1389.8338 637.24609 1285.0488 C 637.24709 1227.0293 637.249 1169.0098 637.25 1110.9902 C 753.86537 1099.5192 866.94739 1050.9022 953.70898 971.83203 C 1036.6427 897.08153 1094.4372 795.63679 1118.3398 686.71289 C 1142.8039 576.4452 1136.0436 458.67677 1094.3887 353.33398 C 1044.4835 224.74362 944.8252 116.42175 820.77539 56.089844 C 815.72077 53.598792 810.60447 51.234804 805.49219 48.865234 z "
|
||||
style="fill:#e0e0e0;fill-opacity:0.99607843;stroke:none;stroke-width:5;stroke-miterlimit:10;stroke-opacity:1"
|
||||
id="path6"
|
||||
d="m 347.90625,16.636719 a 32.251043,32.251043 0 0 0 -12.25,2.798828 C 205.88469,77.531491 99.412299,183.61822 41.001953,313.29102 -22.348317,451.80048 -28.265288,613.88282 23.927734,756.76758 72.647329,892.19719 172.02966,1007.4904 298.65234,1075.8125 c 58.00997,31.5205 121.31762,53.0204 186.51172,63.377 -0.002,48.987 -0.19484,98.1365 0.21875,147.4687 a 32.251043,32.251043 0 0 0 0.51367,5.4727 c 18.87811,104.3311 37.75711,208.6614 56.63477,312.9921 a 32.251043,32.251043 0 0 0 63.35156,0.5899 c 20.97874,-104.7586 42.06315,-209.5337 62.98633,-314.3516 a 32.251043,32.251043 0 0 0 0.625,-6.3125 c 8.4e-4,-48.5498 0.003,-97.1 0.004,-145.6504 112.94142,-16.8214 220.92348,-66.2925 305.84961,-143.65817 88.14404,-79.4662 149.14894,-186.66163 174.48434,-302.08984 25.7675,-116.18424 18.7794,-240.2345 -25.416,-352.06836 C 1071.4778,205.24135 966.47079,91.107321 834.92773,27.115234 829.42805,24.407001 824.10868,21.95088 819.05273,19.607422 A 32.251043,32.251043 0 0 0 773.24414,48.865234 V 52 70.470703 a 32.251043,32.251043 0 0 0 17.08594,28.460938 c 96.0021,51.145979 172.95277,138.924249 209.88872,240.824219 a 32.251043,32.251043 0 0 0 0.057,0.15039 c 39.0531,106.08119 35.7505,227.87147 -10.28517,330.97461 a 32.251043,32.251043 0 0 0 -0.0898,0.20312 C 937.26564,791.18147 829.14264,886.53883 703.4668,923.76562 a 32.251043,32.251043 0 0 0 -0.0762,0.0234 c -11.19816,3.34713 -22.50938,6.2295 -33.90429,8.67578 -0.0393,-50.21706 -0.16903,-100.33931 0.10156,-150.23828 0.86271,-0.9191 2.16446,-1.94384 4.36719,-3.27734 4.48441,-2.7148 12.01568,-5.42153 19.80273,-7.16602 7.58488,-1.65065 15.16509,-2.60516 22.5586,-2.77343 a 32.251043,32.251043 0 0 0 30.64062,-39.69141 c 2.24778,9.46588 0.68077,5.89079 1.03906,0.11719 0.17915,-2.88681 0.72749,-6.86388 -1.11718,-14.20313 -1.84468,-7.33925 -10.07914,-20.50769 -22.20508,-24.94922 a 32.251043,32.251043 0 0 0 -11.0918,-1.96679 H 439 a 32.251043,32.251043 0 0 0 -31.40234,39.58203 c -2.13044,-9.10803 -0.65038,-5.75562 -1.00782,-0.0645 -0.17902,2.85053 -0.68774,6.79348 1.02344,13.91016 1.71118,7.11668 9.00286,19.79227 20.7168,24.76758 a 32.251043,32.251043 0 0 0 10.6582,2.50781 c 20.32647,1.23139 36.93313,5.44255 46.25977,13.48242 a 32.251043,32.251043 0 0 0 -0.0762,1.9375 V 931.33789 C 384.2764,906.36086 293.26933,841.91584 228.66602,758.73438 l -0.0117,-0.0156 C 174.92035,689.43588 137.4999,606.28431 130.23242,520.66211 122.92233,416.31754 155.17118,309.12943 218.16797,225.25977 a 32.251043,32.251043 0 0 0 0.0606,-0.082 c 38.7059,-51.87653 89.03359,-95.47481 145.96484,-126.371089 a 32.251043,32.251043 0 0 0 16.86719,-28.308594 c 0.008,-7.202903 0.0143,-14.404177 0.0195,-21.605469 A 32.251043,32.251043 0 0 0 347.90625,16.636719 Z M 484.59961,781.89453 c 0.6695,0.62405 0.56402,0.47412 0.65039,0.54102 a 32.251043,32.251043 0 0 0 0,0.0449 z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 7.1 KiB |
37
addons/func_godot/icons/icon_quake_file.svg.import
Normal file
@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c0464gp8lby0w"
|
||||
path="res://.godot/imported/icon_quake_file.svg-1718b9a2b5e0b124f6d72bb4c72d2ee6.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/func_godot/icons/icon_quake_file.svg"
|
||||
dest_files=["res://.godot/imported/icon_quake_file.svg-1718b9a2b5e0b124f6d72bb4c72d2ee6.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
|
13
addons/func_godot/icons/icon_slipgate3d.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="#fc7f7f" 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="#fc7f7f" 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="#fc7f7f" 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="#fc7f7f" 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="#fc7f7f" 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="#fc7f7f" 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="#fc7f7f" 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="#fc7f7f" 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="#fc7f7f" 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_slipgate3d.svg.import
Normal file
@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cfvririkaa4tv"
|
||||
path="res://.godot/imported/icon_slipgate3d.svg-f125bef6ff5aa79b5fe3f232a083425e.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/func_godot/icons/icon_slipgate3d.svg"
|
||||
dest_files=["res://.godot/imported/icon_slipgate3d.svg-f125bef6ff5aa79b5fe3f232a083425e.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
|
BIN
addons/func_godot/palette.lmp
Normal file
14
addons/func_godot/palette.lmp.import
Normal file
@ -0,0 +1,14 @@
|
||||
[remap]
|
||||
|
||||
importer="func_godot.palette"
|
||||
type="Resource"
|
||||
uid="uid://drgnc41yfybr"
|
||||
path="res://.godot/imported/palette.lmp-138c33f2ac0cab3ad6373e7c0425cf00.tres"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/func_godot/palette.lmp"
|
||||
dest_files=["res://.godot/imported/palette.lmp-138c33f2ac0cab3ad6373e7c0425cf00.tres"]
|
||||
|
||||
[params]
|
||||
|
7
addons/func_godot/plugin.cfg
Normal file
@ -0,0 +1,7 @@
|
||||
[plugin]
|
||||
|
||||
name="FuncGodot"
|
||||
description="Quake .map file support for Godot."
|
||||
author="Shifty, Hannah Crawford, Emberlynn Bland, Tim Maccabe"
|
||||
version="2024.1"
|
||||
script="src/func_godot_plugin.gd"
|
116
addons/func_godot/src/core/func_godot.gd
Normal file
@ -0,0 +1,116 @@
|
||||
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)
|
||||
var map_settings: FuncGodotMapSettings = null
|
||||
|
||||
func load_map(filename: String, keep_tb_groups: bool) -> void:
|
||||
map_parser.load_map(filename, keep_tb_groups)
|
||||
|
||||
func get_texture_list() -> PackedStringArray:
|
||||
var g_textures: PackedStringArray
|
||||
var tex_count: int = map_data.textures.size()
|
||||
|
||||
g_textures.resize(tex_count)
|
||||
for i in range(tex_count):
|
||||
g_textures.set(i, map_data.textures[i].name)
|
||||
|
||||
return g_textures
|
||||
|
||||
func set_entity_definitions(entity_defs: Dictionary) -> void:
|
||||
for i in range(entity_defs.size()):
|
||||
var classname: String = entity_defs.keys()[i]
|
||||
var spawn_type: int = entity_defs.values()[i].get("spawn_type", FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY)
|
||||
var origin_type: int = entity_defs.values()[i].get("origin_type", FuncGodotMapData.FuncGodotEntityOriginType.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(texture_name: String) -> 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(map_settings.clip_texture)
|
||||
sg.set_skip_filter_texture(map_settings.skip_texture)
|
||||
sg.run()
|
||||
return fetch_surfaces(sg)
|
||||
|
||||
func gather_entity_convex_collision_surfaces(entity_idx: int) -> void:
|
||||
surface_gatherer.reset_params()
|
||||
surface_gatherer.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.BRUSH
|
||||
surface_gatherer.entity_filter_idx = entity_idx
|
||||
surface_gatherer.run()
|
||||
|
||||
func gather_entity_concave_collision_surfaces(entity_idx: int) -> void:
|
||||
surface_gatherer.reset_params()
|
||||
surface_gatherer.split_type = FuncGodotSurfaceGatherer.SurfaceSplitType.NONE
|
||||
surface_gatherer.entity_filter_idx = entity_idx
|
||||
surface_gatherer.set_skip_filter_texture(map_settings.skip_texture)
|
||||
surface_gatherer.run()
|
||||
|
||||
func fetch_surfaces(sg: FuncGodotSurfaceGatherer) -> Array:
|
||||
var surfs: Array[FuncGodotMapData.FuncGodotFaceGeometry] = sg.out_surfaces
|
||||
var surf_array: Array
|
||||
|
||||
for surf in surfs:
|
||||
if surf == null or surf.vertices.size() == 0:
|
||||
surf_array.append(null)
|
||||
continue
|
||||
|
||||
var vertices: PackedVector3Array
|
||||
var normals: PackedVector3Array
|
||||
var tangents: PackedFloat64Array
|
||||
var uvs: PackedVector2Array
|
||||
for v in surf.vertices:
|
||||
vertices.append(Vector3(v.vertex.y, v.vertex.z, v.vertex.x) / map_settings.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
|
327
addons/func_godot/src/core/func_godot_geo_generator.gd
Normal file
@ -0,0 +1,327 @@
|
||||
extends RefCounted
|
||||
|
||||
# Min distance between two verts in a brush before they're merged. Higher values fix angled brushes near extents.
|
||||
const CMP_EPSILON:= 0.008
|
||||
|
||||
const UP_VECTOR:= Vector3(0.0, 0.0, 1.0)
|
||||
const RIGHT_VECTOR:= Vector3(0.0, 1.0, 0.0)
|
||||
const FORWARD_VECTOR:= Vector3(1.0, 0.0, 0.0)
|
||||
|
||||
var map_data: FuncGodotMapData
|
||||
|
||||
var wind_entity_idx: int = 0
|
||||
var wind_brush_idx: int = 0
|
||||
var wind_face_idx: int = 0
|
||||
var wind_face_center: Vector3
|
||||
var wind_face_basis: Vector3
|
||||
var wind_face_normal: Vector3
|
||||
|
||||
func _init(in_map_data: FuncGodotMapData) -> void:
|
||||
map_data = in_map_data
|
||||
|
||||
func sort_vertices_by_winding(a: FuncGodotMapData.FuncGodotFaceVertex, b: FuncGodotMapData.FuncGodotFaceVertex) -> bool:
|
||||
var face: FuncGodotMapData.FuncGodotFace = map_data.entities[wind_entity_idx].brushes[wind_brush_idx].faces[wind_face_idx]
|
||||
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = map_data.entity_geo[wind_entity_idx].brushes[wind_brush_idx].faces[wind_face_idx]
|
||||
|
||||
var u: Vector3 = wind_face_basis.normalized()
|
||||
var v: Vector3 = u.cross(wind_face_normal).normalized()
|
||||
|
||||
var loc_a: Vector3 = a.vertex - wind_face_center
|
||||
var a_pu: float = loc_a.dot(u)
|
||||
var a_pv: float = loc_a.dot(v)
|
||||
|
||||
var loc_b: Vector3 = b.vertex - wind_face_center
|
||||
var b_pu: float = loc_b.dot(u)
|
||||
var b_pv: float = loc_b.dot(v)
|
||||
|
||||
var a_angle: float = atan2(a_pv, a_pu)
|
||||
var b_angle: float = atan2(b_pv, b_pu)
|
||||
|
||||
return a_angle < b_angle
|
||||
|
||||
# returns null if no intersection, else intersection vertex.
|
||||
func intersect_face(f0: FuncGodotMapData.FuncGodotFace, f1: FuncGodotMapData.FuncGodotFace, f2: FuncGodotMapData.FuncGodotFace) -> Variant:
|
||||
var n0:= f0.plane_normal
|
||||
var n1:= f1.plane_normal
|
||||
var n2:= f2.plane_normal
|
||||
var denom: float = n0.cross(n1).dot(n2)
|
||||
if denom < 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 generate_brush_vertices(entity_idx: int, brush_idx: int) -> void:
|
||||
var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[entity_idx]
|
||||
var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[brush_idx]
|
||||
var face_count: int = brush.faces.size()
|
||||
|
||||
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[entity_idx]
|
||||
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[brush_idx]
|
||||
|
||||
var phong: bool = entity.properties.get("_phong", "0") == "1"
|
||||
var phong_angle_str: String = entity.properties.get("_phong_angle", "89")
|
||||
var phong_angle: float = float(phong_angle_str) if phong_angle_str.is_valid_float() else 89.0
|
||||
|
||||
for f0 in range(face_count):
|
||||
var face: FuncGodotMapData.FuncGodotFace = brush.faces[f0]
|
||||
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f0]
|
||||
var texture: FuncGodotMapData.FuncGodotTextureData = map_data.textures[face.texture_idx]
|
||||
|
||||
for f1 in range(face_count):
|
||||
for f2 in range(face_count):
|
||||
var vertex = intersect_face(brush.faces[f0], brush.faces[f1], brush.faces[f2])
|
||||
if not vertex is Vector3:
|
||||
continue
|
||||
if not vertex_in_hull(brush.faces, vertex):
|
||||
continue
|
||||
|
||||
var merged: bool = false
|
||||
for f3 in range(f0):
|
||||
var other_face_geo : FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f3]
|
||||
for i in range(len(other_face_geo.vertices)):
|
||||
if other_face_geo.vertices[i].vertex.distance_to(vertex) < CMP_EPSILON:
|
||||
vertex = other_face_geo.vertices[i].vertex
|
||||
merged = true;
|
||||
break
|
||||
|
||||
if merged:
|
||||
break
|
||||
|
||||
var normal: Vector3 = face.plane_normal
|
||||
if phong:
|
||||
var threshold:= cos((phong_angle + 0.01) * 0.0174533)
|
||||
if face.plane_normal.dot(brush.faces[f1].plane_normal) > threshold:
|
||||
normal += brush.faces[f1].plane_normal
|
||||
if face.plane_normal.dot(brush.faces[f2].plane_normal) > threshold:
|
||||
normal += brush.faces[f2].plane_normal
|
||||
normal = normal.normalized()
|
||||
|
||||
var uv: Vector2
|
||||
var tangent: Vector4
|
||||
if face.is_valve_uv:
|
||||
uv = get_valve_uv(vertex, face, texture.width, texture.height)
|
||||
tangent = get_valve_tangent(face)
|
||||
else:
|
||||
uv = get_standard_uv(vertex, face, texture.width, texture.height)
|
||||
tangent = get_standard_tangent(face)
|
||||
|
||||
# Check for a duplicate vertex in the current face.
|
||||
var duplicate_idx: int = -1
|
||||
for i in range(face_geo.vertices.size()):
|
||||
if face_geo.vertices[i].vertex == vertex:
|
||||
duplicate_idx = i
|
||||
break
|
||||
|
||||
if duplicate_idx < 0:
|
||||
var new_face_vert:= FuncGodotMapData.FuncGodotFaceVertex.new()
|
||||
new_face_vert.vertex = vertex
|
||||
new_face_vert.normal = normal
|
||||
new_face_vert.tangent = tangent
|
||||
new_face_vert.uv = uv
|
||||
face_geo.vertices.append(new_face_vert)
|
||||
elif phong:
|
||||
face_geo.vertices[duplicate_idx].normal += normal
|
||||
|
||||
# maybe optimisable?
|
||||
for face_geo in brush_geo.faces:
|
||||
for i in range(face_geo.vertices.size()):
|
||||
face_geo.vertices[i].normal = face_geo.vertices[i].normal.normalized()
|
||||
|
||||
func run() -> void:
|
||||
map_data.entity_geo.resize(map_data.entities.size())
|
||||
for i in range(map_data.entity_geo.size()):
|
||||
map_data.entity_geo[i] = FuncGodotMapData.FuncGodotEntityGeometry.new()
|
||||
|
||||
for e in range(map_data.entities.size()):
|
||||
var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[e]
|
||||
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e]
|
||||
entity_geo.brushes.resize(entity.brushes.size())
|
||||
for i in range(entity_geo.brushes.size()):
|
||||
entity_geo.brushes[i] = FuncGodotMapData.FuncGodotBrushGeometry.new()
|
||||
|
||||
for b in range(entity.brushes.size()):
|
||||
var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[b]
|
||||
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[b]
|
||||
brush_geo.faces.resize(brush.faces.size())
|
||||
for i in range(brush_geo.faces.size()):
|
||||
brush_geo.faces[i] = FuncGodotMapData.FuncGodotFaceGeometry.new()
|
||||
|
||||
var generate_vertices_task = func(e):
|
||||
var entity: FuncGodotMapData.FuncGodotEntity = map_data.entities[e]
|
||||
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e]
|
||||
entity.center = Vector3.ZERO
|
||||
|
||||
for b in range(entity.brushes.size()):
|
||||
var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[b]
|
||||
brush.center = Vector3.ZERO
|
||||
var vert_count: int = 0
|
||||
|
||||
generate_brush_vertices(e, b)
|
||||
|
||||
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = map_data.entity_geo[e].brushes[b]
|
||||
for face in brush_geo.faces:
|
||||
for vert in face.vertices:
|
||||
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: FuncGodotMapData.FuncGodotEntity = map_data.entities[e]
|
||||
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e]
|
||||
|
||||
for b in range(entity.brushes.size()):
|
||||
var brush: FuncGodotMapData.FuncGodotBrush = entity.brushes[b]
|
||||
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[b]
|
||||
|
||||
for f in range(brush.faces.size()):
|
||||
var face: FuncGodotMapData.FuncGodotFace = brush.faces[f]
|
||||
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f]
|
||||
|
||||
if face_geo.vertices.size() < 3:
|
||||
continue
|
||||
|
||||
wind_entity_idx = e
|
||||
wind_brush_idx = b
|
||||
wind_face_idx = f
|
||||
|
||||
wind_face_basis = face_geo.vertices[1].vertex - face_geo.vertices[0].vertex
|
||||
wind_face_center = Vector3.ZERO
|
||||
wind_face_normal = face.plane_normal
|
||||
|
||||
for v in face_geo.vertices:
|
||||
wind_face_center += v.vertex
|
||||
|
||||
wind_face_center /= face_geo.vertices.size()
|
||||
|
||||
face_geo.vertices.sort_custom(sort_vertices_by_winding)
|
||||
wind_entity_idx = 0
|
||||
|
||||
# index face vertices
|
||||
var index_faces_task:= func(e):
|
||||
var entity_geo: FuncGodotMapData.FuncGodotEntityGeometry = map_data.entity_geo[e]
|
||||
|
||||
for b in range(entity_geo.brushes.size()):
|
||||
var brush_geo: FuncGodotMapData.FuncGodotBrushGeometry = entity_geo.brushes[b]
|
||||
|
||||
for f in range(brush_geo.faces.size()):
|
||||
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f]
|
||||
|
||||
if face_geo.vertices.size() < 3:
|
||||
continue
|
||||
|
||||
var i_count: int = 0
|
||||
face_geo.indicies.resize((face_geo.vertices.size() - 2) * 3)
|
||||
for i in range(face_geo.vertices.size() - 2):
|
||||
face_geo.indicies[i_count] = 0
|
||||
face_geo.indicies[i_count + 1] = i + 1
|
||||
face_geo.indicies[i_count + 2] = i + 2
|
||||
i_count += 3
|
||||
|
||||
var index_faces_task_id:= WorkerThreadPool.add_group_task(index_faces_task, map_data.entities.size(), 4, true)
|
||||
WorkerThreadPool.wait_for_group_task_completion(index_faces_task_id)
|
135
addons/func_godot/src/core/func_godot_map_data.gd
Normal file
@ -0,0 +1,135 @@
|
||||
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
|
326
addons/func_godot/src/core/func_godot_map_parser.gd
Normal file
@ -0,0 +1,326 @@
|
||||
class_name FuncGodotMapParser extends RefCounted
|
||||
|
||||
var scope:= FuncGodotMapParser.ParseScope.FILE
|
||||
var comment: bool = false
|
||||
var entity_idx: int = -1
|
||||
var brush_idx: int = -1
|
||||
var face_idx: int = -1
|
||||
var component_idx: int = 0
|
||||
var prop_key: String = ""
|
||||
var current_property: String = ""
|
||||
var valve_uvs: bool = false
|
||||
|
||||
var current_face: FuncGodotMapData.FuncGodotFace
|
||||
var current_brush: FuncGodotMapData.FuncGodotBrush
|
||||
var current_entity: FuncGodotMapData.FuncGodotEntity
|
||||
|
||||
var map_data: FuncGodotMapData
|
||||
var _keep_tb_groups: bool = false
|
||||
|
||||
func _init(in_map_data: FuncGodotMapData) -> void:
|
||||
map_data = in_map_data
|
||||
|
||||
func load_map(map_file: String, keep_tb_groups: bool) -> bool:
|
||||
current_face = FuncGodotMapData.FuncGodotFace.new()
|
||||
current_brush = FuncGodotMapData.FuncGodotBrush.new()
|
||||
current_entity = FuncGodotMapData.FuncGodotEntity.new()
|
||||
|
||||
scope = FuncGodotMapParser.ParseScope.FILE
|
||||
comment = false
|
||||
entity_idx = -1
|
||||
brush_idx = -1
|
||||
face_idx = -1
|
||||
component_idx = 0
|
||||
valve_uvs = false
|
||||
_keep_tb_groups = keep_tb_groups
|
||||
|
||||
var lines: PackedStringArray = []
|
||||
|
||||
var map: FileAccess = FileAccess.open(map_file, FileAccess.READ)
|
||||
|
||||
if map == null:
|
||||
printerr("Error: Failed to open map file (" + map_file + ")")
|
||||
return false
|
||||
|
||||
if map_file.ends_with(".import"):
|
||||
while not map.eof_reached():
|
||||
var line: String = map.get_line()
|
||||
if line.begins_with("path"):
|
||||
map.close()
|
||||
line = line.replace("path=", "");
|
||||
line = line.replace('"', '')
|
||||
var map_data: String = (load(line) as QuakeMapFile).map_data
|
||||
if map_data.is_empty():
|
||||
printerr("Error: Failed to open map file (" + line + ")")
|
||||
return false
|
||||
lines = map_data.split("\n")
|
||||
break
|
||||
else:
|
||||
while not map.eof_reached():
|
||||
var line: String = map.get_line()
|
||||
lines.append(line)
|
||||
|
||||
for line in lines:
|
||||
if comment:
|
||||
comment = false
|
||||
var tokens := split_string(line, [" ", "\t"], true)
|
||||
for s in tokens:
|
||||
token(s)
|
||||
|
||||
return true
|
||||
|
||||
func split_string(s: String, delimeters: Array[String], allow_empty: bool = true) -> Array[String]:
|
||||
var parts: Array[String] = []
|
||||
|
||||
var start := 0
|
||||
var i := 0
|
||||
|
||||
while i < s.length():
|
||||
if s[i] in delimeters:
|
||||
if allow_empty or start < i:
|
||||
parts.push_back(s.substr(start, i - start))
|
||||
start = i + 1
|
||||
i += 1
|
||||
|
||||
if allow_empty or start < i:
|
||||
parts.push_back(s.substr(start, i - start))
|
||||
|
||||
return parts
|
||||
|
||||
func set_scope(new_scope: FuncGodotMapParser.ParseScope) -> void:
|
||||
"""
|
||||
match new_scope:
|
||||
ParseScope.FILE:
|
||||
print("Switching to file scope.")
|
||||
ParseScope.ENTITY:
|
||||
print("Switching to entity " + str(entity_idx) + "scope")
|
||||
ParseScope.PROPERTY_VALUE:
|
||||
print("Switching to property value scope")
|
||||
ParseScope.BRUSH:
|
||||
print("Switching to brush " + str(brush_idx) + " scope")
|
||||
ParseScope.PLANE_0:
|
||||
print("Switching to face " + str(face_idx) + " plane 0 scope")
|
||||
ParseScope.PLANE_1:
|
||||
print("Switching to face " + str(face_idx) + " plane 1 scope")
|
||||
ParseScope.PLANE_2:
|
||||
print("Switching to face " + str(face_idx) + " plane 2 scope")
|
||||
ParseScope.TEXTURE:
|
||||
print("Switching to texture scope")
|
||||
ParseScope.U:
|
||||
print("Switching to U scope")
|
||||
ParseScope.V:
|
||||
print("Switching to V scope")
|
||||
ParseScope.VALVE_U:
|
||||
print("Switching to Valve U scope")
|
||||
ParseScope.VALVE_V:
|
||||
print("Switching to Valve V scope")
|
||||
ParseScope.ROT:
|
||||
print("Switching to rotation scope")
|
||||
ParseScope.U_SCALE:
|
||||
print("Switching to U scale scope")
|
||||
ParseScope.V_SCALE:
|
||||
print("Switching to V scale scope")
|
||||
"""
|
||||
scope = new_scope
|
||||
|
||||
func token(buf_str: String) -> void:
|
||||
if comment:
|
||||
return
|
||||
elif buf_str == "//":
|
||||
comment = true
|
||||
return
|
||||
|
||||
match scope:
|
||||
FuncGodotMapParser.ParseScope.FILE:
|
||||
if buf_str == "{":
|
||||
entity_idx += 1
|
||||
brush_idx = -1
|
||||
set_scope(FuncGodotMapParser.ParseScope.ENTITY)
|
||||
FuncGodotMapParser.ParseScope.ENTITY:
|
||||
if buf_str.begins_with('"'):
|
||||
prop_key = buf_str.substr(1)
|
||||
if prop_key.ends_with('"'):
|
||||
prop_key = prop_key.left(-1)
|
||||
set_scope(FuncGodotMapParser.ParseScope.PROPERTY_VALUE)
|
||||
elif buf_str == "{":
|
||||
brush_idx += 1
|
||||
face_idx = -1
|
||||
set_scope(FuncGodotMapParser.ParseScope.BRUSH)
|
||||
elif buf_str == "}":
|
||||
commit_entity()
|
||||
set_scope(FuncGodotMapParser.ParseScope.FILE)
|
||||
FuncGodotMapParser.ParseScope.PROPERTY_VALUE:
|
||||
var is_first = buf_str[0] == '"'
|
||||
var is_last = buf_str.right(1) == '"'
|
||||
|
||||
if is_first:
|
||||
if current_property != "":
|
||||
current_property = ""
|
||||
|
||||
if not is_last:
|
||||
current_property += buf_str + " "
|
||||
else:
|
||||
current_property += buf_str
|
||||
|
||||
if is_last:
|
||||
current_entity.properties[prop_key] = current_property.substr(1, len(current_property) - 2)
|
||||
set_scope(FuncGodotMapParser.ParseScope.ENTITY)
|
||||
FuncGodotMapParser.ParseScope.BRUSH:
|
||||
if buf_str == "(":
|
||||
face_idx += 1
|
||||
component_idx = 0
|
||||
set_scope(FuncGodotMapParser.ParseScope.PLANE_0)
|
||||
elif buf_str == "}":
|
||||
commit_brush()
|
||||
set_scope(FuncGodotMapParser.ParseScope.ENTITY)
|
||||
FuncGodotMapParser.ParseScope.PLANE_0:
|
||||
if buf_str == ")":
|
||||
component_idx = 0
|
||||
set_scope(FuncGodotMapParser.ParseScope.PLANE_1)
|
||||
else:
|
||||
match component_idx:
|
||||
0:
|
||||
current_face.plane_points.v0.x = float(buf_str)
|
||||
1:
|
||||
current_face.plane_points.v0.y = float(buf_str)
|
||||
2:
|
||||
current_face.plane_points.v0.z = float(buf_str)
|
||||
|
||||
component_idx += 1
|
||||
FuncGodotMapParser.ParseScope.PLANE_1:
|
||||
if buf_str != "(":
|
||||
if buf_str == ")":
|
||||
component_idx = 0
|
||||
set_scope(FuncGodotMapParser.ParseScope.PLANE_2)
|
||||
else:
|
||||
match component_idx:
|
||||
0:
|
||||
current_face.plane_points.v1.x = float(buf_str)
|
||||
1:
|
||||
current_face.plane_points.v1.y = float(buf_str)
|
||||
2:
|
||||
current_face.plane_points.v1.z = float(buf_str)
|
||||
|
||||
component_idx += 1
|
||||
FuncGodotMapParser.ParseScope.PLANE_2:
|
||||
if buf_str != "(":
|
||||
if buf_str == ")":
|
||||
component_idx = 0
|
||||
set_scope(FuncGodotMapParser.ParseScope.TEXTURE)
|
||||
else:
|
||||
match component_idx:
|
||||
0:
|
||||
current_face.plane_points.v2.x = float(buf_str)
|
||||
1:
|
||||
current_face.plane_points.v2.y = float(buf_str)
|
||||
2:
|
||||
current_face.plane_points.v2.z = float(buf_str)
|
||||
|
||||
component_idx += 1
|
||||
FuncGodotMapParser.ParseScope.TEXTURE:
|
||||
current_face.texture_idx = map_data.register_texture(buf_str)
|
||||
set_scope(FuncGodotMapParser.ParseScope.U)
|
||||
FuncGodotMapParser.ParseScope.U:
|
||||
if buf_str == "[":
|
||||
valve_uvs = true
|
||||
component_idx = 0
|
||||
set_scope(FuncGodotMapParser.ParseScope.VALVE_U)
|
||||
else:
|
||||
valve_uvs = false
|
||||
current_face.uv_standard.x = float(buf_str)
|
||||
set_scope(FuncGodotMapParser.ParseScope.V)
|
||||
FuncGodotMapParser.ParseScope.V:
|
||||
current_face.uv_standard.y = float(buf_str)
|
||||
set_scope(FuncGodotMapParser.ParseScope.ROT)
|
||||
FuncGodotMapParser.ParseScope.VALVE_U:
|
||||
if buf_str == "]":
|
||||
component_idx = 0
|
||||
set_scope(FuncGodotMapParser.ParseScope.VALVE_V)
|
||||
else:
|
||||
match component_idx:
|
||||
0:
|
||||
current_face.uv_valve.u.axis.x = float(buf_str)
|
||||
1:
|
||||
current_face.uv_valve.u.axis.y = float(buf_str)
|
||||
2:
|
||||
current_face.uv_valve.u.axis.z = float(buf_str)
|
||||
3:
|
||||
current_face.uv_valve.u.offset = float(buf_str)
|
||||
|
||||
component_idx += 1
|
||||
FuncGodotMapParser.ParseScope.VALVE_V:
|
||||
if buf_str != "[":
|
||||
if buf_str == "]":
|
||||
set_scope(FuncGodotMapParser.ParseScope.ROT)
|
||||
else:
|
||||
match component_idx:
|
||||
0:
|
||||
current_face.uv_valve.v.axis.x = float(buf_str)
|
||||
1:
|
||||
current_face.uv_valve.v.axis.y = float(buf_str)
|
||||
2:
|
||||
current_face.uv_valve.v.axis.z = float(buf_str)
|
||||
3:
|
||||
current_face.uv_valve.v.offset = float(buf_str)
|
||||
|
||||
component_idx += 1
|
||||
FuncGodotMapParser.ParseScope.ROT:
|
||||
current_face.uv_extra.rot = float(buf_str)
|
||||
set_scope(FuncGodotMapParser.ParseScope.U_SCALE)
|
||||
FuncGodotMapParser.ParseScope.U_SCALE:
|
||||
current_face.uv_extra.scale_x = float(buf_str)
|
||||
set_scope(FuncGodotMapParser.ParseScope.V_SCALE)
|
||||
FuncGodotMapParser.ParseScope.V_SCALE:
|
||||
current_face.uv_extra.scale_y = float(buf_str)
|
||||
commit_face()
|
||||
set_scope(FuncGodotMapParser.ParseScope.BRUSH)
|
||||
|
||||
func commit_entity() -> void:
|
||||
if current_entity.properties.has('_tb_type') and map_data.entities.size() > 0:
|
||||
map_data.entities[0].brushes.append_array(current_entity.brushes)
|
||||
current_entity.brushes.clear()
|
||||
if !_keep_tb_groups:
|
||||
current_entity = FuncGodotMapData.FuncGodotEntity.new()
|
||||
return
|
||||
|
||||
var new_entity:= FuncGodotMapData.FuncGodotEntity.new()
|
||||
new_entity.spawn_type = FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY
|
||||
new_entity.properties = current_entity.properties
|
||||
new_entity.brushes = current_entity.brushes
|
||||
map_data.entities.append(new_entity)
|
||||
|
||||
current_entity = FuncGodotMapData.FuncGodotEntity.new()
|
||||
|
||||
func commit_brush() -> void:
|
||||
current_entity.brushes.append(current_brush)
|
||||
current_brush = FuncGodotMapData.FuncGodotBrush.new()
|
||||
|
||||
func commit_face() -> void:
|
||||
var v0v1: Vector3 = current_face.plane_points.v1 - current_face.plane_points.v0
|
||||
var v1v2: Vector3 = current_face.plane_points.v2 - current_face.plane_points.v1
|
||||
current_face.plane_normal = v1v2.cross(v0v1).normalized()
|
||||
current_face.plane_dist = current_face.plane_normal.dot(current_face.plane_points.v0)
|
||||
current_face.is_valve_uv = valve_uvs
|
||||
|
||||
current_brush.faces.append(current_face)
|
||||
current_face = FuncGodotMapData.FuncGodotFace.new()
|
||||
|
||||
# Nested
|
||||
enum ParseScope{
|
||||
FILE,
|
||||
COMMENT,
|
||||
ENTITY,
|
||||
PROPERTY_VALUE,
|
||||
BRUSH,
|
||||
PLANE_0,
|
||||
PLANE_1,
|
||||
PLANE_2,
|
||||
TEXTURE,
|
||||
U,
|
||||
V,
|
||||
VALVE_U,
|
||||
VALVE_V,
|
||||
ROT,
|
||||
U_SCALE,
|
||||
V_SCALE
|
||||
}
|
119
addons/func_godot/src/core/func_godot_surface_gatherer.gd
Normal file
@ -0,0 +1,119 @@
|
||||
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_face(entity_idx: int, brush_idx: int, face_idx: int) -> bool:
|
||||
var face: FuncGodotMapData.FuncGodotFace = map_data.entities[entity_idx].brushes[brush_idx].faces[face_idx]
|
||||
var face_geo: FuncGodotMapData.FuncGodotFaceGeometry = map_data.entity_geo[entity_idx].brushes[brush_idx].faces[face_idx]
|
||||
|
||||
if face_geo.vertices.size() < 3:
|
||||
return true
|
||||
|
||||
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()):
|
||||
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: FuncGodotMapData.FuncGodotFaceGeometry = brush_geo.faces[f]
|
||||
|
||||
if filter_face(e, b, f):
|
||||
continue
|
||||
|
||||
for v in range(face_geo.vertices.size()):
|
||||
var vert: FuncGodotMapData.FuncGodotFaceVertex = face_geo.vertices[v].duplicate()
|
||||
|
||||
if entity.spawn_type == FuncGodotMapData.FuncGodotEntitySpawnType.ENTITY:
|
||||
vert.vertex -= entity.center
|
||||
|
||||
surf.vertices.append(vert)
|
||||
|
||||
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
|
||||
}
|
7
addons/func_godot/src/fgd/func_godot_fgd_base_class.gd
Normal file
@ -0,0 +1,7 @@
|
||||
@tool
|
||||
## Special inheritance class for [FuncGodotFGDSolidClass] and [FuncGodotFGDPointClass] entity definitions. Useful for adding shared or common properties and descriptions.
|
||||
class_name FuncGodotFGDBaseClass
|
||||
extends FuncGodotFGDEntityClass
|
||||
|
||||
func _init() -> void:
|
||||
prefix = "@BaseClass"
|
209
addons/func_godot/src/fgd/func_godot_fgd_entity_class.gd
Normal file
@ -0,0 +1,209 @@
|
||||
@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
|
||||
|
||||
var prefix: String = ""
|
||||
|
||||
@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.
|
||||
@export var classname : String = ""
|
||||
|
||||
## Entity description that appears in the map editor. Not required.
|
||||
@export_multiline var description : String = ""
|
||||
|
||||
## Entity does not get written to the exported FGD. Entity is only used for [FuncGodotMap] build process.
|
||||
@export var func_godot_internal : bool = false
|
||||
|
||||
## FuncGodotFGDBaseClass resources to inherit [member class_properties] and [member class_descriptions] from.
|
||||
@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.
|
||||
@export var class_properties : Dictionary = {}
|
||||
|
||||
## Descriptions for previously defined key value pair properties.
|
||||
@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.
|
||||
@export var meta_properties : Dictionary = {
|
||||
"size": AABB(Vector3(-8, -8, -8), Vector3(8, 8, 8)),
|
||||
"color": Color(0.8, 0.8, 0.8)
|
||||
}
|
||||
|
||||
@export_group("Node Generation")
|
||||
|
||||
## Node to generate on map build. This can be a built-in Godot class or a GDExtension class. For Point Class entities that use Scene File instantiation leave this blank.
|
||||
@export var node_class := ""
|
||||
|
||||
## Class property to use in naming the generated node. Overrides `name_property` in [FuncGodotMapSettings].
|
||||
## Naming occurs before adding to the [SceneTree] and applying properties.
|
||||
## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior.
|
||||
@export var name_property := ""
|
||||
|
||||
func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
|
||||
# Class prefix
|
||||
var res : String = prefix
|
||||
|
||||
# Meta properties
|
||||
var base_str = ""
|
||||
var meta_props = meta_properties.duplicate()
|
||||
|
||||
for base_class in base_classes:
|
||||
if not 'classname' in base_class:
|
||||
continue
|
||||
|
||||
base_str += base_class.classname
|
||||
|
||||
if base_class != base_classes.back():
|
||||
base_str += ", "
|
||||
|
||||
if base_str != "":
|
||||
meta_props['base'] = base_str
|
||||
|
||||
for prop in meta_props:
|
||||
if prefix == '@SolidClass':
|
||||
if prop == "size" or prop == "model":
|
||||
continue
|
||||
|
||||
if prop == 'model' and target_editor != FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM:
|
||||
continue
|
||||
|
||||
var value = meta_props[prop]
|
||||
res += " " + prop + "("
|
||||
|
||||
if value is AABB:
|
||||
res += "%s %s %s, %s %s %s" % [
|
||||
value.position.x,
|
||||
value.position.y,
|
||||
value.position.z,
|
||||
value.size.x,
|
||||
value.size.y,
|
||||
value.size.z
|
||||
]
|
||||
elif value is Color:
|
||||
res += "%s %s %s" % [
|
||||
value.r8,
|
||||
value.g8,
|
||||
value.b8
|
||||
]
|
||||
elif value is String:
|
||||
res += value
|
||||
|
||||
res += ")"
|
||||
|
||||
res += " = " + classname
|
||||
|
||||
if prefix != "@BaseClass": # having a description in BaseClasses crashes some editors
|
||||
var normalized_description = description.replace("\n", " ").strip_edges() if prefix != "@BaseClass" else ""
|
||||
if normalized_description != "":
|
||||
res += " : \"%s\" " % [normalized_description]
|
||||
else: # Having no description crashes some editors
|
||||
res += " : \"" + classname + "\" "
|
||||
|
||||
if class_properties.size() > 0:
|
||||
res += FuncGodotUtil.newline() + "[" + FuncGodotUtil.newline()
|
||||
else:
|
||||
res += "["
|
||||
|
||||
# Class properties
|
||||
for prop in class_properties:
|
||||
var value = class_properties[prop]
|
||||
var prop_val = null
|
||||
var prop_type := ""
|
||||
var prop_description: String
|
||||
if prop in class_property_descriptions:
|
||||
# Optional default value for Choices can be set up as [String, int]
|
||||
if value is Dictionary and class_property_descriptions[prop] is Array:
|
||||
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):
|
||||
prop_description = "\"" + prop_arr[0] + "\" : " + str(prop_arr[1])
|
||||
else:
|
||||
prop_description = "\"\" : 0"
|
||||
printerr(str(prop) + " has incorrect description format. Should be [String description, int / String default value].")
|
||||
else:
|
||||
prop_description = "\"" + class_property_descriptions[prop] + "\""
|
||||
else:
|
||||
prop_description = "\"\""
|
||||
|
||||
match typeof(value):
|
||||
TYPE_INT:
|
||||
prop_type = "integer"
|
||||
prop_val = str(value)
|
||||
TYPE_FLOAT:
|
||||
prop_type = "float"
|
||||
prop_val = "\"" + str(value) + "\""
|
||||
TYPE_STRING:
|
||||
prop_type = "string"
|
||||
prop_val = "\"" + value + "\""
|
||||
TYPE_BOOL:
|
||||
prop_type = "choices"
|
||||
prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline()
|
||||
prop_val += "\t\t" + str(0) + " : \"No\"" + FuncGodotUtil.newline()
|
||||
prop_val += "\t\t" + str(1) + " : \"Yes\"" + FuncGodotUtil.newline()
|
||||
prop_val += "\t]"
|
||||
TYPE_VECTOR2, TYPE_VECTOR2I:
|
||||
prop_type = "string"
|
||||
prop_val = "\"%s %s\"" % [value.x, value.y]
|
||||
TYPE_VECTOR3, TYPE_VECTOR3I:
|
||||
prop_type = "string"
|
||||
prop_val = "\"%s %s %s\"" % [value.x, value.y, value.z]
|
||||
TYPE_VECTOR4, TYPE_VECTOR4I:
|
||||
prop_type = "string"
|
||||
prop_val = "\"%s %s %s %s\"" % [value[0], value[1], value[2], value[3]]
|
||||
TYPE_COLOR:
|
||||
prop_type = "color255"
|
||||
prop_val = "\"%s %s %s\"" % [value.r8, value.g8, value.b8]
|
||||
TYPE_DICTIONARY:
|
||||
prop_type = "choices"
|
||||
prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline()
|
||||
for choice in value:
|
||||
var choice_val = value[choice]
|
||||
prop_val += "\t\t" + str(choice_val) + " : \"" + choice + "\"" + FuncGodotUtil.newline()
|
||||
prop_val += "\t]"
|
||||
TYPE_ARRAY:
|
||||
prop_type = "flags"
|
||||
prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline()
|
||||
for arr_val in value:
|
||||
prop_val += "\t\t" + str(arr_val[1]) + " : \"" + str(arr_val[0]) + "\" : " + ("1" if arr_val[2] else "0") + FuncGodotUtil.newline()
|
||||
prop_val += "\t]"
|
||||
TYPE_NODE_PATH:
|
||||
prop_type = "target_destination"
|
||||
TYPE_OBJECT:
|
||||
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"
|
||||
|
||||
if prop_val:
|
||||
res += "\t"
|
||||
res += prop
|
||||
res += "("
|
||||
res += prop_type
|
||||
res += ")"
|
||||
|
||||
if not value is Array:
|
||||
if not value is Dictionary or prop_description != "":
|
||||
res += " : "
|
||||
res += prop_description
|
||||
|
||||
if value is bool or value is Dictionary or value is Array:
|
||||
res += " = "
|
||||
else:
|
||||
res += " : "
|
||||
|
||||
res += prop_val
|
||||
res += FuncGodotUtil.newline()
|
||||
|
||||
res += "]" + FuncGodotUtil.newline()
|
||||
|
||||
return res
|
164
addons/func_godot/src/fgd/func_godot_fgd_file.gd
Normal file
@ -0,0 +1,164 @@
|
||||
@tool
|
||||
@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
|
||||
|
||||
## 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.
|
||||
@export var export_file: bool:
|
||||
get:
|
||||
return export_file # TODO Converter40 Non existent get function
|
||||
set(new_export_file):
|
||||
if new_export_file != export_file:
|
||||
do_export_file(target_map_editor)
|
||||
|
||||
func do_export_file(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM, fgd_output_folder: String = "") -> void:
|
||||
if not Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
if fgd_output_folder.is_empty():
|
||||
fgd_output_folder = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.FGD_OUTPUT_FOLDER) as String
|
||||
if fgd_output_folder.is_empty():
|
||||
print("Skipping export: No game config folder")
|
||||
return
|
||||
|
||||
if fgd_name == "":
|
||||
print("Skipping export: Empty FGD name")
|
||||
|
||||
var fgd_file = fgd_output_folder + "/" + fgd_name + ".fgd"
|
||||
|
||||
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.close()
|
||||
|
||||
@export_group("Map Editor")
|
||||
|
||||
## Some map editors do not support the features found in others
|
||||
## (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.
|
||||
@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")
|
||||
|
||||
## FGD output filename without the extension.
|
||||
@export var fgd_name: String = "FuncGodot"
|
||||
|
||||
## Array of [FuncGodotFGDFile] resources to include in FGD file output. All of the entities included with these FuncGodotFGDFile resources will be prepended to the outputted FGD file.
|
||||
@export var base_fgd_files: Array[Resource] = []
|
||||
|
||||
## 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] = []
|
||||
|
||||
func build_class_text(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
|
||||
var res : String = ""
|
||||
|
||||
for base_fgd in base_fgd_files:
|
||||
if base_fgd is FuncGodotFGDFile:
|
||||
res += base_fgd.build_class_text(target_editor)
|
||||
|
||||
var entities = get_fgd_classes()
|
||||
for ent in entities:
|
||||
if not ent is FuncGodotFGDEntityClass:
|
||||
continue
|
||||
if ent.func_godot_internal:
|
||||
continue
|
||||
|
||||
var ent_text = ent.build_def_text(target_editor)
|
||||
res += ent_text
|
||||
if ent != entities[-1]:
|
||||
res += "\n"
|
||||
return res
|
||||
|
||||
## This getter does a little bit of validation. Providing only an array of non-null uniquely-named entity definitions
|
||||
func get_fgd_classes() -> Array:
|
||||
var res : Array = []
|
||||
for cur_ent_def_ind in range(entity_definitions.size()):
|
||||
var cur_ent_def = entity_definitions[cur_ent_def_ind]
|
||||
if cur_ent_def == null:
|
||||
continue
|
||||
elif not (cur_ent_def is FuncGodotFGDEntityClass):
|
||||
printerr("Bad value in entity definition set at position %s! Not an entity defintion." % cur_ent_def_ind)
|
||||
continue
|
||||
res.append(cur_ent_def)
|
||||
return res
|
||||
|
||||
func get_entity_definitions() -> Dictionary:
|
||||
var res : Dictionary = {}
|
||||
|
||||
for base_fgd in base_fgd_files:
|
||||
var fgd_res = base_fgd.get_entity_definitions()
|
||||
for key in fgd_res:
|
||||
res[key] = fgd_res[key]
|
||||
|
||||
for ent in get_fgd_classes():
|
||||
# Skip entities without classnames
|
||||
if ent.classname.replace(" ","") == "":
|
||||
printerr("Skipping " + ent.get_path() + ": Empty classname")
|
||||
continue
|
||||
|
||||
if ent is FuncGodotFGDPointClass or ent is FuncGodotFGDSolidClass:
|
||||
var entity_def = ent.duplicate()
|
||||
var meta_properties := {}
|
||||
var class_properties := {}
|
||||
var class_property_descriptions := {}
|
||||
|
||||
for base_class in _generate_base_class_list(entity_def):
|
||||
for meta_property in base_class.meta_properties:
|
||||
meta_properties[meta_property] = base_class.meta_properties[meta_property]
|
||||
|
||||
for class_property in base_class.class_properties:
|
||||
class_properties[class_property] = base_class.class_properties[class_property]
|
||||
|
||||
for class_property_desc in base_class.class_property_descriptions:
|
||||
class_property_descriptions[class_property_desc] = base_class.class_property_descriptions[class_property_desc]
|
||||
|
||||
for meta_property in entity_def.meta_properties:
|
||||
meta_properties[meta_property] = entity_def.meta_properties[meta_property]
|
||||
|
||||
for class_property in entity_def.class_properties:
|
||||
class_properties[class_property] = entity_def.class_properties[class_property]
|
||||
|
||||
for class_property_desc in entity_def.class_property_descriptions:
|
||||
class_property_descriptions[class_property_desc] = entity_def.class_property_descriptions[class_property_desc]
|
||||
|
||||
entity_def.meta_properties = meta_properties
|
||||
entity_def.class_properties = class_properties
|
||||
entity_def.class_property_descriptions = class_property_descriptions
|
||||
|
||||
res[ent.classname] = entity_def
|
||||
return res
|
||||
|
||||
func _generate_base_class_list(entity_def : Resource, visited_base_classes = []) -> Array:
|
||||
var base_classes : Array = []
|
||||
|
||||
visited_base_classes.append(entity_def.classname)
|
||||
|
||||
# End recursive search if no more base_classes
|
||||
if len(entity_def.base_classes) == 0:
|
||||
return base_classes
|
||||
|
||||
# Traverse up to the next level of hierarchy, if not already visited
|
||||
for base_class in entity_def.base_classes:
|
||||
if not base_class.classname in visited_base_classes:
|
||||
base_classes.append(base_class)
|
||||
base_classes += _generate_base_class_list(base_class, visited_base_classes)
|
||||
else:
|
||||
printerr(str("Entity '", entity_def.classname,"' contains cycle/duplicate to Entity '", base_class.classname, "'"))
|
||||
|
||||
return base_classes
|
162
addons/func_godot/src/fgd/func_godot_fgd_model_point_class.gd
Normal file
@ -0,0 +1,162 @@
|
||||
@tool
|
||||
## A special type of [FuncGodotFGDPointClass] entity that can automatically generate a special simplified GLB model file for the map editor display.
|
||||
## Only supported in map editors that support GLTF or GLB.
|
||||
class_name FuncGodotFGDModelPointClass
|
||||
extends FuncGodotFGDPointClass
|
||||
|
||||
enum TargetMapEditor {
|
||||
GENERIC,
|
||||
TRENCHBROOM
|
||||
}
|
||||
|
||||
## Determines how model interprets [member scale_expression].
|
||||
@export var target_map_editor: TargetMapEditor = TargetMapEditor.GENERIC
|
||||
## Display model export folder relative to the model folder set by [FuncGodotLocalConfig].
|
||||
@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.
|
||||
@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!
|
||||
@export var generate_size_property : bool = false
|
||||
## 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 :
|
||||
set(ignore):
|
||||
var path: String = _get_game_path().path_join(_get_model_folder())
|
||||
var error: Error = DirAccess.make_dir_recursive_absolute(path)
|
||||
if error != Error.OK:
|
||||
printerr("Failed creating dir for GDIgnore file", error)
|
||||
return
|
||||
path = path.path_join('.gdignore')
|
||||
if FileAccess.file_exists(path):
|
||||
return
|
||||
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
||||
file.store_string('')
|
||||
file.close()
|
||||
|
||||
func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
|
||||
_generate_model()
|
||||
return super()
|
||||
|
||||
func _generate_model() -> void:
|
||||
if not scene_file:
|
||||
return
|
||||
|
||||
var gltf_state := GLTFState.new()
|
||||
var path = _get_export_dir()
|
||||
var node = _get_node()
|
||||
if node == null: return
|
||||
if not _create_gltf_file(gltf_state, path, node):
|
||||
printerr("could not create gltf file")
|
||||
return
|
||||
node.queue_free()
|
||||
if target_map_editor == TargetMapEditor.TRENCHBROOM:
|
||||
const model_key: String = "model"
|
||||
if scale_expression.is_empty():
|
||||
meta_properties[model_key] = '"%s"' % _get_local_path()
|
||||
else:
|
||||
meta_properties[model_key] = '{"path": "%s", "scale": %s }' % [
|
||||
_get_local_path(),
|
||||
scale_expression
|
||||
]
|
||||
else:
|
||||
meta_properties["studio"] = '"%s"' % _get_local_path()
|
||||
|
||||
if generate_size_property:
|
||||
meta_properties["size"] = _generate_size_from_aabb(gltf_state.meshes)
|
||||
|
||||
func _get_node() -> Node3D:
|
||||
var node := scene_file.instantiate()
|
||||
if node is Node3D:
|
||||
return node as Node3D
|
||||
node.queue_free()
|
||||
printerr("Scene is not of type 'Node3D'")
|
||||
return null
|
||||
|
||||
func _get_export_dir() -> String:
|
||||
var work_dir: String = _get_game_path()
|
||||
var model_dir: String = _get_model_folder()
|
||||
return work_dir.path_join(model_dir).path_join('%s.glb' % classname)
|
||||
|
||||
func _get_local_path() -> String:
|
||||
return _get_model_folder().path_join('%s.glb' % classname)
|
||||
|
||||
func _get_model_folder() -> String:
|
||||
var model_dir: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.GAME_PATH_MODELS_FOLDER) as String
|
||||
if not models_sub_folder.is_empty():
|
||||
model_dir = model_dir.path_join(models_sub_folder)
|
||||
return model_dir
|
||||
|
||||
func _get_game_path() -> String:
|
||||
return FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.MAP_EDITOR_GAME_PATH) as String
|
||||
|
||||
func _create_gltf_file(gltf_state: GLTFState, path: String, node: Node3D) -> bool:
|
||||
var global_export_path = path
|
||||
var gltf_document := GLTFDocument.new()
|
||||
gltf_state.create_animations = false
|
||||
|
||||
node.rotate_y(deg_to_rad(-90))
|
||||
|
||||
# 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:
|
||||
var scale_factor: Vector3 = Vector3.ONE
|
||||
if scale_expression.is_empty():
|
||||
scale_factor *= FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.DEFAULT_INVERSE_SCALE) as float
|
||||
else:
|
||||
if scale_expression.begins_with('\''):
|
||||
var scale_arr := scale_expression.split_floats(' ', false)
|
||||
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()
|
||||
if scale_factor.length() == 0:
|
||||
scale_factor = Vector3.ONE # Don't let the node scale into oblivion!
|
||||
node.scale *= scale_factor
|
||||
|
||||
var error: Error = gltf_document.append_from_scene(node, gltf_state)
|
||||
if error != Error.OK:
|
||||
printerr("Failed appending to gltf document", error)
|
||||
return false
|
||||
|
||||
call_deferred("_save_to_file_system", gltf_document, gltf_state, global_export_path)
|
||||
return true
|
||||
|
||||
func _save_to_file_system(gltf_document: GLTFDocument, gltf_state: GLTFState, path: String) -> void:
|
||||
var error: Error = DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
||||
if error != Error.OK:
|
||||
printerr("Failed creating dir", error)
|
||||
return
|
||||
|
||||
error = gltf_document.write_to_filesystem(gltf_state, path)
|
||||
if error != OK:
|
||||
printerr("Failed writing to file system", error)
|
||||
return
|
||||
print('Exported model to ', path)
|
||||
|
||||
func _generate_size_from_aabb(meshes: Array[GLTFMesh]) -> AABB:
|
||||
var aabb := AABB()
|
||||
for mesh in meshes:
|
||||
aabb = aabb.merge(mesh.mesh.get_mesh().get_aabb())
|
||||
|
||||
# Reorient the AABB so it matches TrenchBroom's coordinate system
|
||||
var size_prop := AABB()
|
||||
size_prop.position = Vector3(aabb.position.z, aabb.position.x, aabb.position.y)
|
||||
size_prop.size = Vector3(aabb.size.z, aabb.size.x, aabb.size.y)
|
||||
|
||||
# Scale the size bounds to our scale factor
|
||||
# Scale factor will need to be set if we decide to auto-generate our bounds
|
||||
var scale_factor: Vector3 = Vector3.ONE
|
||||
if target_map_editor == TargetMapEditor.TRENCHBROOM:
|
||||
if scale_expression.begins_with('\''):
|
||||
var scale_arr := scale_expression.split_floats(' ', false)
|
||||
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.size *= scale_factor
|
||||
size_prop.size += size_prop.position
|
||||
# Round the size so it can stay on grid level 1 at least
|
||||
for i in 3:
|
||||
size_prop.position[i] = round(size_prop.position[i])
|
||||
size_prop.size[i] = round(size_prop.size[i])
|
||||
return size_prop
|
20
addons/func_godot/src/fgd/func_godot_fgd_point_class.gd
Normal file
@ -0,0 +1,20 @@
|
||||
@tool
|
||||
## FGD PointClass entity definition, used to define point entities.
|
||||
## 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
|
||||
|
||||
func _init() -> void:
|
||||
prefix = "@PointClass"
|
||||
|
||||
@export_group ("Scene")
|
||||
## An optional scene file to instantiate on map build. Overrides `node_class` and `script_class`.
|
||||
@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 var script_class: Script
|
||||
|
||||
@export_group("Build")
|
||||
## Toggles whether entity will use `angles`, `mangle`, or `angle` to determine rotations on [FuncGodotMap] build, prioritizing the key value pairs in that order. Set to `false` if you would like to define how the generated node is rotated yourself.
|
||||
@export var apply_rotation_on_map_build : bool = true
|
59
addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd
Normal file
@ -0,0 +1,59 @@
|
||||
@tool
|
||||
## FGD SolidClass entity definition, used to define brush entities.
|
||||
## 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
|
||||
|
||||
enum SpawnType {
|
||||
WORLDSPAWN = 0, ## Is worldspawn
|
||||
MERGE_WORLDSPAWN = 1, ## Should be combined with worldspawn
|
||||
ENTITY = 2, ## Is its own separate entity
|
||||
}
|
||||
|
||||
enum OriginType {
|
||||
IGNORE = 0, ## Ignore origin property and only use averaged brush vertices for positioning. Standard Quake 1 / Half-Life behavior.
|
||||
ABSOLUTE = 1, ## Use origin property for position center, ignoring brush vertice positions.
|
||||
RELATIVE = 2 ## Use origin relative to averaged brush vertice positions. Use this setting if brush entity vertices have coordinates local to the origin.
|
||||
}
|
||||
|
||||
enum CollisionShapeType {
|
||||
NONE, ## No collision shape is built. Useful for decorative geometry like vines, hanging wires, grass, etc...
|
||||
CONVEX, ## Will build a Convex CollisionShape3D for each brush used to make this Solid Class. Required for non-[StaticBody3D] nodes like [Area3D].
|
||||
CONCAVE ## Should have a concave collision shape
|
||||
}
|
||||
|
||||
## 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
|
||||
## Controls how this Solid Class utilizes the `origin` key value pair to find its position.
|
||||
@export var origin_type: OriginType = OriginType.IGNORE
|
||||
|
||||
@export_group("Visual Build")
|
||||
## Controls whether a [MeshInstance3D] is built for this Solid Class.
|
||||
@export var build_visuals : bool = true
|
||||
## Sets generated [MeshInstance3D] to be available for UV2 unwrapping after [FuncGodotMap] build. Utilized in baked lightmapping.
|
||||
@export var use_in_baked_light : bool = true
|
||||
## Shadow casting setting allows for further lightmapping customization.
|
||||
@export var shadow_casting_setting : GeometryInstance3D.ShadowCastingSetting = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||
## Automatically build [OccluderInstance3D] for this entity.
|
||||
@export var build_occlusion : bool = false
|
||||
## This Solid Class' [MeshInstance3D] will only be visible for [Camera3D]s whose cull mask includes any of these render layers.
|
||||
@export_flags_3d_render var render_layers: int = 1
|
||||
|
||||
@export_group("Collision Build")
|
||||
## Controls how collisions are built for this Solid Class.
|
||||
@export var collision_shape_type: CollisionShapeType = CollisionShapeType.CONVEX
|
||||
## The physics layers this Solid Class can be detected in.
|
||||
@export_flags_3d_physics var collision_layer: int = 1
|
||||
## The physics layers this Solid Class scans.
|
||||
@export_flags_3d_physics var collision_mask: int = 1
|
||||
## The priority used to solve colliding when penetration occurs. The higher the priority is, the lower the penetration into the Solid Class will be. This can for example be used to prevent the player from breaking through the boundaries of a level.
|
||||
@export var collision_priority: float = 1.0
|
||||
## 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_group("Scripting")
|
||||
## An optional script file to attach to the node generated on map build.
|
||||
@export var script_class: Script
|
||||
|
||||
func _init():
|
||||
prefix = "@SolidClass"
|
176
addons/func_godot/src/func_godot_plugin.gd
Normal file
@ -0,0 +1,176 @@
|
||||
@tool
|
||||
class_name FuncGodotPlugin
|
||||
extends EditorPlugin
|
||||
|
||||
var map_import_plugin : QuakeMapImportPlugin = null
|
||||
var palette_import_plugin : QuakePaletteImportPlugin = null
|
||||
var wad_import_plugin: QuakeWadImportPlugin = null
|
||||
|
||||
var func_godot_map_control: Control = null
|
||||
var func_godot_map_progress_bar: Control = null
|
||||
var edited_object_ref: WeakRef = weakref(null)
|
||||
|
||||
func _get_plugin_name() -> String:
|
||||
return "FuncGodot"
|
||||
|
||||
func _handles(object: Object) -> bool:
|
||||
return object is FuncGodotMap
|
||||
|
||||
func _edit(object: Object) -> void:
|
||||
edited_object_ref = weakref(object)
|
||||
|
||||
func _make_visible(visible: bool) -> void:
|
||||
if func_godot_map_control:
|
||||
func_godot_map_control.set_visible(visible)
|
||||
|
||||
if func_godot_map_progress_bar:
|
||||
func_godot_map_progress_bar.set_visible(visible)
|
||||
|
||||
func _enter_tree() -> void:
|
||||
# Import plugins
|
||||
map_import_plugin = QuakeMapImportPlugin.new()
|
||||
palette_import_plugin = QuakePaletteImportPlugin.new()
|
||||
wad_import_plugin = QuakeWadImportPlugin.new()
|
||||
|
||||
add_import_plugin(map_import_plugin)
|
||||
add_import_plugin(palette_import_plugin)
|
||||
add_import_plugin(wad_import_plugin)
|
||||
|
||||
# FuncGodotMap button
|
||||
func_godot_map_control = create_func_godot_map_control()
|
||||
func_godot_map_control.set_visible(false)
|
||||
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)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
remove_custom_type("FuncGodotMap")
|
||||
remove_import_plugin(map_import_plugin)
|
||||
remove_import_plugin(palette_import_plugin)
|
||||
if wad_import_plugin:
|
||||
remove_import_plugin(wad_import_plugin)
|
||||
|
||||
map_import_plugin = null
|
||||
palette_import_plugin = null
|
||||
wad_import_plugin = null
|
||||
|
||||
if func_godot_map_control:
|
||||
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, func_godot_map_control)
|
||||
func_godot_map_control.queue_free()
|
||||
func_godot_map_control = null
|
||||
|
||||
if func_godot_map_progress_bar:
|
||||
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, func_godot_map_progress_bar)
|
||||
func_godot_map_progress_bar.queue_free()
|
||||
func_godot_map_progress_bar = null
|
||||
|
||||
## Create the toolbar controls for [FuncGodotMap] instances in the editor
|
||||
func create_func_godot_map_control() -> Control:
|
||||
var separator = VSeparator.new()
|
||||
|
||||
var icon = TextureRect.new()
|
||||
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)
|
||||
if not edited_object.is_connected("unwrap_uv2_complete", func_godot_map_build_complete):
|
||||
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.
|
||||
func func_godot_map_build_complete(func_godot_map: FuncGodotMap) -> void:
|
||||
var progress_label = func_godot_map_progress_bar.get_node("ProgressLabel")
|
||||
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_complete",Callable(self,"func_godot_map_build_complete")):
|
||||
func_godot_map.disconnect("build_complete",Callable(self,"func_godot_map_build_complete"))
|
||||
|
||||
if func_godot_map.is_connected("build_failed",Callable(self,"func_godot_map_build_complete")):
|
||||
func_godot_map.disconnect("build_failed",Callable(self,"func_godot_map_build_complete"))
|
6
addons/func_godot/src/import/quake_map_file.gd
Normal file
@ -0,0 +1,6 @@
|
||||
@icon("res://addons/func_godot/icons/icon_quake_file.svg")
|
||||
class_name QuakeMapFile
|
||||
extends Resource
|
||||
|
||||
@export var revision: int = 0
|
||||
@export_multiline var map_data: String = ""
|
47
addons/func_godot/src/import/quake_map_import_plugin.gd
Normal file
@ -0,0 +1,47 @@
|
||||
@tool
|
||||
class_name QuakeMapImportPlugin
|
||||
extends EditorImportPlugin
|
||||
|
||||
# Quake super.map import plugin
|
||||
|
||||
func _get_importer_name() -> String:
|
||||
return 'func_godot.map'
|
||||
|
||||
func _get_visible_name() -> String:
|
||||
return 'Quake Map'
|
||||
|
||||
func _get_resource_type() -> String:
|
||||
return 'Resource'
|
||||
|
||||
func _get_recognized_extensions() -> PackedStringArray:
|
||||
return PackedStringArray(['map'])
|
||||
|
||||
func _get_priority():
|
||||
return 1.0
|
||||
|
||||
func _get_save_extension() -> String:
|
||||
return 'tres'
|
||||
|
||||
func _get_import_options(path, preset):
|
||||
return []
|
||||
|
||||
func _get_preset_count() -> int:
|
||||
return 0
|
||||
|
||||
func _get_import_order():
|
||||
return 0
|
||||
|
||||
func _import(source_file, save_path, options, r_platform_variants, r_gen_files) -> Error:
|
||||
var save_path_str = '%s.%s' % [save_path, _get_save_extension()]
|
||||
|
||||
var map_resource : QuakeMapFile = null
|
||||
|
||||
var existing_resource := load(save_path_str) as QuakeMapFile
|
||||
if(existing_resource != null):
|
||||
map_resource = existing_resource
|
||||
map_resource.revision += 1
|
||||
else:
|
||||
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)
|
8
addons/func_godot/src/import/quake_palette_file.gd
Normal file
@ -0,0 +1,8 @@
|
||||
@icon("res://addons/func_godot/icons/icon_quake_file.svg")
|
||||
class_name QuakePaletteFile
|
||||
extends Resource
|
||||
|
||||
@export var colors: PackedColorArray
|
||||
|
||||
func _init(colors):
|
||||
self.colors = colors
|
61
addons/func_godot/src/import/quake_palette_import_plugin.gd
Normal file
@ -0,0 +1,61 @@
|
||||
@tool
|
||||
class_name QuakePaletteImportPlugin
|
||||
extends EditorImportPlugin
|
||||
|
||||
# Quake super.map import plugin
|
||||
|
||||
func _get_importer_name() -> String:
|
||||
return 'func_godot.palette'
|
||||
|
||||
func _get_visible_name() -> String:
|
||||
return 'Quake Palette'
|
||||
|
||||
func _get_resource_type() -> String:
|
||||
return 'Resource'
|
||||
|
||||
func _get_recognized_extensions() -> PackedStringArray:
|
||||
return PackedStringArray(['lmp'])
|
||||
|
||||
func _get_save_extension() -> String:
|
||||
return 'tres'
|
||||
|
||||
func _get_import_options(path, preset):
|
||||
return []
|
||||
|
||||
func _get_preset_count() -> int:
|
||||
return 0
|
||||
|
||||
func _get_priority():
|
||||
return 1.0
|
||||
|
||||
func _get_import_order():
|
||||
return 0
|
||||
|
||||
func _import(source_file, save_path, options, r_platform_variants, r_gen_files) -> Error:
|
||||
var save_path_str : String = '%s.%s' % [save_path, _get_save_extension()]
|
||||
|
||||
var file = FileAccess.open(source_file, FileAccess.READ)
|
||||
if file == null:
|
||||
var err = FileAccess.get_open_error()
|
||||
print(['Error opening super.lmp file: ', err])
|
||||
return err
|
||||
|
||||
var colors := PackedColorArray()
|
||||
|
||||
while true:
|
||||
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)
|
||||
|
||||
colors.append(color)
|
||||
|
||||
if file.eof_reached():
|
||||
break
|
||||
|
||||
if colors.size() == 256:
|
||||
break
|
||||
|
||||
var palette_resource := QuakePaletteFile.new(colors)
|
||||
|
||||
return ResourceSaver.save(palette_resource, save_path_str)
|
8
addons/func_godot/src/import/quake_wad_file.gd
Normal file
@ -0,0 +1,8 @@
|
||||
@icon("res://addons/func_godot/icons/icon_quake_file.svg")
|
||||
class_name QuakeWadFile
|
||||
extends Resource
|
||||
|
||||
@export var textures: Dictionary
|
||||
|
||||
func _init(textures: Dictionary):
|
||||
self.textures = textures
|
209
addons/func_godot/src/import/quake_wad_import_plugin.gd
Normal file
@ -0,0 +1,209 @@
|
||||
@tool
|
||||
class_name QuakeWadImportPlugin extends EditorImportPlugin
|
||||
|
||||
enum WadFormat {
|
||||
Quake,
|
||||
HalfLife
|
||||
}
|
||||
|
||||
enum QuakeWadEntryType {
|
||||
Palette = 0x40,
|
||||
SBarPic = 0x42,
|
||||
MipsTexture = 0x44,
|
||||
ConsolePic = 0x45
|
||||
}
|
||||
|
||||
enum HalfLifeWadEntryType {
|
||||
QPic = 0x42,
|
||||
MipsTexture = 0x43,
|
||||
FixedFont = 0x45
|
||||
}
|
||||
|
||||
const TEXTURE_NAME_LENGTH := 16
|
||||
const MAX_MIP_LEVELS := 4
|
||||
|
||||
func _get_importer_name() -> String:
|
||||
return 'func_godot.wad'
|
||||
|
||||
func _get_visible_name() -> String:
|
||||
return 'Texture2D WAD'
|
||||
|
||||
func _get_resource_type() -> String:
|
||||
return 'Resource'
|
||||
|
||||
func _get_recognized_extensions() -> PackedStringArray:
|
||||
return PackedStringArray(['wad'])
|
||||
|
||||
func _get_save_extension() -> String:
|
||||
return 'res'
|
||||
|
||||
func _get_option_visibility(path: String, option_name: StringName, options: Dictionary) -> bool:
|
||||
return true
|
||||
|
||||
func _get_import_options(path, preset) -> Array[Dictionary]:
|
||||
return [
|
||||
{
|
||||
'name': 'palette_file',
|
||||
'default_value': 'res://addons/func_godot/palette.lmp',
|
||||
'property_hint': PROPERTY_HINT_FILE,
|
||||
'hint_string': '*.lmp'
|
||||
},
|
||||
{
|
||||
'name': 'generate_mipmaps',
|
||||
'default_value': true,
|
||||
'property_hint': PROPERTY_HINT_NONE
|
||||
}
|
||||
]
|
||||
|
||||
func _get_preset_count() -> int:
|
||||
return 0
|
||||
|
||||
func _get_import_order() -> int:
|
||||
return 0
|
||||
|
||||
func _get_priority() -> float:
|
||||
return 1.0
|
||||
|
||||
func _import(source_file, save_path, options, r_platform_variants, r_gen_files) -> Error:
|
||||
var save_path_str : String = '%s.%s' % [save_path, _get_save_extension()]
|
||||
|
||||
var file = FileAccess.open(source_file, FileAccess.READ)
|
||||
if file == null:
|
||||
var err = FileAccess.get_open_error()
|
||||
print(['Error opening super.wad file: ', err])
|
||||
return err
|
||||
|
||||
# Read WAD header
|
||||
var magic : PackedByteArray = file.get_buffer(4)
|
||||
var magic_string : String = magic.get_string_from_ascii()
|
||||
var wad_format: int = WadFormat.Quake
|
||||
|
||||
if magic_string == 'WAD3':
|
||||
wad_format = WadFormat.HalfLife
|
||||
elif magic_string != 'WAD2':
|
||||
print('Error: Invalid WAD magic')
|
||||
return ERR_INVALID_DATA
|
||||
|
||||
var palette_path : String = options['palette_file']
|
||||
var palette_file : QuakePaletteFile = load(palette_path) as QuakePaletteFile
|
||||
if wad_format == WadFormat.Quake and not palette_file:
|
||||
print('Error: Invalid Quake palette file')
|
||||
file.close()
|
||||
return ERR_CANT_ACQUIRE_RESOURCE
|
||||
|
||||
var num_entries : int = file.get_32()
|
||||
var dir_offset : int = file.get_32()
|
||||
|
||||
# Read entry list
|
||||
file.seek(0)
|
||||
file.seek(dir_offset)
|
||||
|
||||
var entries : Array = []
|
||||
|
||||
for entry_idx in range(0, num_entries):
|
||||
var offset : int = file.get_32()
|
||||
var in_wad_size : int = file.get_32()
|
||||
var size : int = file.get_32()
|
||||
var type : int = file.get_8()
|
||||
var compression : int = file.get_8()
|
||||
var unknown : int = file.get_16()
|
||||
var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH)
|
||||
var name_string : String = name.get_string_from_ascii()
|
||||
|
||||
if (wad_format == WadFormat.Quake and type == int(QuakeWadEntryType.MipsTexture)) or (
|
||||
wad_format == WadFormat.HalfLife and type == int(HalfLifeWadEntryType.MipsTexture)):
|
||||
entries.append([
|
||||
offset,
|
||||
in_wad_size,
|
||||
size,
|
||||
type,
|
||||
compression,
|
||||
name_string
|
||||
])
|
||||
|
||||
# Read mip textures
|
||||
var texture_data_array: Array = []
|
||||
for entry in entries:
|
||||
var offset : int = entry[0]
|
||||
file.seek(offset)
|
||||
|
||||
var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH)
|
||||
var name_string : String = name.get_string_from_ascii()
|
||||
|
||||
var width : int = file.get_32()
|
||||
var height : int = file.get_32()
|
||||
|
||||
var mip_offsets : Array = []
|
||||
for idx in range(0, MAX_MIP_LEVELS):
|
||||
mip_offsets.append(file.get_32())
|
||||
|
||||
var num_pixels : int = width * height
|
||||
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
|
||||
var textures : Dictionary = {}
|
||||
|
||||
for texture_data in texture_data_array:
|
||||
var name : String = texture_data[0]
|
||||
var width : int = texture_data[1]
|
||||
var height : int = texture_data[2]
|
||||
var pixels : PackedByteArray = texture_data[3]
|
||||
|
||||
var texture_image : Image
|
||||
var pixels_rgb := PackedByteArray()
|
||||
|
||||
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:
|
||||
texture_image.generate_mipmaps()
|
||||
|
||||
var texture := ImageTexture.create_from_image(texture_image) #,Texture2D.FLAG_MIPMAPS | Texture2D.FLAG_REPEAT | Texture2D.FLAG_ANISOTROPIC_FILTER
|
||||
textures[name] = texture
|
||||
|
||||
# Save WAD resource
|
||||
var wad_resource := QuakeWadFile.new(textures)
|
||||
return ResourceSaver.save(wad_resource, save_path_str)
|
995
addons/func_godot/src/map/func_godot_map.gd
Normal file
@ -0,0 +1,995 @@
|
||||
@tool
|
||||
@icon("res://addons/func_godot/icons/icon_slipgate3d.svg")
|
||||
## A scene generator node that parses a Quake map file using a [FuncGodotFGDFile]. Uses a [FuncGodotMapSettings] resource to define map build settings.
|
||||
## To use this node, select an instance of the node in the Godot editor and select "Quick Build", "Full Build", or "Unwrap UV2" from the toolbar. Alternatively, call [method manual_build] from code.
|
||||
class_name FuncGodotMap extends Node3D
|
||||
|
||||
## How long to wait between child/owner batches
|
||||
const YIELD_DURATION := 0.0
|
||||
|
||||
## Emitted when the build process successfully completes
|
||||
signal build_complete()
|
||||
## Emitted when the build process finishes a step. [code]progress[/code] is from 0.0-1.0
|
||||
signal build_progress(step, progress)
|
||||
## Emitted when the build process fails
|
||||
signal build_failed()
|
||||
|
||||
## Emitted when UV2 unwrapping is completed
|
||||
signal unwrap_uv2_complete()
|
||||
|
||||
@export_category("Map")
|
||||
|
||||
## Local path to Quake map file to build a scene from.
|
||||
@export_file("*.map") var local_map_file: String = ""
|
||||
|
||||
## Global path to Quake map file to build a scene from. Overrides [member local_map_file].
|
||||
@export_global_file("*.map") var global_map_file: String = ""
|
||||
|
||||
## Map path used by code. Do it this way to support both global and local paths.
|
||||
var _map_file_internal: String = ""
|
||||
|
||||
## Map settings resource that defines map build scale, textures location, and more.
|
||||
@export var map_settings: FuncGodotMapSettings = load("res://addons/func_godot/func_godot_default_map_settings.tres")
|
||||
|
||||
@export_category("Build")
|
||||
## If true, print profiling data before and after each build step.
|
||||
@export var print_profiling_data: bool = false
|
||||
## If true, stop the whole editor until build is complete.
|
||||
@export var block_until_complete: bool = false
|
||||
## How many nodes to set the owner of, or add children of, at once. Higher values may lead to quicker build times, but a less responsive editor.
|
||||
@export var set_owner_batch_size: int = 1000
|
||||
|
||||
# Build context variables
|
||||
var func_godot: FuncGodot = null
|
||||
var profile_timestamps: Dictionary = {}
|
||||
var add_child_array: Array = []
|
||||
var set_owner_array: Array = []
|
||||
var should_add_children: bool = true
|
||||
var should_set_owners: bool = true
|
||||
var texture_list: Array = []
|
||||
var texture_loader = null
|
||||
var texture_dict: Dictionary = {}
|
||||
var texture_size_dict: Dictionary = {}
|
||||
var material_dict: Dictionary = {}
|
||||
var entity_definitions: Dictionary = {}
|
||||
var entity_dicts: Array = []
|
||||
var entity_mesh_dict: Dictionary = {}
|
||||
var entity_nodes: Array = []
|
||||
var entity_mesh_instances: Dictionary = {}
|
||||
var entity_occluder_instances: Dictionary = {}
|
||||
var entity_collision_shapes: Array = []
|
||||
|
||||
# Utility
|
||||
## Verify that FuncGodot is functioning and that [member map_file] exists. If so, build the map. If not, signal [signal build_failed]
|
||||
func verify_and_build() -> void:
|
||||
if verify_parameters():
|
||||
build_map()
|
||||
else:
|
||||
emit_signal("build_failed")
|
||||
|
||||
## Build the map.
|
||||
func manual_build() -> void:
|
||||
should_add_children = false
|
||||
should_set_owners = false
|
||||
verify_and_build()
|
||||
|
||||
## Return true if parameters are valid; FuncGodot should be functioning and [member map_file] should exist.
|
||||
func verify_parameters() -> bool:
|
||||
# Prioritize global map file path for building at runtime
|
||||
_map_file_internal = global_map_file if global_map_file != "" else local_map_file
|
||||
|
||||
if _map_file_internal == "":
|
||||
push_error("Error: Map file not set")
|
||||
return false
|
||||
|
||||
if not FileAccess.file_exists(_map_file_internal):
|
||||
if FileAccess.file_exists(_map_file_internal + ".import"):
|
||||
_map_file_internal = _map_file_internal + ".import"
|
||||
else:
|
||||
push_error("Error: No such file %s" % _map_file_internal)
|
||||
return false
|
||||
|
||||
if not map_settings:
|
||||
push_error("Error: Map settings not set")
|
||||
return false
|
||||
|
||||
if not func_godot:
|
||||
func_godot = load("res://addons/func_godot/src/core/func_godot.gd").new()
|
||||
|
||||
if not func_godot:
|
||||
push_error("Error: Failed to load func_godot.")
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
## Reset member variables that affect the current build
|
||||
func reset_build_context() -> void:
|
||||
add_child_array = []
|
||||
set_owner_array = []
|
||||
texture_list = []
|
||||
texture_loader = null
|
||||
texture_dict = {}
|
||||
texture_size_dict = {}
|
||||
material_dict = {}
|
||||
entity_definitions = {}
|
||||
entity_dicts = []
|
||||
entity_mesh_dict = {}
|
||||
entity_nodes = []
|
||||
entity_mesh_instances = {}
|
||||
entity_occluder_instances = {}
|
||||
entity_collision_shapes = []
|
||||
build_step_index = 0
|
||||
build_step_count = 0
|
||||
if func_godot:
|
||||
func_godot = load("res://addons/func_godot/src/core/func_godot.gd").new()
|
||||
func_godot.map_settings = map_settings
|
||||
|
||||
## Record the start time of a build step for profiling
|
||||
func start_profile(item_name: String) -> void:
|
||||
if print_profiling_data:
|
||||
print(item_name)
|
||||
profile_timestamps[item_name] = Time.get_unix_time_from_system()
|
||||
|
||||
## Finish profiling for a build step; print associated timing data
|
||||
func stop_profile(item_name: String) -> void:
|
||||
if print_profiling_data:
|
||||
if item_name in profile_timestamps:
|
||||
var delta: float = Time.get_unix_time_from_system() - profile_timestamps[item_name]
|
||||
print("Completed in %s sec." % snapped(delta, 0.0001))
|
||||
profile_timestamps.erase(item_name)
|
||||
|
||||
## Run a build step. [code]step_name[/code] is the method corresponding to the step.
|
||||
func run_build_step(step_name: String) -> Variant:
|
||||
start_profile(step_name)
|
||||
var result : Variant = call(step_name)
|
||||
stop_profile(step_name)
|
||||
return result
|
||||
|
||||
## Add [code]node[/code] as a child of parent, or as a child of [code]below[/code] if non-null. Also queue for ownership assignment.
|
||||
func add_child_editor(parent: Node, node: Node, below: Node = null) -> void:
|
||||
var prev_parent = node.get_parent()
|
||||
if prev_parent:
|
||||
prev_parent.remove_child(node)
|
||||
|
||||
if below:
|
||||
below.add_sibling(node)
|
||||
else:
|
||||
parent.add_child(node)
|
||||
|
||||
set_owner_array.append(node)
|
||||
|
||||
## Set the owner of [code]node[/code] to the current scene.
|
||||
func set_owner_editor(node: Node) -> void:
|
||||
var tree : SceneTree = get_tree()
|
||||
|
||||
if not tree:
|
||||
return
|
||||
|
||||
var edited_scene_root : Node = tree.get_edited_scene_root()
|
||||
|
||||
if not edited_scene_root:
|
||||
return
|
||||
|
||||
node.set_owner(edited_scene_root)
|
||||
|
||||
var build_step_index : int = 0
|
||||
var build_step_count : int = 0
|
||||
var build_steps : Array = []
|
||||
var post_attach_steps : Array = []
|
||||
|
||||
## Register a build step.
|
||||
## [code]build_step[/code] is a string that corresponds to a method on this class, [code]arguments[/code] a list of arguments to pass to this method, and [code]target[/code] is a property on this class to save the return value of the build step in. If [code]post_attach[/code] is true, the step will be run after the scene hierarchy is completed.
|
||||
func register_build_step(build_step: String, target: String = "", post_attach: bool = false) -> void:
|
||||
(post_attach_steps if post_attach else build_steps).append([build_step, target])
|
||||
build_step_count += 1
|
||||
|
||||
## Run all build steps. Emits [signal build_progress] after each step.
|
||||
## If [code]post_attach[/code] is true, run post-attach steps instead and signal [signal build_complete] when finished.
|
||||
func run_build_steps(post_attach : bool = false) -> void:
|
||||
var target_array : Array = post_attach_steps if post_attach else build_steps
|
||||
|
||||
while target_array.size() > 0:
|
||||
var build_step : Array = target_array.pop_front()
|
||||
emit_signal("build_progress", build_step[0], float(build_step_index + 1) / float(build_step_count))
|
||||
|
||||
var scene_tree : SceneTree = get_tree()
|
||||
if scene_tree and not block_until_complete:
|
||||
await get_tree().create_timer(YIELD_DURATION).timeout
|
||||
|
||||
var result : Variant = run_build_step(build_step[0])
|
||||
var target : String = build_step[1]
|
||||
if target != "":
|
||||
set(target, result)
|
||||
|
||||
build_step_index += 1
|
||||
|
||||
if scene_tree and not block_until_complete:
|
||||
await get_tree().create_timer(YIELD_DURATION).timeout
|
||||
|
||||
if post_attach:
|
||||
_build_complete()
|
||||
else:
|
||||
start_profile('add_children')
|
||||
add_children()
|
||||
|
||||
## Register all steps for the build. See [method register_build_step] and [method run_build_steps]
|
||||
func register_build_steps() -> void:
|
||||
register_build_step('remove_children')
|
||||
register_build_step('load_map')
|
||||
register_build_step('fetch_texture_list', 'texture_list')
|
||||
register_build_step('init_texture_loader', 'texture_loader')
|
||||
register_build_step('load_textures', 'texture_dict')
|
||||
register_build_step('build_texture_size_dict', 'texture_size_dict')
|
||||
register_build_step('build_materials', 'material_dict')
|
||||
register_build_step('fetch_entity_definitions', 'entity_definitions')
|
||||
register_build_step('set_core_entity_definitions')
|
||||
register_build_step('generate_geometry')
|
||||
register_build_step('fetch_entity_dicts', 'entity_dicts')
|
||||
register_build_step('build_entity_nodes', 'entity_nodes')
|
||||
register_build_step('resolve_trenchbroom_group_hierarchy')
|
||||
register_build_step('build_entity_mesh_dict', 'entity_mesh_dict')
|
||||
register_build_step('build_entity_mesh_instances', 'entity_mesh_instances')
|
||||
register_build_step('build_entity_occluder_instances', 'entity_occluder_instances')
|
||||
register_build_step('build_entity_collision_shape_nodes', 'entity_collision_shapes')
|
||||
|
||||
## Register all post-attach steps for the build. See [method register_build_step] and [method run_build_steps]
|
||||
func register_post_attach_steps() -> void:
|
||||
register_build_step('build_entity_collision_shapes', "", true)
|
||||
register_build_step('apply_entity_meshes', "", true)
|
||||
register_build_step('apply_entity_occluders', "", true)
|
||||
register_build_step('apply_properties_and_finish', "", true)
|
||||
|
||||
# Actions
|
||||
## Build the map
|
||||
func build_map() -> void:
|
||||
reset_build_context()
|
||||
if map_settings == null:
|
||||
printerr("Skipping build process: No map settings resource!")
|
||||
emit_signal("build_complete")
|
||||
return
|
||||
print('Building %s' % _map_file_internal)
|
||||
#if print_profiling_data:
|
||||
#print('\n')
|
||||
start_profile('build_map')
|
||||
register_build_steps()
|
||||
register_post_attach_steps()
|
||||
run_build_steps()
|
||||
|
||||
## Recursively unwrap UV2s for [code]node[/code] and its children, in preparation for baked lighting.
|
||||
func unwrap_uv2(node: Node = null) -> void:
|
||||
var target_node: Node = null
|
||||
|
||||
if node:
|
||||
target_node = node
|
||||
else:
|
||||
target_node = self
|
||||
print("Unwrapping mesh UV2s")
|
||||
|
||||
if target_node is MeshInstance3D:
|
||||
if target_node.gi_mode == GeometryInstance3D.GI_MODE_STATIC:
|
||||
var mesh: Mesh = target_node.get_mesh()
|
||||
if mesh is ArrayMesh:
|
||||
mesh.lightmap_unwrap(Transform3D.IDENTITY, map_settings.uv_unwrap_texel_size / map_settings.inverse_scale_factor)
|
||||
|
||||
for child in target_node.get_children():
|
||||
unwrap_uv2(child)
|
||||
|
||||
if not node:
|
||||
print("Unwrap complete")
|
||||
emit_signal("unwrap_uv2_complete")
|
||||
|
||||
# Build Steps
|
||||
## Recursively remove and delete all children of this node
|
||||
func remove_children() -> void:
|
||||
for child in get_children():
|
||||
remove_child(child)
|
||||
child.queue_free()
|
||||
|
||||
## Parse and load [member map_file]
|
||||
func load_map() -> void:
|
||||
func_godot.load_map(_map_file_internal, map_settings.use_trenchbroom_groups_hierarchy)
|
||||
|
||||
## Get textures found in [member map_file]
|
||||
func fetch_texture_list() -> Array:
|
||||
return func_godot.get_texture_list() as Array
|
||||
|
||||
## Initialize texture loader, allowing textures in [member base_texture_dir] and [member texture_wads] to be turned into materials
|
||||
func init_texture_loader() -> FuncGodotTextureLoader:
|
||||
return FuncGodotTextureLoader.new(map_settings)
|
||||
|
||||
## Build a dictionary from Map File texture names to their corresponding Texture2D resources in Godot
|
||||
func load_textures() -> Dictionary:
|
||||
return texture_loader.load_textures(texture_list) as Dictionary
|
||||
|
||||
## Build a dictionary from Map File texture names to Godot materials
|
||||
func build_materials() -> Dictionary:
|
||||
return texture_loader.create_materials(texture_list)
|
||||
|
||||
## Collect entity definitions from [member entity_fgd], as a dictionary from Map File classnames to entity definitions
|
||||
func fetch_entity_definitions() -> Dictionary:
|
||||
return map_settings.entity_fgd.get_entity_definitions()
|
||||
|
||||
## Hand the FuncGodot core the entity definitions
|
||||
func set_core_entity_definitions() -> void:
|
||||
var core_ent_defs: Dictionary = {}
|
||||
for classname in entity_definitions:
|
||||
core_ent_defs[classname] = {}
|
||||
if entity_definitions[classname] is FuncGodotFGDSolidClass:
|
||||
core_ent_defs[classname]['spawn_type'] = entity_definitions[classname].spawn_type
|
||||
core_ent_defs[classname]['origin_type'] = entity_definitions[classname].origin_type
|
||||
func_godot.set_entity_definitions(core_ent_defs)
|
||||
|
||||
## Generate geometry from map file
|
||||
func generate_geometry() -> void:
|
||||
func_godot.generate_geometry(texture_size_dict);
|
||||
|
||||
## Get a list of dictionaries representing each entity from the FuncGodot core
|
||||
func fetch_entity_dicts() -> Array:
|
||||
return func_godot.get_entity_dicts()
|
||||
|
||||
## Build a dictionary from Map File textures to the sizes of their corresponding Godot textures
|
||||
func build_texture_size_dict() -> Dictionary:
|
||||
var texture_size_dict: Dictionary = {}
|
||||
|
||||
for tex_key in texture_dict:
|
||||
var texture: Texture2D = texture_dict[tex_key] as Texture2D
|
||||
if texture:
|
||||
texture_size_dict[tex_key] = texture.get_size()
|
||||
else:
|
||||
texture_size_dict[tex_key] = Vector2.ONE
|
||||
|
||||
return texture_size_dict
|
||||
|
||||
## Build nodes from the entities in [member entity_dicts]
|
||||
func build_entity_nodes() -> Array:
|
||||
var entity_nodes : Array = []
|
||||
|
||||
for entity_idx in range(0, entity_dicts.size()):
|
||||
var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary
|
||||
var properties: Dictionary = entity_dict['properties'] as Dictionary
|
||||
|
||||
var node: Node = Node3D.new()
|
||||
var node_name: String = "entity_%s" % entity_idx
|
||||
|
||||
var should_add_child: bool = should_add_children
|
||||
|
||||
if 'classname' in properties:
|
||||
var classname: String = properties['classname']
|
||||
node_name += "_" + classname
|
||||
if classname in entity_definitions:
|
||||
var entity_definition: FuncGodotFGDEntityClass = entity_definitions[classname] as FuncGodotFGDEntityClass
|
||||
|
||||
var name_prop: String
|
||||
if entity_definition.name_property in properties:
|
||||
name_prop = str(properties[entity_definition.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_definition is FuncGodotFGDSolidClass:
|
||||
if entity_definition.spawn_type == FuncGodotFGDSolidClass.SpawnType.MERGE_WORLDSPAWN:
|
||||
entity_nodes.append(null)
|
||||
continue
|
||||
if entity_definition.node_class != "":
|
||||
node.queue_free()
|
||||
node = ClassDB.instantiate(entity_definition.node_class)
|
||||
elif entity_definition is FuncGodotFGDPointClass:
|
||||
if entity_definition.scene_file:
|
||||
var flag: PackedScene.GenEditState = PackedScene.GEN_EDIT_STATE_DISABLED
|
||||
if Engine.is_editor_hint():
|
||||
flag = PackedScene.GEN_EDIT_STATE_INSTANCE
|
||||
node.queue_free()
|
||||
node = entity_definition.scene_file.instantiate(flag)
|
||||
elif entity_definition.node_class != "":
|
||||
node.queue_free()
|
||||
node = ClassDB.instantiate(entity_definition.node_class)
|
||||
if 'rotation_degrees' in node and entity_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 entity_definition.classname.begins_with('light'):
|
||||
angles = Vector3(angles_raw[1], angles_raw[0], -angles_raw[2])
|
||||
elif entity_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 entity_definition.script_class:
|
||||
node.set_script(entity_definition.script_class)
|
||||
|
||||
node.name = node_name
|
||||
|
||||
if 'origin' in properties and entity_dict.brush_count < 1:
|
||||
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.inverse_scale_factor
|
||||
elif node.position is Vector2:
|
||||
node.position = Vector2(origin_vec.z, -origin_vec.y)
|
||||
else:
|
||||
if entity_idx != 0 and 'position' in node:
|
||||
if node.position is Vector3:
|
||||
node.position = entity_dict['center'] / map_settings.inverse_scale_factor
|
||||
|
||||
entity_nodes.append(node)
|
||||
|
||||
if should_add_child:
|
||||
queue_add_child(self, node)
|
||||
|
||||
return entity_nodes
|
||||
|
||||
## Build [CollisionShape3D] nodes for brush entities
|
||||
func build_entity_collision_shape_nodes() -> Array:
|
||||
var entity_collision_shapes_arr: Array = []
|
||||
|
||||
for entity_idx in range(0, entity_nodes.size()):
|
||||
var entity_collision_shapes: Array = []
|
||||
|
||||
var entity_dict: Dictionary = entity_dicts[entity_idx]
|
||||
var properties: Dictionary = entity_dict['properties']
|
||||
|
||||
var node: Node = entity_nodes[entity_idx] as Node
|
||||
var concave: bool = false
|
||||
|
||||
if 'classname' in properties:
|
||||
var classname: String = properties['classname']
|
||||
if classname in entity_definitions:
|
||||
var entity_definition: FuncGodotFGDSolidClass = entity_definitions[classname] as FuncGodotFGDSolidClass
|
||||
if entity_definition:
|
||||
if entity_definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.NONE:
|
||||
entity_collision_shapes_arr.append(null)
|
||||
continue
|
||||
elif entity_definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.CONCAVE:
|
||||
concave = true
|
||||
|
||||
if entity_definition.spawn_type == FuncGodotFGDSolidClass.SpawnType.MERGE_WORLDSPAWN:
|
||||
# TODO: Find the worldspawn object instead of assuming index 0
|
||||
node = entity_nodes[0] as Node
|
||||
|
||||
if node and node is CollisionObject3D:
|
||||
(node as CollisionObject3D).collision_layer = entity_definition.collision_layer
|
||||
(node as CollisionObject3D).collision_mask = entity_definition.collision_mask
|
||||
(node as CollisionObject3D).collision_priority = entity_definition.collision_priority
|
||||
|
||||
# don't create collision shapes that wont be attached to a CollisionObject3D as they are a waste
|
||||
if not node or (not node is CollisionObject3D):
|
||||
entity_collision_shapes_arr.append(null)
|
||||
continue
|
||||
|
||||
if concave:
|
||||
var collision_shape: CollisionShape3D = CollisionShape3D.new()
|
||||
collision_shape.name = "entity_%s_collision_shape" % entity_idx
|
||||
entity_collision_shapes.append(collision_shape)
|
||||
queue_add_child(node, collision_shape)
|
||||
else:
|
||||
for brush_idx in entity_dict['brush_indices']:
|
||||
var collision_shape: CollisionShape3D = CollisionShape3D.new()
|
||||
collision_shape.name = "entity_%s_brush_%s_collision_shape" % [entity_idx, brush_idx]
|
||||
entity_collision_shapes.append(collision_shape)
|
||||
queue_add_child(node, collision_shape)
|
||||
entity_collision_shapes_arr.append(entity_collision_shapes)
|
||||
|
||||
return entity_collision_shapes_arr
|
||||
|
||||
## Build the concrete [Shape3D] resources for each brush
|
||||
func build_entity_collision_shapes() -> void:
|
||||
for entity_idx in range(0, entity_dicts.size()):
|
||||
var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary
|
||||
var properties: Dictionary = entity_dict['properties']
|
||||
var entity_position: Vector3 = Vector3.ZERO
|
||||
if entity_nodes[entity_idx] != null and entity_nodes[entity_idx].get("position"):
|
||||
if entity_nodes[entity_idx].position is Vector3:
|
||||
entity_position = entity_nodes[entity_idx].position
|
||||
|
||||
if entity_collision_shapes.size() < entity_idx:
|
||||
continue
|
||||
if entity_collision_shapes[entity_idx] == null:
|
||||
continue
|
||||
|
||||
var entity_collision_shape: Array = entity_collision_shapes[entity_idx]
|
||||
var concave: bool = false
|
||||
var shape_margin: float = 0.04
|
||||
|
||||
if 'classname' in properties:
|
||||
var classname: String = properties['classname']
|
||||
if classname in entity_definitions:
|
||||
var entity_definition: FuncGodotFGDSolidClass = entity_definitions[classname] as FuncGodotFGDSolidClass
|
||||
if entity_definition:
|
||||
match(entity_definition.collision_shape_type):
|
||||
FuncGodotFGDSolidClass.CollisionShapeType.NONE:
|
||||
continue
|
||||
FuncGodotFGDSolidClass.CollisionShapeType.CONVEX:
|
||||
concave = false
|
||||
FuncGodotFGDSolidClass.CollisionShapeType.CONCAVE:
|
||||
concave = true
|
||||
shape_margin = entity_definition.collision_shape_margin
|
||||
|
||||
if entity_collision_shapes[entity_idx] == null:
|
||||
continue
|
||||
|
||||
if concave:
|
||||
func_godot.gather_entity_concave_collision_surfaces(entity_idx)
|
||||
else:
|
||||
func_godot.gather_entity_convex_collision_surfaces(entity_idx)
|
||||
var entity_surfaces: Array = func_godot.fetch_surfaces(func_godot.surface_gatherer)
|
||||
|
||||
var entity_verts: PackedVector3Array = PackedVector3Array()
|
||||
|
||||
for surface_idx in range(0, entity_surfaces.size()):
|
||||
if entity_surfaces[surface_idx] == null:
|
||||
continue
|
||||
|
||||
var surface_verts: Array = entity_surfaces[surface_idx]
|
||||
|
||||
if concave:
|
||||
var vertices: PackedVector3Array = surface_verts[Mesh.ARRAY_VERTEX] as PackedVector3Array
|
||||
var indices: PackedInt32Array = surface_verts[Mesh.ARRAY_INDEX] as PackedInt32Array
|
||||
for vert_idx in indices:
|
||||
entity_verts.append(vertices[vert_idx])
|
||||
else:
|
||||
var shape_points = PackedVector3Array()
|
||||
for vertex in surface_verts[Mesh.ARRAY_VERTEX]:
|
||||
if not vertex in shape_points:
|
||||
shape_points.append(vertex)
|
||||
|
||||
var shape: ConvexPolygonShape3D = ConvexPolygonShape3D.new()
|
||||
shape.set_points(shape_points)
|
||||
shape.margin = shape_margin
|
||||
|
||||
var collision_shape: CollisionShape3D = entity_collision_shape[surface_idx]
|
||||
collision_shape.set_shape(shape)
|
||||
|
||||
if concave:
|
||||
if entity_verts.size() == 0:
|
||||
continue
|
||||
|
||||
var shape: ConcavePolygonShape3D = ConcavePolygonShape3D.new()
|
||||
shape.set_faces(entity_verts)
|
||||
shape.margin = shape_margin
|
||||
|
||||
var collision_shape: CollisionShape3D = entity_collision_shapes[entity_idx][0]
|
||||
collision_shape.set_shape(shape)
|
||||
|
||||
## Build Dictionary from entity indices to [ArrayMesh] instances
|
||||
func build_entity_mesh_dict() -> Dictionary:
|
||||
var meshes: Dictionary = {}
|
||||
|
||||
var texture_surf_map: Dictionary
|
||||
for texture in texture_dict:
|
||||
texture_surf_map[texture] = Array()
|
||||
|
||||
var gather_task = func(i):
|
||||
var texture: String = texture_dict.keys()[i]
|
||||
texture_surf_map[texture] = func_godot.gather_texture_surfaces(texture)
|
||||
|
||||
var task_id: int = WorkerThreadPool.add_group_task(gather_task, texture_dict.keys().size(), 4, true)
|
||||
WorkerThreadPool.wait_for_group_task_completion(task_id)
|
||||
|
||||
for texture in texture_dict:
|
||||
var texture_surfaces: Array = texture_surf_map[texture] as Array
|
||||
|
||||
for entity_idx in range(0, texture_surfaces.size()):
|
||||
var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary
|
||||
var properties: Dictionary = entity_dict['properties']
|
||||
|
||||
var entity_surface = texture_surfaces[entity_idx]
|
||||
|
||||
if 'classname' in properties:
|
||||
var classname: String = properties['classname']
|
||||
if classname in entity_definitions:
|
||||
var entity_definition: FuncGodotFGDSolidClass = entity_definitions[classname] as FuncGodotFGDSolidClass
|
||||
if entity_definition:
|
||||
if entity_definition.spawn_type == FuncGodotFGDSolidClass.SpawnType.MERGE_WORLDSPAWN:
|
||||
entity_surface = null
|
||||
|
||||
if not entity_definition.build_visuals and not entity_definition.build_occlusion:
|
||||
entity_surface = null
|
||||
|
||||
if entity_surface == null:
|
||||
continue
|
||||
|
||||
if not entity_idx in meshes:
|
||||
meshes[entity_idx] = ArrayMesh.new()
|
||||
|
||||
var mesh: ArrayMesh = meshes[entity_idx]
|
||||
mesh.add_surface_from_arrays(ArrayMesh.PRIMITIVE_TRIANGLES, entity_surface)
|
||||
mesh.surface_set_name(mesh.get_surface_count() - 1, texture)
|
||||
mesh.surface_set_material(mesh.get_surface_count() - 1, material_dict[texture])
|
||||
|
||||
return meshes
|
||||
|
||||
## Build [MeshInstance3D]s from brush entities and add them to the add child queue
|
||||
func build_entity_mesh_instances() -> Dictionary:
|
||||
var entity_mesh_instances: Dictionary = {}
|
||||
for entity_idx in entity_mesh_dict:
|
||||
var use_in_baked_light: bool = false
|
||||
var shadow_casting_setting: GeometryInstance3D.ShadowCastingSetting = GeometryInstance3D.SHADOW_CASTING_SETTING_DOUBLE_SIDED
|
||||
var render_layers: int = 1
|
||||
|
||||
var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary
|
||||
var properties: Dictionary = entity_dict['properties']
|
||||
var classname: String = properties['classname']
|
||||
if classname in entity_definitions:
|
||||
var entity_definition: FuncGodotFGDSolidClass = entity_definitions[classname] as FuncGodotFGDSolidClass
|
||||
if entity_definition:
|
||||
if not entity_definition.build_visuals:
|
||||
continue
|
||||
|
||||
if entity_definition.use_in_baked_light:
|
||||
use_in_baked_light = true
|
||||
elif '_shadow' in properties:
|
||||
if properties['_shadow'] == "1":
|
||||
use_in_baked_light = true
|
||||
shadow_casting_setting = entity_definition.shadow_casting_setting
|
||||
render_layers = entity_definition.render_layers
|
||||
|
||||
if not entity_mesh_dict[entity_idx]:
|
||||
continue
|
||||
|
||||
var mesh_instance: MeshInstance3D = MeshInstance3D.new()
|
||||
mesh_instance.name = 'entity_%s_mesh_instance' % entity_idx
|
||||
mesh_instance.gi_mode = MeshInstance3D.GI_MODE_STATIC if use_in_baked_light else GeometryInstance3D.GI_MODE_DISABLED
|
||||
mesh_instance.cast_shadow = shadow_casting_setting
|
||||
mesh_instance.layers = render_layers
|
||||
|
||||
queue_add_child(entity_nodes[entity_idx], mesh_instance)
|
||||
|
||||
entity_mesh_instances[entity_idx] = mesh_instance
|
||||
|
||||
return entity_mesh_instances
|
||||
|
||||
func build_entity_occluder_instances() -> Dictionary:
|
||||
var entity_occluder_instances: Dictionary = {}
|
||||
for entity_idx in entity_mesh_dict:
|
||||
var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary
|
||||
var properties: Dictionary = entity_dict['properties']
|
||||
var classname: String = properties['classname']
|
||||
if classname in entity_definitions:
|
||||
var entity_definition: FuncGodotFGDSolidClass = entity_definitions[classname] as FuncGodotFGDSolidClass
|
||||
if entity_definition:
|
||||
if entity_definition.build_occlusion:
|
||||
if not entity_mesh_dict[entity_idx]:
|
||||
continue
|
||||
|
||||
var occluder_instance: OccluderInstance3D = OccluderInstance3D.new()
|
||||
occluder_instance.name = 'entity_%s_occluder_instance' % entity_idx
|
||||
|
||||
queue_add_child(entity_nodes[entity_idx], occluder_instance)
|
||||
entity_occluder_instances[entity_idx] = occluder_instance
|
||||
|
||||
return entity_occluder_instances
|
||||
|
||||
## Assign [ArrayMesh]es to their [MeshInstance3D] counterparts
|
||||
func apply_entity_meshes() -> void:
|
||||
for entity_idx in entity_mesh_instances:
|
||||
var mesh: Mesh = entity_mesh_dict[entity_idx] as Mesh
|
||||
var mesh_instance: MeshInstance3D = entity_mesh_instances[entity_idx] as MeshInstance3D
|
||||
if not mesh or not mesh_instance:
|
||||
continue
|
||||
|
||||
mesh_instance.set_mesh(mesh)
|
||||
queue_add_child(entity_nodes[entity_idx], mesh_instance)
|
||||
|
||||
func apply_entity_occluders() -> void:
|
||||
for entity_idx in entity_mesh_dict:
|
||||
var mesh: Mesh = entity_mesh_dict[entity_idx] as Mesh
|
||||
var occluder_instance: OccluderInstance3D
|
||||
|
||||
if entity_idx in entity_occluder_instances:
|
||||
occluder_instance = entity_occluder_instances[entity_idx]
|
||||
|
||||
if not mesh or not occluder_instance:
|
||||
continue
|
||||
|
||||
var verts: PackedVector3Array
|
||||
var indices: PackedInt32Array
|
||||
for surf_idx in range(mesh.get_surface_count()):
|
||||
var vert_count: int = verts.size()
|
||||
var surf_array: Array = 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.append(new_index + vert_count)
|
||||
|
||||
var occluder: ArrayOccluder3D = ArrayOccluder3D.new()
|
||||
occluder.set_arrays(verts, indices)
|
||||
|
||||
occluder_instance.occluder = occluder
|
||||
|
||||
## Resolve entity group hierarchy, turning Trenchbroom groups into nodes and queueing their contents to be added to said nodes as children
|
||||
func resolve_trenchbroom_group_hierarchy() -> void:
|
||||
if not map_settings.use_trenchbroom_groups_hierarchy:
|
||||
return
|
||||
|
||||
var parent_entities: Dictionary = {}
|
||||
var child_entities: Dictionary = {}
|
||||
|
||||
# Gather all entities which are children in some group or parents in some group
|
||||
for node_idx in range(0, entity_nodes.size()):
|
||||
var node: Node = entity_nodes[node_idx]
|
||||
var properties: Dictionary = entity_dicts[node_idx]['properties']
|
||||
|
||||
if not properties:
|
||||
continue
|
||||
|
||||
if not ('_tb_id' in properties or '_tb_group' in properties or '_tb_layer' in properties):
|
||||
continue
|
||||
|
||||
# identify children
|
||||
if '_tb_group' in properties or '_tb_layer' in properties:
|
||||
child_entities[node_idx] = node
|
||||
|
||||
# identify parents
|
||||
if '_tb_id' in properties:
|
||||
node.set_meta("_tb_type", properties['_tb_type'])
|
||||
if properties['_tb_type'] == "_tb_group":
|
||||
node.name = "group_" + str(properties['_tb_id'])
|
||||
elif properties['_tb_type'] == "_tb_layer":
|
||||
node.name = "layer_" + str(properties['_tb_layer_sort_index'])
|
||||
if properties['_tb_name'] != "Unnamed":
|
||||
node.name = node.name + "_" + properties['_tb_name']
|
||||
parent_entities[node_idx] = node
|
||||
|
||||
var child_to_parent_map: Dictionary = {}
|
||||
|
||||
#For each child,...
|
||||
for node_idx in child_entities:
|
||||
var node: Node = child_entities[node_idx]
|
||||
var properties: Dictionary = entity_dicts[node_idx]['properties']
|
||||
var tb_group: Variant = null
|
||||
if '_tb_group' in properties:
|
||||
tb_group = properties['_tb_group']
|
||||
elif '_tb_layer' in properties:
|
||||
tb_group = properties['_tb_layer']
|
||||
if tb_group == null:
|
||||
continue
|
||||
|
||||
var parent: Node = null
|
||||
var parent_properties: Dictionary = {}
|
||||
var parent_entity = null
|
||||
var parent_idx = null
|
||||
|
||||
#...identify its direct parent out of the parent_entities array
|
||||
for possible_parent in parent_entities:
|
||||
parent_entity = parent_entities[possible_parent]
|
||||
parent_properties = entity_dicts[possible_parent]['properties']
|
||||
|
||||
if parent_properties['_tb_id'] == tb_group:
|
||||
if '_tb_layer_omit_from_export' in parent_properties:
|
||||
properties['_tb_layer_omit_from_export'] = parent_properties['_tb_layer_omit_from_export']
|
||||
parent = parent_entity
|
||||
parent_idx = possible_parent
|
||||
break
|
||||
#if there's a match, pass it on to the child-parent relationship map
|
||||
if parent:
|
||||
child_to_parent_map[node_idx] = parent_idx
|
||||
|
||||
for child_idx in child_to_parent_map:
|
||||
var child = entity_nodes[child_idx]
|
||||
var parent_idx = child_to_parent_map[child_idx]
|
||||
var parent = entity_nodes[parent_idx]
|
||||
queue_add_child(parent, child, null, true)
|
||||
|
||||
## Add a child and its new parent to the add child queue. If [code]below[/code] is a node, add it as a child to that instead. If [code]relative[/code] is true, set the location of node relative to parent.
|
||||
func queue_add_child(parent, node, below = null, relative = false) -> void:
|
||||
add_child_array.append({"parent": parent, "node": node, "below": below, "relative": relative})
|
||||
|
||||
## Assign children to parents based on the contents of the add child queue (see [method queue_add_child])
|
||||
func add_children() -> void:
|
||||
while true:
|
||||
for i in range(0, set_owner_batch_size):
|
||||
if add_child_array.size() > 0:
|
||||
var data: Dictionary = add_child_array.pop_front()
|
||||
if data:
|
||||
add_child_editor(data['parent'], data['node'], data['below'])
|
||||
if data['relative']:
|
||||
if (data['node'] is Node3D and data['parent'] is Node3D) or (data['node'] is Node2D and data['parent'] is Node2D):
|
||||
data['node'].position -= data['parent'].position
|
||||
continue
|
||||
add_children_complete()
|
||||
return
|
||||
|
||||
var scene_tree: SceneTree = get_tree()
|
||||
if scene_tree and not block_until_complete:
|
||||
await get_tree().create_timer(YIELD_DURATION).timeout
|
||||
|
||||
## Set owners and start post-attach build steps
|
||||
func add_children_complete() -> void:
|
||||
stop_profile('add_children')
|
||||
|
||||
if should_set_owners:
|
||||
start_profile('set_owners')
|
||||
set_owners()
|
||||
else:
|
||||
run_build_steps(true)
|
||||
|
||||
## Set owner of nodes generated by FuncGodot to scene root based on [member set_owner_array]
|
||||
func set_owners() -> void:
|
||||
while true:
|
||||
for i in range(0, set_owner_batch_size):
|
||||
var node: Node = set_owner_array.pop_front()
|
||||
if node:
|
||||
set_owner_editor(node)
|
||||
else:
|
||||
set_owners_complete()
|
||||
return
|
||||
|
||||
var scene_tree: SceneTree = get_tree()
|
||||
if scene_tree and not block_until_complete:
|
||||
await get_tree().create_timer(YIELD_DURATION).timeout
|
||||
|
||||
## Finish profiling for set_owners and start post-attach build steps
|
||||
func set_owners_complete() -> void:
|
||||
stop_profile('set_owners')
|
||||
run_build_steps(true)
|
||||
|
||||
## Apply Map File properties to [Node3D] instances, transferring Map File dictionaries to [Node3D.func_godot_properties]
|
||||
## and then calling the appropriate callbacks.
|
||||
func apply_properties_and_finish() -> void:
|
||||
# Array of all entities' properties
|
||||
var properties_arr: Array[Dictionary] = []
|
||||
|
||||
for entity_idx in range(0, entity_nodes.size()):
|
||||
var entity_node: Node = entity_nodes[entity_idx] as Node
|
||||
if not entity_node:
|
||||
continue
|
||||
|
||||
var entity_dict: Dictionary = entity_dicts[entity_idx] as Dictionary
|
||||
var properties: Dictionary = entity_dict['properties'] as Dictionary
|
||||
|
||||
if '_tb_layer_omit_from_export' in properties and properties['_tb_layer_omit_from_export'] == "1":
|
||||
entity_node.queue_free()
|
||||
properties_arr.append({})
|
||||
continue
|
||||
|
||||
if 'classname' in properties:
|
||||
var classname: String = properties['classname']
|
||||
if classname in entity_definitions:
|
||||
var entity_definition: FuncGodotFGDEntityClass = entity_definitions[classname] as FuncGodotFGDEntityClass
|
||||
|
||||
for property in properties:
|
||||
var prop_string = properties[property]
|
||||
if property in entity_definition.class_properties:
|
||||
var prop_default: Variant = entity_definition.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 \'" + 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 \'" + 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 \'" + classname + "\': " + prop_string)
|
||||
properties[property] = prop_color
|
||||
TYPE_DICTIONARY:
|
||||
var prop_desc = entity_definition.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 \'" + 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 \'" + 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 \'" + 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 \'" + classname + "\': " + prop_string)
|
||||
properties[property] = prop_vec
|
||||
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 entity_definitions[classname].class_properties:
|
||||
if not property in properties:
|
||||
var prop_default: Variant = entity_definition.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 = entity_definition.class_property_descriptions[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]
|
||||
else:
|
||||
properties[property] = 0
|
||||
elif prop_default is Resource:
|
||||
properties[property] = prop_default.resource_path
|
||||
# Everything else
|
||||
else:
|
||||
properties[property] = prop_default
|
||||
|
||||
if 'func_godot_properties' in entity_node:
|
||||
entity_node.func_godot_properties = properties
|
||||
|
||||
properties_arr.append(properties.duplicate(true))
|
||||
|
||||
for entity_idx in range(0, entity_nodes.size()):
|
||||
var entity_node: Node = entity_nodes[entity_idx] as Node
|
||||
if entity_node and entity_node.has_method("_func_godot_apply_properties"):
|
||||
entity_node._func_godot_apply_properties(properties_arr[entity_idx])
|
||||
|
||||
for entity_idx in range(0, entity_nodes.size()):
|
||||
var entity_node: Node = entity_nodes[entity_idx] as Node
|
||||
if entity_node and entity_node.has_method("_func_godot_build_complete"):
|
||||
entity_node.call_deferred("_func_godot_build_complete")
|
||||
|
||||
# Cleanup after build is finished (internal)
|
||||
func _build_complete():
|
||||
reset_build_context()
|
||||
stop_profile('build_map')
|
||||
print('Build complete\n')
|
||||
emit_signal("build_complete")
|
79
addons/func_godot/src/map/func_godot_map_settings.gd
Normal file
@ -0,0 +1,79 @@
|
||||
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
||||
## 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.
|
||||
@export var inverse_scale_factor: float = 32.0
|
||||
|
||||
## [FuncGodotFGDFile] that translates map file classnames into Godot nodes and packed scenes.
|
||||
@export var entity_fgd: FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
|
||||
|
||||
## Default class property to use in naming generated nodes. This setting is overridden by `name_property` in [FuncGodotFGDEntityClass].
|
||||
## Naming occurs before adding to the [SceneTree] and applying properties.
|
||||
## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior.
|
||||
@export var entity_name_property: String = ""
|
||||
|
||||
@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.
|
||||
@export_dir var base_texture_dir: String = "res://textures"
|
||||
|
||||
## File extensions to search for texture data.
|
||||
@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].
|
||||
@export var clip_texture: String = "special/clip"
|
||||
|
||||
## 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].
|
||||
@export var skip_texture: String = "special/skip"
|
||||
|
||||
## 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[Resource] = []
|
||||
|
||||
@export_category("Materials")
|
||||
|
||||
## File extension to search for [Material] definitions
|
||||
@export var material_file_extension: String = "tres"
|
||||
|
||||
## [Material] used as template when generating missing materials.
|
||||
@export var default_material: Material = preload("res://addons/func_godot/textures/default_material.tres")
|
||||
|
||||
## Sampler2D uniform that supplies the Albedo in a custom shader when [member default_material] is a [ShaderMaterial].
|
||||
@export var default_material_albedo_uniform: String = ""
|
||||
|
||||
## Automatic PBR material generation albedo map pattern.
|
||||
@export var albedo_map_pattern: String = "%s_albedo.%s"
|
||||
## Automatic PBR material generation normal map pattern.
|
||||
@export var normal_map_pattern: String = "%s_normal.%s"
|
||||
## Automatic PBR material generation metallic map pattern
|
||||
@export var metallic_map_pattern: String = "%s_metallic.%s"
|
||||
## Automatic PBR material generation roughness map pattern
|
||||
@export var roughness_map_pattern: String = "%s_roughness.%s"
|
||||
## Automatic PBR material generation emission map pattern
|
||||
@export var emission_map_pattern: String = "%s_emission.%s"
|
||||
## Automatic PBR material generation ambient occlusion map pattern
|
||||
@export var ao_map_pattern: String = "%s_ao.%s"
|
||||
## Automatic PBR material generation height map pattern
|
||||
@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".
|
||||
@export var unshaded: bool = false
|
||||
|
||||
## 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_category("UV Unwrap")
|
||||
|
||||
## Texel size for UV2 unwrapping.
|
||||
## Actual texel size is uv_unwrap_texel_size / inverse_scale_factor. A ratio of 1/16 is usually a good place to start with (if inverse_scale_factor is 32, start with a uv_unwrap_texel_size of 2).
|
||||
## Larger values will produce less detailed lightmaps. To conserve memory and filesize, use the largest value that still looks good.
|
||||
@export var uv_unwrap_texel_size: float = 2.0
|
||||
|
||||
@export_category("TrenchBroom")
|
||||
|
||||
## 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
|
@ -0,0 +1,273 @@
|
||||
@tool
|
||||
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
||||
## Builds a gamepack for NetRadiant Custom.
|
||||
class_name NetRadiantCustomGamePackConfig
|
||||
extends Resource
|
||||
|
||||
## Button to export / update this gamepack's configuration in the NetRadiant Custom Gamepacks Folder.
|
||||
@export var export_file: bool:
|
||||
get:
|
||||
return export_file
|
||||
set(new_export_file):
|
||||
if new_export_file != 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.
|
||||
@export var gamepack_name : String = "func_godot"
|
||||
|
||||
## Name of the game in NetRadiant Custom's gamepack list.
|
||||
@export var game_name : String = "FuncGodot"
|
||||
|
||||
## Directory path containing your maps, textures, shaders, etc... relative to your project directory.
|
||||
@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.
|
||||
@export var fgd_file : FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
|
||||
|
||||
## [NetRadiantCustomShader] resources for shader file generation.
|
||||
@export var netradiant_custom_shaders : Array[Resource] = [
|
||||
preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres"),
|
||||
preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres")
|
||||
]
|
||||
|
||||
## Supported texture file types.
|
||||
@export var texture_types : PackedStringArray = ["png", "jpg", "jpeg", "bmp", "tga"]
|
||||
|
||||
## Supported model file types.
|
||||
@export var model_types : PackedStringArray = ["glb", "gltf", "obj"]
|
||||
|
||||
## Supported audio file types.
|
||||
@export var sound_types : PackedStringArray = ["wav", "ogg"]
|
||||
|
||||
## Default scale of textures in NetRadiant Custom.
|
||||
@export var default_scale : String = "1.0"
|
||||
|
||||
## Clip texture path that gets applied to weapclip and nodraw shaders.
|
||||
@export var clip_texture: String = "textures/special/clip"
|
||||
|
||||
## Skip texture path that gets applied to caulk and nodrawnonsolid shaders.
|
||||
@export var skip_texture: String = "textures/special/skip"
|
||||
|
||||
## 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_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 Radiant.[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_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 = ""
|
||||
for shader_res in netradiant_custom_shaders:
|
||||
shader_text += (shader_res as NetRadiantCustomShader).texture_path + "\n{\n"
|
||||
for shader_attrib in (shader_res as NetRadiantCustomShader).shader_attributes:
|
||||
shader_text += "\t" + shader_attrib + "\n"
|
||||
shader_text += "}\n"
|
||||
return shader_text
|
||||
|
||||
## Generates completed text for a .gamepack file.
|
||||
func build_gamepack_text() -> String:
|
||||
var texturetypes_str: String = ""
|
||||
for texture_type in texture_types:
|
||||
texturetypes_str += texture_type
|
||||
if texture_type != texture_types[-1]:
|
||||
texturetypes_str += " "
|
||||
|
||||
var modeltypes_str: String = ""
|
||||
for model_type in model_types:
|
||||
modeltypes_str += model_type
|
||||
if model_type != model_types[-1]:
|
||||
modeltypes_str += " "
|
||||
|
||||
var soundtypes_str: String = ""
|
||||
for sound_type in sound_types:
|
||||
soundtypes_str += sound_type
|
||||
if sound_type != sound_types[-1]:
|
||||
soundtypes_str += " "
|
||||
|
||||
var gamepack_text: String = """<?xml version="1.0"?>
|
||||
<game
|
||||
type="q3"
|
||||
index="1"
|
||||
name="%s"
|
||||
enginepath_win32="C:/%s/"
|
||||
engine_win32="%s.exe"
|
||||
enginepath_linux="/usr/local/games/%s/"
|
||||
engine_linux="%s"
|
||||
basegame="%s"
|
||||
basegamename="%s"
|
||||
unknowngamename="Custom %s modification"
|
||||
shaderpath="scripts"
|
||||
archivetypes="pk3"
|
||||
texturetypes="%s"
|
||||
modeltypes="%s"
|
||||
soundtypes="%s"
|
||||
maptypes="mapq1"
|
||||
shaders="quake3"
|
||||
entityclass="halflife"
|
||||
entityclasstype="fgd"
|
||||
entities="quake"
|
||||
brushtypes="quake"
|
||||
patchtypes="quake3"
|
||||
q3map2_type="quake3"
|
||||
default_scale="%s"
|
||||
shader_weapclip="%s"
|
||||
shader_caulk="%s"
|
||||
shader_nodraw="%s"
|
||||
shader_nodrawnonsolid="%s"
|
||||
common_shaders_name="Common"
|
||||
common_shaders_dir="common/"
|
||||
/>
|
||||
"""
|
||||
|
||||
return gamepack_text % [
|
||||
game_name,
|
||||
game_name,
|
||||
gamepack_name,
|
||||
game_name,
|
||||
gamepack_name,
|
||||
base_game_path,
|
||||
game_name,
|
||||
game_name,
|
||||
texturetypes_str,
|
||||
modeltypes_str,
|
||||
soundtypes_str,
|
||||
default_scale,
|
||||
clip_texture,
|
||||
skip_texture,
|
||||
clip_texture,
|
||||
skip_texture
|
||||
]
|
||||
|
||||
## Exports or updates a folder in the /games directory, with an icon, .cfg, and all accompanying FGDs.
|
||||
func do_export_file() -> void:
|
||||
if (FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.MAP_EDITOR_GAME_PATH) as String).is_empty():
|
||||
printerr("Skipping export: Map Editor Game Path not set in Project Configuration")
|
||||
return
|
||||
|
||||
var gamepacks_folder: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.NETRADIANT_CUSTOM_GAMEPACKS_FOLDER) as String
|
||||
if gamepacks_folder.is_empty():
|
||||
printerr("Skipping export: No NetRadiant Custom gamepacks folder")
|
||||
return
|
||||
|
||||
# Make sure FGD file is set
|
||||
if !fgd_file:
|
||||
printerr("Skipping export: No FGD file")
|
||||
return
|
||||
|
||||
# Make sure we're actually in the NetRadiant Custom gamepacks folder
|
||||
if DirAccess.open(gamepacks_folder + "/games") == null:
|
||||
printerr("Skipping export: No \'games\' folder. Is this the NetRadiant Custom gamepacks folder?")
|
||||
return
|
||||
|
||||
# Create gamepack folders in case they do not exist
|
||||
var gamepack_dir_paths: Array = [
|
||||
gamepacks_folder + "/" + gamepack_name + ".game",
|
||||
gamepacks_folder + "/" + gamepack_name + ".game/" + base_game_path,
|
||||
gamepacks_folder + "/" + gamepack_name + ".game/scripts"
|
||||
]
|
||||
var err: Error
|
||||
|
||||
for path in gamepack_dir_paths:
|
||||
if DirAccess.open(path) == null:
|
||||
print("Couldn't open " + path + ", creating...")
|
||||
err = DirAccess.make_dir_recursive_absolute(path)
|
||||
if err != OK:
|
||||
printerr("Skipping export: Failed to create directory")
|
||||
return
|
||||
|
||||
var target_file_path: String
|
||||
var file: FileAccess
|
||||
|
||||
# .gamepack
|
||||
target_file_path = gamepacks_folder + "/games/" + gamepack_name + ".game"
|
||||
print("Exporting NetRadiant Custom Gamepack to ", target_file_path)
|
||||
file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
||||
if file != null:
|
||||
file.store_string(build_gamepack_text())
|
||||
file.close()
|
||||
else:
|
||||
printerr("Error: Could not modify " + target_file_path)
|
||||
|
||||
# .shader
|
||||
target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/" + gamepack_name + ".shader"
|
||||
print("Exporting NetRadiant Custom Shader to ", target_file_path)
|
||||
file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
||||
if file != null:
|
||||
file.store_string(build_shader_text())
|
||||
file.close()
|
||||
else:
|
||||
printerr("Error: Could not modify " + target_file_path)
|
||||
|
||||
# shaderlist.txt
|
||||
target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/shaderlist.txt"
|
||||
print("Exporting NetRadiant Custom Default Buld Menu 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)
|
||||
|
||||
# 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)
|
||||
file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
||||
|
||||
if file != null:
|
||||
file.store_string("<?xml version=\"1.0\"?>\n<project version=\"2.0\">\n")
|
||||
|
||||
for key in default_build_menu_variables.keys():
|
||||
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
|
||||
var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
|
||||
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")
|
@ -0,0 +1,10 @@
|
||||
@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
|
||||
extends Resource
|
||||
|
||||
## Path to texture without extension, eg: `textures/special/clip`.
|
||||
@export var texture_path: String
|
||||
|
||||
## Array of shader properties to apply to faces using [member texture_path].
|
||||
@export var shader_attributes : Array[String] = ["qer_trans 0.4"]
|
221
addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd
Normal file
@ -0,0 +1,221 @@
|
||||
@tool
|
||||
@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
|
||||
|
||||
## Button to export / update this game's configuration and FGD file in the TrenchBroom Games Path.
|
||||
@export var export_file: bool:
|
||||
get:
|
||||
return export_file
|
||||
set(new_export_file):
|
||||
if new_export_file != export_file:
|
||||
if Engine.is_editor_hint():
|
||||
do_export_file()
|
||||
|
||||
## Name of the game in TrenchBroom's game list.
|
||||
@export var game_name : String = "FuncGodot"
|
||||
|
||||
## Icon for TrenchBroom's game list.
|
||||
@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.
|
||||
@export var map_formats: Array[Dictionary] = [
|
||||
{ "format": "Valve", "initialmap": "initial_valve.map" },
|
||||
{ "format": "Standard", "initialmap": "initial_standard.map" },
|
||||
{ "format": "Quake2", "initialmap": "initial_quake2.map" },
|
||||
{ "format": "Quake3" }
|
||||
]
|
||||
|
||||
## Textures matching these patterns will be hidden from TrenchBroom.
|
||||
@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.
|
||||
@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.
|
||||
@export var entity_scale: String = "32"
|
||||
|
||||
## Scale of textures on new brushes.
|
||||
@export var default_uv_scale : Vector2 = Vector2(1, 1)
|
||||
|
||||
## Arrays containing the TrenchBroomTag resource type.
|
||||
@export_category("Editor Hint Tags")
|
||||
|
||||
## TrenchBroomTag resources that apply to brush entities.
|
||||
@export var brush_tags : Array[Resource] = []
|
||||
|
||||
## TrenchBroomTag resources that apply to brush faces.
|
||||
@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_skip.tres")
|
||||
]
|
||||
|
||||
## 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:
|
||||
TrenchBroomTag.TagMatchType.TEXTURE:
|
||||
return "texture"
|
||||
TrenchBroomTag.TagMatchType.CLASSNAME:
|
||||
return "classname"
|
||||
_:
|
||||
push_error("Tag match type %s is not valid" % [tag_match_type])
|
||||
return "ERROR"
|
||||
|
||||
## Generates completed text for a .cfg file.
|
||||
func build_class_text() -> String:
|
||||
var map_formats_str : String = ""
|
||||
for map_format in map_formats:
|
||||
map_formats_str += "{ \"format\": \"" + map_format.format + "\""
|
||||
if map_format.has("initialmap"):
|
||||
map_formats_str += ", \"initialmap\": \"" + map_format.initialmap + "\""
|
||||
if map_format != map_formats[-1]:
|
||||
map_formats_str += " },\n\t\t"
|
||||
else:
|
||||
map_formats_str += " }"
|
||||
|
||||
var texture_exclusion_patterns_str := ""
|
||||
for tex_pattern in texture_exclusion_patterns:
|
||||
texture_exclusion_patterns_str += "\"" + tex_pattern + "\""
|
||||
if tex_pattern != texture_exclusion_patterns[-1]:
|
||||
texture_exclusion_patterns_str += ", "
|
||||
|
||||
var fgd_filename_str : String = "\"" + fgd_file.fgd_name + ".fgd\""
|
||||
|
||||
var brush_tags_str = parse_tags(brush_tags)
|
||||
var brushface_tags_str = parse_tags(brushface_tags)
|
||||
var uv_scale_str = parse_default_uv_scale(default_uv_scale)
|
||||
|
||||
var config_text : String = """{
|
||||
"version": 8,
|
||||
"name": "%s",
|
||||
"icon": "icon.png",
|
||||
"fileformats": [
|
||||
%s
|
||||
],
|
||||
"filesystem": {
|
||||
"searchpath": ".",
|
||||
"packageformat": { "extension": ".zip", "format": "zip" }
|
||||
},
|
||||
"textures": {
|
||||
"root": "textures",
|
||||
"extensions": [".bmp", ".exr", ".hdr", ".jpeg", ".jpg", ".png", ".tga", ".webp"],
|
||||
"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.
|
||||
func parse_tags(tags: Array) -> String:
|
||||
var tags_str := ""
|
||||
for brush_tag in tags:
|
||||
if brush_tag.tag_match_type >= TrenchBroomTag.TagMatchType.size():
|
||||
continue
|
||||
tags_str += "{\n"
|
||||
tags_str += "\t\t\t\t\"name\": \"%s\",\n" % brush_tag.tag_name
|
||||
var attribs_str := ""
|
||||
for brush_tag_attrib in brush_tag.tag_attributes:
|
||||
attribs_str += "\"%s\"" % brush_tag_attrib
|
||||
if brush_tag_attrib != brush_tag.tag_attributes[-1]:
|
||||
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\"pattern\": \"%s\"" % brush_tag.tag_pattern
|
||||
if brush_tag.texture_name != "":
|
||||
tags_str += ",\n"
|
||||
tags_str += "\t\t\t\t\"texture\": \"%s\"" % brush_tag.texture_name
|
||||
tags_str += "\n"
|
||||
tags_str += "\t\t\t}"
|
||||
if brush_tag != tags[-1]:
|
||||
tags_str += ","
|
||||
return tags_str
|
||||
|
||||
## Converts array of flags to .cfg String.
|
||||
func parse_flags(flags: Array) -> String:
|
||||
var flags_str := ""
|
||||
for attrib_flag in flags:
|
||||
flags_str += "{\n"
|
||||
flags_str += "\t\t\t\t\"name\": \"%s\",\n" % attrib_flag.attrib_name
|
||||
flags_str += "\t\t\t\t\"description\": \"%s\"\n" % attrib_flag.attrib_description
|
||||
flags_str += "\t\t\t}"
|
||||
if attrib_flag != flags[-1]:
|
||||
flags_str += ","
|
||||
return flags_str
|
||||
|
||||
## Converts default uv scale vector to .cfg String.
|
||||
func parse_default_uv_scale(texture_scale : Vector2) -> String:
|
||||
var entry_str = "\"scale\": [{x}, {y}]"
|
||||
return entry_str.format({
|
||||
"x": texture_scale.x,
|
||||
"y": texture_scale.y
|
||||
})
|
||||
|
||||
## Exports or updates a folder in the /games directory, with an icon, .cfg, and all accompanying FGDs.
|
||||
func do_export_file() -> void:
|
||||
var config_folder: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.TRENCHBROOM_GAME_CONFIG_FOLDER) as String
|
||||
if config_folder.is_empty():
|
||||
printerr("Skipping export: No TrenchBroom Game folder")
|
||||
return
|
||||
|
||||
# Make sure FGD file is set
|
||||
if !fgd_file:
|
||||
printerr("Skipping export: No FGD file")
|
||||
return
|
||||
|
||||
var config_dir := DirAccess.open(config_folder)
|
||||
# Create config folder in case it does not exist
|
||||
if config_dir == null:
|
||||
print("Couldn't open directory, creating...")
|
||||
var err := DirAccess.make_dir_recursive_absolute(config_folder)
|
||||
if err != OK:
|
||||
printerr("Skipping export: Failed to create directory")
|
||||
return
|
||||
|
||||
# Icon
|
||||
var icon_path : String = config_folder + "/icon.png"
|
||||
print("Exporting icon to ", icon_path)
|
||||
var export_icon : Image = icon.get_image()
|
||||
export_icon.resize(32, 32, Image.INTERPOLATE_LANCZOS)
|
||||
export_icon.save_png(icon_path)
|
||||
|
||||
# .cfg
|
||||
var target_file_path: String = config_folder + "/GameConfig.cfg"
|
||||
print("Exporting TrenchBroom Game Config to ", target_file_path)
|
||||
var file = FileAccess.open(target_file_path, FileAccess.WRITE)
|
||||
file.store_string(build_class_text())
|
||||
file.close()
|
||||
|
||||
# FGD
|
||||
var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
|
||||
export_fgd.do_export_file(FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM, config_folder)
|
||||
print("TrenchBroom Game Config export complete\n")
|
26
addons/func_godot/src/trenchbroom/trenchbroom_tag.gd
Normal file
@ -0,0 +1,26 @@
|
||||
@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.
|
||||
## 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.
|
||||
class_name TrenchBroomTag
|
||||
extends Resource
|
||||
|
||||
enum TagMatchType {
|
||||
TEXTURE, ## Tag applies to any brush face with a texture matching the texture name.
|
||||
CLASSNAME ## Tag applies to any brush entity with a class name matching the tag pattern.
|
||||
}
|
||||
|
||||
## Name to define this tag. Not used as the matching pattern.
|
||||
@export var tag_name: String
|
||||
|
||||
## The attributes applied to matching faces or brush entities. Only "_transparent" is supported in TrenchBroom, which makes matching faces or brush entities transparent.
|
||||
@export var tag_attributes : Array[String] = ["transparent"]
|
||||
|
||||
## Determines how the tag is matched. See [constant 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.
|
||||
## [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
|
||||
|
||||
## A string that filters which textures recieve these attributes. Only used with a [constant TagMatchType] of [i]Texture[/i].
|
||||
@export var texture_name: String
|
142
addons/func_godot/src/util/func_godot_local_config.gd
Normal file
@ -0,0 +1,142 @@
|
||||
@tool
|
||||
@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
|
||||
## Local machine project wide settings. Can define global defaults for some FuncGodot properties.
|
||||
## DO NOT CREATE A NEW RESOURCE! This resource works by saving a configuration file to your game's *user://* folder and pulling the properties from that config file rather than this resource.
|
||||
## Use the premade `addons/func_godot/func_godot_local_config.tres` instead.
|
||||
class_name FuncGodotLocalConfig
|
||||
extends Resource
|
||||
|
||||
enum PROPERTY {
|
||||
FGD_OUTPUT_FOLDER,
|
||||
TRENCHBROOM_GAME_CONFIG_FOLDER,
|
||||
NETRADIANT_CUSTOM_GAMEPACKS_FOLDER,
|
||||
MAP_EDITOR_GAME_PATH,
|
||||
GAME_PATH_MODELS_FOLDER,
|
||||
DEFAULT_INVERSE_SCALE
|
||||
}
|
||||
|
||||
@export var export_func_godot_settings: bool: set = _save_settings
|
||||
@export var reload_func_godot_settings: bool = false :
|
||||
set(value):
|
||||
_load_settings()
|
||||
|
||||
const CONFIG_PROPERTIES: Array[Dictionary] = [
|
||||
{
|
||||
"name": "fgd_output_folder",
|
||||
"usage": PROPERTY_USAGE_EDITOR,
|
||||
"type": TYPE_STRING,
|
||||
"hint": PROPERTY_HINT_GLOBAL_DIR,
|
||||
"func_godot_type": PROPERTY.FGD_OUTPUT_FOLDER
|
||||
},
|
||||
{
|
||||
"name": "trenchbroom_game_config_folder",
|
||||
"usage": PROPERTY_USAGE_EDITOR,
|
||||
"type": TYPE_STRING,
|
||||
"hint": PROPERTY_HINT_GLOBAL_DIR,
|
||||
"func_godot_type": PROPERTY.TRENCHBROOM_GAME_CONFIG_FOLDER
|
||||
},
|
||||
{
|
||||
"name": "netradiant_custom_gamepacks_folder",
|
||||
"usage": PROPERTY_USAGE_EDITOR,
|
||||
"type": TYPE_STRING,
|
||||
"hint": PROPERTY_HINT_GLOBAL_DIR,
|
||||
"func_godot_type": PROPERTY.NETRADIANT_CUSTOM_GAMEPACKS_FOLDER
|
||||
},
|
||||
{
|
||||
"name": "map_editor_game_path",
|
||||
"usage": PROPERTY_USAGE_EDITOR,
|
||||
"type": TYPE_STRING,
|
||||
"hint": PROPERTY_HINT_GLOBAL_DIR,
|
||||
"func_godot_type": PROPERTY.MAP_EDITOR_GAME_PATH
|
||||
},
|
||||
{
|
||||
"name": "game_path_models_folder",
|
||||
"usage": PROPERTY_USAGE_EDITOR,
|
||||
"type": TYPE_STRING,
|
||||
"func_godot_type": PROPERTY.GAME_PATH_MODELS_FOLDER
|
||||
},
|
||||
{
|
||||
"name": "default_inverse_scale_factor",
|
||||
"usage": PROPERTY_USAGE_EDITOR,
|
||||
"type": TYPE_FLOAT,
|
||||
"func_godot_type": PROPERTY.DEFAULT_INVERSE_SCALE
|
||||
}
|
||||
]
|
||||
|
||||
var settings_dict: Dictionary
|
||||
var loaded := false
|
||||
|
||||
static func get_setting(name: PROPERTY) -> Variant:
|
||||
var settings = load("res://addons/func_godot/func_godot_local_config.tres")
|
||||
if not settings.loaded:
|
||||
settings._load_settings()
|
||||
return settings.settings_dict.get(PROPERTY.keys()[name], '') as Variant
|
||||
|
||||
func _get_property_list() -> Array:
|
||||
return CONFIG_PROPERTIES.duplicate()
|
||||
|
||||
func _get(property: StringName) -> Variant:
|
||||
var config = _get_config_property(property)
|
||||
if config == null and not config is Dictionary:
|
||||
return null
|
||||
_try_loading()
|
||||
return settings_dict.get(PROPERTY.keys()[config['func_godot_type']], _get_default_value(config['type']))
|
||||
|
||||
func _set(property: StringName, value: Variant) -> bool:
|
||||
var config = _get_config_property(property)
|
||||
if config == null and not config is Dictionary:
|
||||
return false
|
||||
settings_dict[PROPERTY.keys()[config['func_godot_type']]] = value
|
||||
return true
|
||||
|
||||
func _get_default_value(type) -> Variant:
|
||||
match type:
|
||||
TYPE_STRING: return ''
|
||||
TYPE_INT: return 0
|
||||
TYPE_FLOAT: return 0.0
|
||||
TYPE_BOOL: return false
|
||||
TYPE_VECTOR2: return Vector2.ZERO
|
||||
TYPE_VECTOR3: return Vector3.ZERO
|
||||
TYPE_ARRAY: return []
|
||||
TYPE_DICTIONARY: return {}
|
||||
push_error("Invalid setting type. Returning null")
|
||||
return null
|
||||
|
||||
func _get_config_property(name: StringName) -> Variant:
|
||||
for config in CONFIG_PROPERTIES:
|
||||
if config['name'] == name:
|
||||
return config
|
||||
return null
|
||||
|
||||
func _load_settings() -> void:
|
||||
loaded = true
|
||||
var path = _get_path()
|
||||
if not FileAccess.file_exists(path):
|
||||
return
|
||||
var settings = FileAccess.get_file_as_string(path)
|
||||
settings_dict = {}
|
||||
if not settings or settings.is_empty():
|
||||
return
|
||||
settings = JSON.parse_string(settings)
|
||||
for key in settings.keys():
|
||||
settings_dict[key] = settings[key]
|
||||
notify_property_list_changed()
|
||||
|
||||
func _try_loading() -> void:
|
||||
if not loaded:
|
||||
_load_settings()
|
||||
|
||||
func _save_settings(_s = null) -> void:
|
||||
if settings_dict.size() == 0:
|
||||
return
|
||||
var path = _get_path()
|
||||
var file = FileAccess.open(path, FileAccess.WRITE)
|
||||
var json = JSON.stringify(settings_dict)
|
||||
file.store_line(json)
|
||||
loaded = false
|
||||
print("Saved settings to ", path)
|
||||
|
||||
func _get_path() -> String:
|
||||
var application_name: String = ProjectSettings.get('application/config/name')
|
||||
application_name = application_name.replace(" ", "_")
|
||||
return 'user://' + application_name + '_FuncGodotConfig.json'
|
190
addons/func_godot/src/util/func_godot_texture_loader.gd
Normal file
@ -0,0 +1,190 @@
|
||||
class_name FuncGodotTextureLoader
|
||||
|
||||
enum PBRSuffix {
|
||||
ALBEDO,
|
||||
NORMAL,
|
||||
METALLIC,
|
||||
ROUGHNESS,
|
||||
EMISSION,
|
||||
AO,
|
||||
HEIGHT,
|
||||
ORM
|
||||
}
|
||||
|
||||
# Suffix string / Godot enum / StandardMaterial3D property
|
||||
const PBR_SUFFIX_NAMES: Dictionary = {
|
||||
PBRSuffix.ALBEDO: 'albedo',
|
||||
PBRSuffix.NORMAL: 'normal',
|
||||
PBRSuffix.METALLIC: 'metallic',
|
||||
PBRSuffix.ROUGHNESS: 'roughness',
|
||||
PBRSuffix.EMISSION: 'emission',
|
||||
PBRSuffix.AO: 'ao',
|
||||
PBRSuffix.HEIGHT: 'height',
|
||||
PBRSuffix.ORM: 'orm'
|
||||
}
|
||||
|
||||
const PBR_SUFFIX_PATTERNS: Dictionary = {
|
||||
PBRSuffix.ALBEDO: '%s_albedo.%s',
|
||||
PBRSuffix.NORMAL: '%s_normal.%s',
|
||||
PBRSuffix.METALLIC: '%s_metallic.%s',
|
||||
PBRSuffix.ROUGHNESS: '%s_roughness.%s',
|
||||
PBRSuffix.EMISSION: '%s_emission.%s',
|
||||
PBRSuffix.AO: '%s_ao.%s',
|
||||
PBRSuffix.HEIGHT: '%s_height.%s',
|
||||
PBRSuffix.ORM: '%s_orm.%s'
|
||||
}
|
||||
|
||||
var PBR_SUFFIX_TEXTURES: Dictionary = {
|
||||
PBRSuffix.ALBEDO: StandardMaterial3D.TEXTURE_ALBEDO,
|
||||
PBRSuffix.NORMAL: StandardMaterial3D.TEXTURE_NORMAL,
|
||||
PBRSuffix.METALLIC: StandardMaterial3D.TEXTURE_METALLIC,
|
||||
PBRSuffix.ROUGHNESS: StandardMaterial3D.TEXTURE_ROUGHNESS,
|
||||
PBRSuffix.EMISSION: StandardMaterial3D.TEXTURE_EMISSION,
|
||||
PBRSuffix.AO: StandardMaterial3D.TEXTURE_AMBIENT_OCCLUSION,
|
||||
PBRSuffix.HEIGHT: StandardMaterial3D.TEXTURE_HEIGHTMAP,
|
||||
PBRSuffix.ORM: ORMMaterial3D.TEXTURE_ORM
|
||||
}
|
||||
|
||||
const PBR_SUFFIX_PROPERTIES: Dictionary = {
|
||||
PBRSuffix.NORMAL: 'normal_enabled',
|
||||
PBRSuffix.EMISSION: 'emission_enabled',
|
||||
PBRSuffix.AO: 'ao_enabled',
|
||||
PBRSuffix.HEIGHT: 'heightmap_enabled',
|
||||
}
|
||||
|
||||
var map_settings: FuncGodotMapSettings = FuncGodotMapSettings.new()
|
||||
var texture_wad_resources: Array = []
|
||||
|
||||
# Overrides
|
||||
func _init(new_map_settings: FuncGodotMapSettings) -> void:
|
||||
map_settings = new_map_settings
|
||||
load_texture_wad_resources()
|
||||
|
||||
# Business Logic
|
||||
func load_texture_wad_resources() -> void:
|
||||
texture_wad_resources.clear()
|
||||
for texture_wad in map_settings.texture_wads:
|
||||
if texture_wad and not texture_wad in texture_wad_resources:
|
||||
texture_wad_resources.append(texture_wad)
|
||||
|
||||
func load_textures(texture_list: Array) -> Dictionary:
|
||||
var texture_dict: Dictionary = {}
|
||||
for texture_name in texture_list:
|
||||
texture_dict[texture_name] = load_texture(texture_name)
|
||||
return texture_dict
|
||||
|
||||
func load_texture(texture_name: String) -> Texture2D:
|
||||
# Load albedo texture if it exists
|
||||
for texture_extension in map_settings.texture_file_extensions:
|
||||
var texture_path: String = "%s/%s.%s" % [map_settings.base_texture_dir, texture_name, texture_extension]
|
||||
if ResourceLoader.exists(texture_path, "Texture2D") or ResourceLoader.exists(texture_path + ".import", "Texture2D"):
|
||||
return load(texture_path) as Texture2D
|
||||
|
||||
var texture_name_lower: String = texture_name.to_lower()
|
||||
for texture_wad in texture_wad_resources:
|
||||
if texture_name_lower in texture_wad.textures:
|
||||
return texture_wad.textures[texture_name_lower]
|
||||
|
||||
return load("res://addons/func_godot/textures/default_texture.png") as Texture2D
|
||||
|
||||
func create_materials(texture_list: Array) -> Dictionary:
|
||||
var texture_materials: Dictionary = {}
|
||||
#prints("TEXLI", texture_list)
|
||||
for texture in texture_list:
|
||||
texture_materials[texture] = create_material(texture)
|
||||
return texture_materials
|
||||
|
||||
func create_material(texture_name: String) -> Material:
|
||||
# Autoload material if it exists
|
||||
var material_dict: Dictionary = {}
|
||||
|
||||
var material_path: String = "%s/%s.%s" % [map_settings.base_texture_dir, texture_name, map_settings.material_file_extension]
|
||||
if not material_path in material_dict and (FileAccess.file_exists(material_path) or FileAccess.file_exists(material_path + ".remap")):
|
||||
var loaded_material: Material = load(material_path)
|
||||
if loaded_material:
|
||||
material_dict[material_path] = loaded_material
|
||||
|
||||
# If material already exists, use it
|
||||
if material_path in material_dict:
|
||||
return material_dict[material_path]
|
||||
|
||||
var material: Material = null
|
||||
|
||||
if map_settings.default_material:
|
||||
material = map_settings.default_material.duplicate()
|
||||
else:
|
||||
material = StandardMaterial3D.new()
|
||||
var texture: Texture2D = load_texture(texture_name)
|
||||
if not texture:
|
||||
return material
|
||||
|
||||
if material is 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)
|
||||
elif material is ORMMaterial3D:
|
||||
material.set_texture(ORMMaterial3D.TEXTURE_ALBEDO, texture)
|
||||
|
||||
var pbr_textures : Dictionary = get_pbr_textures(texture_name)
|
||||
|
||||
for pbr_suffix in PBRSuffix.values():
|
||||
var suffix: int = pbr_suffix
|
||||
var tex: Texture2D = pbr_textures[suffix]
|
||||
if tex:
|
||||
if material is ShaderMaterial:
|
||||
material = StandardMaterial3D.new()
|
||||
material.set_texture(StandardMaterial3D.TEXTURE_ALBEDO, texture)
|
||||
var enable_prop: String = PBR_SUFFIX_PROPERTIES[suffix] if suffix in PBR_SUFFIX_PROPERTIES else ""
|
||||
if(enable_prop != ""):
|
||||
material.set(enable_prop, true)
|
||||
material.set_texture(PBR_SUFFIX_TEXTURES[suffix], tex)
|
||||
|
||||
material_dict[material_path] = material
|
||||
|
||||
if (map_settings.save_generated_materials and material
|
||||
and texture_name != map_settings.clip_texture
|
||||
and texture_name != map_settings.skip_texture
|
||||
and texture.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
|
40
addons/func_godot/src/util/func_godot_util.gd
Normal file
@ -0,0 +1,40 @@
|
||||
## General-purpose utility functions namespaced to FuncGodot for compatibility
|
||||
class_name FuncGodotUtil
|
||||
|
||||
## Print debug messages. True to print, false to ignore
|
||||
const DEBUG : bool = true
|
||||
|
||||
## Const-predicated print function to avoid excess log spam. Print msg if [constant DEBUG] is `true`.
|
||||
static func debug_print(msg) -> void:
|
||||
if(DEBUG):
|
||||
print(msg)
|
||||
|
||||
## Return a string that corresponds to the current OS's newline control character(s)
|
||||
static func newline() -> String:
|
||||
if OS.get_name() == "Windows":
|
||||
return "\r\n"
|
||||
else:
|
||||
return "\n"
|
||||
|
||||
## Create a dictionary suitable for creating a category with name when overriding [method Object._get_property_list]
|
||||
static func category_dict(name: String) -> Dictionary:
|
||||
return property_dict(name, TYPE_STRING, -1, "", PROPERTY_USAGE_CATEGORY)
|
||||
|
||||
## Creates a property with name and type from [enum @GlobalScope.Variant.Type].
|
||||
## Optionally, provide hint from [enum @GlobalScope.PropertyHint] and corresponding hint_string, and usage from [enum @GlobalScope.PropertyUsageFlags].
|
||||
static func property_dict(name: String, type: int, hint: int = -1, hint_string: String = "", usage: int = -1) -> Dictionary:
|
||||
var dict := {
|
||||
'name': name,
|
||||
'type': type
|
||||
}
|
||||
|
||||
if hint != -1:
|
||||
dict['hint'] = hint
|
||||
|
||||
if hint_string != "":
|
||||
dict['hint_string'] = hint_string
|
||||
|
||||
if usage != -1:
|
||||
dict['usage'] = usage
|
||||
|
||||
return dict
|
8
addons/func_godot/textures/default_material.tres
Normal file
@ -0,0 +1,8 @@
|
||||
[gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://cvex6toty8yn7"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://cyg2snr1w5xw5" path="res://addons/func_godot/textures/default_texture.png" id="1_ncj77"]
|
||||
|
||||
[resource]
|
||||
albedo_texture = ExtResource("1_ncj77")
|
||||
metallic_specular = 0.0
|
||||
texture_filter = 2
|
BIN
addons/func_godot/textures/default_texture.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
34
addons/func_godot/textures/default_texture.png.import
Normal file
@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cyg2snr1w5xw5"
|
||||
path="res://.godot/imported/default_texture.png-145fbd5fef7f63ace60797fecb133a19.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/func_godot/textures/default_texture.png"
|
||||
dest_files=["res://.godot/imported/default_texture.png-145fbd5fef7f63ace60797fecb133a19.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=3
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
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=0
|
BIN
addons/func_godot/textures/special/clip.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
34
addons/func_godot/textures/special/clip.png.import
Normal file
@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dhmu0toe1itnr"
|
||||
path="res://.godot/imported/clip.png-508a86fa3876d8467d5c9af6188a34df.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/func_godot/textures/special/clip.png"
|
||||
dest_files=["res://.godot/imported/clip.png-508a86fa3876d8467d5c9af6188a34df.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=3
|
||||
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
|
BIN
addons/func_godot/textures/special/skip.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
34
addons/func_godot/textures/special/skip.png.import
Normal file
@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bk5oo263y3u7w"
|
||||
path="res://.godot/imported/skip.png-d741e3eb75a5e289907774cb73d93931.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/func_godot/textures/special/skip.png"
|
||||
dest_files=["res://.godot/imported/skip.png-d741e3eb75a5e289907774cb73d93931.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=3
|
||||
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
|
8
gmtk24.csproj
Normal file
@ -0,0 +1,8 @@
|
||||
<Project Sdk="Godot.NET.Sdk/4.3.0">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net7.0</TargetFramework>
|
||||
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'ios' ">net8.0</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
</PropertyGroup>
|
||||
</Project>
|
19
gmtk24.sln
Normal file
@ -0,0 +1,19 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 2012
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gmtk24", "gmtk24.csproj", "{6BF26BA4-8B1D-4630-B304-3CC3AC40F736}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
ExportDebug|Any CPU = ExportDebug|Any CPU
|
||||
ExportRelease|Any CPU = ExportRelease|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{6BF26BA4-8B1D-4630-B304-3CC3AC40F736}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6BF26BA4-8B1D-4630-B304-3CC3AC40F736}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6BF26BA4-8B1D-4630-B304-3CC3AC40F736}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
|
||||
{6BF26BA4-8B1D-4630-B304-3CC3AC40F736}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
|
||||
{6BF26BA4-8B1D-4630-B304-3CC3AC40F736}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
|
||||
{6BF26BA4-8B1D-4630-B304-3CC3AC40F736}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
BIN
maps/demo.exr
Normal file
26
maps/demo.exr.import
Normal file
@ -0,0 +1,26 @@
|
||||
[remap]
|
||||
|
||||
importer="2d_array_texture"
|
||||
type="CompressedTexture2DArray"
|
||||
uid="uid://ujfyssexski7"
|
||||
path="res://.godot/imported/demo.exr-3842fccf4f3f45bb955d2f579f2e5875.ctexarray"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://maps/demo.exr"
|
||||
dest_files=["res://.godot/imported/demo.exr-3842fccf4f3f45bb955d2f579f2e5875.ctexarray"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=3
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/channel_pack=1
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
slices/horizontal=1
|
||||
slices/vertical=1
|
BIN
maps/demo.lmbake
Normal file
300
maps/demo.tscn
Normal file
28
maps/hazy_env.tscn
Normal file
@ -0,0 +1,28 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://br315evr8x2gt"]
|
||||
|
||||
[ext_resource type="CameraAttributesPhysical" uid="uid://cxyj2tvfksjl6" path="res://maps/hazy_env_camera_attrs.tres" id="1_84mop"]
|
||||
|
||||
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_2n8bf"]
|
||||
sky_top_color = Color(0.749822, 0.794765, 0.857504, 1)
|
||||
sky_horizon_color = Color(1, 1, 1, 1)
|
||||
ground_bottom_color = Color(1, 1, 1, 1)
|
||||
ground_horizon_color = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="Sky" id="Sky_jvem5"]
|
||||
sky_material = SubResource("ProceduralSkyMaterial_2n8bf")
|
||||
|
||||
[sub_resource type="Environment" id="Environment_exkg8"]
|
||||
background_mode = 2
|
||||
sky = SubResource("Sky_jvem5")
|
||||
glow_enabled = true
|
||||
|
||||
[node name="HazyEnv" type="Node3D"]
|
||||
|
||||
[node name="Sun" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(0.707107, 0.5, -0.5, 0, 0.707107, 0.707107, 0.707107, -0.5, 0.5, 0, 2, 0)
|
||||
light_angular_distance = 0.5
|
||||
light_bake_mode = 1
|
||||
|
||||
[node name="Env" type="WorldEnvironment" parent="."]
|
||||
environment = SubResource("Environment_exkg8")
|
||||
camera_attributes = ExtResource("1_84mop")
|
6
maps/hazy_env_camera_attrs.tres
Normal file
@ -0,0 +1,6 @@
|
||||
[gd_resource type="CameraAttributesPhysical" format=3 uid="uid://cxyj2tvfksjl6"]
|
||||
|
||||
[resource]
|
||||
auto_exposure_enabled = true
|
||||
auto_exposure_min_exposure_value = 0.5
|
||||
auto_exposure_max_exposure_value = 1.5
|
1
misc/icon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
|
After Width: | Height: | Size: 994 B |
37
misc/icon.svg.import
Normal file
@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bpwl48we1nsuq"
|
||||
path="res://.godot/imported/icon.svg-2be59eb5925b98a41040443b64335822.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://misc/icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-2be59eb5925b98a41040443b64335822.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
|
35
project.godot
Normal file
@ -0,0 +1,35 @@
|
||||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="gmtk24"
|
||||
run/main_scene="res://maps/demo.tscn"
|
||||
config/features=PackedStringArray("4.3", "C#", "Forward Plus")
|
||||
config/icon="res://misc/icon.svg"
|
||||
|
||||
[dotnet]
|
||||
|
||||
project/assembly_name="gmtk24"
|
||||
|
||||
[editor_plugins]
|
||||
|
||||
enabled=PackedStringArray("res://addons/func_godot/plugin.cfg")
|
||||
|
||||
[importer_defaults]
|
||||
|
||||
texture={
|
||||
"mipmaps/generate": true
|
||||
}
|
||||
|
||||
[rendering]
|
||||
|
||||
lights_and_shadows/use_physical_light_units=true
|
||||
anti_aliasing/quality/msaa_3d=2
|
BIN
textures/asphalt.png
Normal file
After Width: | Height: | Size: 107 B |
35
textures/asphalt.png.import
Normal file
@ -0,0 +1,35 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d1qe6nsgnm668"
|
||||
path.s3tc="res://.godot/imported/asphalt.png-ac61e7ecbfd4001b91872200a6ce765b.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://textures/asphalt.png"
|
||||
dest_files=["res://.godot/imported/asphalt.png-ac61e7ecbfd4001b91872200a6ce765b.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
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=0
|
8
textures/asphalt.tres
Normal file
@ -0,0 +1,8 @@
|
||||
[gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://biro7l4e6n6vi"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://d1qe6nsgnm668" path="res://textures/asphalt.png" id="1_adacw"]
|
||||
|
||||
[resource]
|
||||
albedo_texture = ExtResource("1_adacw")
|
||||
metallic_specular = 0.0
|
||||
texture_filter = 2
|
BIN
textures/default_texture.png
Normal file
After Width: | Height: | Size: 108 B |
34
textures/default_texture.png.import
Normal file
@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cyg2snr1w5xw5"
|
||||
path="res://.godot/imported/default_texture.png-881a2e123e5cebffe367630531266e1b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://textures/default_texture.png"
|
||||
dest_files=["res://.godot/imported/default_texture.png-881a2e123e5cebffe367630531266e1b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=3
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
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=0
|
8
textures/default_texture.tres
Normal file
@ -0,0 +1,8 @@
|
||||
[gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://bs5ogxy0wvvj3"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://cyg2snr1w5xw5" path="res://textures/default_texture.png" id="1_u5p53"]
|
||||
|
||||
[resource]
|
||||
albedo_texture = ExtResource("1_u5p53")
|
||||
metallic_specular = 0.0
|
||||
texture_filter = 2
|
BIN
textures/red_metal.png
Normal file
After Width: | Height: | Size: 107 B |
35
textures/red_metal.png.import
Normal file
@ -0,0 +1,35 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://daehkv152sck7"
|
||||
path.s3tc="res://.godot/imported/red_metal.png-3b795a61edf3045e2e954b9a4785e55f.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://textures/red_metal.png"
|
||||
dest_files=["res://.godot/imported/red_metal.png-3b795a61edf3045e2e954b9a4785e55f.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
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=0
|
8
textures/red_metal.tres
Normal file
@ -0,0 +1,8 @@
|
||||
[gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://cqn1kke18jdyo"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://daehkv152sck7" path="res://textures/red_metal.png" id="1_hsryr"]
|
||||
|
||||
[resource]
|
||||
albedo_texture = ExtResource("1_hsryr")
|
||||
metallic_specular = 0.0
|
||||
texture_filter = 2
|
BIN
textures/special/clip.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
35
textures/special/clip.png.import
Normal file
@ -0,0 +1,35 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dhmu0toe1itnr"
|
||||
path.s3tc="res://.godot/imported/clip.png-b88af128d9765ee842c60c6e4c027bc0.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://textures/special/clip.png"
|
||||
dest_files=["res://.godot/imported/clip.png-b88af128d9765ee842c60c6e4c027bc0.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
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=0
|
BIN
textures/special/skip.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
35
textures/special/skip.png.import
Normal file
@ -0,0 +1,35 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bk5oo263y3u7w"
|
||||
path.s3tc="res://.godot/imported/skip.png-2026d0f841ccf5af70e5ac1bf277c3e0.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://textures/special/skip.png"
|
||||
dest_files=["res://.godot/imported/skip.png-2026d0f841ccf5af70e5ac1bf277c3e0.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
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=0
|
12
trenchbroom/main_fgd.tres
Normal file
@ -0,0 +1,12 @@
|
||||
[gd_resource type="Resource" script_class="FuncGodotFGDFile" load_steps=3 format=3 uid="uid://c7sg28h7vyo3o"]
|
||||
|
||||
[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_53ed1"]
|
||||
[ext_resource type="Script" path="res://addons/func_godot/src/fgd/func_godot_fgd_file.gd" id="1_eav6a"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_eav6a")
|
||||
export_file = false
|
||||
target_map_editor = 1
|
||||
fgd_name = "Gmtk24"
|
||||
base_fgd_files = Array[Resource]([ExtResource("1_53ed1")])
|
||||
entity_definitions = Array[Resource]([])
|
341
trenchbroom/maps/demo.map
Normal file
@ -0,0 +1,341 @@
|
||||
// Game: Gmtk24
|
||||
// Format: Valve
|
||||
// entity 0
|
||||
{
|
||||
"mapversion" "220"
|
||||
"classname" "worldspawn"
|
||||
"_tb_mod" "textures"
|
||||
// brush 0
|
||||
{
|
||||
( -912 -176 32 ) ( -912 176 0 ) ( -912 176 32 ) default_texture [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -768 -176 32 ) ( -912 -176 0 ) ( -912 -176 32 ) default_texture [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -768 176 0 ) ( -912 -176 0 ) ( -768 -176 0 ) default_texture [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -768 176 32 ) ( -912 -176 32 ) ( -912 176 32 ) default_texture [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -768 176 32 ) ( -912 176 0 ) ( -768 176 0 ) default_texture [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -768 176 32 ) ( -768 -176 0 ) ( -768 -176 32 ) default_texture [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 1
|
||||
{
|
||||
( 912 -176 32 ) ( 912 176 0 ) ( 912 176 32 ) default_texture [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 1056 -176 32 ) ( 912 -176 0 ) ( 912 -176 32 ) default_texture [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 1056 176 0 ) ( 912 -176 0 ) ( 1056 -176 0 ) default_texture [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 1056 176 32 ) ( 912 -176 32 ) ( 912 176 32 ) default_texture [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 1056 176 32 ) ( 912 176 0 ) ( 1056 176 0 ) default_texture [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 1056 176 32 ) ( 1056 -176 0 ) ( 1056 -176 32 ) default_texture [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 2
|
||||
{
|
||||
( -824 -80 64 ) ( -824 80 48 ) ( -824 80 64 ) asphalt [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -368 -80 64 ) ( -432 -80 48 ) ( -432 -80 64 ) asphalt [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -368 80 48 ) ( -432 -80 48 ) ( -368 -80 48 ) asphalt [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -368 80 64 ) ( -432 -80 64 ) ( -432 80 64 ) asphalt [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -368 80 64 ) ( -432 80 48 ) ( -368 80 48 ) asphalt [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 944 80 64 ) ( 944 -80 48 ) ( 944 -80 64 ) asphalt [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 3
|
||||
{
|
||||
( -824 -80 64 ) ( -824 80 48 ) ( -824 80 64 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -368 80 64 ) ( -368 80 48 ) ( -432 80 48 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -368 80 32 ) ( -432 -80 32 ) ( -368 -80 32 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -368 80 80 ) ( -432 -80 80 ) ( -432 80 80 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -368 96 64 ) ( -432 96 48 ) ( -368 96 48 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 944 80 64 ) ( 944 -80 48 ) ( 944 -80 64 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 4
|
||||
{
|
||||
( -824 -256 64 ) ( -824 -96 48 ) ( -824 -96 64 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -368 -96 64 ) ( -368 -96 48 ) ( -432 -96 48 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -368 -96 32 ) ( -432 -256 32 ) ( -368 -256 32 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -368 -96 80 ) ( -432 -256 80 ) ( -432 -96 80 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -368 -80 64 ) ( -432 -80 48 ) ( -368 -80 48 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 944 -96 64 ) ( 944 -256 48 ) ( 944 -256 64 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 5
|
||||
{
|
||||
( -672 -112 656 ) ( -688 -128 624 ) ( -688 -96 624 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 -112 656 ) ( -640 -128 624 ) ( -688 -128 624 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -640 -128 624 ) ( -640 -96 624 ) ( -688 -96 624 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -672 -96 656 ) ( -656 -96 656 ) ( -656 -112 656 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -688 -96 624 ) ( -640 -96 624 ) ( -656 -96 656 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 -96 656 ) ( -640 -96 624 ) ( -640 -128 624 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 6
|
||||
{
|
||||
( -672 96 656 ) ( -688 96 624 ) ( -688 128 624 ) red_metal [ -1.2246467991473532e-16 -1 0 0 ] [ 0 0 -1 0 ] 180 1 1
|
||||
( -640 96 624 ) ( -688 96 624 ) ( -672 96 656 ) red_metal [ 1 -1.2246467991473532e-16 0 0 ] [ 0 0 -1 0 ] 180 1 1
|
||||
( -688 128 624 ) ( -688 96 624 ) ( -640 96 624 ) red_metal [ 1 -1.2246467991473532e-16 0 0 ] [ 1.2246467991473532e-16 1 0 0 ] 180 1 1
|
||||
( -656 96 656 ) ( -672 96 656 ) ( -672 112 656 ) red_metal [ -1 1.2246467991473532e-16 0 0 ] [ 1.2246467991473532e-16 1 0 0 ] 180 1 1
|
||||
( -672 112 656 ) ( -688 128 624 ) ( -640 128 624 ) red_metal [ -1 1.2246467991473532e-16 0 0 ] [ 0 0 -1 0 ] 180 1 1
|
||||
( -656 112 656 ) ( -640 128 624 ) ( -640 96 624 ) red_metal [ 1.2246467991473532e-16 1 0 0 ] [ 0 0 -1 0 ] 180 1 1
|
||||
}
|
||||
// brush 7
|
||||
{
|
||||
( -712 -128 80 ) ( -712 -80 32 ) ( -712 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -640 -152 80 ) ( -688 -152 32 ) ( -688 -152 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -640 -80 0 ) ( -688 -128 0 ) ( -640 -128 0 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -656 -128 256 ) ( -688 -112 256 ) ( -560 -112 256 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -640 -96 80 ) ( -688 -96 32 ) ( -640 -96 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -616 -80 80 ) ( -616 -128 32 ) ( -616 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 8
|
||||
{
|
||||
( -712 96 80 ) ( -712 144 32 ) ( -712 144 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -640 96 80 ) ( -688 96 32 ) ( -688 96 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -640 144 0 ) ( -688 96 0 ) ( -640 96 0 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -656 -128 256 ) ( -688 -112 256 ) ( -560 -112 256 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -640 152 80 ) ( -688 152 32 ) ( -640 152 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -616 144 80 ) ( -616 96 32 ) ( -616 96 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 9
|
||||
{
|
||||
( -704 -128 80 ) ( -704 -80 32 ) ( -704 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -640 -144 80 ) ( -688 -144 32 ) ( -688 -144 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 -128 256 ) ( -560 -112 256 ) ( -688 -112 256 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -656 -128 400 ) ( -688 -112 400 ) ( -560 -112 400 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -640 -96 80 ) ( -688 -96 32 ) ( -640 -96 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -624 -80 80 ) ( -624 -128 32 ) ( -624 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 10
|
||||
{
|
||||
( -704 96 80 ) ( -704 144 32 ) ( -704 144 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -640 96 80 ) ( -688 96 32 ) ( -688 96 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 -128 256 ) ( -560 -112 256 ) ( -688 -112 256 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -656 -128 400 ) ( -688 -112 400 ) ( -560 -112 400 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -640 144 80 ) ( -688 144 32 ) ( -640 144 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -624 144 80 ) ( -624 96 32 ) ( -624 96 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 11
|
||||
{
|
||||
( -696 -128 80 ) ( -696 -80 32 ) ( -696 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -640 -136 80 ) ( -688 -136 32 ) ( -688 -136 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 -128 400 ) ( -560 -112 400 ) ( -688 -112 400 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -656 -128 544 ) ( -688 -112 544 ) ( -560 -112 544 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -640 -96 80 ) ( -688 -96 32 ) ( -640 -96 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -632 -80 80 ) ( -632 -128 32 ) ( -632 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 12
|
||||
{
|
||||
( -696 96 80 ) ( -696 144 32 ) ( -696 144 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -640 96 80 ) ( -688 96 32 ) ( -688 96 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 -128 400 ) ( -560 -112 400 ) ( -688 -112 400 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -656 -128 544 ) ( -688 -112 544 ) ( -560 -112 544 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -640 136 80 ) ( -688 136 32 ) ( -640 136 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -632 144 80 ) ( -632 96 32 ) ( -632 96 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 13
|
||||
{
|
||||
( -688 -128 80 ) ( -688 -80 32 ) ( -688 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -640 -128 80 ) ( -688 -128 32 ) ( -688 -128 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 -128 544 ) ( -560 -112 544 ) ( -688 -112 544 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -640 -80 624 ) ( -688 -128 624 ) ( -688 -80 624 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -640 -96 80 ) ( -688 -96 32 ) ( -640 -96 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -640 -80 80 ) ( -640 -128 32 ) ( -640 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 14
|
||||
{
|
||||
( -688 96 80 ) ( -688 144 32 ) ( -688 144 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -640 96 80 ) ( -688 96 32 ) ( -688 96 80 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 -128 544 ) ( -560 -112 544 ) ( -688 -112 544 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -640 144 624 ) ( -688 96 624 ) ( -688 144 624 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -640 128 80 ) ( -688 128 32 ) ( -640 128 32 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -640 144 80 ) ( -640 96 32 ) ( -640 96 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 15
|
||||
{
|
||||
( 712 14.460894756598918 20.519769259644818 ) ( 712 189.82337649086298 207.19595949289317 ) ( 712 155.88225099390866 241.1370849898475 ) red_metal [ 0 -0.7071067811865467 0.7071067811865482 -12.284271 ] [ 0 -0.7071067811865482 -0.7071067811865467 -7.2649384 ] 315 1 1
|
||||
( 808 -25.13708498984772 60.117749006091515 ) ( 808 14.460894756598918 20.519769259644818 ) ( 712 14.460894756598918 20.519769259644818 ) red_metal [ -1 0 0 0 ] [ 0 -0.7071067811865467 0.7071067811865482 -12.284271 ] 0 1 1
|
||||
( 712 155.88225099390866 241.1370849898475 ) ( 808 155.88225099390866 241.1370849898475 ) ( 808 -25.13708498984772 60.117749006091515 ) red_metal [ 1 0 0 0 ] [ 0 -0.7071067811865482 -0.7071067811865467 -7.2649384 ] 0 1 1
|
||||
( 712 189.82337649086298 207.19595949289317 ) ( 808 189.82337649086298 207.19595949289317 ) ( 808 155.88225099390866 241.1370849898475 ) red_metal [ 1 0 0 0 ] [ 0 -0.7071067811865467 0.7071067811865482 -12.284271 ] 0 1 1
|
||||
( 712 14.460894756598918 20.519769259644818 ) ( 808 14.460894756598918 20.519769259644818 ) ( 808 189.82337649086298 207.19595949289317 ) red_metal [ -1 0 0 0 ] [ 0 -0.7071067811865482 -0.7071067811865467 -7.2649384 ] 0 1 1
|
||||
( 808 189.82337649086298 207.19595949289317 ) ( 808 14.460894756598918 20.519769259644818 ) ( 808 -25.13708498984772 60.117749006091515 ) red_metal [ 0 0.7071067811865467 -0.7071067811865482 12.284271 ] [ 0 -0.7071067811865482 -0.7071067811865467 -7.2649384 ] 45 1 1
|
||||
}
|
||||
// brush 16
|
||||
{
|
||||
( 752 -96 288 ) ( 752 96 192 ) ( 752 96 288 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 776 -96 288 ) ( 760 -96 192 ) ( 760 -96 288 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 776 96 192 ) ( 760 -96 192 ) ( 776 -96 192 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 776 96 256 ) ( 760 -96 256 ) ( 760 96 256 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 776 96 288 ) ( 760 96 192 ) ( 776 96 192 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 784 96 288 ) ( 784 -96 192 ) ( 784 -96 288 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 17
|
||||
{
|
||||
( 720 -128 80 ) ( 720 -80 32 ) ( 720 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 792 -152 80 ) ( 744 -152 32 ) ( 744 -152 80 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 792 -80 0 ) ( 744 -128 0 ) ( 792 -128 0 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 776 -128 256 ) ( 744 -112 256 ) ( 872 -112 256 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 792 -96 80 ) ( 744 -96 32 ) ( 792 -96 32 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 816 -80 80 ) ( 816 -128 32 ) ( 816 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 18
|
||||
{
|
||||
( 728 -128 80 ) ( 728 -80 32 ) ( 728 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 792 -144 80 ) ( 744 -144 32 ) ( 744 -144 80 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 776 -128 256 ) ( 872 -112 256 ) ( 744 -112 256 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 776 -128 400 ) ( 744 -112 400 ) ( 872 -112 400 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 792 -96 80 ) ( 744 -96 32 ) ( 792 -96 32 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 808 -80 80 ) ( 808 -128 32 ) ( 808 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 19
|
||||
{
|
||||
( 736 -128 80 ) ( 736 -80 32 ) ( 736 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 792 -136 80 ) ( 744 -136 32 ) ( 744 -136 80 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 776 -128 400 ) ( 872 -112 400 ) ( 744 -112 400 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 776 -128 544 ) ( 744 -112 544 ) ( 872 -112 544 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 792 -96 80 ) ( 744 -96 32 ) ( 792 -96 32 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 800 -80 80 ) ( 800 -128 32 ) ( 800 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 20
|
||||
{
|
||||
( 760 -96 576 ) ( 760 96 480 ) ( 760 96 576 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 776 -96 576 ) ( 760 -96 480 ) ( 760 -96 576 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 776 96 496 ) ( 760 -96 496 ) ( 776 -96 496 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 776 96 544 ) ( 760 -96 544 ) ( 760 96 544 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 776 96 576 ) ( 760 96 480 ) ( 776 96 480 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 776 96 576 ) ( 776 -96 480 ) ( 776 -96 576 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 21
|
||||
{
|
||||
( 736 136 400 ) ( 736 128 544 ) ( 736 96 544 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 736 96 544 ) ( 800 96 544 ) ( 800 96 400 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 800 96 400 ) ( 800 136 400 ) ( 736 136 400 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 736 128 544 ) ( 800 128 544 ) ( 800 96 544 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 736 136 400 ) ( 800 136 400 ) ( 800 128 544 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 800 128 544 ) ( 800 136 400 ) ( 800 96 400 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 22
|
||||
{
|
||||
( 744 96 80 ) ( 744 144 32 ) ( 744 144 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 792 96 80 ) ( 744 96 32 ) ( 744 96 80 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 776 -128 544 ) ( 872 -112 544 ) ( 744 -112 544 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 792 144 624 ) ( 744 96 624 ) ( 744 144 624 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 792 128 80 ) ( 744 128 32 ) ( 792 128 32 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 792 144 80 ) ( 792 96 32 ) ( 792 96 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 23
|
||||
{
|
||||
( 744 -128 80 ) ( 744 -80 32 ) ( 744 -80 80 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 792 -128 80 ) ( 744 -128 32 ) ( 744 -128 80 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 776 -128 544 ) ( 872 -112 544 ) ( 744 -112 544 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 792 -80 624 ) ( 744 -128 624 ) ( 744 -80 624 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 792 -96 80 ) ( 744 -96 32 ) ( 792 -96 32 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 792 -80 80 ) ( 792 -128 32 ) ( 792 -128 80 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 24
|
||||
{
|
||||
( 760 -112 656 ) ( 744 -128 624 ) ( 744 -96 624 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 776 -112 656 ) ( 792 -128 624 ) ( 744 -128 624 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 792 -128 624 ) ( 792 -96 624 ) ( 744 -96 624 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 760 -96 656 ) ( 776 -96 656 ) ( 776 -112 656 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 744 -96 624 ) ( 792 -96 624 ) ( 776 -96 656 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 776 -96 656 ) ( 792 -96 624 ) ( 792 -128 624 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 25
|
||||
{
|
||||
( 832 72 136 ) ( 816 72 104 ) ( 816 104 104 ) red_metal [ -1.2246467991473532e-16 -1 0 8 ] [ 0 0 -1 8 ] 180 1 1
|
||||
( 864 72 104 ) ( 816 72 104 ) ( 832 72 136 ) red_metal [ 1 -1.2246467991473532e-16 0 0 ] [ 0 0 -1 8 ] 180 1 1
|
||||
( 816 104 104 ) ( 816 72 104 ) ( 864 72 104 ) red_metal [ 1 -1.2246467991473532e-16 0 0 ] [ 1.2246467991473532e-16 1 0 -8 ] 180 1 1
|
||||
( 848 72 136 ) ( 832 72 136 ) ( 832 88 136 ) red_metal [ -1 1.2246467991473532e-16 0 0 ] [ 1.2246467991473532e-16 1 0 -8 ] 180 1 1
|
||||
( 832 88 136 ) ( 816 104 104 ) ( 864 104 104 ) red_metal [ -1 1.2246467991473532e-16 0 0 ] [ 0 0 -1 8 ] 180 1 1
|
||||
( 848 88 136 ) ( 864 104 104 ) ( 864 72 104 ) red_metal [ 1.2246467991473532e-16 1 0 -8 ] [ 0 0 -1 8 ] 180 1 1
|
||||
}
|
||||
// brush 26
|
||||
{
|
||||
( 760 -96 656 ) ( 760 96 560 ) ( 760 96 656 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 776 -96 656 ) ( 760 -96 560 ) ( 760 -96 656 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 776 96 592 ) ( 760 -96 592 ) ( 776 -96 592 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 776 96 624 ) ( 760 -96 624 ) ( 760 96 624 ) red_metal [ 1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 776 96 656 ) ( 760 96 560 ) ( 776 96 560 ) red_metal [ -1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 776 96 656 ) ( 776 -96 560 ) ( 776 -96 656 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 27
|
||||
{
|
||||
( -680 -96 288 ) ( -680 96 192 ) ( -680 96 288 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 -96 288 ) ( -672 -96 192 ) ( -672 -96 288 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 96 192 ) ( -672 -96 192 ) ( -656 -96 192 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -656 96 256 ) ( -672 -96 256 ) ( -672 96 256 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -656 96 288 ) ( -672 96 192 ) ( -656 96 192 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -648 96 288 ) ( -648 -96 192 ) ( -648 -96 288 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 28
|
||||
{
|
||||
( -680 -96 432 ) ( -680 96 336 ) ( -680 96 432 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 -96 432 ) ( -672 -96 336 ) ( -672 -96 432 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 96 344 ) ( -672 -96 344 ) ( -656 -96 344 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -656 96 400 ) ( -672 -96 400 ) ( -672 96 400 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -656 96 432 ) ( -672 96 336 ) ( -656 96 336 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -648 96 432 ) ( -648 -96 336 ) ( -648 -96 432 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 29
|
||||
{
|
||||
( -672 -96 576 ) ( -672 96 480 ) ( -672 96 576 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 -96 576 ) ( -672 -96 480 ) ( -672 -96 576 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 96 496 ) ( -672 -96 496 ) ( -656 -96 496 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -656 96 544 ) ( -672 -96 544 ) ( -672 96 544 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -656 96 576 ) ( -672 96 480 ) ( -656 96 480 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 96 576 ) ( -656 -96 480 ) ( -656 -96 576 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 30
|
||||
{
|
||||
( -672 -96 656 ) ( -672 96 560 ) ( -672 96 656 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 -96 656 ) ( -672 -96 560 ) ( -672 -96 656 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 96 592 ) ( -672 -96 592 ) ( -656 -96 592 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -656 96 624 ) ( -672 -96 624 ) ( -672 96 624 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( -656 96 656 ) ( -672 96 560 ) ( -656 96 560 ) red_metal [ -1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( -656 96 656 ) ( -656 -96 560 ) ( -656 -96 656 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 31
|
||||
{
|
||||
( 832 120 400 ) ( 832 80 400 ) ( 832 128 256 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 848 80 256 ) ( 832 80 256 ) ( 848 80 400 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 848 128 256 ) ( 832 128 256 ) ( 848 80 256 ) red_metal [ -1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 848 80 400 ) ( 832 80 400 ) ( 848 120 400 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 848 120 400 ) ( 832 120 400 ) ( 848 128 256 ) red_metal [ -1 0 0 8 ] [ 0 0.055470019622522716 -0.9984603532054125 0.8875122 ] 0 1 1
|
||||
( 848 80 400 ) ( 848 120 400 ) ( 848 80 256 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 32
|
||||
{
|
||||
( 664 72 256 ) ( 664 120 256 ) ( 664 72 400 ) red_metal [ 0 -1 0 8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 664 72 400 ) ( 680 72 400 ) ( 664 72 256 ) red_metal [ 1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 664 72 256 ) ( 680 72 256 ) ( 664 120 256 ) red_metal [ -1 0 0 8 ] [ 0 -1 0 8 ] 0 1 1
|
||||
( 664 112 400 ) ( 680 112 400 ) ( 664 72 400 ) red_metal [ 1 0 0 -8 ] [ 0 -1 0 8 ] 0 1 1
|
||||
( 664 120 256 ) ( 680 120 256 ) ( 664 112 400 ) red_metal [ -1 0 0 0 ] [ 0 0.055470019622522716 -0.9984603532054125 1.3312988 ] 0 1 1
|
||||
( 680 72 256 ) ( 680 72 400 ) ( 680 120 256 ) red_metal [ 0 -1 0 8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 33
|
||||
{
|
||||
( 720 152 408 ) ( 720 152 373.679012345679 ) ( 720 192 408 ) red_metal [ 0 -1 0 7.9999847 ] [ 0 0 -1 8 ] 0 1 1
|
||||
( 768 152 408 ) ( 768 152 373.679012345679 ) ( 720 152 408 ) red_metal [ 1 0 0 0 ] [ 0 0 -1 8 ] 0 1 1
|
||||
( 768 152 408 ) ( 720 152 408 ) ( 768 192 408 ) red_metal [ 1 0 0 0 ] [ 0 -1 0 8 ] 0 1 1
|
||||
( 768 152 373.679012345679 ) ( 768 193.77777777777737 376 ) ( 720 152 373.679012345679 ) red_metal [ 1 0 0 8 ] [ 0 -0.9984603532054125 -0.055470019622523146 8.357285 ] 0 1 1
|
||||
( 720 192 408 ) ( 720 193.77777777777737 376 ) ( 768 192 408 ) red_metal [ -1 0 0 -8 ] [ 0 0.055470019622522716 -0.9984603532054125 4.8813477 ] 0 1 1
|
||||
( 768 192 408 ) ( 768 193.77777777777737 376 ) ( 768 152 408 ) red_metal [ 0 1 0 -7.9999847 ] [ 0 0 -1 8 ] 0 1 1
|
||||
}
|
||||
// brush 34
|
||||
{
|
||||
( 728 208 256 ) ( 728 206.22222222222223 288 ) ( 728 160 256 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 728 160 256 ) ( 728 160 285.43209876543204 ) ( 776 160 256 ) red_metal [ 1 0 0 8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 728 206.22222222222223 288 ) ( 776 206.22222222222223 288 ) ( 728 160 285.43209876543204 ) red_metal [ -1 0 0 0 ] [ 0 -0.9984603532054125 -0.055470019622523146 15.901459 ] 0 1 1
|
||||
( 776 208 256 ) ( 728 208 256 ) ( 776 160 256 ) red_metal [ -1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 776 208 256 ) ( 776 206.22222222222223 288 ) ( 728 208 256 ) red_metal [ -1 0 0 0 ] [ 0 0.055470019622522716 -0.9984603532054125 -3.5500793 ] 0 1 1
|
||||
( 776 160 256 ) ( 776 160 285.43209876543204 ) ( 776 208 256 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 35
|
||||
{
|
||||
( 784 -112 344 ) ( 784 75.03709410492462 344 ) ( 784 -112 400 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 784 -112 400 ) ( 816 -112 400 ) ( 784 -112 344 ) red_metal [ 1 0 0 -8 ] [ 0 0 -1 0 ] 0 1 1
|
||||
( 816 -112 344 ) ( 816 75.03709410492462 344 ) ( 784 -112 344 ) red_metal [ -1 0 0 8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 784 -112 400 ) ( 784 71.92598299381353 400 ) ( 816 -112 400 ) red_metal [ 1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 816 75.03709410492462 344 ) ( 816 71.92598299381353 400 ) ( 784 75.03709410492462 344 ) red_metal [ 1 0 0 0 ] [ 0 0.055470019622522716 -0.9984603532054125 0.8875122 ] 0 1 1
|
||||
( 816 -112 400 ) ( 816 71.92598299381353 400 ) ( 816 -112 344 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 0 ] 0 1 1
|
||||
}
|
||||
// brush 36
|
||||
{
|
||||
( 832 71.92598299381353 440 ) ( 832 73.85168082391803 405.3374390581189 ) ( 832 80 440 ) red_metal [ 0 -1 0 0 ] [ 0 0 -1 8 ] 0 1 1
|
||||
( 864 71.92598299381353 440 ) ( 864 73.85168082391803 405.3374390581189 ) ( 832 71.92598299381353 440 ) red_metal [ 1 0 0 0 ] [ 0 0.055470019622522716 -0.9984603532054125 8.825958 ] 0 1 1
|
||||
( 864 80 440 ) ( 864 71.92598299381353 440 ) ( 832 80 440 ) red_metal [ 1 0 0 -8 ] [ 0 -1 0 0 ] 0 1 1
|
||||
( 864 80 405.679012345679 ) ( 832 80 405.679012345679 ) ( 864 73.85168082391803 405.3374390581189 ) red_metal [ 1 0 0 0 ] [ 0 -0.9984603532054125 -0.055470019622523146 2.243431 ] 0 1 1
|
||||
( 832 80 440 ) ( 832 80 405.679012345679 ) ( 864 80 440 ) red_metal [ -1 0 0 8 ] [ 0 0 -1 8 ] 0 1 1
|
||||
( 864 80 440 ) ( 864 80 405.679012345679 ) ( 864 71.92598299381353 440 ) red_metal [ 0 1 0 0 ] [ 0 0 -1 8 ] 0 1 1
|
||||
}
|
||||
}
|