From 3d17c6a80000811467d88133d18d76678549908e Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Sat, 31 Jan 2026 00:09:45 +0400
Subject: [PATCH 01/27] VMF: Add detail props support
---
addons/godotvmf/godotvdf/vdf_parser.gd | 4 +
addons/godotvmf/src/vmf_config.gd | 28 +++++-
addons/godotvmf/src/vmf_detail_props.gd | 128 ++++++++++++++++++++++++
addons/godotvmf/src/vmf_node.gd | 27 +++++
4 files changed, 186 insertions(+), 1 deletion(-)
create mode 100644 addons/godotvmf/src/vmf_detail_props.gd
diff --git a/addons/godotvmf/godotvdf/vdf_parser.gd b/addons/godotvmf/godotvdf/vdf_parser.gd
index eadfd6b..2faf516 100644
--- a/addons/godotvmf/godotvdf/vdf_parser.gd
+++ b/addons/godotvmf/godotvdf/vdf_parser.gd
@@ -5,6 +5,7 @@ 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]+)\\)');
@@ -31,6 +32,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(
diff --git a/addons/godotvmf/src/vmf_config.gd b/addons/godotvmf/src/vmf_config.gd
index ad5ddaa..8ab3ab0 100644
--- a/addons/godotvmf/src/vmf_config.gd
+++ b/addons/godotvmf/src/vmf_config.gd
@@ -22,6 +22,8 @@ const SETTINGS_TO_LOAD = [
"godot_vmf/import/entities_folder",
"godot_vmf/import/geometry_folder",
"godot_vmf/import/entity_aliases",
+ "godot_vmf/import/detail_props_chunk_size",
+ "godot_vmf/import/detail_props_draw_distance",
];
class ModelsConfig:
@@ -97,9 +99,17 @@ 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
+## 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://");
@@ -317,6 +327,22 @@ static func define_project_settings():
"default_value": {},
})
+ ProjectSettings.add_property_info({
+ "name": "godot_vmf/import/detail_props_chunk_size",
+ "type": TYPE_FLOAT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,1000.0,0.000001",
+ "default_value": 0.2,
+ })
+
+ ProjectSettings.add_property_info({
+ "name": "godot_vmf/import/detail_props_draw_distance",
+ "type": TYPE_FLOAT,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,1000.0,0.000001",
+ "default_value": 0.2,
+ })
+
## Models
ProjectSettings.add_property_info({
"name": "godot_vmf/models/import",
diff --git a/addons/godotvmf/src/vmf_detail_props.gd b/addons/godotvmf/src/vmf_detail_props.gd
new file mode 100644
index 0000000..26a3618
--- /dev/null
+++ b/addons/godotvmf/src/vmf_detail_props.gd
@@ -0,0 +1,128 @@
+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 result: Array[MultiMesh] = []
+
+ for surface_idx in range(original_mesh.get_surface_count()):
+ var material := original_mesh.surface_get_material(surface_idx)
+ if not material:
+ continue
+
+ for prop_index in range(2):
+ var index_postfix = "" if not prop_index else str(prop_index + 1)
+ var material_details := material.get_meta("details", {})
+
+ 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 offset_randomize: float = material_details.get("$detailpropoffsetrandomize" + index_postfix, 1.0)
+ 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;
+
+ 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 chunks: Dictionary = {}
+
+ var num_triangles := indices.size() / 3
+
+ for i in range(num_triangles):
+ 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 edge1 := v2 - v1;
+ var edge2 := v3 - v1;
+ var area := 0.5 * edge1.cross(edge2).length();
+ var count := int(round(area * density));
+
+ if count <= 0: continue;
+
+ 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;
+
+ for j in range(count):
+ var r1 := randf();
+ var r2 := randf();
+ var sqrt_r1 := sqrt(r1);
+
+ var w := 1.0 - sqrt_r1;
+ var u := sqrt_r1 * (1.0 - r2);
+ var v := sqrt_r1 * r2;
+
+ var alpha := abs(prop_index - (c1r * w + c2r * u + c3r * v)) as float;
+ if alpha <= 0.01: continue;
+
+ var point := v1 * w + v2 * u + v3 * v;
+ var normal := (n1 * w + n2 * u + n3 * v).normalized();
+
+ var chunk_key := Vector3i(floor(point.x / chunk_size), floor(point.y / chunk_size), floor(point.z / chunk_size));
+
+ if not chunks.has(chunk_key):
+ chunks[chunk_key] = [];
+
+ chunks[chunk_key].append([point, normal, alpha]);
+
+ for chunk in chunks.values():
+ var mmi := MultiMesh.new();
+ mmi.transform_format = MultiMesh.TRANSFORM_3D;
+ mmi.mesh = prop_mesh;
+ mmi.instance_count = chunk.size();
+
+ for k in range(chunk.size()):
+ var data = chunk[k];
+ var t := Transform3D();
+ var scale := randf_range(scale_range.x, scale_range.y) * (data[2] as float);
+ var rotation := randf_range(rotation_range.x, rotation_range.y) / 180.0 * PI;
+ var normal: Vector3 = data[1];
+
+ t.basis = Basis.IDENTITY.rotated(normal, rotation) \
+ * Basis.looking_at(normal, Vector3.UP) \
+ * Basis.IDENTITY.rotated(Vector3.RIGHT, -PI / 2) \
+ .scaled(Vector3.ONE * scale);
+ t.origin = data[0];
+
+ mmi.set_instance_transform(k, t)
+
+ result.append(mmi)
+
+ return result
diff --git a/addons/godotvmf/src/vmf_node.gd b/addons/godotvmf/src/vmf_node.gd
index 7e023ef..e588589 100644
--- a/addons/godotvmf/src/vmf_node.gd
+++ b/addons/godotvmf/src/vmf_node.gd
@@ -96,6 +96,31 @@ func reimport_geometry() -> void:
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 import_geometry() -> void:
var _t := Time.get_ticks_msec();
@@ -129,6 +154,8 @@ func import_geometry() -> void:
geometry_mesh.mesh = VMFTool.cleanup_mesh(geometry_mesh.mesh);
+ generate_detail_props(geometry_mesh);
+
if VMFConfig.import.generate_lightmap_uv2 and not is_runtime:
geometry_mesh.mesh.lightmap_unwrap(geometry_mesh.global_transform, texel_size);
From 730190c055468cd17f9dbb0302a941349d181f62 Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Sat, 31 Jan 2026 00:10:13 +0400
Subject: [PATCH 02/27] VMT: Fix blend materials creation path
---
.../godotvmf/godotvmt/vmt_material_conversion_context_menu.gd | 2 +-
addons/godotvmf/src/vmf_detail_props.gd | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
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/src/vmf_detail_props.gd b/addons/godotvmf/src/vmf_detail_props.gd
index 26a3618..0c25b8a 100644
--- a/addons/godotvmf/src/vmf_detail_props.gd
+++ b/addons/godotvmf/src/vmf_detail_props.gd
@@ -22,7 +22,6 @@ static func generate(original_mesh: ArrayMesh) -> Array[MultiMesh]:
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 offset_randomize: float = material_details.get("$detailpropoffsetrandomize" + index_postfix, 1.0)
var cast_shadows: bool = material_details.get("$detailpropshadows" + index_postfix, 1) == 1;
if not ResourceLoader.exists(detail_prop_path):
From 8685acdfc522e3c9a7fb6eb68faae38b76235d84 Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Sat, 31 Jan 2026 00:12:21 +0400
Subject: [PATCH 03/27] Bump 2.3.0
---
addons/godotvmf/plugin.cfg | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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"
From 709ebae99367717dc88f19ace92ccaf20c5ad606 Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Wed, 4 Feb 2026 15:24:14 +0400
Subject: [PATCH 04/27] VMF: Add toolsblocklight support
---
addons/godotvmf/godotvmt/vmt_loader.gd | 7 ++-
addons/godotvmf/src/vmf_geometry_corrector.gd | 19 ++++--
addons/godotvmf/src/vmf_node.gd | 38 ++++++++----
addons/godotvmf/src/vmf_tool.gd | 58 ++++++++++++++-----
addons/godotvmf/src/vmf_utils.gd | 36 ++++++++++++
5 files changed, 124 insertions(+), 34 deletions(-)
diff --git a/addons/godotvmf/godotvmt/vmt_loader.gd b/addons/godotvmf/godotvmt/vmt_loader.gd
index abb7225..09f5546 100644
--- a/addons/godotvmf/godotvmt/vmt_loader.gd
+++ b/addons/godotvmf/godotvmt/vmt_loader.gd
@@ -75,8 +75,8 @@ static func load(path: String):
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:
@@ -116,6 +116,9 @@ static func has_material(material: String) -> bool:
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:
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_node.gd b/addons/godotvmf/src/vmf_node.gd
index e588589..7968775 100644
--- a/addons/godotvmf/src/vmf_node.gd
+++ b/addons/godotvmf/src/vmf_node.gd
@@ -121,6 +121,20 @@ func generate_detail_props(geometry_mesh: MeshInstance3D) -> void:
detail_node.add_child(mmi);
mmi.set_owner(_owner);
+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();
@@ -141,8 +155,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:
@@ -152,19 +164,17 @@ func import_geometry() -> void:
if not get_meta("instance", false):
generate_navmesh(geometry_mesh);
- geometry_mesh.mesh = VMFTool.cleanup_mesh(geometry_mesh.mesh);
-
+ generate_shadow_mesh(geometry_mesh.mesh);
+ cleanup_geometry(geometry_mesh);
generate_detail_props(geometry_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);
+ 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();
@@ -193,8 +203,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()):
@@ -206,7 +220,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;
diff --git a/addons/godotvmf/src/vmf_tool.gd b/addons/godotvmf/src/vmf_tool.gd
index 13a50e7..fda1b3a 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] = [];
@@ -26,18 +56,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 +109,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 +128,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 +137,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_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;
From f30121a26377e4238791df26faa4e7ced90d7a17 Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Thu, 19 Feb 2026 02:07:39 +0400
Subject: [PATCH 05/27] VTF: Add support RGBA, ABGR, I, IA formats
---
addons/godotvmf/godotvmt/formats/vtf_dxt.gd | 33 ++++
.../godotvmt/formats/vtf_frame_reader.gd | 6 +
addons/godotvmf/godotvmt/formats/vtf_i8.gd | 27 +++
addons/godotvmf/godotvmt/formats/vtf_rgba.gd | 36 ++++
addons/godotvmf/godotvmt/vtf_loader.gd | 181 ++++++++----------
readme.md | 5 +-
6 files changed, 189 insertions(+), 99 deletions(-)
create mode 100644 addons/godotvmf/godotvmt/formats/vtf_dxt.gd
create mode 100644 addons/godotvmf/godotvmt/formats/vtf_frame_reader.gd
create mode 100644 addons/godotvmf/godotvmt/formats/vtf_i8.gd
create mode 100644 addons/godotvmf/godotvmt/formats/vtf_rgba.gd
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_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_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_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/vtf_loader.gd b/addons/godotvmf/godotvmt/vtf_loader.gd
index 88d8227..648238f 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,15 @@ 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;
+func read_frame(frame, srgb_conversion_method: SRGBConversionMethod):
+ var reader := frame_readers.get(hires_image_format, null) as VTFFrameReader;
+ var tex := reader.read(self, frame, srgb_conversion_method) as Texture;
- 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);
-
- 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);
-
- alpha = flags & Flags.TEXTUREFLAGS_ONEBITALPHA or flags & Flags.TEXTUREFLAGS_EIGHTBITALPHA;
-
- 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/readme.md b/readme.md
index 0a9fce0..d062156 100644
--- a/readme.md
+++ b/readme.md
@@ -25,7 +25,10 @@ Highly recommended to use [Hammer++](https://ficool2.github.io/HammerPlusPlus-We
- Instances support
- Native MDL support
- Native VMT support
-- Native VTF support (only DXT1, DXT3, DXT5 supported)
+- Native VTF support
+ - DXT1, DXT3, DXT5
+ - RGB888, RGBA8888, BGR888, ABGR8888
+ - I8, IA88
- Displacements import (with vertex data)
- WorldVertexTransition materials (blend textures) will be imported as [`WorldVertexTransitionMaterial`](/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gd)
- Entities support
From 749db0c437b4d71d3d38c69a3d55716b641e825b Mon Sep 17 00:00:00 2001
From: Ilya Fedorov <118038102+Withaust@users.noreply.github.com>
Date: Sat, 21 Feb 2026 22:00:30 +0300
Subject: [PATCH 06/27] Config: Make all project settings basic & fix rounding
issues in project settings (#81)
---
addons/godotvmf/src/vmf_config.gd | 31 +++++++++++++++++++++++++------
1 file changed, 25 insertions(+), 6 deletions(-)
diff --git a/addons/godotvmf/src/vmf_config.gd b/addons/godotvmf/src/vmf_config.gd
index 8ab3ab0..44d89ff 100644
--- a/addons/godotvmf/src/vmf_config.gd
+++ b/addons/godotvmf/src/vmf_config.gd
@@ -167,84 +167,100 @@ 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://");
-
+ ProjectSettings.set_as_basic("godot_vmf/import/gameinfo_path", true);
## 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);
+ ProjectSettings.set_as_basic("godot_vmf/import/scale", true);
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);
+ ProjectSettings.set_as_basic("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);
+ ProjectSettings.set_as_basic("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);
+ ProjectSettings.set_as_basic("godot_vmf/import/generate_navigation_mesh", true);
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", "");
+ ProjectSettings.set_as_basic("godot_vmf/import/navigation_mesh_preset", true);
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);
+ ProjectSettings.set_as_basic("godot_vmf/import/lightmap_texel_size", true);
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");
+ ProjectSettings.set_as_basic("godot_vmf/import/instances_folder", true);
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");
+ ProjectSettings.set_as_basic("godot_vmf/import/entities_folder", true);
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");
+ ProjectSettings.set_as_basic("godot_vmf/import/geometry_folder", true);
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", {});
-
+ ProjectSettings.set_as_basic("godot_vmf/import/entity_aliases", true);
## 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);
+ ProjectSettings.set_as_basic("godot_vmf/models/import", true);
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://");
+ ProjectSettings.set_as_basic("godot_vmf/models/target_folder", true);
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);
-
+ ProjectSettings.set_as_basic("godot_vmf/models/lightmap_texel_size", true);
## 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);
+ ProjectSettings.set_as_basic("godot_vmf/materials/import_mode", true);
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");
+ ProjectSettings.set_as_basic("godot_vmf/materials/target_folder", true);
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"]);
+ ProjectSettings.set_as_basic("godot_vmf/materials/ignore", true);
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", "");
+ ProjectSettings.set_as_basic("godot_vmf/materials/fallback_material", true);
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.set_as_basic("godot_vmf/materials/default_texture_size", true);
ProjectSettings.add_property_info({
"name": "godot_vmf/import/gameinfo_path",
@@ -258,7 +274,8 @@ static func define_project_settings():
ProjectSettings.add_property_info({
"name": "godot_vmf/import/scale",
"type": TYPE_FLOAT,
- "hint": PROPERTY_HINT_NONE,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,100.0,0.000001",
"default_value": 0.02,
})
@@ -294,7 +311,8 @@ static func define_project_settings():
ProjectSettings.add_property_info({
"name": "godot_vmf/import/lightmap_texel_size",
"type": TYPE_FLOAT,
- "hint": PROPERTY_HINT_NONE,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,100.0,0.000001",
"default_value": 0.2,
})
@@ -361,7 +379,8 @@ static func define_project_settings():
ProjectSettings.add_property_info({
"name": "godot_vmf/models/lightmap_texel_size",
"type": TYPE_FLOAT,
- "hint": PROPERTY_HINT_NONE,
+ "hint": PROPERTY_HINT_RANGE,
+ "hint_string": "0,100.0,0.000001",
"default_value": 0.4,
})
From 47a04430571e4d8bb6beecfc5130923bee6f6497 Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Sun, 22 Feb 2026 06:08:50 +0400
Subject: [PATCH 07/27] VPK: Add resource extraction from VPKs
---
addons/godotvmf/godotmdl/reader.gd | 10 +-
addons/godotvmf/godotvmt/vtf_loader.gd | 8 +-
.../godotvmf/godotvpk/vpk_directroy_entry.gd | 30 ++++
addons/godotvmf/godotvpk/vpk_header.gd | 20 +++
addons/godotvmf/godotvpk/vpk_header_2.gd | 19 ++
addons/godotvmf/godotvpk/vpk_reader.gd | 131 ++++++++++++++
addons/godotvmf/godotvpk/vpk_stack.gd | 56 ++++++
addons/godotvmf/src/structs/vmf_entity.gd | 10 +-
addons/godotvmf/src/structs/vmf_side.gd | 2 +-
addons/godotvmf/src/vmf_config.gd | 1 +
addons/godotvmf/src/vmf_node.gd | 13 ++
addons/godotvmf/src/vmf_resource_manager.gd | 166 ++++++++++++++----
.../{godotvdf => utils}/vdf_parser.gd | 0
addons/godotvmf/utils/vmf_struct.gd | 51 ++++++
readme.md | 3 +-
15 files changed, 473 insertions(+), 47 deletions(-)
create mode 100644 addons/godotvmf/godotvpk/vpk_directroy_entry.gd
create mode 100644 addons/godotvmf/godotvpk/vpk_header.gd
create mode 100644 addons/godotvmf/godotvpk/vpk_header_2.gd
create mode 100644 addons/godotvmf/godotvpk/vpk_reader.gd
create mode 100644 addons/godotvmf/godotvpk/vpk_stack.gd
rename addons/godotvmf/{godotvdf => utils}/vdf_parser.gd (100%)
create mode 100644 addons/godotvmf/utils/vmf_struct.gd
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/godotvmt/vtf_loader.gd b/addons/godotvmf/godotvmt/vtf_loader.gd
index 648238f..d6bb8ed 100644
--- a/addons/godotvmf/godotvmt/vtf_loader.gd
+++ b/addons/godotvmf/godotvmt/vtf_loader.gd
@@ -246,7 +246,13 @@ static var normal_conversion_shader: Shader;
static var shader_material: ShaderMaterial;
func read_frame(frame, srgb_conversion_method: SRGBConversionMethod):
- var reader := frame_readers.get(hires_image_format, null) as VTFFrameReader;
+ var reader = VTFLoader.frame_readers.get(hires_image_format, null);
+
+ if not reader:
+ VMFLogger.error("Unsupported texture format: {0} in file {1}" \
+ .format([VTFLoader.format_labels[hires_image_format], file.get_path()]));
+ return null;
+
var tex := reader.read(self, frame, srgb_conversion_method) as Texture;
if not tex:
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_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_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_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_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/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_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/vmf_config.gd b/addons/godotvmf/src/vmf_config.gd
index 44d89ff..29f6dd1 100644
--- a/addons/godotvmf/src/vmf_config.gd
+++ b/addons/godotvmf/src/vmf_config.gd
@@ -433,3 +433,4 @@ static func load_config():
file.close();
assign(VMFConfig, json);
+ update_config_field();
diff --git a/addons/godotvmf/src/vmf_node.gd b/addons/godotvmf/src/vmf_node.gd
index 7968775..392e632 100644
--- a/addons/godotvmf/src/vmf_node.gd
+++ b/addons/godotvmf/src/vmf_node.gd
@@ -90,7 +90,10 @@ 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();
@@ -299,6 +302,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:
@@ -339,8 +350,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_resource_manager.gd b/addons/godotvmf/src/vmf_resource_manager.gd
index 4cc5802..aafe67a 100644
--- a/addons/godotvmf/src/vmf_resource_manager.gd
+++ b/addons/godotvmf/src/vmf_resource_manager.gd
@@ -10,10 +10,23 @@ 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);
+
+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,51 +53,103 @@ 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");
+ # Game directory paths
+ var gamedir_path = VMFUtils.normalize_path(VMFConfig.gameinfo_path + "/" + model_path);
+ var mdl_path = VMFUtils.normalize_path(gamedir_path + ".mdl");
+ var vtx_path = VMFUtils.normalize_path(gamedir_path + ".vtx");
+ var vtx_dx90_path = VMFUtils.normalize_path(gamedir_path + ".dx90.vtx");
+ var vvd_path = VMFUtils.normalize_path(gamedir_path + ".vvd");
+ var phy_path = VMFUtils.normalize_path(gamedir_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_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;
+ var found_in_game_dir := FileAccess.file_exists(mdl_path) \
+ and FileAccess.file_exists(vtx_path) \
+ and FileAccess.file_exists(vvd_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 FileAccess.file_exists(phy_path): DirAccess.copy_absolute(phy_path, target_path + ".phy");
+ DirAccess.copy_absolute(mdl_path, target_path + ".mdl");
+
+ 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;
- var model_materials = MDLReader.new(mdl_path).get_possible_material_paths();
+ 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 import_material(material: String) -> bool:
material = material.to_lower();
+ var vpk_path = "materials/" + material + ".vmt";
var vmt_path = VMFUtils.normalize_path(VMFConfig.gameinfo_path + "/materials/" + material + ".vmt");
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);
+ # Trying to find material in game directory first, as it can be overridden by mods and thus differ from the one in VPKs
+ if FileAccess.file_exists(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;
- return true;
+ # 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;
+
+ has_imported_resources = true;
+
+ else:
+ VMFLogger.error("Material not found: " + vpk_path);
+ return false;
+
+ return has_imported_resources;
static func import_materials(vmf_structure: VMFStructure, is_runtime := false) -> void:
var editor_interface = get_editor_interface();
@@ -98,20 +163,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);
@@ -127,11 +191,24 @@ 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);
+ var vmt_vpk_path = "materials/" + material + ".vmt";
+
+ var details: Dictionary = {};
+
+ if FileAccess.file_exists(target_path):
+ 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 +216,29 @@ static func import_textures(material: String) -> bool:
for key in MATERIAL_KEYS_TO_IMPORT:
if key not in details: continue;
+ 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 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 FileAccess.file_exists(vtf_path):
+ 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/godotvdf/vdf_parser.gd b/addons/godotvmf/utils/vdf_parser.gd
similarity index 100%
rename from addons/godotvmf/godotvdf/vdf_parser.gd
rename to addons/godotvmf/utils/vdf_parser.gd
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/readme.md b/readme.md
index d062156..ff2df2c 100644
--- a/readme.md
+++ b/readme.md
@@ -36,6 +36,7 @@ Highly recommended to use [Hammer++](https://ficool2.github.io/HammerPlusPlus-We
- Surface props support
- Material's compile properties support
- FGD generator that compiles a FGD file based on source code of implemented entities in GDScript (see [here](https://github.com/H2xDev/GodotVMF/wiki/FGD-Generation))
+- Import from VPK support
@@ -60,7 +61,6 @@ Or for those who just want to port their map from Source Engine to Godot and see
- [Team Fortress Jumper](https://github.com/Mickeon/team-fortress-jumper) by [Mickeon](https://github.com/Mickeon)
## Known issues
-- Extraction of materials and models from VPKs is not supported
- Some of imported models may have wrong orientation
- Use `Additional Rotation` property in the MDL import options
- Avoid importing a big bunch of models/materials at once it may cause the engine crash or import freeze. There's some issue with threaded import in the engine.
@@ -92,6 +92,7 @@ If you have some ideas, suggestions regarding to quality or solutions of the pro
[ckaiser](https://github.com/ckaiser)
[jamop4](https://github.com/jamop4)
[Catperson6](https://github.com/catperson6real-dev)
+[Withaust](https://github.com/Withaust)
## License
MIT
From 435f40c4d8a093f9964f7d2cbb8e2ea1dce64386 Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Mon, 23 Feb 2026 20:53:48 +0400
Subject: [PATCH 08/27] Detail props: Add option to disable cast shadows
---
addons/godotvmf/godotvmf.gd | 1 -
addons/godotvmf/src/vmf_detail_props.gd | 1 +
2 files changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/godotvmf/godotvmf.gd b/addons/godotvmf/godotvmf.gd
index 578d2e1..6646f7b 100644
--- a/addons/godotvmf/godotvmf.gd
+++ b/addons/godotvmf/godotvmf.gd
@@ -90,7 +90,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
diff --git a/addons/godotvmf/src/vmf_detail_props.gd b/addons/godotvmf/src/vmf_detail_props.gd
index 0c25b8a..96590e9 100644
--- a/addons/godotvmf/src/vmf_detail_props.gd
+++ b/addons/godotvmf/src/vmf_detail_props.gd
@@ -106,6 +106,7 @@ static func generate(original_mesh: ArrayMesh) -> Array[MultiMesh]:
mmi.transform_format = MultiMesh.TRANSFORM_3D;
mmi.mesh = prop_mesh;
mmi.instance_count = chunk.size();
+ mmi.set_meta("cast_shadows", cast_shadows);
for k in range(chunk.size()):
var data = chunk[k];
From d1c262ee42deca1db2b57fdb2f4af997d67e60cf Mon Sep 17 00:00:00 2001
From: Asrael <70919950+asraeldragon@users.noreply.github.com>
Date: Tue, 24 Feb 2026 09:25:13 -0800
Subject: [PATCH 09/27] VMT: Add support for .res in loaded materials (#82)
* Add support for .res in loaded materials
* Added Asrael to contributors
---
addons/godotvmf/godotvmt/vmt_loader.gd | 26 +++++++++++---------------
readme.md | 1 +
2 files changed, 12 insertions(+), 15 deletions(-)
diff --git a/addons/godotvmf/godotvmt/vmt_loader.gd b/addons/godotvmf/godotvmt/vmt_loader.gd
index 09f5546..8b2502e 100644
--- a/addons/godotvmf/godotvmt/vmt_loader.gd
+++ b/addons/godotvmf/godotvmt/vmt_loader.gd
@@ -31,6 +31,14 @@ static func parse_transform(transform_data: String):
translate = translate,
}
+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):
var structure = VDFParser.parse(path, true);
@@ -106,15 +114,7 @@ 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");
-
- if not ResourceLoader.exists(material_path):
- return false;
-
- return true;
+ return get_material_load_path(material) != "";
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()));
@@ -124,12 +124,8 @@ static func get_material(material: String) -> 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/readme.md b/readme.md
index ff2df2c..a1ee5db 100644
--- a/readme.md
+++ b/readme.md
@@ -93,6 +93,7 @@ If you have some ideas, suggestions regarding to quality or solutions of the pro
[jamop4](https://github.com/jamop4)
[Catperson6](https://github.com/catperson6real-dev)
[Withaust](https://github.com/Withaust)
+[Asrael](https://github.com/asraeldragon)
## License
MIT
From 39b5dd660a935aaf55510078410b8c64b8bb48f5 Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Sat, 14 Mar 2026 22:16:25 +0400
Subject: [PATCH 10/27] VDF: Optimize prop parsing
---
addons/godotvmf/utils/vdf_parser.gd | 58 +++++++++++++++++++++++++----
1 file changed, 51 insertions(+), 7 deletions(-)
diff --git a/addons/godotvmf/utils/vdf_parser.gd b/addons/godotvmf/utils/vdf_parser.gd
index 2faf516..eb73a19 100644
--- a/addons/godotvmf/utils/vdf_parser.gd
+++ b/addons/godotvmf/utils/vdf_parser.gd
@@ -3,7 +3,6 @@
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]+)$');
@@ -88,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();
@@ -126,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;
From a9b8d53647e7df319139fa098869fca56908de9d Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Sun, 15 Mar 2026 00:27:22 +0400
Subject: [PATCH 11/27] MDL: Put all collision shapes into one static body (for
static models)
---
addons/godotvmf/entities/prop_static.gd | 8 +-------
addons/godotvmf/entities/prop_studio.gd | 3 ++-
addons/godotvmf/godotmdl/mdl_combiner.gd | 5 +++--
3 files changed, 6 insertions(+), 10 deletions(-)
diff --git a/addons/godotvmf/entities/prop_static.gd b/addons/godotvmf/entities/prop_static.gd
index 194fd31..46e9e7e 100644
--- a/addons/godotvmf/entities/prop_static.gd
+++ b/addons/godotvmf/entities/prop_static.gd
@@ -21,13 +21,7 @@ func _get_static_props_node() -> Node3D:
var static_props_node := geometry_node.get_node_or_null("StaticProps");
- if not static_props_node:
- static_props_node = Node3D.new();
- static_props_node.name = "StaticProps";
- geometry_node.add_child(static_props_node);
- static_props_node.set_owner(geometry_node.owner);
-
- return static_props_node;
+ if not model_instance: return;
func assign_model_properties() -> void:
model_instance.set_owner(get_owner());
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/godotmdl/mdl_combiner.gd b/addons/godotvmf/godotmdl/mdl_combiner.gd
index 47c2228..8d03783 100644
--- a/addons/godotvmf/godotmdl/mdl_combiner.gd
+++ b/addons/godotvmf/godotmdl/mdl_combiner.gd
@@ -203,8 +203,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);
From 49acd02e3d444442ce6949e7f60f8dedb4c170ab Mon Sep 17 00:00:00 2001
From: Ilya Fedorov <118038102+Withaust@users.noreply.github.com>
Date: Fri, 3 Apr 2026 13:48:30 +0300
Subject: [PATCH 12/27] VMF: Optimize vmf_vector_sorter's longer method (#88)
This is a non-intrusive change that simplifies length comparison of two vectors within the same uniform space (normals)
---
addons/godotvmf/src/vmf_vector_sorter.gd | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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;
From 76b8f6f5078bd95371b2370aa88f15fb9a86eff0 Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Tue, 7 Apr 2026 18:03:33 +0400
Subject: [PATCH 13/27] VMF: Update detail props generator
---
addons/godotvmf/src/vmf_detail_props.gd | 248 +++++++++++++++---------
1 file changed, 159 insertions(+), 89 deletions(-)
diff --git a/addons/godotvmf/src/vmf_detail_props.gd b/addons/godotvmf/src/vmf_detail_props.gd
index 96590e9..9839cde 100644
--- a/addons/godotvmf/src/vmf_detail_props.gd
+++ b/addons/godotvmf/src/vmf_detail_props.gd
@@ -3,126 +3,196 @@ 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 material_details := material.get_meta("details", {})
+ 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 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;
+ var cast_shadows: bool = material_details.get("$detailpropshadows" + index_postfix, 1) == 1
- if not ResourceLoader.exists(detail_prop_path):
+ 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);
+ 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;
-
- 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
+ VMFLogger.error("The specified detail prop in %s is not Mesh" % material.resource_path)
+ continue
- if indices.is_empty(): continue
+ # Pass 1: Count instances per triangle to pre-allocate buffers
+ var tri_counts := PackedInt32Array()
+ tri_counts.resize(num_triangles)
+ var total_estimate := 0
- var has_colors := not colors.is_empty()
- var chunks: Dictionary = {}
+ 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
- var num_triangles := indices.size() / 3
+ # 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 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 edge1 := v2 - v1;
- var edge2 := v3 - v1;
- var area := 0.5 * edge1.cross(edge2).length();
- var count := int(round(area * density));
-
- if count <= 0: continue;
-
- var n1 := normals[i1];
- var n2 := normals[i2];
- var n3 := normals[i3];
-
- var c1r := 1.0;
- var c2r := 1.0;
- var c3r := 1.0;
-
+ 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;
-
- for j in range(count):
- var r1 := randf();
- var r2 := randf();
- var sqrt_r1 := sqrt(r1);
-
- var w := 1.0 - sqrt_r1;
- var u := sqrt_r1 * (1.0 - r2);
- var v := sqrt_r1 * r2;
-
- var alpha := abs(prop_index - (c1r * w + c2r * u + c3r * v)) as float;
- if alpha <= 0.01: continue;
-
- var point := v1 * w + v2 * u + v3 * v;
- var normal := (n1 * w + n2 * u + n3 * v).normalized();
-
- var chunk_key := Vector3i(floor(point.x / chunk_size), floor(point.y / chunk_size), floor(point.z / chunk_size));
-
- if not chunks.has(chunk_key):
- chunks[chunk_key] = [];
-
- chunks[chunk_key].append([point, normal, alpha]);
-
- for chunk in chunks.values():
- var mmi := MultiMesh.new();
- mmi.transform_format = MultiMesh.TRANSFORM_3D;
- mmi.mesh = prop_mesh;
- mmi.instance_count = chunk.size();
- mmi.set_meta("cast_shadows", cast_shadows);
-
- for k in range(chunk.size()):
- var data = chunk[k];
- var t := Transform3D();
- var scale := randf_range(scale_range.x, scale_range.y) * (data[2] as float);
- var rotation := randf_range(rotation_range.x, rotation_range.y) / 180.0 * PI;
- var normal: Vector3 = data[1];
-
- t.basis = Basis.IDENTITY.rotated(normal, rotation) \
+ 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) \
- * Basis.IDENTITY.rotated(Vector3.RIGHT, -PI / 2) \
- .scaled(Vector3.ONE * scale);
- t.origin = data[0];
-
- mmi.set_instance_transform(k, t)
+ * 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
From 1fd7d8a497e5cb8faa7890ce3bc0993f81079f3e Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Tue, 7 Apr 2026 18:03:56 +0400
Subject: [PATCH 14/27] prop_static: Place all static props into StaticProps
node
---
addons/godotvmf/entities/prop_static.gd | 1 -
1 file changed, 1 deletion(-)
diff --git a/addons/godotvmf/entities/prop_static.gd b/addons/godotvmf/entities/prop_static.gd
index 46e9e7e..5c15a28 100644
--- a/addons/godotvmf/entities/prop_static.gd
+++ b/addons/godotvmf/entities/prop_static.gd
@@ -25,7 +25,6 @@ func _get_static_props_node() -> Node3D:
func assign_model_properties() -> void:
model_instance.set_owner(get_owner());
- model_instance.scale *= model_scale;
model_instance.gi_mode = GeometryInstance3D.GI_MODE_STATIC;
var fade_margin = fade_max - fade_min;
From a4100aadcce2a0eaebab9bf01c2bdc3d8a523a05 Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Tue, 7 Apr 2026 18:04:13 +0400
Subject: [PATCH 15/27] VMFConfig: Add default values for detail props config
---
addons/godotvmf/src/vmf_config.gd | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/addons/godotvmf/src/vmf_config.gd b/addons/godotvmf/src/vmf_config.gd
index 29f6dd1..47c30e8 100644
--- a/addons/godotvmf/src/vmf_config.gd
+++ b/addons/godotvmf/src/vmf_config.gd
@@ -220,6 +220,17 @@ static func define_project_settings():
ProjectSettings.set_initial_value("godot_vmf/import/entity_aliases", {});
ProjectSettings.set_as_basic("godot_vmf/import/entity_aliases", true);
+ if not ProjectSettings.has_setting("godot_vmf/import/detail_props_chunk_size"):
+ ProjectSettings.set_setting("godot_vmf/import/detail_props_chunk_size", 32.0);
+ ProjectSettings.set_initial_value("godot_vmf/import/detail_props_chunk_size", 32.0);
+ ProjectSettings.set_as_basic("godot_vmf/import/detail_props_chunk_size", true);
+
+ if not ProjectSettings.has_setting("godot_vmf/import/detail_props_draw_distance"):
+ ProjectSettings.set_setting("godot_vmf/import/detail_props_draw_distance", 100.0);
+ ProjectSettings.set_initial_value("godot_vmf/import/detail_props_draw_distance", 100.0);
+ ProjectSettings.set_as_basic("godot_vmf/import/detail_props_draw_distance", true);
+
+
## Models
if not ProjectSettings.has_setting("godot_vmf/models/import"):
ProjectSettings.set_setting("godot_vmf/models/import", false);
@@ -261,7 +272,7 @@ static func define_project_settings():
ProjectSettings.set_setting("godot_vmf/materials/default_texture_size", 512);
ProjectSettings.set_initial_value("godot_vmf/materials/default_texture_size", 512);
ProjectSettings.set_as_basic("godot_vmf/materials/default_texture_size", true);
-
+
ProjectSettings.add_property_info({
"name": "godot_vmf/import/gameinfo_path",
"type": TYPE_STRING,
From e1a68b30fb0fbc8808297d8671f381e719034eae Mon Sep 17 00:00:00 2001
From: H2xDev
Date: Fri, 1 May 2026 13:51:53 +0400
Subject: [PATCH 16/27] VMT: Update WorldVertexTransitionMaterial import logic
---
addons/godotvmf/godotvmt/vmt_loader.gd | 13 ++-
.../shaders/WorldVertexTransitionMaterial.gd | 106 ------------------
.../WorldVertexTransitionMaterial.gdshader | 83 --------------
.../world_vertex_transition_material.gdshader | 82 ++++++++++++++
4 files changed, 91 insertions(+), 193 deletions(-)
delete mode 100644 addons/godotvmf/shaders/WorldVertexTransitionMaterial.gd
delete mode 100644 addons/godotvmf/shaders/WorldVertexTransitionMaterial.gdshader
create mode 100644 addons/godotvmf/shaders/world_vertex_transition_material.gdshader
diff --git a/addons/godotvmf/godotvmt/vmt_loader.gd b/addons/godotvmf/godotvmt/vmt_loader.gd
index 8b2502e..4404e9f 100644
--- a/addons/godotvmf/godotvmt/vmt_loader.gd
+++ b/addons/godotvmf/godotvmt/vmt_loader.gd
@@ -39,13 +39,13 @@ static func get_material_load_path(material: String) -> String:
if ResourceLoader.exists(path_root + ".vmt"): return path_root + ".vmt";
return "";
-static func load(path: String):
+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:
@@ -63,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();
@@ -77,6 +80,8 @@ 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("%");
@@ -87,7 +92,7 @@ static func load(path: String):
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;
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);
+}
From b105fdc0aa04858b533b61138175078ed4943871 Mon Sep 17 00:00:00 2001
From: H2xDev
Date: Fri, 1 May 2026 13:54:06 +0400
Subject: [PATCH 17/27] VMF: Cleanup material's meta processing logic
---
addons/godotvmf/src/vmf_tool.gd | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/addons/godotvmf/src/vmf_tool.gd b/addons/godotvmf/src/vmf_tool.gd
index fda1b3a..5715fda 100644
--- a/addons/godotvmf/src/vmf_tool.gd
+++ b/addons/godotvmf/src/vmf_tool.gd
@@ -40,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:
From 3e309e8e721b90a1c6a325a7410436f939efa501 Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Fri, 1 May 2026 16:03:17 +0400
Subject: [PATCH 18/27] MDL: Fix VTX import path
---
addons/godotvmf/src/vmf_entity_node.gd | 2 +-
addons/godotvmf/src/vmf_resource_manager.gd | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
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_resource_manager.gd b/addons/godotvmf/src/vmf_resource_manager.gd
index aafe67a..2f09a0d 100644
--- a/addons/godotvmf/src/vmf_resource_manager.gd
+++ b/addons/godotvmf/src/vmf_resource_manager.gd
@@ -70,6 +70,8 @@ static func import_models(vmf_structure: VMFStructure) -> bool:
var target_path = VMFUtils.normalize_path(VMFConfig.models.target_folder + "/" + model_path);
if ResourceLoader.exists(target_path + ".mdl"): continue;
+
+ vtx_path = vtx_path if FileAccess.file_exists(vtx_path) and file_exists_in_vpk(vtx_path) else vtx_dx90_path
var found_in_game_dir := FileAccess.file_exists(mdl_path) \
and FileAccess.file_exists(vtx_path) \
From 9d663175604a500a124fa20b675b2796076c30f6 Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Fri, 1 May 2026 16:05:23 +0400
Subject: [PATCH 19/27] VMF: Replace labels with icons and add detail props
reimport option
---
addons/godotvmf/godotvmf.gd | 12 ++++
addons/godotvmf/icons/detailprop.png | Bin 0 -> 2933 bytes
addons/godotvmf/icons/detailprop.png.import | 41 ++++++++++++
addons/godotvmf/icons/discord.png | Bin 0 -> 2467 bytes
addons/godotvmf/icons/discord.png.import | 40 ++++++++++++
addons/godotvmf/icons/entities.png | Bin 0 -> 1155 bytes
addons/godotvmf/icons/entities.png.import | 41 ++++++++++++
addons/godotvmf/icons/full.png | Bin 0 -> 2318 bytes
addons/godotvmf/icons/full.png.import | 41 ++++++++++++
addons/godotvmf/icons/geometry.png | Bin 0 -> 1794 bytes
addons/godotvmf/icons/geometry.png.import | 41 ++++++++++++
addons/godotvmf/plugin.tscn | 65 +++++++++++++++-----
addons/godotvmf/src/vmf_node.gd | 12 ++++
13 files changed, 277 insertions(+), 16 deletions(-)
create mode 100644 addons/godotvmf/icons/detailprop.png
create mode 100644 addons/godotvmf/icons/detailprop.png.import
create mode 100644 addons/godotvmf/icons/discord.png
create mode 100644 addons/godotvmf/icons/discord.png.import
create mode 100644 addons/godotvmf/icons/entities.png
create mode 100644 addons/godotvmf/icons/entities.png.import
create mode 100644 addons/godotvmf/icons/full.png
create mode 100644 addons/godotvmf/icons/full.png.import
create mode 100644 addons/godotvmf/icons/geometry.png
create mode 100644 addons/godotvmf/icons/geometry.png.import
diff --git a/addons/godotvmf/godotvmf.gd b/addons/godotvmf/godotvmf.gd
index 6646f7b..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();
@@ -97,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/icons/detailprop.png b/addons/godotvmf/icons/detailprop.png
new file mode 100644
index 0000000000000000000000000000000000000000..7cf457a11d35bee3cff97cca3ff13609cfecaab8
GIT binary patch
literal 2933
zcmV-*3ySoKP))y#3`0
zELUK;0{dS<=(mjWVW$Gq6eGNB0M_Hd*nnE>u~akzuLzZni`lLRT;V!!tY
zAP9mW2!bGdbSM~*>cN92pwwCY$*O>=y3~&@_5FqY_dE5=FDnoRV^9G=s&BtNg308W
zUhKT#B`6dZP?k&BY~H9>uL=-?F}MIA)ek@9dVycJ2*Guh`gJcvGlU_E03g-T(HuVc
zULiPvh=-?|I9bAIYNU|DW$7J#lYPIf54%6w6
zAheRa0H~jSDu|{4vxP@Gw{QwVBgvb9Eeum|X-z=^31NJc*=(VO`yQaSQVMT#{cjjD
zu{DLUfLcJQ1^6@9CmKz`Awe@9}RaoeHJfz<;8V!I{-+%v7
zpCnqwuP!C~W*OON6SUBa{6s&*u(mCvJ=KxV|6yRpB>>n+vf2CrG|&`$In>pqP1cFN
zdj$2uw^8WZajd*W`cc3&tV4JPG*AUUgTUazYfXhhie{&4@h^??#=GBae6D(<_ETDX
zWRwD6;e<>Jvw%d5hp&`xrLsVq4~a2a0kA531yqPyz}kD?8pF12ExE$fm^A=GHv^~z
zFrx6lEPho{1m`%_p2cwszq#iH2{L6xp#4N`)r0SIiu$izg%;rw$d6(s%`X0lbixkqFI$84$YOLd*ZHTmS72x>{Z;3Q9>zradAeiI}6#Y_s}0)E~k$eqU?
zFzI8WmdRwHwbr)f?7Coj^%i=Y0Cj%;xS=iU0Q>Ou{d0YLtFM?p^zBDZ)yY1Q`%=p1xB
zzdss4;SPkp;!n*LUl*eGFFrP!5EdU{MFG~%;}zrM0G{YS=(Vk@_m8&4Kxf`zvi6%8
zuIbqW)*#F@R8^stZF4?ZjVA0ckSGK>O0@A4`qu{B#b_=Lt=AuF0CfI(Hfu%*>jKoG
z^To_YlV0)wL?J+OV;1_<20A1KPLDuKR?v1N7dQSB$GLjMsC;ZUb1Oida)qzAM35DN
z$3OwK!Gi}c`_KmVYT$&R2OXV4H?=^I>fh#B+#Kt=!IWKa>NOLnzt9qA@PGn_kmwuq
zVG@+$2mtm7G&_S;_3ql^-qgQmyV<3-`?T|Xi{4YgCOYun>TOHjix6$Su-UADLsc!Bp0bt4OZz!&^-oy*UV9C~6@w;|W1u@@
zEFswIta%-vV;_P#)GWAHq1oKhztQvrdL)un-~wRmF?E-(-9gk7Z-0R62%-sU
zV!bBY|7$yk_z|enXpoK|TR2()o$W?X?w?sFgu-P62^iH+n@J4TX*66*7*`8m-=6=L
z@ZlUm3N+P^EFlns2>^SV7s;|`UQq!rBZ!~ZRUUT6c>NUBuZ1{dm+`Poq4l<}9CDw5
z&Kqf43b=V=Uu-tCXr?TKb{Y*T05&(U1P)4_dT0Tj1}Mv3G=S|fI01i(&_93?qb;19
z#+*BA0qv~=hrUbzeM_1Z1ZHpnuw~;FaB$QD>`0(36m-O1{R^P6guuiZ4B6LGRL>t#DyZ|fC7u9q_xC|uet23bZcQKu0@ifUuKrs6^E8(u(FB0K0sXeJ_ydpeck$s$%Ln6>S0R?^xrS!mC(J2Dgf3Q
zH?Z2nD7k>mW&u=qlTF^l7l1y@
zKwBF(g8LaTAXEkE>VH7{k3<)MUd#Z%*B#IE9Kp%tbJv$L)jygvUIEMZ;~))qQs0$q
z!Cg!^;I1gHaMXWo{|5>Gwxl8eT&E}R*z2zV4nBJYR#lfaYJ2Yr1||&w*f(|SOM5u@
zH*oMt2GM@oaQogB3``ONz#il<2T^VFdj^Y%L237w^O54P7{eVT|bJJ-7
zykomA>V4p_UJvR3BIyVKw|&bcA3(?LxGgh<4n)~%FC786K6Oi_p>*3rHZXU5-2$j^
z%@mPj1mMQhEtU4tE$7&s8o2@Zr=it)HDFw5rpPrhjp+x_8)f5vV)VB0Pr2b<|a;VI=H(YFgp8>BqacB
zZyjMYadz-o(-T(c=srMb?bJeA8UW#D1e+oOesgHv_*_>xx*O2|J9t~Wpd%S78Pp8z
zho#=b6?z8g2>>?|Xv*7jz3eu@;zj_Gl0Zm$0>D2RIfS6?D(6lSa~)I$DVl&Vef(t^
z@+9ZLVY4Y95_iu8H1Da;?F!1l_cI+)zx@^^352940Nj!8P$r-qP81ML{UgZ2}o&ZEl0wHM%0QaPLv~hc|f1$6ga^N8X
z@B#c#@G}?+ZW2f|z#h2vdSBZ1pMS!fXl~{)Vh#XFQvd?Mb}pNfJzQ-g0`LJ`6EI{w
zfNKIGb2l}>m=+~s9RNwy2Z)v#^QO<;L?iIg_yEFw?y3Nc#{@_e5P)HQfE+l800=P=
z0rMk4{(qn6ocEmbKJRn>elr{|qhU}(x3oIm4TqH*PR!qds!@Xf70-%j93s
zAD?UjnVSidijEc}(a{U6#IBh}GTE#W#2Kd9ssq8mzoL#H6}~te)tf7K3N2EKj;6!z9GiK4hGmf0mS{+ubEEK(WX{7
zeVK3GZWb<~2TDGw?YzUrbOz&$UUxM})ze@Fvq{|BZlsv=?;!=&io+fh6HaeV)O|IT
zkg^pjycPNV&E9VK&7;%QdOtG#FNgRq)z|^Re|C7vpkefLo5(g|I8;38CZs_jlPg*%
z+`>IZkVi8Z;QU&39Gb{EGnhWL=ytvb_s9lLvJPyDaoKgllV6@J`5ycZXx`o-HQ1x~
zFsT|9z+0A_SN9a!u(_5Z41tTBjenR>k>Y3cjZ46Bnva?_8~XbM?~a|qzN<{{cPJ@K
zcK!rJ(DxiSJWF;s$tBE3<
z0mK;k(~~4<{vCz;%N!~5#OWRm6m{$HeK85T=yco%^VcFG-Js!3f{Y5F$>T(N;DbM|
zAqznxBlSd$=)9E%JIMV$sN|l`gXn@YzO7&lx7%eI8v-j&q8<+d`Y7#3NM;~+cDW?*
zo}+X6)%hL#XAlbt=cL@A?6iIp^n?8#A=t&O&>HN7f`0i?~;ict~E5x0vwI>!@(t1&w4)gD0F5C-{CcSlP
z^so%IiW0XzR|8*{mp_^wu9O7)Xj+nIX*izj4j4YRh*-aThD
zQvNbe9i5MCcQA9b)iuzql0iiN31S?iQ}lINQQF=DdjBbOm8au90$dXvkA0V3b}v27`F`@y}N%-UcMxx0kYf>W+&10WX
zU+XJ{yL)_@HCGKHtOtRIc-Sv5LNs>0yZDksAsadns~7SVBM-^$x8+c^`LDy_Jc%!V
z>g;;cI49QeXf!wm+R!1DBow+UqtMZm%O@)L*OOZwRjUS&hVhe44Hwwg>puD^I8x?<
zCJ_ELNz20kM*tMC_@JtPkYBa`k5tYsI}Hned1;NY@~3w65ws{`?rElRv+##p;cMQx
zyI*}8Gp!~?`a7A4&KfCU9)=n)Ttzw6<}sbWE1j$ClL-j^?gnldx;XvLZSSf;y>63d
z+RuDf!UY|B_|9`N@m2R3KC^XCY?Dtwjmle$b*;I?CfjhsmIC@Te-{2Gftg;~%d}Tn
zThYX>#ZT8f(;r;8vtgekG&c)Sg|8v-0aNN^kvr?9TVh;2D4Km(B%^*N`Zl8yo4h}l
zw_y46(S<+nIP3fC-R`y%KYW{is!OekrI06{OwSft7nozFn;G_C9lUhCRr6a1_y)py
z-8X`zUtl6j7#Z1gui~!uYPA3_6LC__zD%WY@p=;n_O!9bW?g$TLcd80eKIpy0LwT7
znTi+!TBWr}|4|GEb`;k$hx|MyN1vdyNI%ZJ0n%&b+okQG4Tk;7fJApI9KUvm=yqsK
zX`iEwj=$lP9%@#qo#={tC8J!b=2K|*d&&MFyv;os|Y;RR~L8i&?5m{Qb5#
zg4b`0MkcSmfcR74fF<8c4_&vaAH%0Chy3^#Te%l*AKm<{8}!^Rx2^Cdo
zb&K_(;?EH+;|fqMw&JPkQQb0fc26I+%;vkLDsENh-RAQ8YC{x5sGXiIb=6-uXKJ-{
z?NSz_kNTO#n7ZVOd8{bmTqn4t;Qx{5Ch~Amj#&TvUTBV(=P0H;1qNOlt7n(DjXGub
zl*$m$p|tFWtY3H?lpHhBvosr-u-|^2N0TVwPAfZ`vCMXimQrT6biQ)nG!RlRzy
zB&;eMl5P_;`&!I~S#+RG@wfzIJx4GO?1CoK
zZ$|k)fA=q0`3iS_c6qk!eT}`rb_|~T+2G0azvvn@m;qg+?&~SP=M2qD;2^d$y(pKi
zLTYs?y&R0$r2L|3pOkaH>&Ag-+6Sw~*91|*yKWRMh>7R#>jd&X+Zl)>?YmT;df;yC
zJ~P#e9Z+x3+Ewr1i4+}gRkP@!u4UKyFK?%xK6$7-uBYn40n=#4wn6kE(XiyScn{G?
z-f?OKO$Y1K#o@8l^Eez6i-=zO9l(0*Fe5dn&PU(6Kx($ncMXw+A~=ht+%k1X%B9M7
z6^aomO|Gt$%~EJ*D<;&1f}-g(Pk;VK2m`K6xb5Dymu0V5#Bj?7ly6g9&61G3|AzGE
a9HH@S{SDs1e*fbi3b3}gY+h%IP5K+MiF^eB
literal 0
HcmV?d00001
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 0000000000000000000000000000000000000000..0f295defc87b439839f77718ab99585481b95207
GIT binary patch
literal 1155
zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSoCO|{#S9GG!XV7ZFl&wkP>``W
z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBK3|DzL`iUdT1k0gQ7VI5W_oVoyp7Y685o%V
zdAc};R4~518}BFOD8v5YeZ#Layk7Ya1g{BagmmQJ5;n1IEMU%0_?0HIcHV+J3+^0v
z%Ui>C^Q}(ef~cUg*IDV*;>C7l<@|r|)>=$o
zs8ju~F8}X`?*AwMzAyTJL!|rYTnTl`1EE?
z|I<(0t4iyvi|T6%1TKGcG+~V3tqZKVdtl0c^T1YxE`~MB{_fqIugg=HyN!?WZ_x!u
zo>iv%_a0pT=J$bEC8jUN2X;M+Rc_g2Q!x3Y$dMoE49^ek6`IQ}yoYxY-+@(Tx3ru^^N5lP%qM3>&kM=TZz42~n{aY#0&|uWEegCZ$lSFbE!ge>_zHs98
zzbI|UuFx!cq;!H21KX>3;au4#a~O55>{q*Fbz&Dw#GQGej2VG!)f%rAVi|a@?pHfy
zb;6wa#;SVXlF|u=43cY<)-;DO8bth`_2QMnb)JNc|4b)db=;=f@bI>!NQ3UF1o=?e
zC7cZpe_eM8Wv@(Qxczl~=Q2JcRt0A6MOGey3{60ZV0OzihBhE0h&^%|!!iBz5C$_1
zX8A6w3_AuFp!!98x8^hMcojeSr_~982A(w>uNJ&ym~!}G$C`hpU&H@5C#KoU?UVcC
zedNH-jl1{C9#ojj*pc-i+s!#ak0Fq+?rZ{w539nfgX<-^8)q`882&JIRx}W7Si-hH
zO@-wk^Mof4uJg1gWH3yU`xEs*fQ7q(?Y}ysvw#YN)Sq|`k)~_R3l9EoW>C!d9|M&6
r&SYbLOWOi>RDt3UJ!?)p@rS`}s?D`pq4HP2GJ?U=)z4*}Q$iB}Y7gim
literal 0
HcmV?d00001
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 0000000000000000000000000000000000000000..a94b71d60c970beb6db7ce2f256a6afce44f5e6f
GIT binary patch
literal 2318
zcmV+p3Gw!cP)IY+Gx@KHIHKsryT2a;
zvlY$=Kn(or*F7BIPw;Edvl!#i_wMc{z~thD0HX7A8u-spJn(5oFq>U^4-Y;t8?jmd
z(fjvp@VqY&Pxbrbd6(Yp?EsiHtQ0`>;X?~vUas-WHps(=FdTT_zTE(`z$yXkXy^B3
zjAr42%|eVd0*KDex;RyT#=&oa9@sws3j~nZd2;aDpdTLvcZJKwAygD&0!Y^Lm00)F`ZKag{fph^#3*XvG$->G~1%QL!O9sE}Owc48LkXyX
zM3R8Y1&Y9j@nB|_~gAkONMBiIFZq2*v7
zmxB*bLZp}g;rMO9`})<#JLUrKo{ocNVT#v1!0G-tYw#()flJ*MIE<7T06&QTApXEE
z^6K#nWc>CecgeRB-w4C5%fTX9P9UxITR6BsaFDOF?h2d@Mi>e{5usKaAv=%JN}9DY
z2cO~_jm8tW9o$+VYX*qc-#LH1Y?*&B#XGr|cPD4&F&v)()CnM_FrIGMERfk6;);b5
zKhsSo6qcv40CEK|XM#5MG%t)Qf=IR6Cv8HY={^W9a_#&!X=`+F(eL>Vj-pEdbMl=l
zJ!XNDI=#I0yqJ~Z>}A%#`*;lTZ@I~uIb1=v00sC9p{V
zM6p?jKlmRR12;_u>@xG`cPmfZnD$MIU3O1cLh4%qjK{&dz8+AMA~|3_1kFJo4C96XVzw^Rmaw~cB+}Tc@Ao&z
zE^wXAj!Zj-QcJ=V)55!XgkpQj$~Kan7k6qrSZ&y!(aNiM^&!8yxzUg}qwI-G0+`dH
zbgal7ERi^QPZCPer)$ecxe>`_I^wzw;-Uaz;=s!<-n^j?^6(Fn0yxVrY1ybQLDhs;
zuik<|+!cT-qR3x)9fmDXi_!fPuYhR-4MG&P#Z;fJU98o92bCBukE^->CW*bLLuB~Y
zY;qJeQ4~;Cs%-+|8o(9SVhx`6FQ~-xj79{j?gNasI7jWSV8i9|)fPYy6z=j#Ld+`f
zQlmSKs_X+ymZ$rrI09_}8^dyb!Sg5q)C2kWruz<5R{&}IU$3`;*5#f@`F)Y~8pg
zInM_bU}zf*Q{@?8yz;7Ee~W{iJ|`ALq$Jhh+fYtkT^}HA|0xcjkEtzN%;gBcy^Wvi
z_FrE_j&>ngQ(P)X0AXm!C^6N~&T?Y7>nk
z#|fOxZj$oq7P!E={nwR}-Mq@68_3WuBPlb0xbP@C1+@VaSxnL&wAc=QcSiyLwrC2m
zAw;Z*xgdsMsGXj)Re%U+)a%FAJA^rdONk*W1jXY-)FJA_*ya(rUBaj(%+D5OgzWmH>8-Ap$1H;QWjbzYOPRe`cV
z(4s*U*;PBuLJMe{SrGWk-%PfKcR?pX@DCV5c_dKSRWb`UHjt#vA8FegZx7dr(aodU
zLcta`i1OEV{=_?oTA&89^KCqOi6c5)`o1x=CWFW<;6HtIvaXCbh)QMRl1-;7JAXCT
z%FoWu-s1XSZN*UmMzVp45H9cocr?-=DvBm+nYdVxEzpP!ZD);U1Ia8T%f`M|{~!oX
zmtTsz-|cp`dt*g9SacaD|3J^OakS=iQNIE8w7^2FsRvjr8!3LEE)+|_IzQ2}j)11LHy%BU1
z)|Z4G5LWhq8b9M~vMJDFAl^@Th~L06oTv$0@ehj4g6Q*)=O5)RvvkM%Eyby-;3R%<
zlr)H1-?UlKC~9%qyLXg>?*e1bSuMo`2i6XAV`aEL?HC-#IF?F=WvvFm4OW
zBvfJsu;wu@>bs5XOsQvp#ba!v$chqd8!4p#c5LKaX-Xx4Mf>-cm;Cncrl{4dyQb0z
zprDJm9q8MDvNh$I0Spv$lz}p>0$gzPteXK!5eZD1IX{8@Y_r>&cyEIxo+fgaDhwxu}4c>yF9y{<+2T4)Na$Vs?A1N{6s
z)x4))4xZO5+NG@JGN5FL-8ON@ueQPmJ;=iN!=p-H5MsR`!tm5&)2PZ-1X(BuKZy+(
zX|GeslCWj05Cmo|z5Dy`V1UXCAdL}07<_yj3!nfEf}oQ3BQOLDU`M12B8nbx`acCj
zumBF^U_pdo*JRgl7Jy{-5X7;`heogf6jLAwqxPR*0cd7L5cHT@gZ7_c7=~dOhG7_n
oVHk#C7=~dOhG7_nVVH9AKc8f>lyRd&!`!rNmkUO@|^aZ9+Aq2(4Cm);xJ)Y*D<5DH8iuYgIg3Q_k4B
zP{-aONtJ48j9NRj#L}jM77?{%c>3@BG2c1g{mysqx!?WsX1e0-k4dXa0|0Og=U{jJ
zNNGPPDSlLYFA)ZhL@L6;GYSA?6n_u|l$I+WH9=9=?QMZ)oKwq3MvP?RYy$w#^JGK>
z=#iI;zTy$>Mh=XQAw>QM!2UuCBUC;w4gdi0c$}Th4GL(jV&iD)X9H&->sthUSW1$No&)A6FI
z1@;Wj1#NiXfgK(W+gO>5$ttgd=3A=ji?$xX%WmX+Nrn9eWYzm-$Q(q_hw?WTUA=uS
zx)Sa5Xre*7iE19Rpfi!`n1`*-?dW`Cpil*4d6%QB`J>$zY@>rvt9_a&+@rX%NbaX-
zGL?HW^Ceu&i1tf4UEF2-!$<7^P#a}=rwpu%0--VlH>KXg#dK($D5_0@Vi7C}hRM0z
zgJADK8lkSQc3WG?QHCG?Sof9mm8O|d-cR9@+Z}(-=UHxTMSl(OSicHfDW97P5~P!q
z^k;V=NHMN;fg4f<>Br1|i2k?)YL(GULirs>ifqEbO-%$vM9irZ_!Vgx4)kn};61kj
zR#JO{%5?%V`*s^-DekLXTM~FNSEay#py=99_nlIaU>XisT}v4qVhVrSI_0U
zPvuvMCFN?cfBT<{wISnss2xc6>Re0fni-R+Zkpl=I#-rATJQ5M)j1`?S$LAtWn;!+
z)+Z0`@Y>$M0-=8dEoCz+=!w0ATb_~|`;h<5Tk%lT>dkk9d`(ZFzyF=D2Bwf_f+tRY
zwzog5kF#PnY4F(X*{zK*mA+mE#kqPUcauAK!2u8ca3-nM3SwOKU@o^G?j-ALKhCWm
zvo9$Jc}x4J^`I%g?!7g82}*sX%X#?NqKLm`m2!rr@~f@nYdBGax@JK+&esh1iKe><
zTH9>K#tyRrv<3`)FYG_#%SV)xZ`J2z-~h9&zt{qSC&4t{znL&%gN1i?88SW)ba`g{kNp}cXADp2im)yeV9d1h8vw%mM
zjGFJAP00zGAnL2at1ov$-2qz+u>EF0GZ3ozv7Y9$
zX|0o7A_uxE6#7G#^oqoPHlmFN%
zx0kd^UCk%llVOxVCnmNv!POowtlwVMb`b3p9M{gTO-LNiH!{k$O!r)oMc83l($#Dc
zX6Gr*bGI&0nx!Y)*nMp0`}sgr-P=_RBFD}C@xsIZE8ny$myxnu}U+u|$qQu=~^>`B3ltZ5%gA~ob
zhmY|=DIN2~51lLZ2`aOa{@B}#9k5A@URx7zM4V0?VzvTidhxl@X(}VW_>tr9CQ@D<
zHpWh$$WaR-8`7^A=a>Dv+;Db8rE+y4s$3Jh|QdI-AR7a7M16%BVQt?%c1{NBw=uw
zmp1ain)W$|(WCed_sESJ5i|lPDg~7zb-E^+PEs+^!2|Gt void:
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);
From 2e4d79bf13a34c97fc89d04748c71e50648d2168 Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Sat, 2 May 2026 14:32:24 +0400
Subject: [PATCH 20/27] MDL: Add option to generate collision from the mesh
itself
---
addons/godotvmf/entities/prop_static.gd | 9 +++-
addons/godotvmf/godotmdl/import.gd | 12 +++++
addons/godotvmf/godotmdl/mdl_combiner.gd | 33 ++++++++++--
addons/godotvmf/utils/mesh_utils.gd | 66 ++++++++++++++++++++++++
4 files changed, 115 insertions(+), 5 deletions(-)
create mode 100644 addons/godotvmf/utils/mesh_utils.gd
diff --git a/addons/godotvmf/entities/prop_static.gd b/addons/godotvmf/entities/prop_static.gd
index 5c15a28..194fd31 100644
--- a/addons/godotvmf/entities/prop_static.gd
+++ b/addons/godotvmf/entities/prop_static.gd
@@ -21,10 +21,17 @@ func _get_static_props_node() -> Node3D:
var static_props_node := geometry_node.get_node_or_null("StaticProps");
- if not model_instance: return;
+ if not static_props_node:
+ static_props_node = Node3D.new();
+ static_props_node.name = "StaticProps";
+ geometry_node.add_child(static_props_node);
+ static_props_node.set_owner(geometry_node.owner);
+
+ return static_props_node;
func assign_model_properties() -> void:
model_instance.set_owner(get_owner());
+ model_instance.scale *= model_scale;
model_instance.gi_mode = GeometryInstance3D.GI_MODE_STATIC;
var fade_margin = fade_max - fade_min;
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/mdl_combiner.gd b/addons/godotvmf/godotmdl/mdl_combiner.gd
index 8d03783..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;
@@ -215,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/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));
From 97b0335d23ca37aabc955486aea426987c698886 Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Sat, 2 May 2026 14:33:47 +0400
Subject: [PATCH 21/27] Remove *.uid from .gitignore (solves #90)
---
.gitignore | 2 --
addons/godotfgd/main.gd.uid | 1 +
addons/godotvmf/entities/func_detail.gd.uid | 1 +
addons/godotvmf/entities/func_instance.gd.uid | 1 +
addons/godotvmf/entities/light.gd.uid | 1 +
addons/godotvmf/entities/light_environment.gd.uid | 1 +
addons/godotvmf/entities/light_spot.gd.uid | 1 +
addons/godotvmf/entities/prop_static.gd.uid | 1 +
addons/godotvmf/entities/prop_studio.gd.uid | 1 +
addons/godotvmf/godotmdl/ani_reader.gd.uid | 1 +
addons/godotvmf/godotmdl/import.gd.uid | 1 +
addons/godotvmf/godotmdl/mdl_combiner.gd.uid | 1 +
addons/godotvmf/godotmdl/mdl_reader.gd.uid | 1 +
addons/godotvmf/godotmdl/mesh_generator.gd.uid | 1 +
addons/godotvmf/godotmdl/phy_reader.gd.uid | 1 +
addons/godotvmf/godotmdl/reader.gd.uid | 1 +
addons/godotvmf/godotmdl/struct.gd.uid | 1 +
addons/godotvmf/godotmdl/vtx_reader.gd.uid | 1 +
addons/godotvmf/godotmdl/vvd_reader.gd.uid | 1 +
addons/godotvmf/godotvmf.gd.uid | 1 +
addons/godotvmf/godotvmt/formats/vtf_dxt.gd.uid | 1 +
addons/godotvmf/godotvmt/formats/vtf_frame_reader.gd.uid | 1 +
addons/godotvmf/godotvmt/formats/vtf_i8.gd.uid | 1 +
addons/godotvmf/godotvmt/formats/vtf_rgba.gd.uid | 1 +
addons/godotvmf/godotvmt/vmt_context_menu.gd.uid | 1 +
addons/godotvmf/godotvmt/vmt_import.gd.uid | 1 +
addons/godotvmf/godotvmt/vmt_loader.gd.uid | 1 +
.../godotvmt/vmt_material_conversion_context_menu.gd.uid | 1 +
addons/godotvmf/godotvmt/vmt_shader_based_material.gd.uid | 1 +
addons/godotvmf/godotvmt/vmt_transformer.gd.uid | 1 +
addons/godotvmf/godotvmt/vtf_import.gd.uid | 1 +
addons/godotvmf/godotvmt/vtf_loader.gd.uid | 1 +
addons/godotvmf/godotvpk/vpk_directroy_entry.gd.uid | 1 +
addons/godotvmf/godotvpk/vpk_header.gd.uid | 1 +
addons/godotvmf/godotvpk/vpk_header_2.gd.uid | 1 +
addons/godotvmf/godotvpk/vpk_reader.gd.uid | 1 +
addons/godotvmf/godotvpk/vpk_stack.gd.uid | 1 +
addons/godotvmf/shaders/WorldVertexTransitionMaterial.gd.uid | 1 +
.../godotvmf/shaders/WorldVertexTransitionMaterial.gdshader.uid | 1 +
.../shaders/world_vertex_transition_material.gdshader.uid | 1 +
addons/godotvmf/src/structs/vmf_displacement_info.gd.uid | 1 +
addons/godotvmf/src/structs/vmf_entity.gd.uid | 1 +
addons/godotvmf/src/structs/vmf_entity_type.gd.uid | 1 +
addons/godotvmf/src/structs/vmf_side.gd.uid | 1 +
addons/godotvmf/src/structs/vmf_solid.gd.uid | 1 +
addons/godotvmf/src/structs/vmf_structure.gd.uid | 1 +
addons/godotvmf/src/structs/vmf_tex_coord.gd.uid | 1 +
addons/godotvmf/src/valve_io_node.gd.uid | 1 +
addons/godotvmf/src/vmf_cache.gd.uid | 1 +
addons/godotvmf/src/vmf_config.gd.uid | 1 +
addons/godotvmf/src/vmf_csg.gd.uid | 1 +
addons/godotvmf/src/vmf_detail_props.gd.uid | 1 +
addons/godotvmf/src/vmf_displacement_tool.gd.uid | 1 +
addons/godotvmf/src/vmf_entity_context_menu.gd.uid | 1 +
addons/godotvmf/src/vmf_entity_node.gd.uid | 1 +
addons/godotvmf/src/vmf_geometry_corrector.gd.uid | 1 +
addons/godotvmf/src/vmf_instance_manager.gd.uid | 1 +
addons/godotvmf/src/vmf_logger.gd.uid | 1 +
addons/godotvmf/src/vmf_node.gd.uid | 1 +
addons/godotvmf/src/vmf_output_executor.gd.uid | 1 +
addons/godotvmf/src/vmf_resource_manager.gd.uid | 1 +
addons/godotvmf/src/vmf_runtime_controller.gd.uid | 1 +
addons/godotvmf/src/vmf_tool.gd.uid | 1 +
addons/godotvmf/src/vmf_utils.gd.uid | 1 +
addons/godotvmf/src/vmf_vector_sorter.gd.uid | 1 +
addons/godotvmf/ui.gd.uid | 1 +
addons/godotvmf/utils/mesh_utils.gd.uid | 1 +
addons/godotvmf/utils/vdf_parser.gd.uid | 1 +
addons/godotvmf/utils/vmf_struct.gd.uid | 1 +
69 files changed, 68 insertions(+), 2 deletions(-)
create mode 100644 addons/godotfgd/main.gd.uid
create mode 100644 addons/godotvmf/entities/func_detail.gd.uid
create mode 100644 addons/godotvmf/entities/func_instance.gd.uid
create mode 100644 addons/godotvmf/entities/light.gd.uid
create mode 100644 addons/godotvmf/entities/light_environment.gd.uid
create mode 100644 addons/godotvmf/entities/light_spot.gd.uid
create mode 100644 addons/godotvmf/entities/prop_static.gd.uid
create mode 100644 addons/godotvmf/entities/prop_studio.gd.uid
create mode 100644 addons/godotvmf/godotmdl/ani_reader.gd.uid
create mode 100644 addons/godotvmf/godotmdl/import.gd.uid
create mode 100644 addons/godotvmf/godotmdl/mdl_combiner.gd.uid
create mode 100644 addons/godotvmf/godotmdl/mdl_reader.gd.uid
create mode 100644 addons/godotvmf/godotmdl/mesh_generator.gd.uid
create mode 100644 addons/godotvmf/godotmdl/phy_reader.gd.uid
create mode 100644 addons/godotvmf/godotmdl/reader.gd.uid
create mode 100644 addons/godotvmf/godotmdl/struct.gd.uid
create mode 100644 addons/godotvmf/godotmdl/vtx_reader.gd.uid
create mode 100644 addons/godotvmf/godotmdl/vvd_reader.gd.uid
create mode 100644 addons/godotvmf/godotvmf.gd.uid
create mode 100644 addons/godotvmf/godotvmt/formats/vtf_dxt.gd.uid
create mode 100644 addons/godotvmf/godotvmt/formats/vtf_frame_reader.gd.uid
create mode 100644 addons/godotvmf/godotvmt/formats/vtf_i8.gd.uid
create mode 100644 addons/godotvmf/godotvmt/formats/vtf_rgba.gd.uid
create mode 100644 addons/godotvmf/godotvmt/vmt_context_menu.gd.uid
create mode 100644 addons/godotvmf/godotvmt/vmt_import.gd.uid
create mode 100644 addons/godotvmf/godotvmt/vmt_loader.gd.uid
create mode 100644 addons/godotvmf/godotvmt/vmt_material_conversion_context_menu.gd.uid
create mode 100644 addons/godotvmf/godotvmt/vmt_shader_based_material.gd.uid
create mode 100644 addons/godotvmf/godotvmt/vmt_transformer.gd.uid
create mode 100644 addons/godotvmf/godotvmt/vtf_import.gd.uid
create mode 100644 addons/godotvmf/godotvmt/vtf_loader.gd.uid
create mode 100644 addons/godotvmf/godotvpk/vpk_directroy_entry.gd.uid
create mode 100644 addons/godotvmf/godotvpk/vpk_header.gd.uid
create mode 100644 addons/godotvmf/godotvpk/vpk_header_2.gd.uid
create mode 100644 addons/godotvmf/godotvpk/vpk_reader.gd.uid
create mode 100644 addons/godotvmf/godotvpk/vpk_stack.gd.uid
create mode 100644 addons/godotvmf/shaders/WorldVertexTransitionMaterial.gd.uid
create mode 100644 addons/godotvmf/shaders/WorldVertexTransitionMaterial.gdshader.uid
create mode 100644 addons/godotvmf/shaders/world_vertex_transition_material.gdshader.uid
create mode 100644 addons/godotvmf/src/structs/vmf_displacement_info.gd.uid
create mode 100644 addons/godotvmf/src/structs/vmf_entity.gd.uid
create mode 100644 addons/godotvmf/src/structs/vmf_entity_type.gd.uid
create mode 100644 addons/godotvmf/src/structs/vmf_side.gd.uid
create mode 100644 addons/godotvmf/src/structs/vmf_solid.gd.uid
create mode 100644 addons/godotvmf/src/structs/vmf_structure.gd.uid
create mode 100644 addons/godotvmf/src/structs/vmf_tex_coord.gd.uid
create mode 100644 addons/godotvmf/src/valve_io_node.gd.uid
create mode 100644 addons/godotvmf/src/vmf_cache.gd.uid
create mode 100644 addons/godotvmf/src/vmf_config.gd.uid
create mode 100644 addons/godotvmf/src/vmf_csg.gd.uid
create mode 100644 addons/godotvmf/src/vmf_detail_props.gd.uid
create mode 100644 addons/godotvmf/src/vmf_displacement_tool.gd.uid
create mode 100644 addons/godotvmf/src/vmf_entity_context_menu.gd.uid
create mode 100644 addons/godotvmf/src/vmf_entity_node.gd.uid
create mode 100644 addons/godotvmf/src/vmf_geometry_corrector.gd.uid
create mode 100644 addons/godotvmf/src/vmf_instance_manager.gd.uid
create mode 100644 addons/godotvmf/src/vmf_logger.gd.uid
create mode 100644 addons/godotvmf/src/vmf_node.gd.uid
create mode 100644 addons/godotvmf/src/vmf_output_executor.gd.uid
create mode 100644 addons/godotvmf/src/vmf_resource_manager.gd.uid
create mode 100644 addons/godotvmf/src/vmf_runtime_controller.gd.uid
create mode 100644 addons/godotvmf/src/vmf_tool.gd.uid
create mode 100644 addons/godotvmf/src/vmf_utils.gd.uid
create mode 100644 addons/godotvmf/src/vmf_vector_sorter.gd.uid
create mode 100644 addons/godotvmf/ui.gd.uid
create mode 100644 addons/godotvmf/utils/mesh_utils.gd.uid
create mode 100644 addons/godotvmf/utils/vdf_parser.gd.uid
create mode 100644 addons/godotvmf/utils/vmf_struct.gd.uid
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.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.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.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.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/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.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.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.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.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.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.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.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.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.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.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.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.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.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/shaders/WorldVertexTransitionMaterial.gd.uid b/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gd.uid
new file mode 100644
index 0000000..74b1d98
--- /dev/null
+++ b/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gd.uid
@@ -0,0 +1 @@
+uid://qh1wsx8xt7lv
diff --git a/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gdshader.uid b/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gdshader.uid
new file mode 100644
index 0000000..2dc5c88
--- /dev/null
+++ b/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gdshader.uid
@@ -0,0 +1 @@
+uid://3odi6ahnlvlo
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.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.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.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.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.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.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.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_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.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.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.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.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.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.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/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.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
From 45b90f412f83f04cad164405cd6b1bf2a12ddcb8 Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Sun, 3 May 2026 03:09:51 +0400
Subject: [PATCH 22/27] VMF: Optimize merged faces cleanup
---
addons/godotvmf/godotvdf/vdf_parser.gd.uid | 1 +
.../godotvmf/shaders/WorldVertexTransitionMaterial.gd.uid | 1 -
.../shaders/WorldVertexTransitionMaterial.gdshader.uid | 1 -
addons/godotvmf/src/structs/vmf_displacement_info.gd | 4 ++--
addons/godotvmf/src/structs/vmf_solid.gd | 2 +-
addons/godotvmf/utils/vdf_parser.gd | 6 +++---
6 files changed, 7 insertions(+), 8 deletions(-)
create mode 100644 addons/godotvmf/godotvdf/vdf_parser.gd.uid
delete mode 100644 addons/godotvmf/shaders/WorldVertexTransitionMaterial.gd.uid
delete mode 100644 addons/godotvmf/shaders/WorldVertexTransitionMaterial.gdshader.uid
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/shaders/WorldVertexTransitionMaterial.gd.uid b/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gd.uid
deleted file mode 100644
index 74b1d98..0000000
--- a/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://qh1wsx8xt7lv
diff --git a/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gdshader.uid b/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gdshader.uid
deleted file mode 100644
index 2dc5c88..0000000
--- a/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gdshader.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://3odi6ahnlvlo
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_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/utils/vdf_parser.gd b/addons/godotvmf/utils/vdf_parser.gd
index eb73a19..6091647 100644
--- a/addons/godotvmf/utils/vdf_parser.gd
+++ b/addons/godotvmf/utils/vdf_parser.gd
@@ -8,7 +8,7 @@ static var _vector2Regex := RegEx.create_from_string('^([-\\d\\.e]+)\\s([-\\d\\.
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.
@@ -60,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;
@@ -71,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 = {};
From c15e04597bf5989fdb763f7399e696472931e3e8 Mon Sep 17 00:00:00 2001
From: Radik Khamatdinov
Date: Tue, 5 May 2026 15:22:44 +0400
Subject: [PATCH 23/27] Update readme.md
---
readme.md | 27 ++++++++++-----------------
1 file changed, 10 insertions(+), 17 deletions(-)
diff --git a/readme.md b/readme.md
index a1ee5db..dbd96a7 100644
--- a/readme.md
+++ b/readme.md
@@ -11,17 +11,16 @@
-
+
## Description
-An importer of [VMF files](https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)) into [Godot Engine](https://godotengine.org/).
-
-Highly recommended to use [Hammer++](https://ficool2.github.io/HammerPlusPlus-Website/) since it supports precised vertex data.
+An asset pipeline bridge between [Source Engine](https://developer.valvesoftware.com/wiki/Source) and Godot Engine. Allows to import VMF maps, MDLs, VTF and VMT from Source Engine games into Godot Engine.
+Highly recommended to use it with [Hammer++](https://ficool2.github.io/HammerPlusPlus-Website/).
### Features
-- Brushes geometry import (including UVs, materials IDs and smoothing groups)
+- VMF geometry import
- Instances support
- Native MDL support
- Native VMT support
@@ -29,12 +28,13 @@ Highly recommended to use [Hammer++](https://ficool2.github.io/HammerPlusPlus-We
- DXT1, DXT3, DXT5
- RGB888, RGBA8888, BGR888, ABGR8888
- I8, IA88
+- Godot materials/textures export to VMT/VTF (see [here](https://github.com/h2xdev/godotvmf/wiki/materials))
- Displacements import (with vertex data)
- - WorldVertexTransition materials (blend textures) will be imported as [`WorldVertexTransitionMaterial`](/addons/godotvmf/shaders/WorldVertexTransitionMaterial.gd)
+ - WorldVertexTransition materials (blend textures) will be imported as [`WorldVertexTransitionMaterial`](/addons/godotvmf/shaders/world_vertex_transition_material.gdshader)
- Entities support
-- Hammer's Input/Output system support
+- Hammer's Input/Output system support
- Surface props support
-- Material's compile properties support
+- Detail props generation (via `$detailprop` in VMT)
- FGD generator that compiles a FGD file based on source code of implemented entities in GDScript (see [here](https://github.com/H2xDev/GodotVMF/wiki/FGD-Generation))
- Import from VPK support
@@ -59,11 +59,11 @@ Or for those who just want to port their map from Source Engine to Godot and see
- [Vampire Bloodlines map example](https://www.youtube.com/watch?v=dV3nllCZYNM) by Rendara
- [SurfsUp](https://store.steampowered.com/app/3454830/SurfsUp) by [@bearlikelion](https://github.com/bearlikelion)
- [Team Fortress Jumper](https://github.com/Mickeon/team-fortress-jumper) by [Mickeon](https://github.com/Mickeon)
+- [Cirno! Lifts a Boulder](https://store.steampowered.com/app/4173110/Cirno_Lifts_a_Boulder/) by [fluuury](https://x.com/fluuury)
## Known issues
-- Some of imported models may have wrong orientation
+- Non-static models or their collision may have wrong orientation
- Use `Additional Rotation` property in the MDL import options
-- Avoid importing a big bunch of models/materials at once it may cause the engine crash or import freeze. There's some issue with threaded import in the engine.
## Legality of use
If you would like to use the Source Engine SDK or other Valve Developer Tools for commercial use, please contact Valve at sourceengine@valvesoftware.com. There shouldn’t be any issues if you’re using it for non-commercial projects.
@@ -73,13 +73,6 @@ If you have some ideas, suggestions regarding to quality or solutions of the pro
- If you've added a new feature please add the relevant documentation.
- Add yourself to the contributors section below
-### How to test the addon after adding new features or fixing some bugs
-1. Install any of Source Engine Games (L4D, HL2, TF2)
-2. Unpack all textures and models from VPKs
-3. Decompile most complex maps
-4. Try to import decompiled maps in Godot
-5. Check for errors if they appear
-
## Credits
[H2xDev](https://github.com/H2xDev) - main contributor
[Ambiabstract](https://github.com/Ambiabstract) - tech help and inspiration
From 9964365f571311e6926b2c687783818b4bae8ecc Mon Sep 17 00:00:00 2001
From: Radik
Date: Sun, 21 Jun 2026 18:30:04 +0400
Subject: [PATCH 24/27] Enhance README with images and feature updates
Added an image for Hammer Editor and Godot Engine comparison
---
readme.md | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/readme.md b/readme.md
index dbd96a7..74e198e 100644
--- a/readme.md
+++ b/readme.md
@@ -19,6 +19,10 @@
An asset pipeline bridge between [Source Engine](https://developer.valvesoftware.com/wiki/Source) and Godot Engine. Allows to import VMF maps, MDLs, VTF and VMT from Source Engine games into Godot Engine.
Highly recommended to use it with [Hammer++](https://ficool2.github.io/HammerPlusPlus-Website/).
+
+
+
+
### Features
- VMF geometry import
- Instances support
@@ -38,7 +42,6 @@ Highly recommended to use it with [Hammer++](https://ficool2.github.io/HammerPlu
- FGD generator that compiles a FGD file based on source code of implemented entities in GDScript (see [here](https://github.com/H2xDev/GodotVMF/wiki/FGD-Generation))
- Import from VPK support
-
## Why?
We with my friend [Ambiabstract](https://github.com/Ambiabstract) did not find a convenient plugin for us to create levels for Godot and so we decided to use our favorite and familiar editor :)
From 001ce48e75dd0f510fc5804c9298b4930418fc12 Mon Sep 17 00:00:00 2001
From: Radik
Date: Mon, 22 Jun 2026 17:33:09 +0400
Subject: [PATCH 25/27] Include LICENSE into the release archive (#102)
---
.gitattributes | 1 +
1 file changed, 1 insertion(+)
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
From f4f7ff2c204f42620aa29d4a0240bba1bdc8f213 Mon Sep 17 00:00:00 2001
From: Radik
Date: Mon, 22 Jun 2026 17:33:33 +0400
Subject: [PATCH 26/27] VMT: Add $normalmap parse support (#101)
---
addons/godotvmf/godotvmt/vmt_transformer.gd | 6 ++++++
1 file changed, 6 insertions(+)
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));
From d6ae60fcd3d085f04fcb3e0a946c7198cb402c95 Mon Sep 17 00:00:00 2001
From: Radik
Date: Tue, 7 Jul 2026 18:13:21 +0400
Subject: [PATCH 27/27] Import: Add multiple game import paths support (#100)
* VMFConfig: Add Additional Import Paths option
* VMFConfig: add VMFGameFolder class to work with additional game paths
* VPK: Add Additional Import Paths support
---
addons/godotvmf/src/vmf_config.gd | 427 +++++++-------------
addons/godotvmf/src/vmf_game_folder.gd | 21 +
addons/godotvmf/src/vmf_game_folder.gd.uid | 1 +
addons/godotvmf/src/vmf_resource_manager.gd | 62 +--
4 files changed, 197 insertions(+), 314 deletions(-)
create mode 100644 addons/godotvmf/src/vmf_game_folder.gd
create mode 100644 addons/godotvmf/src/vmf_game_folder.gd.uid
diff --git a/addons/godotvmf/src/vmf_config.gd b/addons/godotvmf/src/vmf_config.gd
index 47c30e8..4c174f0 100644
--- a/addons/godotvmf/src/vmf_config.gd
+++ b/addons/godotvmf/src/vmf_config.gd
@@ -3,28 +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",
- "godot_vmf/import/detail_props_chunk_size",
- "godot_vmf/import/detail_props_draw_distance",
-];
+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
@@ -109,6 +210,8 @@ class ImportConfig:
var gameinfo_path: String = ProjectSettings.get_setting("godot_vmf/import/gameinfo_path", "res://");
+ 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://");
@@ -164,275 +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://");
- ProjectSettings.set_as_basic("godot_vmf/import/gameinfo_path", true);
-
- ## 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);
- ProjectSettings.set_as_basic("godot_vmf/import/scale", true);
-
- 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);
- ProjectSettings.set_as_basic("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);
- ProjectSettings.set_as_basic("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);
- ProjectSettings.set_as_basic("godot_vmf/import/generate_navigation_mesh", true);
-
- 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", "");
- ProjectSettings.set_as_basic("godot_vmf/import/navigation_mesh_preset", true);
-
- 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);
- ProjectSettings.set_as_basic("godot_vmf/import/lightmap_texel_size", true);
-
- 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");
- ProjectSettings.set_as_basic("godot_vmf/import/instances_folder", true);
-
- 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");
- ProjectSettings.set_as_basic("godot_vmf/import/entities_folder", true);
-
- 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");
- ProjectSettings.set_as_basic("godot_vmf/import/geometry_folder", true);
-
- 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", {});
- ProjectSettings.set_as_basic("godot_vmf/import/entity_aliases", true);
-
- if not ProjectSettings.has_setting("godot_vmf/import/detail_props_chunk_size"):
- ProjectSettings.set_setting("godot_vmf/import/detail_props_chunk_size", 32.0);
- ProjectSettings.set_initial_value("godot_vmf/import/detail_props_chunk_size", 32.0);
- ProjectSettings.set_as_basic("godot_vmf/import/detail_props_chunk_size", true);
-
- if not ProjectSettings.has_setting("godot_vmf/import/detail_props_draw_distance"):
- ProjectSettings.set_setting("godot_vmf/import/detail_props_draw_distance", 100.0);
- ProjectSettings.set_initial_value("godot_vmf/import/detail_props_draw_distance", 100.0);
- ProjectSettings.set_as_basic("godot_vmf/import/detail_props_draw_distance", true);
-
-
- ## 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);
- ProjectSettings.set_as_basic("godot_vmf/models/import", true);
-
- 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://");
- ProjectSettings.set_as_basic("godot_vmf/models/target_folder", true);
-
- 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);
- ProjectSettings.set_as_basic("godot_vmf/models/lightmap_texel_size", true);
-
- ## 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);
- ProjectSettings.set_as_basic("godot_vmf/materials/import_mode", true);
-
- 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");
- ProjectSettings.set_as_basic("godot_vmf/materials/target_folder", true);
-
- 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"]);
- ProjectSettings.set_as_basic("godot_vmf/materials/ignore", true);
-
- 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", "");
- ProjectSettings.set_as_basic("godot_vmf/materials/fallback_material", true);
-
- 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.set_as_basic("godot_vmf/materials/default_texture_size", true);
-
- 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_RANGE,
- "hint_string": "0,100.0,0.000001",
- "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_RANGE,
- "hint_string": "0,100.0,0.000001",
- "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": {},
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/import/detail_props_chunk_size",
- "type": TYPE_FLOAT,
- "hint": PROPERTY_HINT_RANGE,
- "hint_string": "0,1000.0,0.000001",
- "default_value": 0.2,
- })
-
- ProjectSettings.add_property_info({
- "name": "godot_vmf/import/detail_props_draw_distance",
- "type": TYPE_FLOAT,
- "hint": PROPERTY_HINT_RANGE,
- "hint_string": "0,1000.0,0.000001",
- "default_value": 0.2,
- })
-
- ## 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_RANGE,
- "hint_string": "0,100.0,0.000001",
- "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);
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_resource_manager.gd b/addons/godotvmf/src/vmf_resource_manager.gd
index 2f09a0d..8672ccb 100644
--- a/addons/godotvmf/src/vmf_resource_manager.gd
+++ b/addons/godotvmf/src/vmf_resource_manager.gd
@@ -22,6 +22,9 @@ 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();
@@ -53,29 +56,30 @@ static func import_models(vmf_structure: VMFStructure) -> bool:
var model_path = entity.data.get("model", "").to_lower().get_basename();
if not model_path: continue;
- # Game directory paths
- var gamedir_path = VMFUtils.normalize_path(VMFConfig.gameinfo_path + "/" + model_path);
- var mdl_path = VMFUtils.normalize_path(gamedir_path + ".mdl");
- var vtx_path = VMFUtils.normalize_path(gamedir_path + ".vtx");
- var vtx_dx90_path = VMFUtils.normalize_path(gamedir_path + ".dx90.vtx");
- var vvd_path = VMFUtils.normalize_path(gamedir_path + ".vvd");
- var phy_path = VMFUtils.normalize_path(gamedir_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;
-
- vtx_path = vtx_path if FileAccess.file_exists(vtx_path) and file_exists_in_vpk(vtx_path) else vtx_dx90_path
- var found_in_game_dir := FileAccess.file_exists(mdl_path) \
- and FileAccess.file_exists(vtx_path) \
- and FileAccess.file_exists(vvd_path);
+ # 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) \
@@ -89,7 +93,7 @@ static func import_models(vmf_structure: VMFStructure) -> bool:
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");
+ if phy_path != "": DirAccess.copy_absolute(phy_path, target_path + ".phy");
DirAccess.copy_absolute(mdl_path, target_path + ".mdl");
elif found_in_vpk:
@@ -123,23 +127,32 @@ 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 vpk_path = "materials/" + material + ".vmt";
- var vmt_path = VMFUtils.normalize_path(VMFConfig.gameinfo_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;
- # Trying to find material in game directory first, as it can be overridden by mods and thus differ from the one in VPKs
- if FileAccess.file_exists(vmt_path):
+ if vmt_path:
DirAccess.make_dir_recursive_absolute(target_path.get_base_dir());
if DirAccess.copy_absolute(vmt_path, target_path): return false;
has_imported_resources = true;
-
- # Trying to find material in VPKs
+ # 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);
@@ -153,6 +166,7 @@ static func import_material(material: String) -> bool:
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();
@@ -192,12 +206,13 @@ 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");
+ ## 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 FileAccess.file_exists(target_path):
+ 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):
@@ -219,13 +234,14 @@ static func import_textures(material: String) -> bool:
for key in MATERIAL_KEYS_TO_IMPORT:
if key not in details: continue;
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 = 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 ResourceLoader.exists(target_vtf_path): continue;
# Trying to find texture in game directory first, as it can be overridden by mods and thus differ from the one in VPKs
- if FileAccess.file_exists(vtf_path):
+ 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):