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

+ Hammer Editior - Godot Engine Comparison +

### Features -- Brushes geometry import (including UVs, materials IDs and smoothing groups) +- VMF geometry import - 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 +- 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 - ## 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 :) @@ -55,12 +62,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 -- Extraction of materials and models from VPKs is not supported -- 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. @@ -70,13 +76,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 @@ -89,6 +88,8 @@ 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) +[Asrael](https://github.com/asraeldragon) ## License MIT