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 @@ Godot Asset Library - + Godot Asset Store

## 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/). +

+ Hammer Editior - Godot Engine Comparison +

+ ### 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):