diff --git a/.gitattributes b/.gitattributes
index 8e24626..7aa8032 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -6,3 +6,4 @@
/addons/** !export-ignore
/script_templates !export-ignore
/script_templates/** !export-ignore
+LICENSE !export-ignore
diff --git a/.gitignore b/.gitignore
index e6bc268..c2540ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,5 +4,3 @@
!script_templates/
!addons/**/*
!script_templates/**/*
-
-addons/**/*.uid
diff --git a/addons/godotfgd/main.gd.uid b/addons/godotfgd/main.gd.uid
new file mode 100644
index 0000000..9a1941f
--- /dev/null
+++ b/addons/godotfgd/main.gd.uid
@@ -0,0 +1 @@
+uid://bf3qmyehwiuea
diff --git a/addons/godotvmf/entities/func_detail.gd.uid b/addons/godotvmf/entities/func_detail.gd.uid
new file mode 100644
index 0000000..d4205f4
--- /dev/null
+++ b/addons/godotvmf/entities/func_detail.gd.uid
@@ -0,0 +1 @@
+uid://d2dc4sqf30jtj
diff --git a/addons/godotvmf/entities/func_instance.gd.uid b/addons/godotvmf/entities/func_instance.gd.uid
new file mode 100644
index 0000000..8738aca
--- /dev/null
+++ b/addons/godotvmf/entities/func_instance.gd.uid
@@ -0,0 +1 @@
+uid://e2t7hchk4u82
diff --git a/addons/godotvmf/entities/light.gd.uid b/addons/godotvmf/entities/light.gd.uid
new file mode 100644
index 0000000..cd882fe
--- /dev/null
+++ b/addons/godotvmf/entities/light.gd.uid
@@ -0,0 +1 @@
+uid://c4wae4v6rpk72
diff --git a/addons/godotvmf/entities/light_environment.gd.uid b/addons/godotvmf/entities/light_environment.gd.uid
new file mode 100644
index 0000000..fb7c75e
--- /dev/null
+++ b/addons/godotvmf/entities/light_environment.gd.uid
@@ -0,0 +1 @@
+uid://c6ylaipt6p030
diff --git a/addons/godotvmf/entities/light_spot.gd.uid b/addons/godotvmf/entities/light_spot.gd.uid
new file mode 100644
index 0000000..0ac14ae
--- /dev/null
+++ b/addons/godotvmf/entities/light_spot.gd.uid
@@ -0,0 +1 @@
+uid://bqjn8xm2aihuw
diff --git a/addons/godotvmf/entities/prop_static.gd.uid b/addons/godotvmf/entities/prop_static.gd.uid
new file mode 100644
index 0000000..115fda7
--- /dev/null
+++ b/addons/godotvmf/entities/prop_static.gd.uid
@@ -0,0 +1 @@
+uid://nswhg1njw5xv
diff --git a/addons/godotvmf/entities/prop_studio.gd b/addons/godotvmf/entities/prop_studio.gd
index b928756..318a427 100644
--- a/addons/godotvmf/entities/prop_studio.gd
+++ b/addons/godotvmf/entities/prop_studio.gd
@@ -24,7 +24,8 @@ func _entity_setup(_entity: VMFEntity) -> void:
if not model_scene:
if not ResourceLoader.exists(model_path):
- if VMFCache.is_file_logged(model): return
+ queue_free();
+ if VMFCache.is_file_logged(model): return
VMFLogger.warn("Model not found: " + model_path);
VMFCache.add_logged_file(model);
diff --git a/addons/godotvmf/entities/prop_studio.gd.uid b/addons/godotvmf/entities/prop_studio.gd.uid
new file mode 100644
index 0000000..e1cd8dc
--- /dev/null
+++ b/addons/godotvmf/entities/prop_studio.gd.uid
@@ -0,0 +1 @@
+uid://bpw635t676c6s
diff --git a/addons/godotvmf/godotmdl/ani_reader.gd.uid b/addons/godotvmf/godotmdl/ani_reader.gd.uid
new file mode 100644
index 0000000..3cfa471
--- /dev/null
+++ b/addons/godotvmf/godotmdl/ani_reader.gd.uid
@@ -0,0 +1 @@
+uid://c0nnkq7jx5l8k
diff --git a/addons/godotvmf/godotmdl/import.gd b/addons/godotvmf/godotmdl/import.gd
index db460e8..337a0f4 100644
--- a/addons/godotvmf/godotmdl/import.gd
+++ b/addons/godotvmf/godotmdl/import.gd
@@ -34,6 +34,18 @@ func _get_import_options(str, int): return [
default_value = false,
type = TYPE_BOOL,
},
+ {
+ name = "collision_from_mesh",
+ description = "Creates a collision model from the mesh itself, not from PHY file",
+ default_value = false,
+ type = TYPE_BOOL,
+ },
+ {
+ name = "collision_from_mesh_simplify_level",
+ description = "Specifies how simple the collision mesh will be. 1.0 - most simple, 0.0 - original mesh",
+ default_value = 1.0,
+ type = TYPE_FLOAT,
+ },
{
name = "generate_lods",
default_value = true,
diff --git a/addons/godotvmf/godotmdl/import.gd.uid b/addons/godotvmf/godotmdl/import.gd.uid
new file mode 100644
index 0000000..c455465
--- /dev/null
+++ b/addons/godotvmf/godotmdl/import.gd.uid
@@ -0,0 +1 @@
+uid://bk28rdclb504g
diff --git a/addons/godotvmf/godotmdl/mdl_combiner.gd b/addons/godotvmf/godotmdl/mdl_combiner.gd
index 47c2228..8327a46 100644
--- a/addons/godotvmf/godotmdl/mdl_combiner.gd
+++ b/addons/godotvmf/godotmdl/mdl_combiner.gd
@@ -22,7 +22,10 @@ var rotation_radians: Vector3:
var additional_basis: Basis:
get: return Basis.from_euler(rotation_radians);
-
+
+var is_static_prop: bool:
+ get: return (mdl.header.flags & mdl.MDLFlag.STATIC_PROP) != 0;
+
## Apply a skin to a mesh instance
## directly means that skin is applied to internal mesh itself
static func apply_skin(mesh_instance: MeshInstance3D, skin_id: int, directly: bool = false):
@@ -122,10 +125,16 @@ func create_occluder():
var begin_vid = 0;
st.begin(Mesh.PRIMITIVE_TRIANGLES);
-
for child in colliders:
- var s: ConcavePolygonShape3D = child.shape;
- var points = s.get_faces();
+ var s: Shape3D = child.shape;
+ var points = [];
+
+ if s is ConvexPolygonShape3D:
+ points = s.points;
+ elif s is ConcavePolygonShape3D:
+ points = s.get_faces();
+ else:
+ continue;
for p in points:
st.add_vertex(p);
@@ -152,8 +161,22 @@ func create_occluder():
mesh_instance.add_child(occluder);
occluder.set_owner(mesh_instance);
+func generate_collision_from_mesh():
+ var simplified_mesh := VMFMeshUtils.simplify_mesh(mesh_instance.mesh, options.collision_from_mesh_simplify_level).create_trimesh_shape();
+ var static_body := StaticBody3D.new();
+ var collision := CollisionShape3D.new();
+ collision.name = "collision";
+ collision.shape = simplified_mesh;
+ static_body.name = "static_body";
+ static_body.add_child(collision);
+ mesh_instance.add_child(static_body);
+ static_body.set_owner(mesh_instance);
+ collision.set_owner(mesh_instance);
+
# TODO: For non-static props collision should be rotated by 90 degrees around y-axis
func generate_collision():
+ if options.collision_from_mesh: return generate_collision_from_mesh();
+
var yup_to_zup = Basis().rotated(Vector3.RIGHT, PI / 2);
var yup_to_zup_transform = Transform3D(yup_to_zup, Vector3.ZERO);
var static_body: StaticBody3D;
@@ -203,8 +226,9 @@ func generate_collision():
static_body.set_owner(mesh_instance);
else:
# NOTE: We don't need bone attachment for static bodies since they has only one bone
- mesh_instance.add_child(static_body);
- static_body.set_owner(mesh_instance);
+ if static_body.get_parent() != mesh_instance:
+ mesh_instance.add_child(static_body);
+ static_body.set_owner(mesh_instance);
static_body.add_child(collision);
collision.set_owner(mesh_instance);
@@ -214,6 +238,8 @@ func generate_collision():
surface_index += 1;
func setup_skeleton():
+ if is_static_prop: return;
+
if Engine.get_version_info().minor < 4: return;
if is_static_body: return;
skeleton.basis = additional_basis.inverse();
diff --git a/addons/godotvmf/godotmdl/mdl_combiner.gd.uid b/addons/godotvmf/godotmdl/mdl_combiner.gd.uid
new file mode 100644
index 0000000..3f1378e
--- /dev/null
+++ b/addons/godotvmf/godotmdl/mdl_combiner.gd.uid
@@ -0,0 +1 @@
+uid://bd4ctjcovsf7w
diff --git a/addons/godotvmf/godotmdl/mdl_reader.gd.uid b/addons/godotvmf/godotmdl/mdl_reader.gd.uid
new file mode 100644
index 0000000..94f1421
--- /dev/null
+++ b/addons/godotvmf/godotmdl/mdl_reader.gd.uid
@@ -0,0 +1 @@
+uid://sj5ua4ctykyh
diff --git a/addons/godotvmf/godotmdl/mesh_generator.gd.uid b/addons/godotvmf/godotmdl/mesh_generator.gd.uid
new file mode 100644
index 0000000..bbf8de3
--- /dev/null
+++ b/addons/godotvmf/godotmdl/mesh_generator.gd.uid
@@ -0,0 +1 @@
+uid://bfbefl04wexpp
diff --git a/addons/godotvmf/godotmdl/phy_reader.gd.uid b/addons/godotvmf/godotmdl/phy_reader.gd.uid
new file mode 100644
index 0000000..da42a49
--- /dev/null
+++ b/addons/godotvmf/godotmdl/phy_reader.gd.uid
@@ -0,0 +1 @@
+uid://tbul0tcs2ot
diff --git a/addons/godotvmf/godotmdl/reader.gd b/addons/godotvmf/godotmdl/reader.gd
index 8c95fee..66423da 100644
--- a/addons/godotvmf/godotmdl/reader.gd
+++ b/addons/godotvmf/godotmdl/reader.gd
@@ -76,6 +76,10 @@ static func read_by_structure(file: FileAccess, Clazz, read_from = -1):
return result;
+static func read_char(file: FileAccess):
+ var byte = file.get_8();
+ return char(byte) if byte != 0 else "";
+
static func _read_data(file: FileAccess, type: Type):
match type:
Type.FLOAT: return file.get_float();
@@ -84,10 +88,10 @@ static func _read_data(file: FileAccess, type: Type):
Type.SHORT: return read_signed_short(file);
Type.LONG: return file.get_64() - 0x80000000;
Type.UNSIGNED_SHORT: return file.get_16();
- Type.UNSIGNED_CHAR: return file.get_8();
+ Type.UNSIGNED_CHAR: return read_char(file);
- Type.STRING: return char(file.get_8());
- Type.CHAR: return char(file.get_8());
+ Type.STRING: return read_char(file);
+ Type.CHAR: return read_char(file);
Type.VECTOR2: return Vector2(file.get_float(), file.get_float());
Type.VECTOR4: return Vector4(file.get_float(), file.get_float(), file.get_float(), file.get_float());
diff --git a/addons/godotvmf/godotmdl/reader.gd.uid b/addons/godotvmf/godotmdl/reader.gd.uid
new file mode 100644
index 0000000..267f5f3
--- /dev/null
+++ b/addons/godotvmf/godotmdl/reader.gd.uid
@@ -0,0 +1 @@
+uid://b8qxk8wgly1f4
diff --git a/addons/godotvmf/godotmdl/struct.gd.uid b/addons/godotvmf/godotmdl/struct.gd.uid
new file mode 100644
index 0000000..d529243
--- /dev/null
+++ b/addons/godotvmf/godotmdl/struct.gd.uid
@@ -0,0 +1 @@
+uid://begr2w44t20kb
diff --git a/addons/godotvmf/godotmdl/vtx_reader.gd.uid b/addons/godotvmf/godotmdl/vtx_reader.gd.uid
new file mode 100644
index 0000000..94f1021
--- /dev/null
+++ b/addons/godotvmf/godotmdl/vtx_reader.gd.uid
@@ -0,0 +1 @@
+uid://c5n88vi41i5tu
diff --git a/addons/godotvmf/godotmdl/vvd_reader.gd.uid b/addons/godotvmf/godotmdl/vvd_reader.gd.uid
new file mode 100644
index 0000000..cbf3b76
--- /dev/null
+++ b/addons/godotvmf/godotmdl/vvd_reader.gd.uid
@@ -0,0 +1 @@
+uid://daydea6sm0c2d
diff --git a/addons/godotvmf/godotvdf/vdf_parser.gd.uid b/addons/godotvmf/godotvdf/vdf_parser.gd.uid
new file mode 100644
index 0000000..89f1ebf
--- /dev/null
+++ b/addons/godotvmf/godotvdf/vdf_parser.gd.uid
@@ -0,0 +1 @@
+uid://dljwvdlrqbe7s
diff --git a/addons/godotvmf/godotvmf.gd b/addons/godotvmf/godotvmf.gd
index 578d2e1..e8df082 100644
--- a/addons/godotvmf/godotvmf.gd
+++ b/addons/godotvmf/godotvmf.gd
@@ -23,6 +23,7 @@ func _enter_tree() -> void:
dock.get_node('ReimportGeometry').pressed.connect(ReimportGeometry);
dock.get_node('Docs').pressed.connect(func(): OS.shell_open("https://github.com/H2xDev/GodotVMF/wiki"));
dock.get_node('DiscordSupport').pressed.connect(func(): OS.shell_open("https://discord.gg/wtSK94fPxd"));
+ dock.get_node('ReimportDetailProps').pressed.connect(ReimportDetailProps);
mdl_import_plugin = preload("res://addons/godotvmf/godotmdl/import.gd").new();
vmt_import_plugin = preload("res://addons/godotvmf/godotvmt/vmt_import.gd").new();
@@ -90,7 +91,6 @@ func ReimportVMF():
func ReimportEntities():
var nodes := GetExistingVMFNodes();
- dock.get_node('ProgressBar').show();
dock.get_node('ProgressBar').show();
await get_tree().create_timer(0.1).timeout
@@ -98,6 +98,17 @@ func ReimportEntities():
node.reimport_entities();
dock.get_node('ProgressBar').hide();
+func ReimportDetailProps():
+ var nodes := GetExistingVMFNodes();
+
+ dock.get_node('ProgressBar').show();
+ await get_tree().create_timer(0.1).timeout
+
+ for node in nodes:
+ node.reimport_detail_props();
+
+ dock.get_node('ProgressBar').hide();
+
func ReimportGeometry():
var nodes := GetExistingVMFNodes();
diff --git a/addons/godotvmf/godotvmf.gd.uid b/addons/godotvmf/godotvmf.gd.uid
new file mode 100644
index 0000000..39ecd3e
--- /dev/null
+++ b/addons/godotvmf/godotvmf.gd.uid
@@ -0,0 +1 @@
+uid://dlvt67aflbr5e
diff --git a/addons/godotvmf/godotvmt/formats/vtf_dxt.gd b/addons/godotvmf/godotvmt/formats/vtf_dxt.gd
new file mode 100644
index 0000000..c12f7bb
--- /dev/null
+++ b/addons/godotvmf/godotvmt/formats/vtf_dxt.gd
@@ -0,0 +1,33 @@
+class_name VTFDXT extends VTFFrameReader
+
+static func read(vtf: VTFLoader, frame: int, srgb_conversion_method: VTFLoader.SRGBConversionMethod) -> ImageTexture:
+ var data = PackedByteArray();
+ var bytes_read := 0;
+ var is_dxt_1 := vtf.hires_image_format == vtf.ImageFormat.DXT1;
+ var multiplier = 8 if is_dxt_1 else 16;
+ var format := vtf.format_map[str(vtf.hires_image_format)] as Image.Format;
+ var use_mipmaps := not (vtf.flags & vtf.Flags.TEXTUREFLAGS_NOMIP);
+
+ frame = vtf.frames - 1 - frame;
+
+ for i in range(vtf.mipmap_count):
+ var mipWidth = max(1, vtf.width >> i);
+ var mipHeight = max(1, vtf.height >> i);
+
+ var mip_size = max(1, mipWidth / 4) * max(1, mipHeight / 4) * multiplier;
+
+ vtf.file.seek(vtf.file.get_length() - bytes_read - mip_size - mip_size * frame);
+ data += vtf.file.get_buffer(mip_size);
+
+ bytes_read += mip_size + mip_size * (vtf.frames - 1);
+
+ var img := Image.create_from_data(vtf.width, vtf.height, use_mipmaps, format, data);
+ if not img: return null;
+
+ if srgb_conversion_method == vtf.SRGBConversionMethod.DURING_IMPORT:
+ img.decompress();
+ img.compress(Image.COMPRESS_S3TC);
+
+ vtf.alpha = vtf.flags & vtf.Flags.TEXTUREFLAGS_ONEBITALPHA or vtf.flags & vtf.Flags.TEXTUREFLAGS_EIGHTBITALPHA;
+
+ return ImageTexture.create_from_image(img);
diff --git a/addons/godotvmf/godotvmt/formats/vtf_dxt.gd.uid b/addons/godotvmf/godotvmt/formats/vtf_dxt.gd.uid
new file mode 100644
index 0000000..6a3d075
--- /dev/null
+++ b/addons/godotvmf/godotvmt/formats/vtf_dxt.gd.uid
@@ -0,0 +1 @@
+uid://dphnyrf0rcafa
diff --git a/addons/godotvmf/godotvmt/formats/vtf_frame_reader.gd b/addons/godotvmf/godotvmt/formats/vtf_frame_reader.gd
new file mode 100644
index 0000000..0a3b75a
--- /dev/null
+++ b/addons/godotvmf/godotvmt/formats/vtf_frame_reader.gd
@@ -0,0 +1,6 @@
+class_name VTFFrameReader extends RefCounted
+
+
+## Base class for reading frames from a VTF file. Each image format should have its own implementation of this class.
+static func read(vtf: VTFLoader, frame: int, srgb_conversion_method: VTFLoader.SRGBConversionMethod) -> ImageTexture:
+ return null;
diff --git a/addons/godotvmf/godotvmt/formats/vtf_frame_reader.gd.uid b/addons/godotvmf/godotvmt/formats/vtf_frame_reader.gd.uid
new file mode 100644
index 0000000..0305192
--- /dev/null
+++ b/addons/godotvmf/godotvmt/formats/vtf_frame_reader.gd.uid
@@ -0,0 +1 @@
+uid://b5ihhe3066q25
diff --git a/addons/godotvmf/godotvmt/formats/vtf_i8.gd b/addons/godotvmf/godotvmt/formats/vtf_i8.gd
new file mode 100644
index 0000000..cd808cc
--- /dev/null
+++ b/addons/godotvmf/godotvmt/formats/vtf_i8.gd
@@ -0,0 +1,27 @@
+class_name VTFI8 extends VTFFrameReader
+
+static func read(vtf: VTFLoader, frame: int, srgb_conversion_method: VTFLoader.SRGBConversionMethod) -> ImageTexture:
+ var data := PackedByteArray();
+ var bytes_read := 0;
+ var use_mipmaps := not (vtf.flags & vtf.Flags.TEXTUREFLAGS_NOMIP);
+ var multiplier := 1 if vtf.hires_image_format == vtf.ImageFormat.I8 else 2;
+ var output_format := Image.FORMAT_L8 if vtf.hires_image_format == vtf.ImageFormat.I8 else Image.FORMAT_LA8;
+
+ frame = vtf.frames - 1 - frame;
+
+ for i in range(vtf.mipmap_count):
+ var mip_width = max(1, vtf.width >> i);
+ var mip_height = max(1, vtf.height >> i);
+ var mip_size = mip_width * mip_height * multiplier; # I8 has 1 byte per pixel
+
+ vtf.file.seek(vtf.file.get_length() - bytes_read - mip_size - mip_size * frame);
+ var chunk := vtf.file.get_buffer(mip_size);
+
+ data += chunk;
+ bytes_read += mip_size + mip_size * (vtf.frames - 1);
+
+ var img := Image.create_from_data(vtf.width, vtf.height, use_mipmaps, output_format, data);
+
+ if not img: return null;
+
+ return ImageTexture.create_from_image(img);
diff --git a/addons/godotvmf/godotvmt/formats/vtf_i8.gd.uid b/addons/godotvmf/godotvmt/formats/vtf_i8.gd.uid
new file mode 100644
index 0000000..086baba
--- /dev/null
+++ b/addons/godotvmf/godotvmt/formats/vtf_i8.gd.uid
@@ -0,0 +1 @@
+uid://dd3xyrn3lduxg
diff --git a/addons/godotvmf/godotvmt/formats/vtf_rgba.gd b/addons/godotvmf/godotvmt/formats/vtf_rgba.gd
new file mode 100644
index 0000000..aec6924
--- /dev/null
+++ b/addons/godotvmf/godotvmt/formats/vtf_rgba.gd
@@ -0,0 +1,36 @@
+class_name VTFRGBA extends VTFFrameReader
+
+static func read(vtf: VTFLoader, frame: int, srgb_conversion_method: VTFLoader.SRGBConversionMethod) -> ImageTexture:
+ var data := PackedByteArray();
+ var bytes_read := 0;
+ var use_mipmaps := not (vtf.flags & vtf.Flags.TEXTUREFLAGS_NOMIP);
+ var is_abgr := vtf.hires_image_format == vtf.ImageFormat.ABGR8888 || vtf.hires_image_format == vtf.ImageFormat.BGR888;
+ var is_no_alpha := vtf.hires_image_format >= vtf.ImageFormat.RGB888;
+ var channels_count := 3 if is_no_alpha else 4;
+ var output_format := Image.FORMAT_RGB8 if is_no_alpha else Image.FORMAT_RGBA8;
+
+
+ frame = vtf.frames - 1 - frame;
+
+ for i in range(vtf.mipmap_count):
+ var mip_width = max(1, vtf.width >> i);
+ var mip_height = max(1, vtf.height >> i);
+ var mip_size = mip_width * mip_height * channels_count; # RGBA8888 has 4 bytes per pixel
+
+ vtf.file.seek(vtf.file.get_length() - bytes_read - mip_size - mip_size * frame);
+ var chunk := vtf.file.get_buffer(mip_size);
+
+ if is_abgr: chunk.reverse();
+
+ data += chunk;
+ bytes_read += mip_size + mip_size * (vtf.frames - 1);
+
+ var img := Image.create_from_data(vtf.width, vtf.height, use_mipmaps, output_format, data);
+
+ if is_abgr:
+ img.flip_x();
+ img.flip_y();
+
+ if not img: return null;
+
+ return ImageTexture.create_from_image(img);
diff --git a/addons/godotvmf/godotvmt/formats/vtf_rgba.gd.uid b/addons/godotvmf/godotvmt/formats/vtf_rgba.gd.uid
new file mode 100644
index 0000000..336e5a3
--- /dev/null
+++ b/addons/godotvmf/godotvmt/formats/vtf_rgba.gd.uid
@@ -0,0 +1 @@
+uid://bolwkc1500o7x
diff --git a/addons/godotvmf/godotvmt/vmt_context_menu.gd.uid b/addons/godotvmf/godotvmt/vmt_context_menu.gd.uid
new file mode 100644
index 0000000..5c6aade
--- /dev/null
+++ b/addons/godotvmf/godotvmt/vmt_context_menu.gd.uid
@@ -0,0 +1 @@
+uid://d4en2bmq1eoyb
diff --git a/addons/godotvmf/godotvmt/vmt_import.gd.uid b/addons/godotvmf/godotvmt/vmt_import.gd.uid
new file mode 100644
index 0000000..bbfd66e
--- /dev/null
+++ b/addons/godotvmf/godotvmt/vmt_import.gd.uid
@@ -0,0 +1 @@
+uid://b5c8p8i51m8mr
diff --git a/addons/godotvmf/godotvmt/vmt_loader.gd b/addons/godotvmf/godotvmt/vmt_loader.gd
index abb7225..4404e9f 100644
--- a/addons/godotvmf/godotvmt/vmt_loader.gd
+++ b/addons/godotvmf/godotvmt/vmt_loader.gd
@@ -31,13 +31,21 @@ static func parse_transform(transform_data: String):
translate = translate,
}
-static func load(path: String):
+static func get_material_load_path(material: String) -> String:
+ var path_root = normalize_path(VMFConfig.materials.target_folder + "/" + material).to_lower();
+
+ if ResourceLoader.exists(path_root + ".tres"): return path_root + ".tres";
+ if ResourceLoader.exists(path_root + ".res"): return path_root + ".res";
+ if ResourceLoader.exists(path_root + ".vmt"): return path_root + ".vmt";
+ return "";
+
+static func load(path: String) -> Material:
var structure = VDFParser.parse(path, true);
var shader_name = structure.keys()[0];
var details = structure[shader_name];
var material = null;
- var is_blend_texture = shader_name.trim_suffix(" ") == "worldvertextransition";
+ var is_blend_material = shader_name.trim_suffix(" ") == "worldvertextransition";
# NOTE: CS:GO/L4D
if "insert" in details:
@@ -55,8 +63,11 @@ static func load(path: String):
material.shader = ResourceLoader.load(shader_path);
else:
VMFLogger.warn("Shader %s doesn't exists for %s" % [shader_path, path]);
+ elif is_blend_material:
+ material = ShaderMaterial.new();
+ material.shader = ResourceLoader.load("res://addons/godotvmf/shaders/world_vertex_transition_material.gdshader");
else:
- material = StandardMaterial3D.new() if not is_blend_texture else WorldVertexTransitionMaterial.new();
+ material = StandardMaterial3D.new();
var transformer = VMTTransformer.new();
@@ -69,17 +80,19 @@ static func load(path: String):
elif shader_name == "vertexlitgeneric":
material.shading_mode = 2
+ if not material: return null;
+
for key in details.keys():
var value = details[key];
var is_compile_key = key.begins_with("%");
key = key.replace('$', '').replace('%', '');
if is_compile_key and value and key != "keywords":
- var compile_keys = material.get_meta("compile_keys", []);
- compile_keys.append(key);
+ var compile_keys := material.get_meta("compile_keys", []) as Array;
+ compile_keys.append(key.to_lower());
material.set_meta("compile_keys", compile_keys);
- if material is ShaderMaterial && not is_blend_texture:
+ if material is ShaderMaterial:
var mat: ShaderMaterial = material;
var uniform_index = uniforms.find_custom(func(field): return field.name == key);
if uniform_index == -1: continue;
@@ -106,27 +119,18 @@ static func normalize_path(path: String) -> String:
return path.replace('\\', '/').replace('//', '/').replace('res:/', 'res://');
static func has_material(material: String) -> bool:
- var material_path = normalize_path(VMFConfig.materials.target_folder + "/" + material + ".tres").to_lower();
-
- if not ResourceLoader.exists(material_path):
- material_path = material_path.replace(".tres", ".vmt");
+ return get_material_load_path(material) != "";
- if not ResourceLoader.exists(material_path):
- return false;
-
- return true;
+static func is_material_ignored(material_name: String) -> bool:
+ return VMFConfig.materials.ignore.any(func(rx: String) -> bool: return material_name.match(rx.to_lower()));
static func get_material(material: String) -> Material:
var cached_material = VMFCache.get_cached(material);
if cached_material:
return cached_material as Material;
- var material_path = normalize_path(VMFConfig.materials.target_folder + "/" + material + ".tres").to_lower();
-
- if not ResourceLoader.exists(material_path):
- material_path = material_path.replace(".tres", ".vmt");
-
- if not ResourceLoader.exists(material_path):
+ var material_path = get_material_load_path(material);
+ if material_path == "":
if not VMFCache.is_file_logged(material_path):
VMFLogger.warn("Material not found: " + material_path);
VMFCache.add_logged_file(material_path);
diff --git a/addons/godotvmf/godotvmt/vmt_loader.gd.uid b/addons/godotvmf/godotvmt/vmt_loader.gd.uid
new file mode 100644
index 0000000..d8304e5
--- /dev/null
+++ b/addons/godotvmf/godotvmt/vmt_loader.gd.uid
@@ -0,0 +1 @@
+uid://cuplnlota2v0a
diff --git a/addons/godotvmf/godotvmt/vmt_material_conversion_context_menu.gd b/addons/godotvmf/godotvmt/vmt_material_conversion_context_menu.gd
index 4c8db63..e2f29b0 100644
--- a/addons/godotvmf/godotvmt/vmt_material_conversion_context_menu.gd
+++ b/addons/godotvmf/godotvmt/vmt_material_conversion_context_menu.gd
@@ -44,7 +44,7 @@ func create_blend_material(paths: PackedStringArray):
var path1 = (vmts[0] as String).get_base_dir()
var path2 = (vmts[1] as String).get_base_dir()
- var save_path = path1.get_base_dir() + '/' + blend_file_name;
+ var save_path = path1 + '/' + blend_file_name;
print("Saving blended VMT to: " + save_path);
var file := FileAccess.open(save_path, FileAccess.WRITE);
diff --git a/addons/godotvmf/godotvmt/vmt_material_conversion_context_menu.gd.uid b/addons/godotvmf/godotvmt/vmt_material_conversion_context_menu.gd.uid
new file mode 100644
index 0000000..f6d63ac
--- /dev/null
+++ b/addons/godotvmf/godotvmt/vmt_material_conversion_context_menu.gd.uid
@@ -0,0 +1 @@
+uid://cofptwu041h4i
diff --git a/addons/godotvmf/godotvmt/vmt_shader_based_material.gd.uid b/addons/godotvmf/godotvmt/vmt_shader_based_material.gd.uid
new file mode 100644
index 0000000..f92a554
--- /dev/null
+++ b/addons/godotvmf/godotvmt/vmt_shader_based_material.gd.uid
@@ -0,0 +1 @@
+uid://xkb30gqv5iu7
diff --git a/addons/godotvmf/godotvmt/vmt_transformer.gd b/addons/godotvmf/godotvmt/vmt_transformer.gd
index a208cca..7a19f05 100644
--- a/addons/godotvmf/godotvmt/vmt_transformer.gd
+++ b/addons/godotvmf/godotvmt/vmt_transformer.gd
@@ -27,6 +27,12 @@ func bumpmap(material: Material, value: Variant):
if "normal_enabled" in material:
material.normal_enabled = true;
+func normalmap(material: Material, value: Variant):
+ if "normal_texture" in material:
+ material.set("normal_texture", VTFLoader.get_texture(value));
+ if "normal_enabled" in material:
+ material.normal_enabled = true;
+
func bumpmap2(material: Material, value: Variant):
if "normal_texture2" in material:
material.set("normal_texture2", VTFLoader.get_texture(value));
diff --git a/addons/godotvmf/godotvmt/vmt_transformer.gd.uid b/addons/godotvmf/godotvmt/vmt_transformer.gd.uid
new file mode 100644
index 0000000..d1d6c6e
--- /dev/null
+++ b/addons/godotvmf/godotvmt/vmt_transformer.gd.uid
@@ -0,0 +1 @@
+uid://bumie02erxnug
diff --git a/addons/godotvmf/godotvmt/vtf_import.gd.uid b/addons/godotvmf/godotvmt/vtf_import.gd.uid
new file mode 100644
index 0000000..652d434
--- /dev/null
+++ b/addons/godotvmf/godotvmt/vtf_import.gd.uid
@@ -0,0 +1 @@
+uid://dd5wqqdolqq3y
diff --git a/addons/godotvmf/godotvmt/vtf_loader.gd b/addons/godotvmf/godotvmt/vtf_loader.gd
index 88d8227..d6bb8ed 100644
--- a/addons/godotvmf/godotvmt/vtf_loader.gd
+++ b/addons/godotvmf/godotvmt/vtf_loader.gd
@@ -6,34 +6,34 @@ enum SRGBConversionMethod {
}
enum ImageFormat {
- IMAGE_FORMAT_RGBA8888,
- IMAGE_FORMAT_ABGR8888,
- IMAGE_FORMAT_RGB888,
- IMAGE_FORMAT_BGR888,
- IMAGE_FORMAT_RGB565,
- IMAGE_FORMAT_I8,
- IMAGE_FORMAT_IA88,
- IMAGE_FORMAT_P8,
- IMAGE_FORMAT_A8,
- IMAGE_FORMAT_RGB888_BLUESCREEN,
- IMAGE_FORMAT_BGR888_BLUESCREEN,
- IMAGE_FORMAT_ARGB8888,
- IMAGE_FORMAT_BGRA8888,
- IMAGE_FORMAT_DXT1,
- IMAGE_FORMAT_DXT3,
- IMAGE_FORMAT_DXT5,
- IMAGE_FORMAT_BGRX8888,
- IMAGE_FORMAT_BGR565,
- IMAGE_FORMAT_BGRX5551,
- IMAGE_FORMAT_BGRA4444,
- IMAGE_FORMAT_DXT1_ONEBITALPHA,
- IMAGE_FORMAT_BGRA5551,
- IMAGE_FORMAT_UV88,
- IMAGE_FORMAT_UVWQ8888,
- IMAGE_FORMAT_RGBA16161616F,
- IMAGE_FORMAT_RGBA16161616,
- IMAGE_FORMAT_UVLX8888,
- IMAGE_FORMAT_NONE = -1
+ RGBA8888,
+ ABGR8888,
+ RGB888,
+ BGR888,
+ RGB565,
+ I8,
+ IA88,
+ P8,
+ A8,
+ RGB888_BLUESCREEN,
+ BGR888_BLUESCREEN,
+ ARGB8888,
+ BGRA8888,
+ DXT1,
+ DXT3,
+ DXT5,
+ BGRX8888,
+ BGR565,
+ BGRX5551,
+ BGRA4444,
+ DXT1_ONEBITALPHA,
+ BGRA5551,
+ UV88,
+ UVWQ8888,
+ RGBA16161616F,
+ RGBA16161616,
+ UVLX8888,
+ NONE = -1
}
enum Flags
@@ -66,7 +66,7 @@ enum Flags
TEXTUREFLAGS_BORDER = 0x20000000,
};
-static var formatLabels:
+static var format_labels:
get: return [
"RGBA8888",
"ABGR8888",
@@ -98,6 +98,19 @@ static var formatLabels:
"NONE",
];
+static var frame_readers: Dictionary:
+ get: return {
+ ImageFormat.DXT1: VTFDXT,
+ ImageFormat.DXT3: VTFDXT,
+ ImageFormat.DXT5: VTFDXT,
+ ImageFormat.RGBA8888: VTFRGBA,
+ ImageFormat.ABGR8888: VTFRGBA,
+ ImageFormat.RGB888: VTFRGBA,
+ ImageFormat.BGR888: VTFRGBA,
+ ImageFormat.I8: VTFI8,
+ ImageFormat.IA88: VTFI8,
+ };
+
static var format_map:
get: return {
"13": Image.Format.FORMAT_DXT1,
@@ -105,92 +118,84 @@ static var format_map:
"15": Image.Format.FORMAT_DXT5,
};
-static var supported_formats:
- get: return [
- ImageFormat.IMAGE_FORMAT_DXT1,
- ImageFormat.IMAGE_FORMAT_DXT3,
- ImageFormat.IMAGE_FORMAT_DXT5,
- ];
-
-var file = null;
-var signature:
+var file: FileAccess;
+var signature: String:
get: return seek(0).get_buffer(16).get_string_from_utf8();
-var version:
+var version: float:
get:
file.seek(4);
return float(".".join([file.get_32(), file.get_32()]));
-var header_size:
+var header_size: int:
get: return seek(12).get_32();
-var width:
+var width: int:
get:
var width = seek(16).get_16();
return width if width > 0 else 512;
-var height:
+var height: int:
get:
var height = seek(18).get_16();
return height if height > 0 else 512;
-var flags:
+var flags: int:
get: return seek(20).get_32();
-var frames:
+var frames: int:
get: return seek(24).get_16();
-var first_frame:
+var first_frame: int:
get: return seek(26).get_16();
-var reflectivity:
+var reflectivity: Vector3:
get:
file.seek(32);
return Vector3(file.get_float(), file.get_float(), file.get_float());
-var bump_scale:
+var bump_scale: float:
get: return seek(48).get_float();
-var hires_image_format:
+var hires_image_format: int:
get: return seek(52).get_32();
-var mipmap_count:
+var mipmap_count: int:
get:
if not use_mipmaps: return 1;
return seek(56).get_8();
-var low_res_image_format:
+var low_res_image_format: int:
get: return seek(57).get_32();
-var low_res_image_width:
+var low_res_image_width: int:
get: return seek(61).get_8();
-var low_res_image_height:
+var low_res_image_height: int:
get: return seek(62).get_8();
-var depth:
+var depth: int:
get:
if version < 7.2: return 0;
return seek(63).get_8();
-var num_resources:
+var num_resources: int:
get:
if version < 7.3: return 0;
return seek(75).get_32();
-var use_mipmaps:
+var use_mipmaps: bool:
get: return not (flags & Flags.TEXTUREFLAGS_NOMIP);
-var frame_duration = 0;
-var path = '';
-var alpha = false;
+var frame_duration: float = 0;
+var path: String = '';
+var alpha: bool = false;
var file_name: String = '':
get: return path.get_file().get_basename();
-func seek(v: int):
- file.seek(v);
- return file;
+var is_format_supported: bool:
+ get: return hires_image_format in frame_readers;
static func create(path: String, duration: float = 0):
if not FileAccess.file_exists(path):
@@ -199,33 +204,37 @@ static func create(path: String, duration: float = 0):
var vtf = VTFLoader.new(path, duration);
- if not supported_formats.has(vtf.hires_image_format):
- push_warning("Texture format {0} in not supported ({1})" \
- .format([VTFLoader.formatLabels[vtf.hires_image_format], path.get_file()]));
+ if not vtf.is_format_supported:
+ VMFLogger.warn("Texture format {0} in not supported ({1})" \
+ .format([VTFLoader.format_labels[vtf.hires_image_format], path.get_file()]));
vtf.done();
return null;
return vtf;
+func seek(v: int) -> FileAccess:
+ file.seek(v);
+ return file;
+
func done(): file.close();
func compile_texture(srgb_conversion_method: SRGBConversionMethod):
if width == 0 or height == 0:
- push_error("Corrupted file: {0}".format([file.get_path()]));
+ VMFLogger.error("Corrupted file: {0}".format([file.get_path()]));
return null;
- var tex;
+ var tex: Texture;
if frames > 1:
tex = AnimatedTexture.new();
tex.frames = frames;
for frame in range(0, frames):
- tex.set_frame_texture(frame, _read_frame(frame, srgb_conversion_method));
+ tex.set_frame_texture(frame, read_frame(frame, srgb_conversion_method));
tex.set_frame_duration(frame, frame_duration);
else:
- tex = _read_frame(0, srgb_conversion_method);
+ tex = read_frame(0, srgb_conversion_method);
if not tex:
push_error("Texture not loaded: {0}".format([path]));
@@ -236,39 +245,21 @@ func compile_texture(srgb_conversion_method: SRGBConversionMethod):
static var normal_conversion_shader: Shader;
static var shader_material: ShaderMaterial;
-func _read_frame(frame, srgb_conversion_method: SRGBConversionMethod):
- var data = PackedByteArray();
- var byteRead = 0;
- var is_dxt_1 = hires_image_format == ImageFormat.IMAGE_FORMAT_DXT1;
- var format = format_map[str(hires_image_format)];
- var use_mipmaps = not (flags & Flags.TEXTUREFLAGS_NOMIP);
- frame = frames - 1 - frame;
-
- for i in range(mipmap_count):
- var mipWidth = max(1, width >> i);
- var mipHeight = max(1, height >> i);
-
- var multiplier = 8 if is_dxt_1 else 16;
- var mip_size = max(1, mipWidth / 4) * max(1, mipHeight / 4) * multiplier;
-
- file.seek(file.get_length() - byteRead - mip_size - mip_size * frame);
- data += file.get_buffer(mip_size);
-
- byteRead += mip_size + mip_size * (frames - 1);
+func read_frame(frame, srgb_conversion_method: SRGBConversionMethod):
+ var reader = VTFLoader.frame_readers.get(hires_image_format, null);
- var img := Image.create_from_data(width, height, use_mipmaps, format, data);
-
- if srgb_conversion_method == SRGBConversionMethod.DURING_IMPORT:
- img.decompress();
- img.compress(Image.COMPRESS_S3TC);
+ if not reader:
+ VMFLogger.error("Unsupported texture format: {0} in file {1}" \
+ .format([VTFLoader.format_labels[hires_image_format], file.get_path()]));
+ return null;
- alpha = flags & Flags.TEXTUREFLAGS_ONEBITALPHA or flags & Flags.TEXTUREFLAGS_EIGHTBITALPHA;
+ var tex := reader.read(self, frame, srgb_conversion_method) as Texture;
- if not img:
- push_error("Corrupted file: {0}".format([file.get_path()]));
+ if not tex:
+ VMFLogger.error("Corrupted file: {0}".format([file.get_path()]));
return null;
- return ImageTexture.create_from_image(img);
+ return tex;
func _init(path, duration):
self.path = path;
diff --git a/addons/godotvmf/godotvmt/vtf_loader.gd.uid b/addons/godotvmf/godotvmt/vtf_loader.gd.uid
new file mode 100644
index 0000000..7ca84dc
--- /dev/null
+++ b/addons/godotvmf/godotvmt/vtf_loader.gd.uid
@@ -0,0 +1 @@
+uid://cj6nnrg47yryc
diff --git a/addons/godotvmf/godotvpk/vpk_directroy_entry.gd b/addons/godotvmf/godotvpk/vpk_directroy_entry.gd
new file mode 100644
index 0000000..e755da3
--- /dev/null
+++ b/addons/godotvmf/godotvpk/vpk_directroy_entry.gd
@@ -0,0 +1,30 @@
+class_name VPKDirectoryEntry extends RefCounted
+
+static func _schema(): return {
+ "crc32": VMFStruct.Type.UINT_32,
+ "preload_bytes": VMFStruct.Type.UINT_16,
+ "archive_index": VMFStruct.Type.UINT_16,
+ "offset": VMFStruct.Type.UINT_32,
+ "length": VMFStruct.Type.UINT_32,
+ "terminator": VMFStruct.Type.UINT_16,
+}
+
+var crc32: int;
+var preload_bytes: int;
+var archive_index: int;
+var offset: int;
+var length: int;
+var terminator: int;
+
+func _to_string() -> String:
+ return "VPKDirectoryEntry(crc32=0x%X, preload_bytes=%d, archive_index=%d, entry_offset=%d, entry_length=%d, terminator=0x%X)" % [crc32, preload_bytes, archive_index, offset, length, terminator];
+
+func as_dict() -> Dictionary:
+ return {
+ "crc32": crc32,
+ "preload_bytes": preload_bytes,
+ "archive_index": archive_index,
+ "offset": offset,
+ "length": length,
+ "terminator": terminator,
+ }
diff --git a/addons/godotvmf/godotvpk/vpk_directroy_entry.gd.uid b/addons/godotvmf/godotvpk/vpk_directroy_entry.gd.uid
new file mode 100644
index 0000000..4f114a6
--- /dev/null
+++ b/addons/godotvmf/godotvpk/vpk_directroy_entry.gd.uid
@@ -0,0 +1 @@
+uid://b46kypmble8d
diff --git a/addons/godotvmf/godotvpk/vpk_header.gd b/addons/godotvmf/godotvpk/vpk_header.gd
new file mode 100644
index 0000000..3db6987
--- /dev/null
+++ b/addons/godotvmf/godotvpk/vpk_header.gd
@@ -0,0 +1,20 @@
+class_name VPKHeader extends RefCounted
+
+const CORRECT_SIGNATURE: int = 0x55AA1234;
+
+static func _schema(): return {
+ "signature": VMFStruct.Type.UINT_32,
+ "version": VMFStruct.Type.UINT_32,
+ "tree_size": VMFStruct.Type.UINT_32,
+}
+
+var signature: int;
+var version: int;
+var tree_size: int;
+
+var is_valid: bool:
+ get: return signature == CORRECT_SIGNATURE;
+
+func _to_string() -> String:
+ # Signature should be provided in hex format
+ return "VPKHeader(signature=0x%X, version=%d, tree_size=%d)" % [signature, version, tree_size];
diff --git a/addons/godotvmf/godotvpk/vpk_header.gd.uid b/addons/godotvmf/godotvpk/vpk_header.gd.uid
new file mode 100644
index 0000000..8bcfc71
--- /dev/null
+++ b/addons/godotvmf/godotvpk/vpk_header.gd.uid
@@ -0,0 +1 @@
+uid://dkwfvotbsbu4
diff --git a/addons/godotvmf/godotvpk/vpk_header_2.gd b/addons/godotvmf/godotvpk/vpk_header_2.gd
new file mode 100644
index 0000000..40d3742
--- /dev/null
+++ b/addons/godotvmf/godotvpk/vpk_header_2.gd
@@ -0,0 +1,19 @@
+class_name VPKHeaderV2 extends VPKHeader
+
+static func _schema(): return {
+ "signature": VMFStruct.Type.UINT_32,
+ "version": VMFStruct.Type.UINT_32,
+ "tree_size": VMFStruct.Type.UINT_32,
+ "file_data_section_size": VMFStruct.Type.UINT_32,
+ "archive_md5_section_size": VMFStruct.Type.UINT_32,
+ "other_md5_section_size": VMFStruct.Type.UINT_32,
+ "signature_section_size": VMFStruct.Type.UINT_32,
+}
+
+var file_data_section_size: int;
+var archive_md5_section_size: int;
+var other_md5_section_size: int;
+var signature_section_size: int;
+
+func _to_string() -> String:
+ return "VPKHeaderV2(signature=0x%X, version=%d, tree_size=%d, file_data_section_size=%d, archive_md5_section_size=%d, other_md5_section_size=%d, signature_section_size=%d)" % [signature, version, tree_size, file_data_section_size, archive_md5_section_size, other_md5_section_size, signature_section_size];
diff --git a/addons/godotvmf/godotvpk/vpk_header_2.gd.uid b/addons/godotvmf/godotvpk/vpk_header_2.gd.uid
new file mode 100644
index 0000000..287ac85
--- /dev/null
+++ b/addons/godotvmf/godotvpk/vpk_header_2.gd.uid
@@ -0,0 +1 @@
+uid://c3dduurqgs1ui
diff --git a/addons/godotvmf/godotvpk/vpk_reader.gd b/addons/godotvmf/godotvpk/vpk_reader.gd
new file mode 100644
index 0000000..c1d917f
--- /dev/null
+++ b/addons/godotvmf/godotvpk/vpk_reader.gd
@@ -0,0 +1,131 @@
+class_name VPKReader extends RefCounted
+
+var vpk_content: Dictionary;
+var vpk_file: FileAccess;
+var archives: Array[FileAccess] = [];
+var debug_mode: bool = false;
+var header: VPKHeader;
+
+## developer.valvesoftware.com - If ArchiveIndex is 0x7fff, the offset of the file data relative to the end of the directory (see the header for more details).
+const ZERO_ARCHIVE: int = 0x7fff;
+
+static func open(file_path: String) -> VPKReader:
+ if not FileAccess.file_exists(file_path): return null;
+
+ return VPKReader.new(file_path);
+
+func _init(file_path: String = ""):
+ vpk_file = FileAccess.open(file_path, FileAccess.READ);
+ read_vpk_header();
+ read_directory();
+
+func free_vpk():
+ if vpk_file: vpk_file.close();
+
+ if archives.size() > 0:
+ for archive in archives:
+ if archive: archive.close();
+
+ if debug_mode: print("Closed VPK file.");
+
+func is_file_exists(file_path: String) -> bool:
+ return vpk_content.has(file_path);
+
+## Returns the file data as a PackedByteArray, or null if the file is not found or an error occurs
+func get_file_data(file_path: String) -> Variant:
+ if not is_file_exists(file_path):
+ if debug_mode: print("File not found in VPK: %s" % file_path);
+ return null;
+
+ var entry: VPKDirectoryEntry = vpk_content.get(file_path, null);
+ var dataset: PackedByteArray;
+
+ if entry.archive_index == ZERO_ARCHIVE:
+ vpk_file.seek(header.tree_size + entry.offset);
+ dataset = vpk_file.get_buffer(entry.length);
+ else:
+ var archive_path = "%s_%03d.vpk" % [vpk_file.get_path().get_basename().replace("_dir", ""), entry.archive_index];
+ var archive_file: FileAccess;
+
+ if debug_mode: print("Reading from archive: %s (index: %d)" % [archive_path, entry.archive_index]);
+
+ if not FileAccess.file_exists(archive_path):
+ if debug_mode: print("Archive file not found: %s" % archive_path);
+ return null;
+
+ # Check if the archive is already open
+ for archive in archives:
+ if archive and archive.get_path() == archive_path:
+ archive_file = archive;
+ break;
+
+ if not archive_file:
+ archive_file = FileAccess.open(archive_path, FileAccess.READ);
+ if not archive_file:
+ if debug_mode: print("Failed to open archive file: %s" % archive_path);
+ return null;
+ archives.append(archive_file);
+
+ archive_file.seek(entry.offset);
+ dataset = archive_file.get_buffer(entry.length);
+
+ return dataset;
+
+func read_vpk_header() -> void:
+ var header := VMFStruct.transform_struct(vpk_file, VPKHeader, VPKHeader._schema());
+
+ if header.version == 2:
+ vpk_file.seek(0);
+ header = VMFStruct.transform_struct(vpk_file, VPKHeaderV2, VPKHeaderV2._schema());
+
+ if not header.is_valid:
+ if debug_mode:
+ print("Invalid VPK file: signature mismatch (0x%X)" % header.signature);
+ print("Expected signature: 0x55AA1234");
+ return;
+
+func read_directory() -> void:
+ while not vpk_file.eof_reached():
+ var extension := VMFStruct.get_null_terminated_string(vpk_file);
+ if extension == "": break;
+
+ while not vpk_file.eof_reached():
+ var path := VMFStruct.get_null_terminated_string(vpk_file);
+ if path == "": break;
+
+ while not vpk_file.eof_reached():
+ var filename := VMFStruct.get_null_terminated_string(vpk_file);
+ if filename == "": break;
+
+ var full_path = VMFUtils.normalize_path("%s/%s.%s" % [path, filename, extension]);
+
+ if debug_mode:
+ print("Reading directory entry for file: %s" % full_path);
+
+ vpk_content[full_path] = read_directory_entry(vpk_file);
+
+
+func read_directory_entry(file: FileAccess) -> VPKDirectoryEntry:
+ return VMFStruct.transform_struct(file, VPKDirectoryEntry, VPKDirectoryEntry._schema());
+
+func extract_file(file_path: String, output_path: String) -> bool:
+ var file_data = get_file_data(file_path);
+ if file_data == null:
+ VMFLogger.error("Failed to extract file: %s (file not found or error occurred)" % file_path);
+ return false;
+
+ if DirAccess.dir_exists_absolute(output_path.get_base_dir()) == false:
+ if DirAccess.make_dir_recursive_absolute(output_path.get_base_dir()):
+ VMFLogger.error("Failed to create output directory: %s" % output_path.get_base_dir());
+ return false;
+
+ var output_file = FileAccess.open(output_path, FileAccess.WRITE);
+ if not output_file:
+ VMFLogger.error("Failed to open output file for writing: %s" % output_path);
+ return false;
+
+ output_file.store_buffer(file_data);
+ output_file.close();
+
+ if debug_mode: print("Successfully extracted file '%s' to '%s'" % [file_path, output_path]);
+ return true;
diff --git a/addons/godotvmf/godotvpk/vpk_reader.gd.uid b/addons/godotvmf/godotvpk/vpk_reader.gd.uid
new file mode 100644
index 0000000..2b5fa75
--- /dev/null
+++ b/addons/godotvmf/godotvpk/vpk_reader.gd.uid
@@ -0,0 +1 @@
+uid://3jmu8wy3o5gd
diff --git a/addons/godotvmf/godotvpk/vpk_stack.gd b/addons/godotvmf/godotvpk/vpk_stack.gd
new file mode 100644
index 0000000..1c2ab93
--- /dev/null
+++ b/addons/godotvmf/godotvpk/vpk_stack.gd
@@ -0,0 +1,56 @@
+class_name VPKStack extends RefCounted
+
+var vpks: Array[VPKReader] = [];
+
+## Creates a new VPKStack instance and initializes it by loading all VPK files from the specified gameinfo directory.
+static func create(gameinfo_dir: String) -> VPKStack:
+ return VPKStack.new(gameinfo_dir);
+
+func _init(gameinfo_dir: String = ""):
+ if gameinfo_dir != "":
+ append(gameinfo_dir);
+
+## Appends all VPK files from the specified gameinfo directory to the stack.
+func append(gameinfo_dir: String):
+ var vpk_files := Array(DirAccess.open(gameinfo_dir) \
+ .get_files()) \
+ .filter(func(file_name: String): return file_name.ends_with("_dir.vpk"));
+
+ for vpk_file in vpk_files:
+ var vpk_path = "%s/%s" % [gameinfo_dir, vpk_file];
+ var vpk_reader = VPKReader.open(vpk_path);
+ if vpk_reader:
+ vpks.append(vpk_reader);
+ else:
+ VMFLogger.error("Failed to open VPK file: %s" % vpk_path);
+
+
+## Frees all VPK readers in the stack and clears the list.
+func free_vpks():
+ for vpk in vpks:
+ if vpk: vpk.free_vpk();
+
+ vpks.clear();
+
+## Checks if the specified file exists in any of the VPKs in the stack.
+func exists(file_path: String) -> bool:
+ for vpk in vpks:
+ if vpk.is_file_exists(file_path): return true;
+
+ return false;
+
+## Extracts the specified file from the VPK stack and saves it to the output path.
+## Returns true on success, or false if the file is not found or an error occurs.
+func extract(file_path: String, output_path: String) -> bool:
+ for vpk in vpks:
+ if vpk and vpk.is_file_exists(file_path):
+ return vpk.extract_file(file_path, output_path);
+
+ return false;
+
+func get_file_data(file_path: String) -> Variant:
+ for vpk in vpks:
+ if vpk and vpk.is_file_exists(file_path):
+ return vpk.get_file_data(file_path);
+
+ return null;
diff --git a/addons/godotvmf/godotvpk/vpk_stack.gd.uid b/addons/godotvmf/godotvpk/vpk_stack.gd.uid
new file mode 100644
index 0000000..87875b9
--- /dev/null
+++ b/addons/godotvmf/godotvpk/vpk_stack.gd.uid
@@ -0,0 +1 @@
+uid://elbb86va8ugo
diff --git a/addons/godotvmf/icons/detailprop.png b/addons/godotvmf/icons/detailprop.png
new file mode 100644
index 0000000..7cf457a
Binary files /dev/null and b/addons/godotvmf/icons/detailprop.png differ
diff --git a/addons/godotvmf/icons/detailprop.png.import b/addons/godotvmf/icons/detailprop.png.import
new file mode 100644
index 0000000..23881d5
--- /dev/null
+++ b/addons/godotvmf/icons/detailprop.png.import
@@ -0,0 +1,41 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://di4shis6ksy0d"
+path.s3tc="res://.godot/imported/detailprop.png-fd9d3af67be602c230d9579ce2ad07a9.s3tc.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc"],
+"vram_texture": true
+}
+
+[deps]
+
+source_file="res://addons/godotvmf/icons/detailprop.png"
+dest_files=["res://.godot/imported/detailprop.png-fd9d3af67be602c230d9579ce2ad07a9.s3tc.ctex"]
+
+[params]
+
+compress/mode=2
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=true
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/godotvmf/icons/discord.png b/addons/godotvmf/icons/discord.png
new file mode 100644
index 0000000..7cb5db0
Binary files /dev/null and b/addons/godotvmf/icons/discord.png differ
diff --git a/addons/godotvmf/icons/discord.png.import b/addons/godotvmf/icons/discord.png.import
new file mode 100644
index 0000000..131d306
--- /dev/null
+++ b/addons/godotvmf/icons/discord.png.import
@@ -0,0 +1,40 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dr1cxxpaorsnn"
+path="res://.godot/imported/discord.png-d5e6dc140c160052b78d0aeaa8e39629.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/godotvmf/icons/discord.png"
+dest_files=["res://.godot/imported/discord.png-d5e6dc140c160052b78d0aeaa8e39629.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/godotvmf/icons/entities.png b/addons/godotvmf/icons/entities.png
new file mode 100644
index 0000000..0f295de
Binary files /dev/null and b/addons/godotvmf/icons/entities.png differ
diff --git a/addons/godotvmf/icons/entities.png.import b/addons/godotvmf/icons/entities.png.import
new file mode 100644
index 0000000..70bf7ba
--- /dev/null
+++ b/addons/godotvmf/icons/entities.png.import
@@ -0,0 +1,41 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://5t8xfn8s1b42"
+path.s3tc="res://.godot/imported/entities.png-02d9cc630984bc1c3e794c43258d3344.s3tc.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc"],
+"vram_texture": true
+}
+
+[deps]
+
+source_file="res://addons/godotvmf/icons/entities.png"
+dest_files=["res://.godot/imported/entities.png-02d9cc630984bc1c3e794c43258d3344.s3tc.ctex"]
+
+[params]
+
+compress/mode=2
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=true
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/godotvmf/icons/full.png b/addons/godotvmf/icons/full.png
new file mode 100644
index 0000000..a94b71d
Binary files /dev/null and b/addons/godotvmf/icons/full.png differ
diff --git a/addons/godotvmf/icons/full.png.import b/addons/godotvmf/icons/full.png.import
new file mode 100644
index 0000000..254ed4a
--- /dev/null
+++ b/addons/godotvmf/icons/full.png.import
@@ -0,0 +1,41 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bjqrp2xglwigg"
+path.s3tc="res://.godot/imported/full.png-0f45be293ad7d77ee784b026b7b02843.s3tc.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc"],
+"vram_texture": true
+}
+
+[deps]
+
+source_file="res://addons/godotvmf/icons/full.png"
+dest_files=["res://.godot/imported/full.png-0f45be293ad7d77ee784b026b7b02843.s3tc.ctex"]
+
+[params]
+
+compress/mode=2
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=true
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/godotvmf/icons/geometry.png b/addons/godotvmf/icons/geometry.png
new file mode 100644
index 0000000..8a5ccbf
Binary files /dev/null and b/addons/godotvmf/icons/geometry.png differ
diff --git a/addons/godotvmf/icons/geometry.png.import b/addons/godotvmf/icons/geometry.png.import
new file mode 100644
index 0000000..2a3f70f
--- /dev/null
+++ b/addons/godotvmf/icons/geometry.png.import
@@ -0,0 +1,41 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://1qin3e0fu440"
+path.s3tc="res://.godot/imported/geometry.png-6469d46ae919fd72e94790e84e98e209.s3tc.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc"],
+"vram_texture": true
+}
+
+[deps]
+
+source_file="res://addons/godotvmf/icons/geometry.png"
+dest_files=["res://.godot/imported/geometry.png-6469d46ae919fd72e94790e84e98e209.s3tc.ctex"]
+
+[params]
+
+compress/mode=2
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=true
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/godotvmf/plugin.cfg b/addons/godotvmf/plugin.cfg
index 1320faf..3780491 100644
--- a/addons/godotvmf/plugin.cfg
+++ b/addons/godotvmf/plugin.cfg
@@ -3,5 +3,5 @@
name="GodotVMF"
description="Allows use VMF files in Godot"
author="H2xDev"
-version="2.2.11"
+version="2.3.0"
script="godotvmf.gd"
diff --git a/addons/godotvmf/plugin.tscn b/addons/godotvmf/plugin.tscn
index c8ffc43..d2b7d08 100644
--- a/addons/godotvmf/plugin.tscn
+++ b/addons/godotvmf/plugin.tscn
@@ -1,6 +1,12 @@
[gd_scene format=3 uid="uid://d2ikk3rylrg4r"]
-[node name="Plugin" type="HBoxContainer"]
+[ext_resource type="Texture2D" uid="uid://bjqrp2xglwigg" path="res://addons/godotvmf/icons/full.png" id="1_51cpy"]
+[ext_resource type="Texture2D" uid="uid://5t8xfn8s1b42" path="res://addons/godotvmf/icons/entities.png" id="2_1uylb"]
+[ext_resource type="Texture2D" uid="uid://1qin3e0fu440" path="res://addons/godotvmf/icons/geometry.png" id="3_f1fjy"]
+[ext_resource type="Texture2D" uid="uid://di4shis6ksy0d" path="res://addons/godotvmf/icons/detailprop.png" id="4_f1fjy"]
+[ext_resource type="Texture2D" uid="uid://dr1cxxpaorsnn" path="res://addons/godotvmf/icons/discord.png" id="5_2stca"]
+
+[node name="Plugin" type="HBoxContainer" unique_id=1189765496]
anchors_preset = 4
anchor_top = 0.5
anchor_bottom = 0.5
@@ -12,52 +18,79 @@ size_flags_horizontal = 0
size_flags_vertical = 0
theme_override_constants/separation = 10
-[node name="Label" type="Button" parent="."]
+[node name="Label" type="Button" parent="." unique_id=1664938702]
layout_mode = 2
size_flags_horizontal = 0
disabled = true
text = "GodotVMF:"
flat = true
-[node name="ReimportVMF" type="Button" parent="."]
+[node name="ReimportVMF" type="Button" parent="." unique_id=777385769]
+custom_minimum_size = Vector2(32, 0)
+layout_mode = 2
+tooltip_text = "Import everything"
+icon = ExtResource("1_51cpy")
+flat = true
+expand_icon = true
+
+[node name="ReimportEntities" type="Button" parent="." unique_id=1044169889]
+custom_minimum_size = Vector2(32, 0)
layout_mode = 2
-text = "Full"
+tooltip_text = "Import entities only"
+icon = ExtResource("2_1uylb")
flat = true
+icon_alignment = 1
+expand_icon = true
-[node name="ReimportEntities" type="Button" parent="."]
+[node name="ReimportGeometry" type="Button" parent="." unique_id=584470251]
+custom_minimum_size = Vector2(32, 0)
layout_mode = 2
-text = "Entities"
+tooltip_text = "Import geometry only
+"
+icon = ExtResource("3_f1fjy")
flat = true
+icon_alignment = 1
+expand_icon = true
-[node name="ReimportGeometry" type="Button" parent="."]
+[node name="ReimportDetailProps" type="Button" parent="." unique_id=1750751317]
+custom_minimum_size = Vector2(32, 0)
layout_mode = 2
-text = "Geometry"
+tooltip_text = "Import geometry only
+"
+icon = ExtResource("4_f1fjy")
flat = true
+icon_alignment = 1
+expand_icon = true
-[node name="VSeparator" type="VSeparator" parent="."]
+[node name="VSeparator" type="VSeparator" parent="." unique_id=551191452]
layout_mode = 2
-[node name="Docs" type="Button" parent="."]
+[node name="Docs" type="Button" parent="." unique_id=2116350026]
layout_mode = 2
text = "Docs"
flat = true
+icon_alignment = 1
+expand_icon = true
-[node name="VSeparator2" type="VSeparator" parent="."]
+[node name="VSeparator2" type="VSeparator" parent="." unique_id=848749026]
layout_mode = 2
-[node name="DiscordSupport" type="Button" parent="."]
+[node name="DiscordSupport" type="Button" parent="." unique_id=1436039780]
+custom_minimum_size = Vector2(32, 0)
layout_mode = 2
-text = "Support In Discord"
+tooltip_text = "Support In Discord"
+icon = ExtResource("5_2stca")
flat = true
+expand_icon = true
-[node name="ProgressBar" type="HBoxContainer" parent="."]
+[node name="ProgressBar" type="HBoxContainer" parent="." unique_id=1489779559]
visible = false
layout_mode = 2
-[node name="VSeparator2" type="VSeparator" parent="ProgressBar"]
+[node name="VSeparator2" type="VSeparator" parent="ProgressBar" unique_id=1370242883]
layout_mode = 2
-[node name="label" type="Button" parent="ProgressBar"]
+[node name="label" type="Button" parent="ProgressBar" unique_id=1755800943]
layout_mode = 2
size_flags_horizontal = 0
disabled = true
diff --git a/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gd b/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gd
deleted file mode 100644
index cbf2c93..0000000
--- a/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gd
+++ /dev/null
@@ -1,106 +0,0 @@
-@tool
-class_name WorldVertexTransitionMaterial extends ShaderMaterial
-
-@export var blend_modulate_texture: Texture = null:
- set(value):
- set_shader_parameter("blend_modulate_texture", value);
- blend_modulate_texture = value;
- emit_changed();
-
-@export var convert_to_srgb: bool = false:
- set(value):
- set_shader_parameter("convert_to_srgb", value);
- convert_to_srgb = value;
- emit_changed();
-
-@export_group("Albedo")
-@export var albedo_texture: Texture = null:
- set(value):
- set_shader_parameter("albedo_texture", value);
- albedo_texture = value;
- emit_changed();
-
-@export var albedo_texture2: Texture = null:
- set(value):
- set_shader_parameter("albedo_texture2", value);
- albedo_texture2 = value;
- emit_changed();
-
-@export_group("Normal")
-@export var normal_texture: Texture = null:
- set(value):
- set_shader_parameter("normal_texture", value);
- normal_texture = value;
- emit_changed();
-
-@export var normal_texture2: Texture = null:
- set(value):
- set_shader_parameter("normal_texture2", value);
- normal_texture2 = value;
- emit_changed();
-
-@export_group("Displacement")
-@export var displacement_texture: Texture = null:
- set(value):
- set_shader_parameter("displacement_texture", value);
- displacement_texture = value;
- emit_changed();
-
-@export var displacement_texture2: Texture = null:
- set(value):
- set_shader_parameter("displacement_texture2", value);
- displacement_texture2 = value;
- emit_changed();
-
-@export_group("Metallic")
-@export_range(0, 1, 0.01) var metallic: float = 0.0:
- set(value):
- set_shader_parameter("metallic", value);
- metallic = value;
- emit_changed();
-
-@export_range(0, 1, 0.01) var specular: float = 0.0:
- set(value):
- set_shader_parameter("specular", value);
- specular = value;
- emit_changed();
-
-@export var metallic_texture: Texture = null:
- set(value):
- set_shader_parameter("metallic_texture", value);
- metallic_texture = value;
- emit_changed();
-
-@export var metallic_texture2: Texture = null:
- set(value):
- set_shader_parameter("metallic_texture2", value);
- metallic_texture2 = value;
- emit_changed();
-
-@export_group("Roughness")
-@export_range(0, 1, 0.01) var roughness: float = 1.0:
- set(value):
- set_shader_parameter("roughness", value);
- roughness = value;
- emit_changed();
-
-@export_range(0, 1, 0.01) var roughness2: float = 1.0:
- set(value):
- set_shader_parameter("roughness2", value);
- roughness2 = value;
- emit_changed();
-
-@export var roughness_texture1: Texture = null:
- set(value):
- set_shader_parameter("roughness_texture", value);
- roughness_texture1 = value;
- emit_changed();
-
-@export var roughness_texture2: Texture = null:
- set(value):
- set_shader_parameter("roughness_texture2", value);
- roughness_texture2 = value;
- emit_changed();
-
-func _init():
- shader = load("res://addons/godotvmf/shaders/WorldVertexTransitionMaterial.gdshader");
diff --git a/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gdshader b/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gdshader
deleted file mode 100644
index 36eff12..0000000
--- a/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gdshader
+++ /dev/null
@@ -1,83 +0,0 @@
-// WorldVertexTransition shader implementation
-// made by H2xDev and based on StandardMaterial3D
-
-shader_type spatial;
-render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx;
-
-uniform vec4 albedo_color : source_color = vec4(1.0);
-uniform sampler2D albedo_texture : source_color,filter_linear_mipmap,repeat_enable;
-
-uniform vec4 albedo_color2 : source_color = vec4(1.0);
-uniform sampler2D albedo_texture2: source_color,filter_linear_mipmap,repeat_enable;
-
-uniform float point_size : hint_range(0, 128);
-uniform float roughness : hint_range(0, 1);
-uniform float roughness2 : hint_range(0, 1);
-
-uniform float metallic;
-uniform sampler2D metallic_texture : hint_default_white,filter_linear_mipmap,repeat_enable;
-uniform sampler2D metallic_texture2 : hint_default_white,filter_linear_mipmap,repeat_enable;
-uniform vec4 metallic_texture_channel;
-
-uniform sampler2D roughness_texture : hint_roughness_r,filter_linear_mipmap,repeat_enable;
-uniform sampler2D roughness_texture2 : hint_roughness_r,filter_linear_mipmap,repeat_enable;
-
-uniform sampler2D blend_modulate_texture: source_color, filter_linear_mipmap, repeat_enable;
-
-uniform float specular;
-uniform vec3 uv1_scale = vec3(1.0);
-uniform vec3 uv1_offset = vec3(0.0);
-uniform vec3 uv2_scale = vec3(1.0);
-uniform vec3 uv2_offset = vec3(0.0);
-uniform bool convert_to_srgb = true;
-
-vec4 from_linear(vec4 linearRGB) {
- bvec3 cutoff = lessThan(linearRGB.rgb, vec3(0.0031308));
- vec3 higher = vec3(1.055)*pow(linearRGB.rgb, vec3(1.0/2.4)) - vec3(0.055);
- vec3 lower = linearRGB.rgb * vec3(12.92);
-
- return vec4(mix(higher, lower, cutoff), linearRGB.a);
-}
-
-void vertex() {
- UV = UV * uv1_scale.xy + uv1_offset.xy;
- UV2 = UV2 * uv2_scale.xy + uv2_offset.xy;
- COLOR.a = 1.0 - COLOR.a;
-}
-
-void fragment() {
- vec4 albedo_tex1 = texture(albedo_texture, UV);
- vec4 albedo_tex2 = texture(albedo_texture2, UV);
-
- vec4 modulate_value = convert_to_srgb
- ? from_linear(texture(blend_modulate_texture, UV))
- : texture(blend_modulate_texture, UV);
-
- float minb = max(modulate_value.g - modulate_value.r, 0.0);
- float maxb = min(modulate_value.g + modulate_value.r, 1.0);
-
- float blendfactor = COLOR.r;
- blendfactor = smoothstep(minb, maxb, blendfactor);
-
- vec4 albedo = mix(albedo_color, albedo_color2, blendfactor);
- vec4 albedo_tex = mix(albedo_tex1, albedo_tex2, blendfactor);
-
- ALBEDO = albedo_tex.rgb * albedo.rgb;
-
- float metallic_tex1 = dot(texture(metallic_texture, UV), metallic_texture_channel);
- float metallic_tex2 = dot(texture(metallic_texture2, UV), metallic_texture_channel);
-
- float metallic_tex = mix(metallic_tex1, metallic_tex2, COLOR.r);
-
- METALLIC = metallic_tex * metallic;
-
- vec4 roughness_texture_channel = vec4(1.0,0.0,0.0,0.0);
-
- float roughness_tex1 = dot(texture(roughness_texture, UV), roughness_texture_channel);
- float roughness_tex2 = dot(texture(roughness_texture2, UV), roughness_texture_channel);
-
- float roughness_tex = mix(roughness_tex1 * roughness, roughness_tex2 * roughness2, COLOR.r);
-
- ROUGHNESS = roughness_tex;
- SPECULAR = specular;
-}
diff --git a/addons/godotvmf/shaders/world_vertex_transition_material.gdshader b/addons/godotvmf/shaders/world_vertex_transition_material.gdshader
new file mode 100644
index 0000000..c433fc5
--- /dev/null
+++ b/addons/godotvmf/shaders/world_vertex_transition_material.gdshader
@@ -0,0 +1,82 @@
+shader_type spatial;
+render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
+
+uniform bool convert_to_srgb;
+uniform sampler2D blendmodulatetexture : hint_default_white, filter_linear_mipmap, repeat_enable;
+
+group_uniforms albedo;
+uniform sampler2D basetexture : source_color, filter_linear_mipmap, repeat_enable;
+uniform sampler2D basetexture2 : source_color, filter_linear_mipmap, repeat_enable;
+
+group_uniforms normal;
+uniform sampler2D bumpmap : hint_roughness_normal, filter_linear_mipmap, repeat_enable;
+uniform float bumpscale : hint_range(-16.0, 16.0) = 10.0;
+
+uniform sampler2D bumpmap2 : hint_roughness_normal, filter_linear_mipmap, repeat_enable;
+uniform float bumpscale2 : hint_range(-16.0, 16.0) = 1.0;
+
+group_uniforms metallic;
+uniform sampler2D metallictexture : hint_default_white, filter_linear_mipmap, repeat_enable;
+uniform float metallic : hint_range(0.0, 1.0, 0.01) = 0.0;
+uniform float specular : hint_range(0.0, 1.0, 0.01) = 0.5;
+
+uniform sampler2D metallictexture2 : hint_default_white, filter_linear_mipmap, repeat_enable;
+uniform float metallic2 : hint_range(0.0, 1.0, 0.01) = 0.0;
+uniform float specular2 : hint_range(0.0, 1.0, 0.01) = 0.5;
+
+group_uniforms roughness;
+uniform sampler2D roughnesstexture : hint_roughness_r, filter_linear_mipmap, repeat_enable;
+uniform float roughness : hint_range(0.0, 1.0) = 1.0;
+
+uniform sampler2D roughnesstexture2 : hint_roughness_r, filter_linear_mipmap, repeat_enable;
+uniform float roughness2 : hint_range(0.0, 1.0) = 1.0;
+
+group_uniforms height;
+uniform sampler2D heighttexture : hint_default_black, filter_linear_mipmap, repeat_enable;
+uniform sampler2D heighttexture2 : hint_default_black, filter_linear_mipmap, repeat_enable;
+
+group_uniforms uv_transform;
+// --- UV transforms ---
+uniform vec3 uv1_scale = vec3(1.0, 1.0, 1.0);
+uniform vec3 uv1_offset = vec3(0.0, 0.0, 0.0);
+uniform vec3 uv2_scale = vec3(1.0, 1.0, 1.0);
+uniform vec3 uv2_offset = vec3(0.0, 0.0, 0.0);
+
+void vertex() {
+ UV = UV * uv1_scale.xy + uv1_offset.xy;
+ UV2 = UV2 * uv2_scale.xy + uv2_offset.xy;
+}
+
+void fragment() {
+ vec4 modulate_value = texture(blendmodulatetexture, UV);
+
+ float minb = max(modulate_value.g - modulate_value.r, 0.0);
+ float maxb = min(modulate_value.g + modulate_value.r, 1.0);
+
+ float blend = 1.0 - COLOR.r;
+ blend = smoothstep(minb, maxb, blend);
+
+ // Layer 1
+ vec4 alb1 = texture(basetexture, UV);
+ vec3 nrm1 = texture(bumpmap, UV).rgb;
+ float met1 = texture(metallictexture, UV).r * metallic;
+ float rgh1 = texture(roughnesstexture, UV).r * roughness;
+
+ // Layer 2
+ vec4 alb2 = texture(basetexture2, UV);
+ vec3 nrm2 = texture(bumpmap2, UV).rgb;
+ float met2 = texture(metallictexture2, UV).r * metallic2;
+ float rgh2 = texture(roughnesstexture2, UV).r * roughness2;
+
+ vec3 albedo_col = mix(alb1.rgb, alb2.rgb, blend);
+ if (convert_to_srgb) {
+ albedo_col = pow(max(albedo_col, vec3(0.0)), vec3(1.0 / 2.2));
+ }
+
+ ALBEDO = albedo_col;
+ METALLIC = mix(met1, met2, blend);
+ SPECULAR = mix(specular, specular2, blend);
+ ROUGHNESS = mix(rgh1, rgh2, blend);
+ NORMAL_MAP = mix(nrm1, nrm2, blend);
+ NORMAL_MAP_DEPTH = mix(bumpscale, bumpscale2, blend);
+}
diff --git a/addons/godotvmf/shaders/world_vertex_transition_material.gdshader.uid b/addons/godotvmf/shaders/world_vertex_transition_material.gdshader.uid
new file mode 100644
index 0000000..275982a
--- /dev/null
+++ b/addons/godotvmf/shaders/world_vertex_transition_material.gdshader.uid
@@ -0,0 +1 @@
+uid://dh6ihfix4xggk
diff --git a/addons/godotvmf/src/structs/vmf_displacement_info.gd b/addons/godotvmf/src/structs/vmf_displacement_info.gd
index 6e68bd2..8bf5ca2 100644
--- a/addons/godotvmf/src/structs/vmf_displacement_info.gd
+++ b/addons/godotvmf/src/structs/vmf_displacement_info.gd
@@ -68,9 +68,9 @@ func get_color(x: float, y: float) -> Color:
var index = y + x * verts_count;
if alphas.size() == 0:
- return Color8(255, 0, 0);
+ return Color8(0, 0, 0);
- return Color8(int(alphas[index]), 0, 0);
+ return Color8(255 - int(alphas[index]), 0, 0);
func get_vertices() -> Array[Vector3]:
if side.vertices.size() < 3:
diff --git a/addons/godotvmf/src/structs/vmf_displacement_info.gd.uid b/addons/godotvmf/src/structs/vmf_displacement_info.gd.uid
new file mode 100644
index 0000000..45c8b4f
--- /dev/null
+++ b/addons/godotvmf/src/structs/vmf_displacement_info.gd.uid
@@ -0,0 +1 @@
+uid://dfgyx1jear054
diff --git a/addons/godotvmf/src/structs/vmf_entity.gd b/addons/godotvmf/src/structs/vmf_entity.gd
index 89a148a..6b15b85 100644
--- a/addons/godotvmf/src/structs/vmf_entity.gd
+++ b/addons/godotvmf/src/structs/vmf_entity.gd
@@ -15,9 +15,15 @@ func _init(raw: Dictionary) -> void:
id = int(raw.get("id", -1));
classname = raw.get("classname", "");
data = raw;
- has_solid = "solid" in raw and raw.solid is Dictionary;
+ has_solid = "solid" in raw and (raw.solid is Array or raw.solid is Dictionary);
angles = raw.get("angles", Vector3.ZERO);
- origin = raw.get("origin", Vector3.ZERO);
+
+ ## NOTE: In some entities there might be more than one "origin" key (d1_town_01.vmf)
+ var origin_value = raw.get("origin", Vector3.ZERO);
+ if origin_value is Array:
+ origin = origin_value[0]
+ else:
+ origin = origin_value;
if has_solid:
var raw_solids: Variant = raw.get("solid", []);
diff --git a/addons/godotvmf/src/structs/vmf_entity.gd.uid b/addons/godotvmf/src/structs/vmf_entity.gd.uid
new file mode 100644
index 0000000..407ff17
--- /dev/null
+++ b/addons/godotvmf/src/structs/vmf_entity.gd.uid
@@ -0,0 +1 @@
+uid://de4kb6rr0atwx
diff --git a/addons/godotvmf/src/structs/vmf_entity_type.gd.uid b/addons/godotvmf/src/structs/vmf_entity_type.gd.uid
new file mode 100644
index 0000000..1f0d73e
--- /dev/null
+++ b/addons/godotvmf/src/structs/vmf_entity_type.gd.uid
@@ -0,0 +1 @@
+uid://cd47kc0ba15tw
diff --git a/addons/godotvmf/src/structs/vmf_side.gd b/addons/godotvmf/src/structs/vmf_side.gd
index 1dcf647..c596ffe 100644
--- a/addons/godotvmf/src/structs/vmf_side.gd
+++ b/addons/godotvmf/src/structs/vmf_side.gd
@@ -21,7 +21,7 @@ func _to_string() -> String:
func _init(raw: Dictionary, _solid: VMFSolid) -> void:
id = raw.get("id", -1);
- material = raw.get("material", "");
+ material = raw.get("material", "").to_lower();
is_displacement = "dispinfo" in raw;
smoothing_groups = raw.get("smoothing_groups", 0);
solid = _solid;
diff --git a/addons/godotvmf/src/structs/vmf_side.gd.uid b/addons/godotvmf/src/structs/vmf_side.gd.uid
new file mode 100644
index 0000000..e40955a
--- /dev/null
+++ b/addons/godotvmf/src/structs/vmf_side.gd.uid
@@ -0,0 +1 @@
+uid://bgba2ti4g60uh
diff --git a/addons/godotvmf/src/structs/vmf_solid.gd b/addons/godotvmf/src/structs/vmf_solid.gd
index 1e335b4..23972a9 100644
--- a/addons/godotvmf/src/structs/vmf_solid.gd
+++ b/addons/godotvmf/src/structs/vmf_solid.gd
@@ -73,7 +73,7 @@ func _compute_vertices_from_planes() -> void:
var valid := true;
for s in sides:
- if s.plane.distance_to(vertex) > 0.001:
+ if s.plane.distance_to(vertex) > 0.01:
valid = false;
break;
if not valid: continue;
diff --git a/addons/godotvmf/src/structs/vmf_solid.gd.uid b/addons/godotvmf/src/structs/vmf_solid.gd.uid
new file mode 100644
index 0000000..69589fe
--- /dev/null
+++ b/addons/godotvmf/src/structs/vmf_solid.gd.uid
@@ -0,0 +1 @@
+uid://c58p11qrmoydj
diff --git a/addons/godotvmf/src/structs/vmf_structure.gd.uid b/addons/godotvmf/src/structs/vmf_structure.gd.uid
new file mode 100644
index 0000000..ba99b44
--- /dev/null
+++ b/addons/godotvmf/src/structs/vmf_structure.gd.uid
@@ -0,0 +1 @@
+uid://ddq5sixse5isw
diff --git a/addons/godotvmf/src/structs/vmf_tex_coord.gd.uid b/addons/godotvmf/src/structs/vmf_tex_coord.gd.uid
new file mode 100644
index 0000000..d06ccb3
--- /dev/null
+++ b/addons/godotvmf/src/structs/vmf_tex_coord.gd.uid
@@ -0,0 +1 @@
+uid://j6e43fgkmwlp
diff --git a/addons/godotvmf/src/valve_io_node.gd.uid b/addons/godotvmf/src/valve_io_node.gd.uid
new file mode 100644
index 0000000..ce34085
--- /dev/null
+++ b/addons/godotvmf/src/valve_io_node.gd.uid
@@ -0,0 +1 @@
+uid://ud26daj6nrfr
diff --git a/addons/godotvmf/src/vmf_cache.gd.uid b/addons/godotvmf/src/vmf_cache.gd.uid
new file mode 100644
index 0000000..afdd6fd
--- /dev/null
+++ b/addons/godotvmf/src/vmf_cache.gd.uid
@@ -0,0 +1 @@
+uid://n2sfmrrl18vl
diff --git a/addons/godotvmf/src/vmf_config.gd b/addons/godotvmf/src/vmf_config.gd
index ad5ddaa..4c174f0 100644
--- a/addons/godotvmf/src/vmf_config.gd
+++ b/addons/godotvmf/src/vmf_config.gd
@@ -3,26 +3,129 @@ class_name VMFConfig extends RefCounted
const CONFIG_FILE_PATH = "res://vmf.config.json";
-const SETTINGS_TO_LOAD = [
- "godot_vmf/models/import",
- "godot_vmf/models/target_folder",
- "godot_vmf/models/lightmap_texel_size",
- "godot_vmf/materials/import_mode",
- "godot_vmf/materials/target_folder",
- "godot_vmf/materials/ignore",
- "godot_vmf/materials/fallback_material",
- "godot_vmf/materials/default_texture_size",
- "godot_vmf/import/scale",
- "godot_vmf/import/generate_lightmap_uv2",
- "godot_vmf/import/generate_collision",
- "godot_vmf/import/generate_navigation_mesh",
- "godot_vmf/import/navigation_mesh_preset",
- "godot_vmf/import/lightmap_texel_size",
- "godot_vmf/import/instances_folder",
- "godot_vmf/import/entities_folder",
- "godot_vmf/import/geometry_folder",
- "godot_vmf/import/entity_aliases",
-];
+const SETTINGS_TO_LOAD = {
+ "godot_vmf/import/gameinfo_path": {
+ "default_value": "res://",
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_GLOBAL_DIR,
+ },
+ "godot_vmf/models/import": {
+ "default_value": false,
+ "type": TYPE_BOOL,
+ "hint": PROPERTY_HINT_NONE,
+ },
+ "godot_vmf/models/target_folder": {
+ "default_value": "res://",
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_NONE,
+ },
+ "godot_vmf/models/lightmap_texel_size": {
+ "default_value": 0.4,
+ "type": TYPE_FLOAT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,100.0,0.000001",
+ },
+ "godot_vmf/materials/import_mode": {
+ "default_value": 0,
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_ENUM,
+ "hint_string": "Use Existing, Import from the GameInfo folder",
+ },
+ "godot_vmf/materials/target_folder": {
+ "default_value": "res://materials",
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_NONE,
+ },
+ "godot_vmf/materials/ignore": {
+ "default_value": ["tools/toolsnodraw", "tools/toolsskybox", "tools/toolsinvisible"],
+ "type": TYPE_ARRAY,
+ "hint": PROPERTY_HINT_ARRAY_TYPE,
+ "hint_string": "%d:String" % TYPE_STRING,
+ },
+ "godot_vmf/materials/fallback_material": {
+ "default_value": "",
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_RESOURCE_TYPE,
+ "hint_string": "Material",
+ },
+ "godot_vmf/materials/default_texture_size": {
+ "default_value": 512,
+ "type": TYPE_INT,
+ "hint": PROPERTY_HINT_NONE,
+ },
+ "godot_vmf/import/scale": {
+ "default_value": 0.02,
+ "type": TYPE_FLOAT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,100.0,0.000001",
+ },
+ "godot_vmf/import/generate_lightmap_uv2": {
+ "default_value": true,
+ "type": TYPE_BOOL,
+ "hint": PROPERTY_HINT_NONE,
+ },
+ "godot_vmf/import/generate_collision": {
+ "default_value": true,
+ "type": TYPE_BOOL,
+ "hint": PROPERTY_HINT_NONE,
+ },
+ "godot_vmf/import/generate_navigation_mesh": {
+ "default_value": false,
+ "type": TYPE_BOOL,
+ "hint": PROPERTY_HINT_NONE,
+ },
+ "godot_vmf/import/navigation_mesh_preset": {
+ "default_value": "",
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_RESOURCE_TYPE,
+ "hint_string": "NavigationMesh",
+ },
+ "godot_vmf/import/lightmap_texel_size": {
+ "default_value": 0.2,
+ "type": TYPE_FLOAT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,100.0,0.000001",
+ },
+ "godot_vmf/import/instances_folder": {
+ "default_value": "res://instances",
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_DIR,
+ },
+ "godot_vmf/import/entities_folder": {
+ "default_value": "res://entities",
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_DIR,
+ },
+ "godot_vmf/import/geometry_folder": {
+ "default_value": "res://geometry",
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_DIR,
+ },
+ "godot_vmf/import/entity_aliases": {
+ "default_value": {},
+ "type": TYPE_DICTIONARY,
+ "hint": PROPERTY_HINT_DICTIONARY_TYPE,
+ "hint_string": "%d:;%d/%d:*.tscn" % [TYPE_STRING, TYPE_STRING, PROPERTY_HINT_FILE],
+ },
+ "godot_vmf/import/detail_props_chunk_size": {
+ "default_value": 32.0,
+ "type": TYPE_FLOAT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,1000.0,0.000001",
+ },
+ "godot_vmf/import/detail_props_draw_distance": {
+ "default_value": 100.0,
+ "type": TYPE_FLOAT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,1000.0,0.000001",
+ },
+ "godot_vmf/import/additional_import_paths": {
+ "default_value": [],
+ "type": TYPE_ARRAY,
+ "hint": PROPERTY_HINT_ARRAY_TYPE,
+ "hint_string": "%d/%d:" % [TYPE_STRING, PROPERTY_HINT_GLOBAL_DIR],
+ }
+};
class ModelsConfig:
## If true, the importer will import models from the mod folder
@@ -97,9 +200,19 @@ class ImportConfig:
## If specified, the importer will use this preset for the navigation mesh
var navigation_mesh_preset: String = ProjectSettings.get_setting("godot_vmf/import/navigation_mesh_preset", "");
+ ## Detail props are meshes instanced across the level geometry based on material metadata.
+ ## This field defines a size of one chunk of detail props. The bigger the chunk size, the less multimesh instances will be created,
+ ## but the more detail props will be in one chunk, which can lead to worse performance.
+ var detail_props_chunk_size: float = ProjectSettings.get_setting("godot_vmf/import/detail_props_chunk_size", 32.0);
+
+ ## The maximum distance at which detail props will be visible. This is important for performance, as rendering too many detail props at long distances can be costly.
+ var detail_props_draw_distance: float = ProjectSettings.get_setting("godot_vmf/import/detail_props_draw_distance", 100.0);
+
var gameinfo_path: String = ProjectSettings.get_setting("godot_vmf/import/gameinfo_path", "res://");
- ## NOTE: Support previous version of this config where this field wasn't a part of ImportConfig
+ var additional_import_paths: Array = ProjectSettings.get_setting("godot_vmf/import/additional_import_paths", []);
+
+## NOTE: Support previous version of this config where this field wasn't a part of ImportConfig
static var gameinfo_path: String:
get: return ProjectSettings.get_setting("godot_vmf/import/gameinfo_path", "res://");
@@ -154,229 +267,17 @@ static func detach_signals():
ProjectSettings.settings_changed.disconnect(update_config_field);
static func define_project_settings():
- if not ProjectSettings.has_setting("godot_vmf/import/gameinfo_path"):
- ProjectSettings.set_setting("godot_vmf/import/gameinfo_path", "res://");
- ProjectSettings.set_initial_value("godot_vmf/import/gameinfo_path", "res://");
-
-
- ## Import
- if not ProjectSettings.has_setting("godot_vmf/import/scale"):
- ProjectSettings.set_setting("godot_vmf/import/scale", 0.02);
- ProjectSettings.set_initial_value("godot_vmf/import/scale", 0.02);
-
- if not ProjectSettings.has_setting("godot_vmf/import/generate_lightmap_uv2"):
- ProjectSettings.set_setting("godot_vmf/import/generate_lightmap_uv2", true);
- ProjectSettings.set_initial_value("godot_vmf/import/generate_lightmap_uv2", true);
-
- if not ProjectSettings.has_setting("godot_vmf/import/generate_collision"):
- ProjectSettings.set_setting("godot_vmf/import/generate_collision", true);
- ProjectSettings.set_initial_value("godot_vmf/import/generate_collision", true);
-
- if not ProjectSettings.has_setting("godot_vmf/import/generate_navigation_mesh"):
- ProjectSettings.set_setting("godot_vmf/import/generate_navigation_mesh", false);
- ProjectSettings.set_initial_value("godot_vmf/import/generate_navigation_mesh", false);
-
- if not ProjectSettings.has_setting("godot_vmf/import/navigation_mesh_preset"):
- ProjectSettings.set_setting("godot_vmf/import/navigation_mesh_preset", "");
- ProjectSettings.set_initial_value("godot_vmf/import/navigation_mesh_preset", "");
-
- if not ProjectSettings.has_setting("godot_vmf/import/lightmap_texel_size"):
- ProjectSettings.set_setting("godot_vmf/import/lightmap_texel_size", 0.2);
- ProjectSettings.set_initial_value("godot_vmf/import/lightmap_texel_size", 0.2);
-
- if not ProjectSettings.has_setting("godot_vmf/import/instances_folder"):
- ProjectSettings.set_setting("godot_vmf/import/instances_folder", "res://instances");
- ProjectSettings.set_initial_value("godot_vmf/import/instances_folder", "res://instances");
-
- if not ProjectSettings.has_setting("godot_vmf/import/entities_folder"):
- ProjectSettings.set_setting("godot_vmf/import/entities_folder", "res://entities");
- ProjectSettings.set_initial_value("godot_vmf/import/entities_folder", "res://entities");
-
- if not ProjectSettings.has_setting("godot_vmf/import/geometry_folder"):
- ProjectSettings.set_setting("godot_vmf/import/geometry_folder", "res://geometry");
- ProjectSettings.set_initial_value("godot_vmf/import/geometry_folder", "res://geometry");
-
- if not ProjectSettings.has_setting("godot_vmf/import/entity_aliases"):
- ProjectSettings.set_setting("godot_vmf/import/entity_aliases", {});
- ProjectSettings.set_initial_value("godot_vmf/import/entity_aliases", {});
-
-
- ## Models
- if not ProjectSettings.has_setting("godot_vmf/models/import"):
- ProjectSettings.set_setting("godot_vmf/models/import", false);
- ProjectSettings.set_initial_value("godot_vmf/models/import", false);
-
- if not ProjectSettings.has_setting("godot_vmf/models/target_folder"):
- ProjectSettings.set_setting("godot_vmf/models/target_folder", "res://");
- ProjectSettings.set_initial_value("godot_vmf/models/target_folder", "res://");
-
- if not ProjectSettings.has_setting("godot_vmf/models/lightmap_texel_size"):
- ProjectSettings.set_setting("godot_vmf/models/lightmap_texel_size", 0.4);
- ProjectSettings.set_initial_value("godot_vmf/models/lightmap_texel_size", 0.4);
-
-
- ## Materials
- if not ProjectSettings.has_setting("godot_vmf/materials/import_mode"):
- ProjectSettings.set_setting("godot_vmf/materials/import_mode", 0);
- ProjectSettings.set_initial_value("godot_vmf/materials/import_mode", 0);
-
- if not ProjectSettings.has_setting("godot_vmf/materials/target_folder"):
- ProjectSettings.set_setting("godot_vmf/materials/target_folder", "res://materials");
- ProjectSettings.set_initial_value("godot_vmf/materials/target_folder", "res://materials");
-
- if not ProjectSettings.has_setting("godot_vmf/materials/ignore"):
- ProjectSettings.set_setting("godot_vmf/materials/ignore", ["tools/toolsnodraw", "tools/toolsskybox", "tools/toolsinvisible"]);
- ProjectSettings.set_initial_value("godot_vmf/materials/ignore", ["tools/toolsnodraw", "tools/toolsskybox", "tools/toolsinvisible"]);
-
- if not ProjectSettings.has_setting("godot_vmf/materials/fallback_material"):
- ProjectSettings.set_setting("godot_vmf/materials/fallback_material", "");
- ProjectSettings.set_initial_value("godot_vmf/materials/fallback_material", "");
-
- if not ProjectSettings.has_setting("godot_vmf/materials/default_texture_size"):
- ProjectSettings.set_setting("godot_vmf/materials/default_texture_size", 512);
- ProjectSettings.set_initial_value("godot_vmf/materials/default_texture_size", 512);
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/import/gameinfo_path",
- "type": TYPE_STRING,
- "hint": PROPERTY_HINT_GLOBAL_FILE ,
- "hint_string": "gameinfo.txt,GameInfo.txt",
- "default_value": 'res://',
- })
-
- ## Import
- ProjectSettings.add_property_info({
- "name": "godot_vmf/import/scale",
- "type": TYPE_FLOAT,
- "hint": PROPERTY_HINT_NONE,
- "default_value": 0.02,
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/import/generate_lightmap_uv2",
- "type": TYPE_BOOL,
- "hint": PROPERTY_HINT_NONE,
- "default_value": true,
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/import/generate_navigation_mesh",
- "type": TYPE_BOOL,
- "hint": PROPERTY_HINT_NONE,
- "default_value": false,
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/import/navigation_mesh_preset",
- "type": TYPE_STRING,
- "hint": PROPERTY_HINT_RESOURCE_TYPE,
- "hint_string": "NavigationMesh",
- "default_value": "",
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/import/generate_collision",
- "type": TYPE_BOOL,
- "hint": PROPERTY_HINT_NONE,
- "default_value": false,
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/import/lightmap_texel_size",
- "type": TYPE_FLOAT,
- "hint": PROPERTY_HINT_NONE,
- "default_value": 0.2,
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/import/instances_folder",
- "type": TYPE_STRING,
- "hint": PROPERTY_HINT_DIR,
- "default_value": "res://instances",
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/import/entities_folder",
- "type": TYPE_STRING,
- "hint": PROPERTY_HINT_DIR,
- "default_value": "res://entities",
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/import/geometry_folder",
- "type": TYPE_STRING,
- "hint": PROPERTY_HINT_DIR,
- "default_value": "res://geometry",
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/import/entity_aliases",
- "type": TYPE_DICTIONARY,
- "hint": PROPERTY_HINT_DICTIONARY_TYPE,
- "hint_string": "%d:;%d/%d:*.tscn" % [TYPE_STRING, TYPE_STRING, PROPERTY_HINT_FILE],
- "default_value": {},
- })
-
- ## Models
- ProjectSettings.add_property_info({
- "name": "godot_vmf/models/import",
- "type": TYPE_BOOL,
- "hint": PROPERTY_HINT_NONE,
- "default_value": false
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/models/target_folder",
- "type": TYPE_STRING,
- "hint": PROPERTY_HINT_NONE,
- "default_value": "res://"
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/models/lightmap_texel_size",
- "type": TYPE_FLOAT,
- "hint": PROPERTY_HINT_NONE,
- "default_value": 0.4,
- })
-
- ## Materials
- ProjectSettings.add_property_info({
- "name": "godot_vmf/materials/import_mode",
- "type": TYPE_INT,
- "hint": PROPERTY_HINT_ENUM,
- "hint_string": "Use Existing, Import from the GameInfo folder",
- "default_value": 0,
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/materials/target_folder",
- "type": TYPE_STRING,
- "hint": PROPERTY_HINT_NONE,
- "default_value": "res://materials",
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/materials/ignore",
- "type": TYPE_ARRAY,
- "hint": PROPERTY_HINT_ARRAY_TYPE,
- "hint_string": "%d:String" % TYPE_STRING,
- "default_value": ["tools/toolsnodraw", "tools/toolsskybox", "tools/toolsinvisible"],
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/materials/fallback_material",
- "type": TYPE_STRING,
- "hint": PROPERTY_HINT_RESOURCE_TYPE,
- "hint_string": "Material",
- "default_value": "",
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/materials/default_texture_size",
- "type": TYPE_INT,
- "hint": PROPERTY_HINT_NONE,
- "default_value": 512,
- })
+ for setting in SETTINGS_TO_LOAD:
+ var info = SETTINGS_TO_LOAD[setting]
+ var default_val = info["default_value"]
+ if not ProjectSettings.has_setting(setting):
+ ProjectSettings.set_setting(setting, default_val)
+ ProjectSettings.set_initial_value(setting, default_val)
+ ProjectSettings.set_as_basic(setting, true)
+
+ var prop_info = info.duplicate()
+ prop_info["name"] = setting
+ ProjectSettings.add_property_info(prop_info)
ProjectSettings.settings_changed.connect.call_deferred(update_config_field);
@@ -388,3 +289,4 @@ static func load_config():
file.close();
assign(VMFConfig, json);
+ update_config_field();
diff --git a/addons/godotvmf/src/vmf_config.gd.uid b/addons/godotvmf/src/vmf_config.gd.uid
new file mode 100644
index 0000000..1c8cc06
--- /dev/null
+++ b/addons/godotvmf/src/vmf_config.gd.uid
@@ -0,0 +1 @@
+uid://cxxd8yjc6jd2d
diff --git a/addons/godotvmf/src/vmf_csg.gd.uid b/addons/godotvmf/src/vmf_csg.gd.uid
new file mode 100644
index 0000000..c34defa
--- /dev/null
+++ b/addons/godotvmf/src/vmf_csg.gd.uid
@@ -0,0 +1 @@
+uid://bhh0ifaosse5p
diff --git a/addons/godotvmf/src/vmf_detail_props.gd b/addons/godotvmf/src/vmf_detail_props.gd
new file mode 100644
index 0000000..9839cde
--- /dev/null
+++ b/addons/godotvmf/src/vmf_detail_props.gd
@@ -0,0 +1,198 @@
+class_name VMFDetailProps extends RefCounted
+
+## Generates detail props from mesh based on material metadata
+static func generate(original_mesh: ArrayMesh) -> Array[MultiMesh]:
+ var chunk_size := VMFConfig.import.detail_props_chunk_size;
+ var inv_chunk_size := 1.0 / chunk_size
+
+ var result: Array[MultiMesh] = []
+
+ # Pre-compute constant basis tilt (rotation around X by -PI/2)
+ var tilt_basis := Basis.IDENTITY.rotated(Vector3.RIGHT, -PI / 2)
+ var rotation_to_rad := PI / 180.0
+
+ for surface_idx in range(original_mesh.get_surface_count()):
+ var material := original_mesh.surface_get_material(surface_idx)
+ if not material:
+ continue
+ var material_details := material.get_meta("details", {})
+
+ var arrays := original_mesh.surface_get_arrays(surface_idx)
+ var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
+ var normals := arrays[Mesh.ARRAY_NORMAL] as PackedVector3Array
+ var colors := arrays[Mesh.ARRAY_COLOR] as PackedColorArray
+ var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
+
+ if indices.is_empty():
+ continue
+
+ var has_colors := not colors.is_empty()
+ var num_triangles := indices.size() / 3
+
+ for prop_index in range(2):
+ var index_postfix := "" if not prop_index else str(prop_index + 1)
+
+ var detail_prop_path := material_details.get("$detailprop" + index_postfix, "") as String
+ if detail_prop_path.is_empty():
+ continue
+
+ var density: float = material_details.get("$detailpropdensity" + index_postfix, 1.0)
+ var scale_range: Vector2 = material_details.get("$detailpropscale" + index_postfix, Vector2(1.0, 1.0)) as Vector2
+ var rotation_range: Vector2 = material_details.get("$detailproprotation" + index_postfix, Vector2(0.0, 360.0)) as Vector2
+ var cast_shadows: bool = material_details.get("$detailpropshadows" + index_postfix, 1) == 1
+
+ if not ResourceLoader.exists(detail_prop_path):
+ VMFLogger.warn("Detail prop scene not found: " + detail_prop_path)
+ continue
+
+ var prop_mesh = ResourceLoader.load(detail_prop_path)
+ if not prop_mesh:
+ VMFLogger.error("Failed to load detail prop in %s" % material.resource_path)
+ continue
+
+ if prop_mesh is not Mesh:
+ VMFLogger.error("The specified detail prop in %s is not Mesh" % material.resource_path)
+ continue
+
+ # Pass 1: Count instances per triangle to pre-allocate buffers
+ var tri_counts := PackedInt32Array()
+ tri_counts.resize(num_triangles)
+ var total_estimate := 0
+
+ for i in range(num_triangles):
+ var i1 := indices[i * 3]
+ var i2 := indices[i * 3 + 1]
+ var i3 := indices[i * 3 + 2]
+ var edge1 := vertices[i2] - vertices[i1]
+ var edge2 := vertices[i3] - vertices[i1]
+ var area := 0.5 * edge1.cross(edge2).length()
+ var count := int(round(area * density))
+ tri_counts[i] = count
+ total_estimate += count
+
+ if total_estimate == 0:
+ continue
+
+ # Pass 2: Generate points into a flat pre-allocated buffer
+ var point_data := PackedFloat32Array()
+ point_data.resize(total_estimate * 7)
+ var chunk_indices := PackedInt32Array()
+ chunk_indices.resize(total_estimate * 3)
+ var actual_count := 0
+
+ for i in range(num_triangles):
+ var count := tri_counts[i]
+ if count <= 0:
+ continue
+
+ var i1 := indices[i * 3]
+ var i2 := indices[i * 3 + 1]
+ var i3 := indices[i * 3 + 2]
+
+ var v1 := vertices[i1]
+ var v2 := vertices[i2]
+ var v3 := vertices[i3]
+ var n1 := normals[i1]
+ var n2 := normals[i2]
+ var n3 := normals[i3]
+
+ var c1r := 1.0
+ var c2r := 1.0
+ var c3r := 1.0
+
+ if has_colors:
+ c1r = colors[i1].r
+ c2r = colors[i2].r
+ c3r = colors[i3].r
+
+ # Cache component values to avoid repeated property access
+ var v1x := v1.x; var v1y := v1.y; var v1z := v1.z
+ var v2x := v2.x; var v2y := v2.y; var v2z := v2.z
+ var v3x := v3.x; var v3y := v3.y; var v3z := v3.z
+ var n1x := n1.x; var n1y := n1.y; var n1z := n1.z
+ var n2x := n2.x; var n2y := n2.y; var n2z := n2.z
+ var n3x := n3.x; var n3y := n3.y; var n3z := n3.z
+
+ for _j in range(count):
+ var r1 := randf()
+ var r2 := randf()
+ var sqrt_r1 := sqrt(r1)
+
+ var bw := 1.0 - sqrt_r1
+ var bu := sqrt_r1 * (1.0 - r2)
+ var bv := sqrt_r1 * r2
+
+ var alpha := absf(prop_index - (c1r * bw + c2r * bu + c3r * bv))
+ if alpha <= 0.01:
+ continue
+
+ var px := v1x * bw + v2x * bu + v3x * bv
+ var py := v1y * bw + v2y * bu + v3y * bv
+ var pz := v1z * bw + v2z * bu + v3z * bv
+
+ var nx := n1x * bw + n2x * bu + n3x * bv
+ var ny := n1y * bw + n2y * bu + n3y * bv
+ var nz := n1z * bw + n2z * bu + n3z * bv
+ var inv_len := 1.0 / sqrt(nx * nx + ny * ny + nz * nz)
+ nx *= inv_len; ny *= inv_len; nz *= inv_len
+
+ var base := actual_count * 7
+ point_data[base] = px; point_data[base + 1] = py; point_data[base + 2] = pz
+ point_data[base + 3] = nx; point_data[base + 4] = ny; point_data[base + 5] = nz
+ point_data[base + 6] = alpha
+
+ var ci := actual_count * 3
+ chunk_indices[ci] = int(floor(px * inv_chunk_size))
+ chunk_indices[ci + 1] = int(floor(py * inv_chunk_size))
+ chunk_indices[ci + 2] = int(floor(pz * inv_chunk_size))
+
+ actual_count += 1
+
+ if actual_count == 0:
+ continue
+
+ # Pass 3: Group instance indices by chunk key
+ var chunk_map: Dictionary = {}
+ for k in range(actual_count):
+ var ci := k * 3
+ var chunk_key := Vector3i(chunk_indices[ci], chunk_indices[ci + 1], chunk_indices[ci + 2])
+ if not chunk_map.has(chunk_key):
+ chunk_map[chunk_key] = PackedInt32Array()
+ chunk_map[chunk_key].append(k)
+
+ # Pass 4: Build MultiMesh transforms per chunk
+ for instance_ids: PackedInt32Array in chunk_map.values():
+ var instance_count := instance_ids.size()
+ var mmi := MultiMesh.new()
+ mmi.transform_format = MultiMesh.TRANSFORM_3D
+ mmi.mesh = prop_mesh
+ mmi.instance_count = instance_count
+ mmi.set_meta("cast_shadows", cast_shadows)
+
+ var transforms := PackedFloat32Array()
+ transforms.resize(instance_count * 12)
+ var ti := 0
+
+ for k in range(instance_count):
+ var base := instance_ids[k] * 7
+ var point := Vector3(point_data[base], point_data[base + 1], point_data[base + 2])
+ var normal := Vector3(point_data[base + 3], point_data[base + 4], point_data[base + 5])
+ var alpha: float = point_data[base + 6]
+
+ var scale := randf_range(scale_range.x, scale_range.y) * alpha
+ var rotation := randf_range(rotation_range.x, rotation_range.y) * rotation_to_rad
+
+ var basis := Basis.IDENTITY.rotated(normal, rotation) \
+ * Basis.looking_at(normal, Vector3.UP) \
+ * tilt_basis \
+ .scaled(Vector3.ONE * scale)
+
+ transforms[ti] = basis.x.x; transforms[ti + 1] = basis.y.x; transforms[ti + 2] = basis.z.x; transforms[ti + 3] = point.x
+ transforms[ti + 4] = basis.x.y; transforms[ti + 5] = basis.y.y; transforms[ti + 6] = basis.z.y; transforms[ti + 7] = point.y
+ transforms[ti + 8] = basis.x.z; transforms[ti + 9] = basis.y.z; transforms[ti + 10] = basis.z.z; transforms[ti + 11] = point.z
+ ti += 12
+
+ mmi.buffer = transforms
+ result.append(mmi)
+
+ return result
diff --git a/addons/godotvmf/src/vmf_detail_props.gd.uid b/addons/godotvmf/src/vmf_detail_props.gd.uid
new file mode 100644
index 0000000..5f291b0
--- /dev/null
+++ b/addons/godotvmf/src/vmf_detail_props.gd.uid
@@ -0,0 +1 @@
+uid://caj7drt0cl0kb
diff --git a/addons/godotvmf/src/vmf_displacement_tool.gd.uid b/addons/godotvmf/src/vmf_displacement_tool.gd.uid
new file mode 100644
index 0000000..74d11c4
--- /dev/null
+++ b/addons/godotvmf/src/vmf_displacement_tool.gd.uid
@@ -0,0 +1 @@
+uid://dgsx0tjvfl1pf
diff --git a/addons/godotvmf/src/vmf_entity_context_menu.gd.uid b/addons/godotvmf/src/vmf_entity_context_menu.gd.uid
new file mode 100644
index 0000000..d6b0f2d
--- /dev/null
+++ b/addons/godotvmf/src/vmf_entity_context_menu.gd.uid
@@ -0,0 +1 @@
+uid://t3i0qqi4gdav
diff --git a/addons/godotvmf/src/vmf_entity_node.gd b/addons/godotvmf/src/vmf_entity_node.gd
index 7022fc0..35949bb 100644
--- a/addons/godotvmf/src/vmf_entity_node.gd
+++ b/addons/godotvmf/src/vmf_entity_node.gd
@@ -255,7 +255,7 @@ static func get_movement_vector(v) -> Vector3:
return Vector3(movement.z, movement.y, movement.x);
-## Returns the shape of the entity that depends on solids that it have
+## Returns the shape of the entity that depends on solids that it has
func get_entity_shape() -> Shape3D:
var use_convex_shape = entity.solid is Dictionary or entity.solid.size() == 1;
diff --git a/addons/godotvmf/src/vmf_entity_node.gd.uid b/addons/godotvmf/src/vmf_entity_node.gd.uid
new file mode 100644
index 0000000..7e96b56
--- /dev/null
+++ b/addons/godotvmf/src/vmf_entity_node.gd.uid
@@ -0,0 +1 @@
+uid://dypijng81dvh2
diff --git a/addons/godotvmf/src/vmf_game_folder.gd b/addons/godotvmf/src/vmf_game_folder.gd
new file mode 100644
index 0000000..8c1706f
--- /dev/null
+++ b/addons/godotvmf/src/vmf_game_folder.gd
@@ -0,0 +1,21 @@
+@static_unload
+class_name VMFGameFolder extends RefCounted
+
+## Returns an array of all game folder paths, including the main gameinfo path and any additional import paths
+static func get_all_paths() -> Array[String]:
+ var paths: Array[String] = [VMFConfig.gameinfo_path];
+ paths += VMFConfig.import.additional_import_paths;
+
+ return paths;
+
+## Returns the full path to the file if it exists in any of the game folder paths, otherwise returns an empty string
+static func get_import_path(file_path: String) -> String:
+ for path in get_all_paths():
+ var full_path = VMFUtils.normalize_path(path + "/" + file_path);
+ if FileAccess.file_exists(full_path):
+ return full_path;
+
+ return "";
+
+static func file_exists(file_path: String) -> bool:
+ return get_import_path(file_path) != "";
diff --git a/addons/godotvmf/src/vmf_game_folder.gd.uid b/addons/godotvmf/src/vmf_game_folder.gd.uid
new file mode 100644
index 0000000..94259a7
--- /dev/null
+++ b/addons/godotvmf/src/vmf_game_folder.gd.uid
@@ -0,0 +1 @@
+uid://bygdwe5ixcppv
diff --git a/addons/godotvmf/src/vmf_geometry_corrector.gd b/addons/godotvmf/src/vmf_geometry_corrector.gd
index b8b5d08..a5f1789 100644
--- a/addons/godotvmf/src/vmf_geometry_corrector.gd
+++ b/addons/godotvmf/src/vmf_geometry_corrector.gd
@@ -1,17 +1,26 @@
class_name VMFGeometryCorrector extends RefCounted
-const norender = [
+const SHADOW_MESH_KEYS = [
+ 'compilenodraw',
+]
+
+const NO_COLLISION_KEYS = [
+ 'compilesky',
+ 'compilenonsolid'
+]
+
+const NO_RENDER_KEYS = [
'compileclip',
'compilenodraw',
'compilesky',
'npcclip',
'compileplayerclip',
'compilenpcclip',
-];
+]
-const nocollision = [
- 'compilesky',
-];
+func _get_norender_keys(): return NO_RENDER_KEYS;
+func _get_nocollision_keys(): return NO_COLLISION_KEYS;
+func _get_shadowmesh_keys(): return SHADOW_MESH_KEYS;
func compileplayerclip(solid: StaticBody3D):
solid.collision_layer = 1 << 1
diff --git a/addons/godotvmf/src/vmf_geometry_corrector.gd.uid b/addons/godotvmf/src/vmf_geometry_corrector.gd.uid
new file mode 100644
index 0000000..a34c68f
--- /dev/null
+++ b/addons/godotvmf/src/vmf_geometry_corrector.gd.uid
@@ -0,0 +1 @@
+uid://r367qjcf5o6r
diff --git a/addons/godotvmf/src/vmf_instance_manager.gd.uid b/addons/godotvmf/src/vmf_instance_manager.gd.uid
new file mode 100644
index 0000000..d9e1b58
--- /dev/null
+++ b/addons/godotvmf/src/vmf_instance_manager.gd.uid
@@ -0,0 +1 @@
+uid://d2qdfbxnq16xy
diff --git a/addons/godotvmf/src/vmf_logger.gd.uid b/addons/godotvmf/src/vmf_logger.gd.uid
new file mode 100644
index 0000000..d73361f
--- /dev/null
+++ b/addons/godotvmf/src/vmf_logger.gd.uid
@@ -0,0 +1 @@
+uid://iqtouohg8vq5
diff --git a/addons/godotvmf/src/vmf_node.gd b/addons/godotvmf/src/vmf_node.gd
index 7e023ef..0bfcb93 100644
--- a/addons/godotvmf/src/vmf_node.gd
+++ b/addons/godotvmf/src/vmf_node.gd
@@ -64,6 +64,9 @@ var geometry: Node3D:
return node;
+var detail_props: Node3D:
+ get: return geometry.get_node_or_null("DetailProps") if geometry else null;
+
var entities: Node3D:
get: return get_node_or_null("Entities");
@@ -90,12 +93,63 @@ func clear_scene_groups():
func reimport_geometry() -> void:
VMFConfig.load_config();
read_vmf();
+
+ VMFResourceManager.init_vpk_stack();
VMFResourceManager.import_materials(vmf_structure, is_runtime);
+ VMFResourceManager.free_vpk_stack();
await VMFResourceManager.for_resource_import();
import_geometry();
+func generate_detail_props(geometry_mesh: MeshInstance3D) -> void:
+ var detail_props := VMFDetailProps.generate(geometry_mesh.mesh);
+ if detail_props.size() == 0: return;
+
+ var detail_node := Node3D.new();
+
+ detail_node.name = "DetailProps";
+ detail_node.set_display_folded(true);
+ geometry_mesh.add_child(detail_node);
+ detail_node.set_owner(_owner);
+
+ for prop in detail_props:
+ var mmi := MultiMeshInstance3D.new();
+ mmi.multimesh = prop;
+ mmi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON \
+ if prop.get_meta("cast_shadows", true) \
+ else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF;
+
+ mmi.visibility_range_end = VMFConfig.import.detail_props_draw_distance;
+ mmi.visibility_range_end_margin = VMFConfig.import.detail_props_draw_distance / 2.0;
+ mmi.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF;
+
+ detail_node.add_child(mmi);
+ mmi.set_owner(_owner);
+
+func reimport_detail_props():
+ var geometry_mesh = geometry as MeshInstance3D;
+ if not geometry_mesh: return;
+ VMFConfig.load_config();
+
+ if detail_props: detail_props.free();
+
+ generate_detail_props(geometry_mesh);
+
+func generate_shadow_mesh(raw_geometry_mesh: ArrayMesh) -> void:
+ var shadow_mesh := VMFTool.generate_shadow_mesh(raw_geometry_mesh);
+ var shadow_mesh_instance := MeshInstance3D.new();
+ shadow_mesh_instance.name = "ShadowMesh";
+ shadow_mesh_instance.mesh = shadow_mesh;
+ shadow_mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY;
+ geometry.add_child(shadow_mesh_instance);
+ shadow_mesh_instance.set_owner(_owner);
+
+func unwrap_lightmap(geometry_mesh: MeshInstance3D) -> void:
+ var texel_size = VMFConfig.import.lightmap_texel_size;
+ if VMFConfig.import.generate_lightmap_uv2 and not is_runtime:
+ geometry_mesh.mesh.lightmap_unwrap(geometry_mesh.global_transform, texel_size);
+
func import_geometry() -> void:
var _t := Time.get_ticks_msec();
@@ -116,8 +170,6 @@ func import_geometry() -> void:
geometry_mesh.set_owner(_owner);
var transform = geometry_mesh.global_transform;
- var texel_size = VMFConfig.import.lightmap_texel_size;
-
geometry_mesh.mesh = mesh;
if VMFConfig.import.generate_collision:
@@ -127,17 +179,17 @@ func import_geometry() -> void:
if not get_meta("instance", false):
generate_navmesh(geometry_mesh);
- geometry_mesh.mesh = VMFTool.cleanup_mesh(geometry_mesh.mesh);
-
- if VMFConfig.import.generate_lightmap_uv2 and not is_runtime:
- geometry_mesh.mesh.lightmap_unwrap(geometry_mesh.global_transform, texel_size);
-
- geometry_mesh.mesh = save_geometry_file(geometry_mesh.mesh);
+ generate_shadow_mesh(geometry_mesh.mesh);
+ cleanup_geometry(geometry_mesh);
+ generate_detail_props(geometry_mesh);
+ unwrap_lightmap(geometry_mesh);
+ save_geometry_file(geometry_mesh);
VMFLogger.log("import_geometry took %d ms" % (Time.get_ticks_msec() - _t));
func generate_navmesh(geometry_mesh: MeshInstance3D):
if not VMFConfig.import.use_navigation_mesh: return;
+ if get_meta("instance", false): return;
var navreg := NavigationRegion3D.new();
@@ -166,8 +218,12 @@ func generate_navmesh(geometry_mesh: MeshInstance3D):
navreg.bake_navigation_mesh.call_deferred();
-func save_geometry_file(target_mesh: Mesh):
- if not save_geometry: return target_mesh;
+func cleanup_geometry(target_mesh_instance: MeshInstance3D) -> void:
+ target_mesh_instance.mesh = VMFTool.cleanup_mesh(target_mesh_instance.mesh);
+
+func save_geometry_file(target_mesh_instance: MeshInstance3D) -> void:
+ var target_mesh: Mesh = target_mesh_instance.mesh;
+ if not save_geometry: return;
var resource_path: String = "%s/%s_import.mesh" % [VMFConfig.import.geometry_folder, _vmf_identifer()];
if not DirAccess.dir_exists_absolute(resource_path.get_base_dir()):
@@ -179,7 +235,7 @@ func save_geometry_file(target_mesh: Mesh):
return;
target_mesh.take_over_path(resource_path);
- return target_mesh;
+ target_mesh_instance.mesh = load(resource_path);
func save_collision_file() -> void:
if save_collision == false: return;
@@ -258,6 +314,14 @@ func reset_entities_node():
func reimport_entities():
read_vmf();
+
+ VMFResourceManager.init_vpk_stack();
+ VMFResourceManager.import_materials(vmf_structure, is_runtime);
+ VMFResourceManager.import_models(vmf_structure);
+ VMFResourceManager.free_vpk_stack();
+
+ await VMFResourceManager.for_resource_import();
+
import_entities();
func import_entities() -> void:
@@ -298,8 +362,10 @@ func import_map() -> void:
clear_scene_groups();
read_vmf();
+ VMFResourceManager.init_vpk_stack();
VMFResourceManager.import_materials(vmf_structure, is_runtime);
VMFResourceManager.import_models(vmf_structure);
+ VMFResourceManager.free_vpk_stack();
await VMFResourceManager.for_resource_import();
diff --git a/addons/godotvmf/src/vmf_node.gd.uid b/addons/godotvmf/src/vmf_node.gd.uid
new file mode 100644
index 0000000..5b29d0c
--- /dev/null
+++ b/addons/godotvmf/src/vmf_node.gd.uid
@@ -0,0 +1 @@
+uid://dd0kk35qmld4q
diff --git a/addons/godotvmf/src/vmf_output_executor.gd.uid b/addons/godotvmf/src/vmf_output_executor.gd.uid
new file mode 100644
index 0000000..2659f28
--- /dev/null
+++ b/addons/godotvmf/src/vmf_output_executor.gd.uid
@@ -0,0 +1 @@
+uid://cttukojq70aun
diff --git a/addons/godotvmf/src/vmf_resource_manager.gd b/addons/godotvmf/src/vmf_resource_manager.gd
index 4cc5802..8672ccb 100644
--- a/addons/godotvmf/src/vmf_resource_manager.gd
+++ b/addons/godotvmf/src/vmf_resource_manager.gd
@@ -10,10 +10,26 @@ const MATERIAL_KEYS_TO_IMPORT = [
];
static var has_imported_resources: bool = false;
+static var vpk_stack: VPKStack;
static func get_editor_interface():
return Engine.get_singleton("EditorInterface") if Engine.has_singleton("EditorInterface") else null;
+## Initializes the VPK stack by loading all VPK files from the specified gameinfo directory.
+## This should be called before attempting to import any resources, as it allows the manager
+## to check for the existence of files within the VPKs and extract them if necessary.
+static func init_vpk_stack() -> void:
+ if not VMFConfig.models.import and VMFConfig.materials.import_mode == VMFConfig.MaterialsConfig.ImportMode.USE_EXISTING: return;
+ vpk_stack = VPKStack.create(VMFConfig.gameinfo_path);
+
+ for path in VMFConfig.import.additional_import_paths:
+ vpk_stack.append(path);
+
+static func free_vpk_stack() -> void:
+ if vpk_stack:
+ vpk_stack.free_vpks();
+ vpk_stack = null;
+
static func for_resource_import() -> void:
if not has_imported_resources: return;
@@ -40,52 +56,117 @@ static func import_models(vmf_structure: VMFStructure) -> bool:
var model_path = entity.data.get("model", "").to_lower().get_basename();
if not model_path: continue;
- var mdl_path = VMFUtils.normalize_path(VMFConfig.gameinfo_path + "/" + model_path + ".mdl");
- var vtx_path = VMFUtils.normalize_path(VMFConfig.gameinfo_path + "/" + model_path + ".vtx");
- var vtx_dx90_path = VMFUtils.normalize_path(VMFConfig.gameinfo_path + "/" + model_path + ".dx90.vtx");
- var vvd_path = VMFUtils.normalize_path(VMFConfig.gameinfo_path + "/" + model_path + ".vvd");
- var phy_path = VMFUtils.normalize_path(VMFConfig.gameinfo_path + "/" + model_path + ".phy");
+ # VPK paths
+ var vpk_mdl_path = VMFUtils.normalize_path(model_path + ".mdl");
+ var vpk_vtx_path = VMFUtils.normalize_path(model_path + ".dx90.vtx");
+ var vpk_vtx_fallback_path = VMFUtils.normalize_path(model_path + ".vtx");
+ var vpk_vvd_path = VMFUtils.normalize_path(model_path + ".vvd");
+ var vpk_phy_path = VMFUtils.normalize_path(model_path + ".phy");
+
var target_path = VMFUtils.normalize_path(VMFConfig.models.target_folder + "/" + model_path);
if ResourceLoader.exists(target_path + ".mdl"): continue;
- if not FileAccess.file_exists(mdl_path): continue;
- if not FileAccess.file_exists(vtx_path): vtx_path = vtx_dx90_path;
- if not FileAccess.file_exists(vtx_path): continue;
- if not FileAccess.file_exists(vvd_path): continue;
+ # Game directory paths
+ var mdl_path = VMFGameFolder.get_import_path(vpk_mdl_path);
+ var vtx_path = VMFGameFolder.get_import_path(vpk_vtx_path);
+ if not vtx_path:
+ vtx_path = VMFGameFolder.get_import_path(vpk_vtx_fallback_path);
+ var vvd_path = VMFGameFolder.get_import_path(vpk_vvd_path);
+ var phy_path = VMFGameFolder.get_import_path(vpk_phy_path);
+
+ var found_in_game_dir := mdl_path != "" and vtx_path != "" and vvd_path != "";
+
+ # Update vpk_vtx_path if fallback is used in VPK
+ if not file_exists_in_vpk(vpk_vtx_path) and file_exists_in_vpk(vpk_vtx_fallback_path):
+ vpk_vtx_path = vpk_vtx_fallback_path;
+
+ var found_in_vpk := file_exists_in_vpk(vpk_mdl_path) \
+ and file_exists_in_vpk(vpk_vtx_path) \
+ and file_exists_in_vpk(vpk_vvd_path);
+
+ if not found_in_game_dir and not found_in_vpk:
+ VMFLogger.error("Model files not found for: " + vpk_mdl_path);
+ continue;
+
+ if found_in_game_dir:
+ DirAccess.make_dir_recursive_absolute(target_path.get_base_dir());
+ DirAccess.copy_absolute(vtx_path, target_path + '.dx90.vtx');
+ DirAccess.copy_absolute(vvd_path, target_path + ".vvd");
+ if phy_path != "": DirAccess.copy_absolute(phy_path, target_path + ".phy");
+ DirAccess.copy_absolute(mdl_path, target_path + ".mdl");
- var model_materials = MDLReader.new(mdl_path).get_possible_material_paths();
+ elif found_in_vpk:
+ if not vpk_stack.extract(vpk_vtx_path, target_path + '.dx90.vtx'):
+ VMFLogger.error("Failed to extract VTX from VPK: " + vpk_vtx_path);
+ continue;
+
+ if not vpk_stack.extract(vpk_vvd_path, target_path + ".vvd"):
+ VMFLogger.error("Failed to extract VVD from VPK: " + vpk_vvd_path);
+ continue;
+
+ if file_exists_in_vpk(vpk_phy_path):
+ if not vpk_stack.extract(vpk_phy_path, target_path + ".phy"):
+ VMFLogger.error("Failed to extract PHY from VPK: " + vpk_phy_path);
+
+ if not vpk_stack.extract(vpk_mdl_path, target_path + ".mdl"):
+ VMFLogger.error("Failed to extract MDL from VPK: " + vpk_mdl_path);
+ continue;
+
+ var model_materials = MDLReader.new(target_path + ".mdl").get_possible_material_paths();
for material_path in model_materials:
import_textures(material_path);
import_material(material_path);
- DirAccess.make_dir_recursive_absolute(target_path.get_base_dir());
- DirAccess.copy_absolute(vtx_path, target_path + '.dx90.vtx');
- DirAccess.copy_absolute(vvd_path, target_path + ".vvd");
- if FileAccess.file_exists(phy_path): DirAccess.copy_absolute(phy_path, target_path + ".phy");
- DirAccess.copy_absolute(mdl_path, target_path + ".mdl");
-
has_imported_resources = true;
return has_imported_resources;
+static func file_exists_in_vpk(vpk_file_path: String) -> bool:
+ if not vpk_stack: return false;
+ return vpk_stack.exists(vpk_file_path);
+
+static func file_exists_in_additional_dir(file_path: String) -> bool:
+ if VMFConfig.import.additional_import_paths.size() == 0: return false;
+
+ for path in VMFConfig.import.additional_import_paths:
+ var full_path = VMFUtils.normalize_path(path + "/" + file_path);
+ if FileAccess.file_exists(full_path):
+ return true;
+
+ return false;
+
static func import_material(material: String) -> bool:
material = material.to_lower();
- var vmt_path = VMFUtils.normalize_path(VMFConfig.gameinfo_path + "/materials/" + material + ".vmt");
+ var vpk_path = "materials/" + material + ".vmt";
+ #var vmt_path = VMFUtils.normalize_path(VMFConfig.gameinfo_path + "/materials/" + material + ".vmt");
+ var vmt_path = VMFGameFolder.get_import_path(vpk_path); ## Returns the full path to the file if it exists in any of the game folder paths, otherwise returns an empty string
var target_path = VMFUtils.normalize_path(VMFConfig.materials.target_folder + "/" + material + ".vmt");
if ResourceLoader.exists(target_path): return false;
- if not FileAccess.file_exists(vmt_path): return false;
- DirAccess.make_dir_recursive_absolute(target_path.get_base_dir());
- var has_error = DirAccess.copy_absolute(vmt_path, target_path);
+ if vmt_path:
+ DirAccess.make_dir_recursive_absolute(target_path.get_base_dir());
+ if DirAccess.copy_absolute(vmt_path, target_path): return false;
- if has_error: return false;
+ has_imported_resources = true;
+ # In case the file is not found in any of game folder paths - trying to find material in VPKs
+ elif file_exists_in_vpk(vpk_path):
+ if not vpk_stack.extract(vpk_path, target_path):
+ VMFLogger.error("Failed to extract material from VPK: " + vpk_path);
+ return false;
- return true;
+ has_imported_resources = true;
+
+ else:
+ VMFLogger.error("Material not found: " + vpk_path);
+ return false;
+
+ return has_imported_resources;
+## Looks through the VMF structure and imports all materials used in solids and entities, unless they are in the ignore list.
static func import_materials(vmf_structure: VMFStructure, is_runtime := false) -> void:
var editor_interface = get_editor_interface();
@@ -98,20 +179,19 @@ static func import_materials(vmf_structure: VMFStructure, is_runtime := false) -
for solid in vmf_structure.solids:
for side in solid.sides:
- var isIgnored = ignore_list.any(func(rx: String) -> bool: return side.material.match(rx));
- if isIgnored: continue;
+ var is_ignored = ignore_list.any(func(rx: String) -> bool: return side.material.match(rx));
+ if is_ignored: continue;
if not list.has(side.material):
list.append(side.material);
for entity in vmf_structure.entities:
- if not entity.has_solid:
- continue;
+ if not entity.has_solid: continue;
for solid in entity.solids:
for side in solid.sides:
- var isIgnored = ignore_list.any(func(rx): return side.material.match(rx));
- if isIgnored: continue;
+ var is_ignored = ignore_list.any(func(rx): return side.material.match(rx));
+ if is_ignored: continue;
if not list.has(side.material):
list.append(side.material);
@@ -126,12 +206,26 @@ static func import_materials(vmf_structure: VMFStructure, is_runtime := false) -
static func import_textures(material: String) -> bool:
material = material.to_lower();
- var target_path = VMFUtils.normalize_path(VMFConfig.gameinfo_path + "/materials/" + material + ".vmt");
- if not FileAccess.file_exists(target_path):
- VMFLogger.error("Material not found: " + target_path);
+ ## Returns the full path to the file if it exists in any of the game folder paths, otherwise returns an empty string
+ var vmt_vpk_path = "materials/" + material + ".vmt";
+ var target_path = VMFGameFolder.get_import_path(vmt_vpk_path);
+
+ var details: Dictionary = {};
+
+ if target_path: # If the material file exists in any of the game folder paths, parse it to get the details
+ details = VDFParser.parse(target_path, true).values()[0];
+
+ elif file_exists_in_vpk(vmt_vpk_path):
+ var vmt_data = vpk_stack.get_file_data(vmt_vpk_path);
+ if not vmt_data:
+ VMFLogger.error("Failed to read material data from VPK: " + vmt_vpk_path);
+ return false;
+ details = VDFParser.parse_from_string(vmt_data.get_string_from_utf8(), true).values()[0];
+
+ else:
+ VMFLogger.error("Failed to find material for texture import: " + material);
return false;
- var details = VDFParser.parse(target_path, true).values()[0];
# NOTE: CS:GO/L4D
if "insert" in details:
@@ -139,18 +233,30 @@ static func import_textures(material: String) -> bool:
for key in MATERIAL_KEYS_TO_IMPORT:
if key not in details: continue;
- var vtf_path = VMFUtils.normalize_path(VMFConfig.gameinfo_path + "/materials/" + details[key].to_lower() + ".vtf");
+ var vpk_path = VMFUtils.normalize_path("materials/" + details[key].to_lower() + ".vtf");
+ ## var vtf_path = VMFUtils.normalize_path(VMFConfig.gameinfo_path + "/materials/" + details[key].to_lower() + ".vtf");
+ var vtf_path = VMFGameFolder.get_import_path(vpk_path); ## Returns the full path to the file if it exists in any of the game folder paths, otherwise returns an empty string
var target_vtf_path = VMFUtils.normalize_path(VMFConfig.materials.target_folder + "/" + details[key].to_lower() + ".vtf");
- if not FileAccess.file_exists(vtf_path): continue;
if ResourceLoader.exists(target_vtf_path): continue;
- DirAccess.make_dir_recursive_absolute(target_vtf_path.get_base_dir());
- var has_error = DirAccess.copy_absolute(vtf_path, target_vtf_path);
+ # Trying to find texture in game directory first, as it can be overridden by mods and thus differ from the one in VPKs
+ if vtf_path: # If the texture file exists in any of the game folder paths, copy it to the target location
+ DirAccess.make_dir_recursive_absolute(target_vtf_path.get_base_dir());
+
+ if DirAccess.copy_absolute(vtf_path, target_vtf_path):
+ VMFLogger.error("Failed to copy texture: " + vtf_path);
+ else:
+ has_imported_resources = true;
+
+ # Trying to find texture in VPKs
+ elif file_exists_in_vpk(vpk_path):
+ if not vpk_stack.extract(vpk_path, target_vtf_path):
+ VMFLogger.error("Failed to extract texture from VPK: " + vpk_path);
+ else:
+ has_imported_resources = true;
+
+ else:
+ VMFLogger.error("Failed to copy texture: " + vpk_path);
- if not has_error:
- has_imported_resources = true;
- continue;
- VMFLogger.error("Failed to copy texture: " + str(has_error));
-
- return has_imported_resources;
+ return true;
diff --git a/addons/godotvmf/src/vmf_resource_manager.gd.uid b/addons/godotvmf/src/vmf_resource_manager.gd.uid
new file mode 100644
index 0000000..8a8aba9
--- /dev/null
+++ b/addons/godotvmf/src/vmf_resource_manager.gd.uid
@@ -0,0 +1 @@
+uid://bvd5u6osaoo8h
diff --git a/addons/godotvmf/src/vmf_runtime_controller.gd.uid b/addons/godotvmf/src/vmf_runtime_controller.gd.uid
new file mode 100644
index 0000000..b6c1df6
--- /dev/null
+++ b/addons/godotvmf/src/vmf_runtime_controller.gd.uid
@@ -0,0 +1 @@
+uid://cis0mvbh0slp7
diff --git a/addons/godotvmf/src/vmf_tool.gd b/addons/godotvmf/src/vmf_tool.gd
index 13a50e7..5715fda 100644
--- a/addons/godotvmf/src/vmf_tool.gd
+++ b/addons/godotvmf/src/vmf_tool.gd
@@ -1,6 +1,36 @@
@static_unload
class_name VMFTool extends RefCounted
+static func generate_shadow_mesh(source_mesh: ArrayMesh) -> ArrayMesh:
+ var shadow_mesh := ArrayMesh.new();
+ var extend_corrector := Engine.get_main_loop().root.get_node_or_null("VMFExtendGeometryCorrector") as VMFGeometryCorrector;
+ var corrector := VMFGeometryCorrector.new() if not extend_corrector else extend_corrector;
+
+ for surface_idx in range(source_mesh.get_surface_count()):
+ var material = source_mesh.surface_get_material(surface_idx);
+
+ if not material:
+ shadow_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, source_mesh.surface_get_arrays(surface_idx));
+ continue;
+
+ var material_compile_keys: Array = material.get_meta("compile_keys", []);
+ var is_shadowmesh := false;
+
+ for key in material_compile_keys:
+ if extend_corrector:
+ is_shadowmesh = extend_corrector._get_shadowmesh_keys().has(key);
+ if is_shadowmesh: break;
+ continue;
+
+ is_shadowmesh = corrector._get_shadowmesh_keys().has(key);
+ if is_shadowmesh: break;
+
+ if not is_shadowmesh: continue;
+
+ shadow_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, source_mesh.surface_get_arrays(surface_idx));
+
+ return VMFUtils.merge_surfaces(shadow_mesh);
+
## Generates collisions from mesh for each surface. It adds ability to use sufraceprop values
static func generate_collisions(mesh_instance: MeshInstance3D, physics_mask: int):
var bodies: Array[StaticBody3D] = [];
@@ -10,14 +40,17 @@ static func generate_collisions(mesh_instance: MeshInstance3D, physics_mask: int
var extend_corrector = Engine.get_main_loop().root.get_node_or_null("VMFExtendGeometryCorrector");
for surface_idx in range(mesh.get_surface_count()):
- var material = mesh.surface_get_material(surface_idx);
- var material_name = mesh.get_meta("surface_material_" + str(surface_idx), "").to_lower();
+ var material := mesh.surface_get_material(surface_idx);
+ if not material: continue;
+
+ var material_details := material.get_meta("details", {});
+ var material_name := (mesh.get_meta("surface_material_" + str(surface_idx), "") as String).to_lower();
var is_ignored = VMFConfig.materials.ignore.any(func(rx: String) -> bool: return material_name.match(rx.to_lower()));
if is_ignored: continue;
- var compilekeys = material.get_meta("compile_keys", []) if material else [];
- var surface_prop = (material.get_meta("surfaceprop", "default") if material else "default");
+ var compilekeys := material.get_meta("compile_keys", []) if material else [];
+ var surface_prop = material_details.get("$surfaceprop", "default");
# NOTE: Blend textures can have more than one surface prop. In this case we'll choose the first one.
if surface_prop is Array:
@@ -26,18 +59,16 @@ static func generate_collisions(mesh_instance: MeshInstance3D, physics_mask: int
if compilekeys.size() > 0:
surface_prop = "tool_" + compilekeys[0];
- var has_nocollision_extender = extend_corrector and "nocollision" in extend_corrector;
- var is_no_collision = false;
+ var is_no_collision := false;
for key in compilekeys:
- if has_nocollision_extender:
- if extend_corrector.nocollision.has(key):
- is_no_collision = true;
- break;
+ if extend_corrector and extend_corrector._get_nocollision_keys().has(key):
+ is_no_collision = true;
+ break;
if is_no_collision: break;
- if corrector.nocollision.has(key):
+ if corrector._get_nocollision_keys().has(key):
is_no_collision = true;
break;
@@ -81,13 +112,13 @@ static func generate_collisions(mesh_instance: MeshInstance3D, physics_mask: int
VMFUtils.set_owner_recursive(static_body, mesh_instance.get_owner());
## Clear mesh from ignored textures and materials
-static func cleanup_mesh(original_mesh: ArrayMesh):
+static func cleanup_mesh(original_mesh: ArrayMesh) -> ArrayMesh:
var is_44 = Engine.get_version_info().minor >= 4;
var ignored_textures = VMFConfig.materials.ignore;
var duplicated_mesh = ArrayMesh.new() if not is_44 else null;
var corrector := VMFGeometryCorrector.new();
- var extend_corrector = Engine.get_main_loop().root.get_node_or_null("VMFExtendGeometryCorrector");
+ var extend_corrector := Engine.get_main_loop().root.get_node_or_null("VMFExtendGeometryCorrector") as VMFGeometryCorrector;
var mt = MeshDataTool.new() if not is_44 else null;
var surface_removed = 0;
@@ -100,8 +131,8 @@ static func cleanup_mesh(original_mesh: ArrayMesh):
var material = original_mesh.surface_get_material(surface_idx);
var compilekeys = material.get_meta("compile_keys", []) if material else [];
- var is_ignored = ignored_textures.any(func(rx: String) -> bool: return material_name.match(rx.to_lower()));
- if is_ignored and is_44:
+ var is_material_ignored = VMTLoader.is_material_ignored(material_name);
+ if is_material_ignored and is_44:
original_mesh.surface_remove(surface_idx);
surface_removed += 1;
continue;
@@ -109,12 +140,12 @@ static func cleanup_mesh(original_mesh: ArrayMesh):
var is_norender = false;
for key in compilekeys:
- if is_ignored: break;
- if extend_corrector and "norender" in extend_corrector.norender:
- is_norender = extend_corrector.norender.has(key);
+ if is_material_ignored: break;
+ if extend_corrector:
+ is_norender = extend_corrector._get_norender_keys().has(key);
break;
- is_norender = corrector.norender.has(key);
+ is_norender = corrector._get_norender_keys().has(key);
if is_norender: break;
if is_norender and is_44:
diff --git a/addons/godotvmf/src/vmf_tool.gd.uid b/addons/godotvmf/src/vmf_tool.gd.uid
new file mode 100644
index 0000000..1735196
--- /dev/null
+++ b/addons/godotvmf/src/vmf_tool.gd.uid
@@ -0,0 +1 @@
+uid://ks6qqa8i7gbt
diff --git a/addons/godotvmf/src/vmf_utils.gd b/addons/godotvmf/src/vmf_utils.gd
index 29e1991..7080e97 100644
--- a/addons/godotvmf/src/vmf_utils.gd
+++ b/addons/godotvmf/src/vmf_utils.gd
@@ -23,3 +23,39 @@ static func object_assign(target: Object, source: Dictionary) -> void:
for key in source.keys():
if key in target:
target[key] = source[key];
+
+static func merge_surfaces(mesh: ArrayMesh) -> ArrayMesh:
+ var result := ArrayMesh.new();
+ var surface_count := mesh.get_surface_count();
+ if surface_count == 0:
+ return result;
+
+ var combined_vertices := PackedVector3Array();
+ var combined_indices := PackedInt32Array();
+ var vertex_offset := 0;
+ var primitive_type := mesh.surface_get_primitive_type(0);
+
+ for surface_index in range(surface_count):
+ var arrays := mesh.surface_get_arrays(surface_index);
+ var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX];
+ if vertices.is_empty():
+ continue;
+
+ combined_vertices.append_array(vertices);
+
+ var indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX];
+ if indices.is_empty():
+ for i in range(vertices.size()):
+ combined_indices.append(vertex_offset + i);
+ else:
+ for index in indices:
+ combined_indices.append(index + vertex_offset);
+
+ vertex_offset += vertices.size();
+
+ var surface_arrays := [];
+ surface_arrays.resize(Mesh.ARRAY_MAX);
+ surface_arrays[Mesh.ARRAY_VERTEX] = combined_vertices;
+ surface_arrays[Mesh.ARRAY_INDEX] = combined_indices;
+ result.add_surface_from_arrays(primitive_type, surface_arrays);
+ return result;
diff --git a/addons/godotvmf/src/vmf_utils.gd.uid b/addons/godotvmf/src/vmf_utils.gd.uid
new file mode 100644
index 0000000..ef74428
--- /dev/null
+++ b/addons/godotvmf/src/vmf_utils.gd.uid
@@ -0,0 +1 @@
+uid://c2d7qetw3fje6
diff --git a/addons/godotvmf/src/vmf_vector_sorter.gd b/addons/godotvmf/src/vmf_vector_sorter.gd
index 9b03ae5..8415e1b 100644
--- a/addons/godotvmf/src/vmf_vector_sorter.gd
+++ b/addons/godotvmf/src/vmf_vector_sorter.gd
@@ -6,7 +6,7 @@ var pp: Vector3;
var qp: Vector3;
func longer(a: Vector3, b: Vector3) -> Vector3:
- return a if a.length() > b.length() else b;
+ return a if a.length_squared() > b.length_squared() else b;
func _init(normal, center) -> void:
self.normal = normal;
diff --git a/addons/godotvmf/src/vmf_vector_sorter.gd.uid b/addons/godotvmf/src/vmf_vector_sorter.gd.uid
new file mode 100644
index 0000000..b7f63f1
--- /dev/null
+++ b/addons/godotvmf/src/vmf_vector_sorter.gd.uid
@@ -0,0 +1 @@
+uid://bele2kg3cg6fo
diff --git a/addons/godotvmf/ui.gd.uid b/addons/godotvmf/ui.gd.uid
new file mode 100644
index 0000000..78f691c
--- /dev/null
+++ b/addons/godotvmf/ui.gd.uid
@@ -0,0 +1 @@
+uid://bod1gwpf8i0fg
diff --git a/addons/godotvmf/utils/mesh_utils.gd b/addons/godotvmf/utils/mesh_utils.gd
new file mode 100644
index 0000000..593889d
--- /dev/null
+++ b/addons/godotvmf/utils/mesh_utils.gd
@@ -0,0 +1,66 @@
+class_name VMFMeshUtils extends RefCounted
+
+static func simplify_mesh(mesh: Mesh, target_reduction: float) -> Mesh:
+ if not mesh or mesh.get_surface_count() == 0:
+ return mesh;
+
+ var reduction := clampf(target_reduction, 0.0, 1.0);
+ if is_zero_approx(reduction):
+ return mesh;
+
+ var importer_mesh := ImporterMesh.from_mesh(mesh);
+ importer_mesh.generate_lods(60.0, -1.0, []);
+
+ var simplified_mesh := ArrayMesh.new();
+ for surface_idx in range(importer_mesh.get_surface_count()):
+ var surface_arrays: Array = importer_mesh.get_surface_arrays(surface_idx).duplicate(true);
+ var lod_indices := _pick_lod_indices(importer_mesh, surface_idx, reduction, surface_arrays);
+ if lod_indices != null:
+ surface_arrays[Mesh.ARRAY_INDEX] = lod_indices;
+
+ simplified_mesh.add_surface_from_arrays(
+ importer_mesh.get_surface_primitive_type(surface_idx),
+ surface_arrays
+ );
+
+ var material := importer_mesh.get_surface_material(surface_idx);
+ if material:
+ simplified_mesh.surface_set_material(surface_idx, material);
+
+ _copy_metadata(mesh, simplified_mesh);
+ return simplified_mesh;
+
+static func _pick_lod_indices(importer_mesh: ImporterMesh, surface_idx: int, target_reduction: float, surface_arrays: Array) -> Variant:
+ var lod_count := importer_mesh.get_surface_lod_count(surface_idx);
+ if lod_count == 0:
+ return null;
+
+ var base_indices: PackedInt32Array = surface_arrays[Mesh.ARRAY_INDEX] if surface_arrays[Mesh.ARRAY_INDEX] else PackedInt32Array();
+ var base_vertices: PackedVector3Array = surface_arrays[Mesh.ARRAY_VERTEX];
+ var base_index_count := base_indices.size() if not base_indices.is_empty() else base_vertices.size();
+ if base_index_count == 0:
+ return null;
+
+ var best_difference := target_reduction;
+ var best_indices: PackedInt32Array = PackedInt32Array();
+ var found_match := false;
+
+ for lod_idx in range(lod_count):
+ var lod_indices := importer_mesh.get_surface_lod_indices(surface_idx, lod_idx);
+ if lod_indices.is_empty():
+ continue;
+
+ var lod_reduction := 1.0 - (float(lod_indices.size()) / float(base_index_count));
+ var reduction_difference := absf(target_reduction - lod_reduction);
+ if reduction_difference >= best_difference:
+ continue;
+
+ best_difference = reduction_difference;
+ best_indices = lod_indices;
+ found_match = true;
+
+ return best_indices if found_match else null;
+
+static func _copy_metadata(source: Object, target: Object) -> void:
+ for meta_key in source.get_meta_list():
+ target.set_meta(meta_key, source.get_meta(meta_key));
diff --git a/addons/godotvmf/utils/mesh_utils.gd.uid b/addons/godotvmf/utils/mesh_utils.gd.uid
new file mode 100644
index 0000000..4af65aa
--- /dev/null
+++ b/addons/godotvmf/utils/mesh_utils.gd.uid
@@ -0,0 +1 @@
+uid://dnenpsu7nivl
diff --git a/addons/godotvmf/godotvdf/vdf_parser.gd b/addons/godotvmf/utils/vdf_parser.gd
similarity index 72%
rename from addons/godotvmf/godotvdf/vdf_parser.gd
rename to addons/godotvmf/utils/vdf_parser.gd
index eadfd6b..6091647 100644
--- a/addons/godotvmf/godotvdf/vdf_parser.gd
+++ b/addons/godotvmf/utils/vdf_parser.gd
@@ -3,12 +3,12 @@
class_name VDFParser extends RefCounted;
# Precompile the regular expressions only once
-static var _propRegex := RegEx.create_from_string('^"?(.*?)?"?\\s+"?(.*?)?"?(?:$|(\\s\\[.+\\]))$');
static var _vectorRegex := RegEx.create_from_string('^([-\\d\\.e]+)\\s([-\\d\\.e]+)\\s([-\\d\\.e]+)$');
+static var _vector2Regex := RegEx.create_from_string('^([-\\d\\.e]+)\\s([-\\d\\.e]+)$');
static var _colorRegex := RegEx.create_from_string('^([-\\d\\.e]+)\\s([-\\d\\.e]+)\\s([-\\d\\.e]+)\\s([-\\d\\.e]+)$');
static var _uvRegex := RegEx.create_from_string('\\[([-\\d\\.e]+)\\s([-\\d\\.e]+)\\s([-\\d\\.e]+)\\s([-\\d\\.e]+)\\]\\s([-\\d\\.e]+)');
static var _planeRegex := RegEx.create_from_string('\\(([\\d\\-\\.e]+\\s[\\d\\-\\.e]+\\s[\\d\\-\\.e]+)\\)\\s?\\(([\\d\\-\\.e]+\\s[\\d\\-\\.e]+\\s[\\d\\-\\.e]+)\\)\\s?\\(([\\d\\-\\.e]+\\s[\\d\\-\\.e]+\\s[\\d\\-\\.e]+)\\)');
-static var _commentRegex := RegEx.create_from_string('\\s+?\\/\\/.+');
+static var _commentRegex := RegEx.create_from_string('\\s*\\/\\/.+');
## Returns Dictionary representation of the Valve Data Format file located at [file_path].
## Returns null and prints an error if the file could not be read or parsed.
@@ -31,6 +31,9 @@ static func parse_value(line: String) -> Variant:
var m = _vectorRegex.search(line)
if m:
return Vector3(m.get_string(1).to_float(), m.get_string(2).to_float(), m.get_string(3).to_float());
+ m = _vector2Regex.search(line)
+ if m:
+ return Vector2(m.get_string(1).to_float(), m.get_string(2).to_float());
m = _colorRegex.search(line)
if m:
return Color(
@@ -57,7 +60,7 @@ static func parse_value(line: String) -> Variant:
var v = Plane(plane[0], plane[1], plane[2]);
- if not v:
+ if v.normal == Vector3.ZERO:
push_error('ValveFormatParser: Failed to create plane from: ' + line);
return null;
@@ -68,7 +71,7 @@ static func parse_value(line: String) -> Variant:
return float(line);
return line;
static func define_structure(hierarchy: Array, line: String, keys_to_lower = false):
- var _name := line.strip_edges().replace('{', '').replace('"', '');
+ var _name := line.strip_edges().replace('{', '').replace('"', '').strip_edges();
if keys_to_lower:
_name = _name.to_lower();
var newStruct = {};
@@ -84,10 +87,53 @@ static func define_structure(hierarchy: Array, line: String, keys_to_lower = fal
hierarchy.append(newStruct);
-static func define_property(hierarchy: Array, line: String, keys_to_lower = false):
- var m := _propRegex.search(line);
- var propName := m.get_string(1);
- var propValue := parse_value(m.get_string(2));
+static func _get_key_value(line: String) -> Array:
+ var key_start := -1
+ var key_end := -1
+ var value_start := -1
+ var value_end := -1
+
+ var i := 0
+ var length := line.length()
+
+ # Parse Key
+ if i < length and line[i] == '"':
+ i += 1
+ key_start = i
+ while i < length and line[i] != '"':
+ i += 1
+ key_end = i
+ i += 1
+ else:
+ key_start = i
+ while i < length and line[i] != ' ' and line[i] != '\t':
+ i += 1
+ key_end = i
+
+ # Skip whitespace
+ while i < length and (line[i] == ' ' or line[i] == '\t'):
+ i += 1
+
+ if i >= length:
+ return []
+
+ # Parse Value
+ if i < length and line[i] == '"':
+ i += 1
+ value_start = i
+ while i < length and line[i] != '"':
+ i += 1
+ value_end = i
+ else:
+ value_start = i
+ while i < length and line[i] != ' ' and line[i] != '\t':
+ i += 1
+ value_end = i
+
+ return [line.substr(key_start, key_end - key_start), line.substr(value_start, value_end - value_start)]
+
+static func define_property(hierarchy: Array, propName: String, propValueStr: String, keys_to_lower = false):
+ var propValue := parse_value(propValueStr);
if keys_to_lower:
propName = propName.to_lower();
@@ -122,8 +168,10 @@ static func parse_from_string(source: String, keys_to_lower := false):
define_structure(hierarchy, line, keys_to_lower);
elif line[0] == '}' or line.ends_with('}'):
finish_structure(hierarchy);
- elif _propRegex.search(line):
- define_property(hierarchy, line, keys_to_lower);
+ else:
+ var kv = _get_key_value(line)
+ if kv:
+ define_property(hierarchy, kv[0], kv[1], keys_to_lower);
previous_line = line;
diff --git a/addons/godotvmf/utils/vdf_parser.gd.uid b/addons/godotvmf/utils/vdf_parser.gd.uid
new file mode 100644
index 0000000..1a7dd80
--- /dev/null
+++ b/addons/godotvmf/utils/vdf_parser.gd.uid
@@ -0,0 +1 @@
+uid://bmcaq84eivuh5
diff --git a/addons/godotvmf/utils/vmf_struct.gd b/addons/godotvmf/utils/vmf_struct.gd
new file mode 100644
index 0000000..0223cce
--- /dev/null
+++ b/addons/godotvmf/utils/vmf_struct.gd
@@ -0,0 +1,51 @@
+class_name VMFStruct extends RefCounted
+
+enum Type {
+ BYTE,
+ INT_16,
+ INT_32,
+ UBYTE,
+ UINT_16,
+ UINT_32,
+ FLOAT,
+ STRING,
+ ARRAY,
+}
+
+static func in_case(lambda: Callable) -> Callable:
+ return lambda;
+
+static func transform_struct(file: FileAccess, clazz: GDScript, struct_map: Dictionary) -> Variant:
+ var result = clazz.new();
+
+ for key in struct_map.keys():
+ var type = struct_map[key];
+
+ match type:
+ Type.BYTE:
+ result.set(key, file.get_8());
+ Type.INT_16:
+ result.set(key, file.get_buffer(2).decode_s16(0));
+ Type.INT_32:
+ result.set(key, file.get_buffer(4).decode_s32(0));
+ Type.UBYTE:
+ result.set(key, file.get_8());
+ Type.UINT_16:
+ result.set(key, file.get_16());
+ Type.UINT_32:
+ result.set(key, file.get_32());
+ Type.FLOAT:
+ result.set(key, file.get_float());
+ Type.STRING:
+ result.set(key, get_null_terminated_string(file));
+ Type.ARRAY:
+ result.set(key, []);
+ return result;
+
+static func get_null_terminated_string(file: FileAccess) -> String:
+ var result = "";
+ while true:
+ var c = file.get_8();
+ if c == 0: break;
+ result += char(c);
+ return result;
diff --git a/addons/godotvmf/utils/vmf_struct.gd.uid b/addons/godotvmf/utils/vmf_struct.gd.uid
new file mode 100644
index 0000000..16ac8ce
--- /dev/null
+++ b/addons/godotvmf/utils/vmf_struct.gd.uid
@@ -0,0 +1 @@
+uid://uls5y01y1gov
diff --git a/readme.md b/readme.md
index 0a9fce0..74e198e 100644
--- a/readme.md
+++ b/readme.md
@@ -11,30 +11,37 @@
-
+
+
+