Skip to content

Commit 02871e2

Browse files
Patch/v2.2.6 (#72)
* VMT: Fix shader material processing * func_detail: Add lightmap unwrapping * prop_static: Set GI mode to static * light: Update ranges calculation * Instances: Remove visibility range parameter definition * prop_static: Move skins applying logic to prop_studio.gd * Bump v2.2.6 * VMT: Add roughnessfactor2 support for blend materials * VMT: Fix worldvertextransition material import * VMF: Fix micro-seams during import * prop_studio: Fix skin property * VMT: Update transformer class * VTF: Extend extensions to import in case VTF is not exist * Entities: Update group assignment approach * VMFEntityNode: Update output trigger processing * VMT: Add material shading changing depending on VMT's shader * VMT: Add mipmap support for VTF conversion * VMF: Add option to enable double-sided shadow casting * QoL: Add option to create blend materials * VMT: Assign shading_mode only for StandardMaterial3D --------- Co-authored-by: catperson6real-dev <[email protected]>
1 parent 34c38bb commit 02871e2

22 files changed

+360
-154
lines changed

addons/godotvmf/entities/func_detail.gd

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ class_name func_detail extends VMFEntityNode
33

44
func _entity_setup(entity: VMFEntity):
55
var mesh = get_mesh();
6+
mesh.lightmap_unwrap(global_transform, config.import.lightmap_texel_size);
7+
68
$MeshInstance3D.cast_shadow = entity.data.get("disableshadows", 0) == 0;
79

810
if !mesh or mesh.get_surface_count() == 0:
911
queue_free();
1012
return;
1113

12-
$MeshInstance3D.set_mesh(get_mesh());
14+
$MeshInstance3D.set_mesh(mesh);
1315
$MeshInstance3D/StaticBody3D/CollisionShape3D.shape = $MeshInstance3D.mesh.create_trimesh_shape();

addons/godotvmf/entities/light.gd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func _entity_setup(entity: VMFEntity) -> void:
111111
var omni_light: OmniLight3D = light; # To avoid further warnings
112112

113113
# TODO: implement constant linear quadratic calculation the right way
114-
var radius := (1 / config.import.scale) * sqrt(light.light_energy);
114+
var radius := (1 / config.import.scale) * sqrt(light.light_energy * config.import.scale);
115115
var attenuation := 1.44;
116116

117117
var fifty_percent_distance: float = entity_data._fifty_percent_distance;

addons/godotvmf/entities/light_spot.gd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@ func _entity_setup(entity: VMFEntity) -> void:
55
super._entity_setup(entity);
66
var entity_data := v_light.LightType.new(entity);
77
var spot_light := light as SpotLight3D;
8+
var radius := (1 / config.import.scale) * sqrt(light.light_energy * config.import.scale);
9+
var attenuation := 1.44;
810

911
spot_light.spot_angle = entity_data._cone;
1012
spot_light.light_energy = entity_data._light.a;
13+
14+
spot_light.spot_range = radius;
15+
spot_light.spot_angle_attenuation = attenuation;
16+
spot_light.spot_range = entity.data.get("distance", spot_light.spot_range / config.import.scale) * config.import.scale;
17+
1118
default_light_energy = light.light_energy;
1219
entity.angles.z = entity_data.pitch;
1320
entity.angles.x = 0;

addons/godotvmf/entities/prop_static.gd

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
class_name prop_static
33
extends prop_studio
44

5-
var skin: int = 0:
6-
get: return entity.get('skin', 0)
7-
85
var screen_space_fade: bool = false:
96
get: return entity.get('screenspacefade', 0) == 1
107

@@ -23,7 +20,7 @@ func _entity_setup(e: VMFEntity):
2320

2421
model_instance.set_owner(get_owner());
2522
model_instance.scale *= model_scale;
26-
MDLCombiner.apply_skin(model_instance, skin);
23+
model_instance.gi_mode = GeometryInstance3D.GI_MODE_STATIC;
2724

2825
var fade_margin = fade_max - fade_min;
2926

addons/godotvmf/entities/prop_studio.gd

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ var model_name: String = "":
1515
var model_scale: float = 1.0:
1616
get: return entity.get("modelscale", 1.0);
1717

18+
var skin: int = 0:
19+
get: return entity.get('skin', 0)
20+
1821
func _entity_setup(_entity: VMFEntity) -> void:
1922
var model_path = VMFUtils.normalize_path(VMFConfig.models.target_folder + "/" + model);
2023
var model_scene: PackedScene = VMFCache.get_cached(model);
@@ -37,5 +40,7 @@ func _entity_setup(_entity: VMFEntity) -> void:
3740
instance.name = "model";
3841
instance.scale *= model_scale;
3942

43+
MDLCombiner.apply_skin(instance, skin);
44+
4045
add_child(instance);
4146
model_instance.set_owner(get_owner());

addons/godotvmf/godotvmt/vmt_loader.gd

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ static func load(path: String):
3737
var shader_name = structure.keys()[0];
3838
var details = structure[shader_name];
3939
var material = null;
40-
var is_blend_texture = shader_name == "worldvertextransition";
41-
40+
var is_blend_texture = shader_name.trim_suffix(" ") == "worldvertextransition";
41+
4242
# NOTE: CS:GO/L4D
4343
if "insert" in details:
4444
details.merge(details["insert"]);
@@ -48,15 +48,27 @@ static func load(path: String):
4848

4949
if "$shader" in details:
5050
var extension = ".gdshader" if not details["$shader"].get_extension() else "";
51-
var shader_path = "res://" + details["$shader"] + extension;
52-
material = VMTShaderBasedMaterial.load(shader_path);
51+
var shader_path = "res://" + details["$shader"].replace("res://", "") + extension;
52+
53+
if ResourceLoader.exists(shader_path):
54+
material = ShaderMaterial.new();
55+
material.shader = ResourceLoader.load(shader_path);
56+
else:
57+
VMFLogger.warn("Shader %s doesn't exists for %s" % [shader_path, path]);
5358
else:
5459
material = StandardMaterial3D.new() if not is_blend_texture else WorldVertexTransitionMaterial.new();
5560

61+
5662
var transformer = VMTTransformer.new();
5763
var extend_transformer = Engine.get_main_loop().root.get_node_or_null("VMTExtend");
5864
var uniforms: Array = material.shader.get_shader_uniform_list() if material is ShaderMaterial else [];
5965

66+
if material is StandardMaterial3D:
67+
if shader_name == "unlitgeneric":
68+
material.shading_mode = 0
69+
elif shader_name == "vertexlitgeneric":
70+
material.shading_mode = 2
71+
6072
for key in details.keys():
6173
var value = details[key];
6274
var is_compile_key = key.begins_with("%");
@@ -67,12 +79,17 @@ static func load(path: String):
6779
compile_keys.append(key);
6880
material.set_meta("compile_keys", compile_keys);
6981

70-
if material is ShaderMaterial:
82+
if material is ShaderMaterial && not is_blend_texture:
7183
var mat: ShaderMaterial = material;
72-
if uniforms.find(key) > -1:
73-
var is_texture = value.has("/");
74-
mat.set_shader_parameter(key, VTFLoader.get_texture(value) if is_texture else value);
75-
continue;
84+
var uniform_index = uniforms.find_custom(func(field): return field.name == key);
85+
if uniform_index == -1: continue;
86+
87+
var is_texture = uniforms[uniform_index].hint_string == "Texture2D";
88+
var is_boolean = uniforms[uniform_index].type == TYPE_BOOL;
89+
90+
value = value if not is_boolean else value == "true"
91+
mat.set_shader_parameter(key, VTFLoader.get_texture(value) if is_texture else value);
92+
continue;
7693

7794
if extend_transformer and key in extend_transformer:
7895
extend_transformer[key].call(material, value);

addons/godotvmf/godotvmt/vmt_material_conversion_context_menu.gd

Lines changed: 155 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,100 @@
11
class_name VMFMaterialConversionContextMenu extends EditorContextMenuPlugin
22

3+
const VMT_TEMPLATE := ("\"LightmappedGeneric\" {\n"\
4+
+ "\t$basetexture \"%s\" \n" \
5+
+ "}");
6+
7+
const VMT_BLEND_TEMPLATE := ("\"WorldVertexTransition\" {\n"\
8+
+ "\t$basetexture \"%s\" \n" \
9+
+ "\t$bumpmap \"%s\" \n" \
10+
+ "\t$roughnesstexture \"%s\" \n" \
11+
+ "\t$ambientocclusiontexture \"%s\" \n" \
12+
13+
+ "\t$basetexture2 \"%s\" \n" \
14+
+ "\t$bumpmap2 \"%s\" \n" \
15+
+ "\t$roughnesstexture2 \"%s\" \n" \
16+
+ "\t$ambientocclusiontexture2 \"%s\" \n" \
17+
+ "}");
18+
319
func is_resource(p: String) -> bool:
420
return p.ends_with(".tres");
521

22+
func is_texture(p: String) -> bool:
23+
return ['png', 'jpg', 'tga'].has(p.get_extension());
24+
25+
func is_vmt(p: String) -> bool:
26+
return p.get_extension() == 'vmt';
27+
628
func _popup_menu(paths: PackedStringArray):
729
var has_resources = Array(paths).filter(is_resource).size() > 0;
8-
if not has_resources: return;
30+
if has_resources: add_context_menu_item("Convert to VMT", convert_resource_to_vmt);
31+
32+
var has_textures = Array(paths).filter(is_texture).size() > 0;
33+
if has_textures: add_context_menu_item("Create VMT materials", create_vmts_from_textures);
34+
35+
var able_to_blend = Array(paths).filter(is_vmt).size() > 1;
36+
if able_to_blend: add_context_menu_item("Create VMT Blend Material", create_blend_material);
37+
38+
func create_blend_material(paths: PackedStringArray):
39+
var vmts = Array(paths).filter(is_vmt);
40+
var vmt1 = ResourceLoader.load(vmts[0]);
41+
var vmt2 = ResourceLoader.load(vmts[1]);
42+
43+
var blend_file_name = vmts[0].get_basename().get_file() + '_' + vmts[1].get_basename().get_file() + '_blend.vmt';
44+
var path1 = (vmts[0] as String).get_base_dir()
45+
var path2 = (vmts[1] as String).get_base_dir()
46+
47+
var save_path = path1.get_base_dir() + '/' + blend_file_name;
48+
print("Saving blended VMT to: " + save_path);
49+
50+
var file := FileAccess.open(save_path, FileAccess.WRITE);
51+
52+
var details_1 = vmt1.get_meta("details", {});
53+
var details_2 = vmt2.get_meta("details", {});
54+
55+
var basetexture = details_1.get("$basetexture", "");
56+
var bumpmap = details_1.get("$bumpmap", "");
57+
var roughnesstexture = details_1.get("$roughnesstexture", "");
58+
var ambientocclusiontexture = details_1.get("$ambientocclusiontexture", "");
59+
var basetexture2 = details_2.get("$basetexture", "");
60+
var bumpmap2 = details_2.get("$bumpmap", "");
61+
var roughnesstexture2 = details_2.get("$roughnesstexture","");
62+
var ambientocclusiontexture2 = details_2.get("$ambientocclusiontexture", "");
63+
64+
#file.store_string(VMT_BLEND_TEMPLATE % [basetexture, basetexture2]);
65+
file.store_string(VMT_BLEND_TEMPLATE % [
66+
basetexture,
67+
bumpmap,
68+
roughnesstexture,
69+
ambientocclusiontexture,
70+
basetexture2,
71+
bumpmap2,
72+
roughnesstexture2,
73+
ambientocclusiontexture2
74+
]);
75+
file.close();
76+
EditorInterface.get_resource_filesystem().scan();
977

10-
add_context_menu_item("Convert to VMT", convert_resource_to_vmt);
78+
func create_vmts_from_textures(paths: PackedStringArray):
79+
var only_textures := Array(paths).filter(func(path: String):
80+
return ['png', 'jpg', 'tga'].has(path.get_extension())
81+
);
82+
83+
for path in only_textures:
84+
var texture: Texture = ResourceLoader.load(path);
85+
var basetexture := path.replace(VMFConfig.materials.target_folder, '').substr(1).replace('.' + path.get_extension(), '') as String;
86+
var vmt_path := VMFUtils.normalize_path(VMFConfig.materials.target_folder + '/' + basetexture + '.vmt');
87+
88+
var file := FileAccess.open(vmt_path, FileAccess.WRITE);
89+
file.store_string(VMT_TEMPLATE % basetexture);
90+
file.close();
91+
92+
var bytes = generate_vtf_file(texture);
93+
var vtf_file = FileAccess.open(vmt_path.replace('.vmt', '.vtf'), FileAccess.WRITE);
94+
vtf_file.store_buffer(bytes);
95+
vtf_file.close();
96+
97+
EditorInterface.get_resource_filesystem().scan();
1198

1299
func convert_resource_to_vmt(paths: PackedStringArray):
13100
var resources := Array(paths).filter(is_resource);
@@ -20,7 +107,7 @@ func convert_resource_to_vmt(paths: PackedStringArray):
20107
continue;
21108

22109
create_vmt_file(resource);
23-
110+
24111
EditorInterface.get_resource_filesystem().scan();
25112

26113
func create_vmt_file(material: BaseMaterial3D):
@@ -37,12 +124,8 @@ func create_vmt_file(material: BaseMaterial3D):
37124
if base_texture_path.begins_with("/"):
38125
base_texture_path = base_texture_path.substr(1, base_texture_path.length());
39126

40-
var vmt_string := ("\"LightmappedGeneric\" {\n"\
41-
+ "\t\"$basetexture\" \"%s\" \n" \
42-
+ "}") % base_texture_path;
43-
44127
var file := FileAccess.open(vmt_path, FileAccess.WRITE);
45-
file.store_string(vmt_string);
128+
file.store_string(VMT_TEMPLATE % base_texture_path);
46129
file.close();
47130

48131
if ResourceLoader.exists(vtf_path):
@@ -92,26 +175,36 @@ func generate_vtf_file(texture: Texture2D) -> PackedByteArray:
92175
var aspect_ratio := float(image.get_width()) / float(image.get_height());
93176
var bytes := PackedByteArray();
94177

95-
var is_dxt: bool = image.get_format() == Image.FORMAT_DXT1 or image.get_format() == Image.FORMAT_DXT5 or image.get_format() == Image.FORMAT_DXT3;
178+
var is_dxt: bool = image.get_format() == Image.FORMAT_DXT1 \
179+
or image.get_format() == Image.FORMAT_DXT5 \
180+
or image.get_format() == Image.FORMAT_DXT3;
181+
182+
if not image.is_compressed():
183+
VMFLogger.warn("The albedo texture is not VRAM Compressed: %s" % texture.resource_path);
96184

97185
if not is_dxt:
98186
image.decompress();
99187
image.compress(Image.COMPRESS_S3TC);
100188
image.convert(Image.FORMAT_DXT5);
101189

102-
image.clear_mipmaps();
103-
104190
bytes += "VTF".to_utf8_buffer(); # signature
105191
bytes.append(0);
106192

107193
bytes += int_to_int32(7); # version major
108-
bytes += int_to_int32(1); # version minor
109-
bytes += int_to_int32(64); # header size for version 7.1
194+
bytes += int_to_int32(4); # version minor (7.4 is standard for Source)
195+
bytes += int_to_int32(80); # header size for version 7.4
110196

111197
bytes += int_to_short(image.get_width()); # width
112198
bytes += int_to_short(image.get_height()); # height
113199

114-
bytes += int_to_int32(VTFLoader.Flags.TEXTUREFLAGS_NOMIP | VTFLoader.Flags.TEXTUREFLAGS_SRGB);
200+
var flags: int = VTFLoader.Flags.TEXTUREFLAGS_SRGB | VTFLoader.Flags.TEXTUREFLAGS_NOMIP;
201+
var mip_levels := 1;
202+
203+
if image.has_mipmaps():
204+
flags &= ~VTFLoader.Flags.TEXTUREFLAGS_NOMIP;
205+
mip_levels = image.get_mipmap_count() + 1;
206+
207+
bytes += int_to_int32(flags);
115208
bytes += int_to_short(1); # frames
116209
bytes += int_to_short(0); # first frame
117210
bytes += PackedByteArray([0, 0, 0, 0]); # padding
@@ -128,12 +221,15 @@ func generate_vtf_file(texture: Texture2D) -> PackedByteArray:
128221
else 15;
129222

130223
bytes += int_to_int32(format); # high res image format
131-
bytes += int_to_byte(1) # mipmap count
224+
bytes += int_to_byte(mip_levels) # mipmap count
132225
bytes += int_to_int32(13) # low res image format
133226

134227
var lowres_image := image.duplicate() as Image;
135228
var lowres_width := 16;
136-
var lowres_height := int(16 / aspect_ratio);
229+
var lowres_height := max(1, int(16 / aspect_ratio));
230+
231+
# Ensure lowres dimensions are valid
232+
lowres_height = max(1, min(16, lowres_height));
137233

138234
lowres_image.decompress();
139235
lowres_image.resize(lowres_width, lowres_height, Image.INTERPOLATE_BILINEAR);
@@ -144,9 +240,48 @@ func generate_vtf_file(texture: Texture2D) -> PackedByteArray:
144240

145241
bytes += int_to_byte(lowres_width); # low res image width
146242
bytes += int_to_byte(lowres_height); # low res image height
147-
bytes += PackedByteArray([0]); # padding
148-
149-
bytes += lowres_image.get_data(); # low res image data
150-
bytes += image.get_data()
243+
bytes += int_to_byte(1); # depth
244+
245+
# Padding 2 (3 bytes)
246+
bytes += PackedByteArray([0, 0, 0]);
247+
248+
# Resource Count (4 bytes)
249+
bytes += int_to_int32(0);
250+
251+
# Pad to 80 bytes header
252+
while bytes.size() < 80:
253+
bytes.append(0);
254+
255+
# Ensure we only write the first mip level of the lowres image
256+
var lowres_data = lowres_image.get_data();
257+
var lowres_size = int(lowres_width * lowres_height * 0.5); # DXT1 is 4 bits/pixel (0.5 bytes)
258+
if lowres_data.size() > lowres_size:
259+
lowres_data = lowres_data.slice(0, lowres_size);
260+
261+
bytes += lowres_data; # low res image data
262+
263+
# VTF stores mipmaps smallest to largest (loader reads from end backwards)
264+
# Godot stores mipmaps largest to smallest
265+
# We need to reverse the order
266+
var image_data = image.get_data();
267+
var mip_data = []
268+
var offset := 0;
269+
270+
# First, extract each mipmap from Godot's data (largest to smallest)
271+
for mip_level in range(mip_levels):
272+
var block_size := 16 if format in [14, 15] else 8;
273+
var mip_width := max(1, image.get_width() >> mip_level);
274+
var mip_height := max(1, image.get_height() >> mip_level);
275+
var num_blocks_x := max(1, (mip_width + 3) / 4);
276+
var num_blocks_y := max(1, (mip_height + 3) / 4);
277+
var mip_size := num_blocks_x * num_blocks_y * block_size as int;
278+
279+
mip_data.append(image_data.slice(offset, offset + mip_size));
280+
offset += mip_size;
281+
282+
# Now write them in reverse order (smallest to largest for VTF)
283+
mip_data.reverse();
284+
for data in mip_data:
285+
bytes += data;
151286

152287
return bytes;

0 commit comments

Comments
 (0)