Skip to content

Instantly share code, notes, and snippets.

@rickomax
Last active April 29, 2025 02:32
Show Gist options
  • Save rickomax/3a8d7b9f46e951422e33371832b8a205 to your computer and use it in GitHub Desktop.
Save rickomax/3a8d7b9f46e951422e33371832b8a205 to your computer and use it in GitHub Desktop.
This Python script converts FBX textures/materials to the s&box game format. Converting materials to VMAT, and extracting textures to the selected folder
#This Python script converts FBX textures/materials to the s&box game format. Converting materials to VMAT, and extracting textures to the selected folder
#This script expects all the textures to be located at the same folder as the FBX file
import fbx
import os
import shutil
import tkinter as tk
from tkinter import filedialog
# Initialize tkinter
root = tk.Tk()
root.withdraw() # Hide the main window
# Select FBX file
fbx_file = filedialog.askopenfilename(title="Select FBX File", filetypes=[("FBX files", "*.fbx")])
if not fbx_file:
print("No FBX file selected.")
exit(1)
# Select output directory for VMAT files
output_dir = filedialog.askdirectory(title="Select Output Directory for VMAT Files")
if not output_dir:
print("No output directory selected.")
exit(1)
# Initialize FBX SDK
manager = fbx.FbxManager.Create()
importer = fbx.FbxImporter.Create(manager, "")
if not importer.Initialize(fbx_file):
print("Failed to initialize FBX importer.")
exit(1)
scene = fbx.FbxScene.Create(manager, "")
if not importer.Import(scene):
print("Failed to import FBX scene.")
exit(1)
# Get the directory of the FBX file
fbx_dir = os.path.dirname(fbx_file)
# Function to copy texture to output folder with relative path
def copy_texture(texture_path, output_dir, fbx_dir):
if not texture_path:
return None
rel_texture_path = os.path.basename(texture_path)
abs_texture_path = os.path.join(fbx_dir, rel_texture_path)
output_texture_path = os.path.join(output_dir, "textures", rel_texture_path)
output_texture_dir = os.path.dirname(output_texture_path)
os.makedirs(output_texture_dir, exist_ok=True)
shutil.copy2(abs_texture_path, output_texture_path)
return rel_texture_path.replace("\\", "/")
# Process each material in the scene
material_count = scene.GetMaterialCount()
for i in range(material_count):
material = scene.GetMaterial(i)
material_name = material.GetName()
# Create VMAT file path
vmat_file_path = os.path.join(output_dir, f"materials/{material_name}.vmat")
output_material_dir = os.path.dirname(vmat_file_path)
os.makedirs(output_material_dir, exist_ok=True)
# Create and write to the .vmat file
with open(vmat_file_path, "w") as vmat_file:
vmat_file.write("// THIS FILE IS AUTO-GENERATED\n\n")
vmat_file.write("Layer0\n{\n")
vmat_file.write('\tshader "shaders/complex.shader"\n\n')
# Ambient Occlusion - using defaults
vmat_file.write('\t//---- Ambient Occlusion ----\n')
vmat_file.write('\tg_flAmbientOcclusionDirectDiffuse "0.000"\n')
vmat_file.write('\tg_flAmbientOcclusionDirectSpecular "0.000"\n')
vmat_file.write('\tTextureAmbientOcclusion "materials/default/default_ao.tga"\n')
# Color - map diffuse color and texture
diffuse_color = material.Diffuse.Get()
color_str = "[%.6f %.6f %.6f 1.000000]" % (diffuse_color[0], diffuse_color[1], diffuse_color[2])
vmat_file.write('\n\t//---- Color ----\n')
vmat_file.write('\tg_flModelTintAmount "1.000"\n')
vmat_file.write(f'\tg_vColorTint "{color_str}"\n')
diffuse_texture = material.Diffuse.GetSrcObject(0)
if diffuse_texture:
texture_path = diffuse_texture.GetRelativeFileName()
rel_texture_path = copy_texture(texture_path, output_dir, fbx_dir)
if rel_texture_path:
vmat_file.write(f'\tTextureColor "textures/{rel_texture_path}"\n')
else:
vmat_file.write('\tTextureColor "materials/default/default_color.tga"\n')
else:
vmat_file.write('\tTextureColor "materials/default/default_color.tga"\n')
# Fade - using default
vmat_file.write('\n\t//---- Fade ----\n')
vmat_file.write('\tg_flFadeExponent "1.000"\n')
# Fog - using default
vmat_file.write('\n\t//---- Fog ----\n')
vmat_file.write('\tg_bFogEnabled "1"\n')
# Metalness - using default
vmat_file.write('\n\t//---- Metalness ----\n')
vmat_file.write('\tg_flMetalness "0.000"\n')
# Normal - map bump/normal map texture
bump_prop = material.FindProperty("Bump")
if bump_prop.IsValid():
normal_texture = bump_prop.GetSrcObject(0)
if normal_texture:
texture_path = normal_texture.GetRelativeFileName()
rel_texture_path = copy_texture(texture_path, output_dir, fbx_dir)
if rel_texture_path:
vmat_file.write('\n\t//---- Normal ----\n')
vmat_file.write(f'\tTextureNormal "textures/{rel_texture_path}"\n')
else:
vmat_file.write('\n\t//---- Normal ----\n')
vmat_file.write('\tTextureNormal "materials/default/default_normal.tga"\n')
else:
vmat_file.write('\n\t//---- Normal ----\n')
vmat_file.write('\tTextureNormal "materials/default/default_normal.tga"\n')
else:
vmat_file.write('\n\t//---- Normal ----\n')
vmat_file.write('\tTextureNormal "materials/default/default_normal.tga"\n')
# Roughness - compute from shininess, use default texture
shininess_prop = material.FindProperty("Shininess")
if shininess_prop.IsValid():
shininess_prop = fbx.FbxPropertyDouble1( shininess_prop )
shininess = shininess_prop.Get()
roughness = 1.0 - min(shininess / 100.0, 1.0)
else:
roughness = 1.0 # Default to maximum roughness
vmat_file.write('\n\t//---- Roughness ----\n')
vmat_file.write(f'\tg_flRoughnessScaleFactor "{roughness:.3f}"\n')
vmat_file.write('\tTextureRoughness "materials/default/default_rough.tga"\n')
# Texture Coordinates - using defaults
vmat_file.write('\n\t//---- Texture Coordinates ----\n')
vmat_file.write('\tg_nScaleTexCoordUByModelScaleAxis "0"\n')
vmat_file.write('\tg_nScaleTexCoordVByModelScaleAxis "0"\n')
vmat_file.write('\tg_vTexCoordOffset "[0.000 0.000]"\n')
vmat_file.write('\tg_vTexCoordScale "[1.000 1.000]"\n')
vmat_file.write('\tg_vTexCoordScrollSpeed "[0.000 0.000]"\n')
vmat_file.write("}\n")
# Clean up FBX SDK resources
manager.Destroy()
print("VMAT files generated successfully.")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment