Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3d17c6a
VMF: Add detail props support
H2xDev Jan 30, 2026
730190c
VMT: Fix blend materials creation path
H2xDev Jan 30, 2026
8685acd
Bump 2.3.0
H2xDev Jan 30, 2026
709ebae
VMF: Add toolsblocklight support
H2xDev Feb 4, 2026
f30121a
VTF: Add support RGBA, ABGR, I, IA formats
H2xDev Feb 18, 2026
749db0c
Config: Make all project settings basic & fix rounding issues in proj…
Withaust Feb 21, 2026
47a0443
VPK: Add resource extraction from VPKs
H2xDev Feb 22, 2026
435f40c
Detail props: Add option to disable cast shadows
H2xDev Feb 23, 2026
d1c262e
VMT: Add support for .res in loaded materials (#82)
asraeldragon Feb 24, 2026
39b5dd6
VDF: Optimize prop parsing
H2xDev Mar 14, 2026
a9b8d53
MDL: Put all collision shapes into one static body (for static models)
H2xDev Mar 14, 2026
49acd02
VMF: Optimize vmf_vector_sorter's longer method (#88)
Withaust Apr 3, 2026
76b8f6f
VMF: Update detail props generator
H2xDev Apr 7, 2026
1fd7d8a
prop_static: Place all static props into StaticProps node
H2xDev Apr 7, 2026
a4100aa
VMFConfig: Add default values for detail props config
H2xDev Apr 7, 2026
e1a68b3
VMT: Update WorldVertexTransitionMaterial import logic
H2xDev May 1, 2026
b105fdc
VMF: Cleanup material's meta processing logic
H2xDev May 1, 2026
3e309e8
MDL: Fix VTX import path
H2xDev May 1, 2026
9d66317
VMF: Replace labels with icons and add detail props reimport option
H2xDev May 1, 2026
2e4d79b
MDL: Add option to generate collision from the mesh itself
H2xDev May 2, 2026
97b0335
Remove *.uid from .gitignore (solves #90)
H2xDev May 2, 2026
45b90f4
VMF: Optimize merged faces cleanup
H2xDev May 2, 2026
c15e045
Update readme.md
H2xDev May 5, 2026
9964365
Enhance README with images and feature updates
H2xDev Jun 21, 2026
001ce48
Include LICENSE into the release archive (#102)
H2xDev Jun 22, 2026
f4f7ff2
VMT: Add $normalmap parse support (#101)
H2xDev Jun 22, 2026
d6ae60f
Import: Add multiple game import paths support (#100)
H2xDev Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
/addons/** !export-ignore
/script_templates !export-ignore
/script_templates/** !export-ignore
LICENSE !export-ignore
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,3 @@
!script_templates/
!addons/**/*
!script_templates/**/*

addons/**/*.uid
1 change: 1 addition & 0 deletions addons/godotfgd/main.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://bf3qmyehwiuea
1 change: 1 addition & 0 deletions addons/godotvmf/entities/func_detail.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://d2dc4sqf30jtj
1 change: 1 addition & 0 deletions addons/godotvmf/entities/func_instance.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://e2t7hchk4u82
1 change: 1 addition & 0 deletions addons/godotvmf/entities/light.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://c4wae4v6rpk72
1 change: 1 addition & 0 deletions addons/godotvmf/entities/light_environment.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://c6ylaipt6p030
1 change: 1 addition & 0 deletions addons/godotvmf/entities/light_spot.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://bqjn8xm2aihuw
1 change: 1 addition & 0 deletions addons/godotvmf/entities/prop_static.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://nswhg1njw5xv
3 changes: 2 additions & 1 deletion addons/godotvmf/entities/prop_studio.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions addons/godotvmf/entities/prop_studio.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://bpw635t676c6s
1 change: 1 addition & 0 deletions addons/godotvmf/godotmdl/ani_reader.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://c0nnkq7jx5l8k
12 changes: 12 additions & 0 deletions addons/godotvmf/godotmdl/import.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions addons/godotvmf/godotmdl/import.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://bk28rdclb504g
38 changes: 32 additions & 6 deletions addons/godotvmf/godotmdl/mdl_combiner.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down
1 change: 1 addition & 0 deletions addons/godotvmf/godotmdl/mdl_combiner.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://bd4ctjcovsf7w
1 change: 1 addition & 0 deletions addons/godotvmf/godotmdl/mdl_reader.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://sj5ua4ctykyh
1 change: 1 addition & 0 deletions addons/godotvmf/godotmdl/mesh_generator.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://bfbefl04wexpp
1 change: 1 addition & 0 deletions addons/godotvmf/godotmdl/phy_reader.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://tbul0tcs2ot
10 changes: 7 additions & 3 deletions addons/godotvmf/godotmdl/reader.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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());

Expand Down
1 change: 1 addition & 0 deletions addons/godotvmf/godotmdl/reader.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://b8qxk8wgly1f4
1 change: 1 addition & 0 deletions addons/godotvmf/godotmdl/struct.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://begr2w44t20kb
1 change: 1 addition & 0 deletions addons/godotvmf/godotmdl/vtx_reader.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://c5n88vi41i5tu
1 change: 1 addition & 0 deletions addons/godotvmf/godotmdl/vvd_reader.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://daydea6sm0c2d
1 change: 1 addition & 0 deletions addons/godotvmf/godotvdf/vdf_parser.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://dljwvdlrqbe7s
13 changes: 12 additions & 1 deletion addons/godotvmf/godotvmf.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -90,14 +91,24 @@ 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

for node in nodes:
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();

Expand Down
1 change: 1 addition & 0 deletions addons/godotvmf/godotvmf.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://dlvt67aflbr5e
33 changes: 33 additions & 0 deletions addons/godotvmf/godotvmt/formats/vtf_dxt.gd
Original file line number Diff line number Diff line change
@@ -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);
1 change: 1 addition & 0 deletions addons/godotvmf/godotvmt/formats/vtf_dxt.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://dphnyrf0rcafa
6 changes: 6 additions & 0 deletions addons/godotvmf/godotvmt/formats/vtf_frame_reader.gd
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions addons/godotvmf/godotvmt/formats/vtf_frame_reader.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://b5ihhe3066q25
27 changes: 27 additions & 0 deletions addons/godotvmf/godotvmt/formats/vtf_i8.gd
Original file line number Diff line number Diff line change
@@ -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);
1 change: 1 addition & 0 deletions addons/godotvmf/godotvmt/formats/vtf_i8.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://dd3xyrn3lduxg
36 changes: 36 additions & 0 deletions addons/godotvmf/godotvmt/formats/vtf_rgba.gd
Original file line number Diff line number Diff line change
@@ -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);
1 change: 1 addition & 0 deletions addons/godotvmf/godotvmt/formats/vtf_rgba.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://bolwkc1500o7x
1 change: 1 addition & 0 deletions addons/godotvmf/godotvmt/vmt_context_menu.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://d4en2bmq1eoyb
1 change: 1 addition & 0 deletions addons/godotvmf/godotvmt/vmt_import.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://b5c8p8i51m8mr
Loading