Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
REDxEYE committed Jun 16, 2020
0 parents commit 089e0b9
Show file tree
Hide file tree
Showing 4 changed files with 330 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "SourceIO"]
path = SourceIO
url = https://github.com/REDxEYE/SourceIO
1 change: 1 addition & 0 deletions SourceIO
Submodule SourceIO added at 25d882
323 changes: 323 additions & 0 deletions convert_s1_to_s2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
import os
import shlex
import sys
from pathlib import Path

import numpy as np

os.environ['NO_BPY'] = '1'

from PIL import Image, ImageOps

from SourceIO.source1.new_mdl.mdl import Mdl
from SourceIO.source1.new_vvd.vvd import Vvd
from SourceIO.source1.new_vtx.vtx import Vtx
from SourceIO.source1.vtf.VTFWrapper import VTFLib
from SourceIO.source1.vtf.VTFWrapper.VTFLibEnums import ImageFlag

from SourceIO.source2.utils.kv3_generator import KV3mdl
from SourceIO.source1.source1_to_dmx import decompile

vtf_lib = VTFLib.VTFLib()

from SourceIO.utilities.valve_utils import encode_quotes
from SourceIO.utilities import valve_utils

s2fm_addon_folder = Path(input("Source2 addon folder:"))

s1_model = Path(input("Source1 model:"))

s1_mdl = Mdl(s1_model)
s1_mdl.read()


def decompile_s1_model(mdl: Mdl, mdl_path: Path, output, gameinfo):
vvd_path = mdl_path.with_suffix(".vvd")
vtx_path = mdl_path.with_suffix(".dx90.vtx")
vvd = Vvd(vvd_path)
vtx = Vtx(vtx_path)
vvd.read()
vtx.read()
return decompile(mdl, vvd, vtx, output, gameinfo)


s1_materials = []
s1_textures = []
refl_range = 0.5

# handle S1 materials
model_name = s1_model.stem
mod_path = valve_utils.get_mod_path(s1_model)
rel_model_path = s1_model.relative_to(mod_path)
gi_path = mod_path / 'gameinfo.txt'
if not gi_path.exists():
raise FileNotFoundError("Failed to find gameinfo file")
else:
print('\033[94mFound \033[95mgameinfo.txt\033[94m file\033[0m'.format(s1_model))
gi = valve_utils.GameInfoFile(gi_path)

for material in s1_mdl.materials:
for mat_path in s1_mdl.materials_paths:
mat = gi.find_material(Path(mat_path) / material.name, True)
if mat:
s1_materials.append((material.name, mat_path, mat))
break

os.makedirs(s2fm_addon_folder / rel_model_path.with_suffix(''), exist_ok=True)

if 1:
mesh_files = decompile_s1_model(s1_mdl, s1_model, s2fm_addon_folder / rel_model_path.with_suffix(''), gi)

s2_vmodel = s2fm_addon_folder / rel_model_path.with_suffix("") / Path(model_name).with_suffix('.vmdl')
os.makedirs(s2_vmodel.parent, exist_ok=True)
vmdl = KV3mdl()
for mesh_name, mesh_path in mesh_files.items():
vmdl.add_render_mesh(mesh_name, str(mesh_path.relative_to(s2fm_addon_folder)))

for s1_bodygroup in s1_mdl.body_parts:
bodygroup = vmdl.add_bodygroup(s1_bodygroup.name)
for mesh in s1_bodygroup.models:
if len(mesh.meshes) == 0:
continue
vmdl.add_bodygroup_choice(bodygroup, mesh.name)

with s2_vmodel.open('w') as f:
f.write(vmdl.dump())

s1_to_s2_shader = {
"vertexlitgeneric": "vr_complex",
"unlitgeneric": "vr_complex",
}


def normalized_parse_line(line):
"""
Line parser that extracts key value pairs from a line and returns a list of the tokens with escaped quotes.
"""
# Fix any trailing slashes that are escaping quotes
if line.endswith('\\"'):
line = line.rsplit('\\"', 1)
line = '\\\\"'.join(line)
elif line.endswith("\\'"):
line = line.rsplit("\\'", 1)
line = "\\\\'".join(line)

lex = shlex.shlex(line, posix=True)
lex.escapedquotes = '\"\''
lex.whitespace = ' \n\t='
lex.wordchars += '|.:/\\+*%$' # Do not split on these chars
# Escape all quotes in result
tokens = [encode_quotes(token) for token in lex]
tokens[0] = tokens[0].lower()
if len(tokens) == 1:
# Key with no value gets empty string as value
tokens.append('')
elif len(tokens) > 2:
# Multiple value tokens, invalid
raise TypeError
vals = tokens[1].split(" ")
if vals and len(vals) > 1:
a = [val.isnumeric() for val in vals]
if all(a):
tokens[1] = list(map(int, vals))
return tokens


if 1: # convert S1 mat to S2 material

def load_vtf(path):
texture_path = gi.find_texture(path)
if texture_path and vtf_lib.image_load(texture_path):
texture = Image.frombytes("RGBA", (vtf_lib.width(), vtf_lib.height()),
vtf_lib.get_rgba8888().contents)
return texture
return None


def vector_type(str_vector):
if str_vector[0] == "{" and str_vector[-1] == "}":
return int
elif str_vector[0] == "[" and str_vector[-1] == "]":
return float
return None


def convert_vector(str_vector, multiplier):
vector_t = vector_type(str_vector)
str_vector = str_vector.strip("{}[]")
vector_value = map(vector_t, str_vector.split(" "))
return [int(v * multiplier) for v in vector_value]


base_map: Image.Image = None
bump_map: Image.Image = None
phong_map: Image.Image = None
phong_exp_map: Image.Image = None

env_map: Image.Image = None
illum_map: Image.Image = None
ao_map: Image.Image = None
detail_map: Image.Image = None


def write_vmat(mat_name, mat_props, mat_path):
mat_name = mat_name.replace(' ', '_').lower()
os.makedirs(s2fm_addon_folder / 'materials' / mat_path, exist_ok=True)
material_file = (s2fm_addon_folder / 'materials' / mat_path / mat_name).with_suffix('.vmat')
relative_material_file = (Path('materials') / mat_path / mat_name).with_suffix("")
print(f"Converting {mat_name}")
print(f"Saving to {material_file}\n\n")

def write_settings(filepath, props):
with open(filepath, 'w') as settings:
settings.write('"settings"\n{\n')
for key, value in props.items():
settings.write(f'\t"{key}"\t{value}\n')
settings.write(' }\n')

with material_file.open('w') as vmat_file:
vmat_file.write('// Converted with SourceIO Source1 converter\n\n')
vmat_file.write('Layer0\n{\n\tshader "' + s2_shader + '.vfx"\n\n')
vmat_file.write("F_MORPH_SUPPORTED 1\n")
if base_map is not None:
base_map.convert("RGB").save(str(material_file.with_suffix("")) + '_color.tga')
write_settings(str(material_file.with_suffix("")) + "_color.txt", {"nolod": 1})
vmat_file.write('\tTextureColor "' + str(relative_material_file) + '_color.tga' + '"\n')

if bump_map is not None:
final_bump = Image.merge("RGB", [bump_map.getchannel("R"),
ImageOps.invert(bump_map.getchannel("G")),
bump_map.getchannel("B")])
write_settings(str(material_file.with_suffix("")) + "_normal.txt", {"nolod": 1})
final_bump.save(str(material_file.with_suffix("")) + '_normal.tga')
vmat_file.write('\tTextureNormal "' + str(relative_material_file) + '_normal.tga' + '"\n')

if "$basemapalphaphongmask" in mat_props:
phong_map = bump_map.getchannel("A")
phong_map.convert("L").save(str(material_file.with_suffix("")) + '_ao.tga')
if "$phongboost" in mat_props:
ao_settings_file_name = str(material_file.with_suffix("")) + "_ao.txt"
write_settings(ao_settings_file_name, {"brightness": mat_props["$phongboost"], "nolod": 1})
vmat_file.write(f'\tg_vReflectanceRange "[0.000 {refl_range}]"\n')
vmat_file.write('\tTextureAmbientOcclusion "' + str(relative_material_file) + '_ao.tga' + '"\n')
vmat_file.write('\tg_flAmbientOcclusionDirectSpecular "1.000"\n')

if "$normalmapalphaenvmapmask" in mat_props:
env_map = bump_map.getchannel("A")
env_map.convert("L").save(str(material_file.with_suffix("")) + '_ao.tga')
write_settings(str(material_file.with_suffix("")) + "_ao.txt", {"nolod": 1})
vmat_file.write('\tTextureAmbientOcclusion "' + str(relative_material_file) + '_ao.tga' + '"\n')
vmat_file.write('\tg_flAmbientOcclusionDirectSpecular "0.000"\n')

if "$phong" in mat_props:
vmat_file.write('\tF_SPECULAR 1\n')
if phong_exp_map is not None:
phong_exp_map_flip = phong_exp_map.convert('RGB')
phong_exp_map_flip = ImageOps.invert(phong_exp_map_flip)
phong_exp_map_flip.save(str(material_file.with_suffix("")) + '_rough.tga')
write_settings(str(material_file.with_suffix("")) + "_rough.txt", {"nolod": 1})

vmat_file.write('\tTextureRoughness "' + str(relative_material_file) + '_rough.tga' + '"\n')
elif "$phongexponent" in mat_props:
spec_value = mat_props["$phongexponent"]
final_spec = (-10642.28 + (254.2042 - -10642.28) / (
1 + (float(spec_value) / 2402433000000) ** 0.1705696)) / 255
vmat_file.write('\tTextureRoughness "[{0} {0} {0} 0.000]"\n'.format(final_spec))
else:
final_spec = 60
vmat_file.write('\tTextureRoughness "[{0} {0} {0} 0.000]"\n'.format(final_spec))

if "$translucent" in mat_props or "$alphatest" in mat_props:
if "$translucent" in mat_props:
vmat_file.write('\tF_TRANSLUCENT 1\n')
elif "$alphatest" in mat_props:
vmat_file.write('\tF_ALPHA_TEST 1\n')
if "$additive" in mat_props:
vmat_file.write('\tF_ADDITIVE_BLEND 1\n')
base_map.getchannel("A").convert('L').save(str(material_file.with_suffix("")) + '_trans.tga')
vmat_file.write('\tTextureTranslucency "' + str(relative_material_file) + '_trans.tga' + '"\n')

if "$color" in mat_props:
color_vector_type = vector_type(mat_props["$color"])
if color_vector_type is int:
vmat_file.write('\tg_vColorTint {} {} {}\n'.format(*convert_vector(mat_props["$color"], 255)))
elif color_vector_type is float:
vmat_file.write('\tg_vColorTint {} {} {}\n'.format(*convert_vector(mat_props["$color"], 1)))
elif "$color2" in mat_props:
color_vector_type = vector_type(mat_props["$color"])
if "$blendtintbybasealpha" in mat_props:
mask_map_convert = base_map.getchannel('A').convert("L")
mask_map_convert.save(str(relative_material_file) + '_colormask.tga')
vmat_file.write(
'\tF_TINT_MASK 1\n\tTextureTintMask "{}_colormask.tga"\n'.format(str(relative_material_file)))
if color_vector_type is int:
vmat_file.write('\tg_vColorTint {} {} {}\n'.format(*convert_vector(mat_props["$color2"], 255)))
elif color_vector_type is float:
vmat_file.write('\tg_vColorTint {} {} {}\n'.format(*convert_vector(mat_props["$color2"], 1)))

if "$selfillum" in mat_props:
raise NotImplementedError(
"open an issue on github and attach model with material that you tried to convert")
# vmat_file.write('\tF_SELF_ILLUM 1\n')
# vmat_file.write('\tTextureSelfIllumMask "' + str(relative_material_file) + '_selfillum.tga' + '"\n')
# if "$selfillumtint" in mat_props:
# print(mat_props["$selfillumtint"])
# vmat_file.write('\tg_vSelfIllumTint ' + fixVector(mat_props["$selfillumtint"]) + '\n')
# if "$selfillummaskscale" in mat_props:
# vmat_file.write('\tg_flSelfIllumScale "' + mat_props['$selfillummaskscale'] + '"\n')

# if "$detail" in mat_props:
# # Detail textures are unique since they're almost always shared with other materials,
# # So in this case we just copy it once and then continue to process like normal
# tgaPath = modPath + "materials\\" + parseVMTPath(mat_props["$detail"]) + ".tga"
# if not os.path.exists(addFolderExtension(tgaPath)):
# try:
# copyfile(tgaPath, addFolderExtension(tgaPath))
# print("+ " + addFolderExtension(tgaPath) + " copied to target directory!")
# except:
# print("- ERROR: $detail file " + parseVMTPath(mat_props["$detail"]) + " in TGA does not exist. Skipping!")
#
# vmat_file.write('\tTextureDetail "' + 'materials/' + parseVMTPath(mat_props["$detail"]) + '.tga"\n')
# if "$detailblendmode" in mat_props:
# vmat_file.write('\tF_DETAIL_TEXTURE 2\n') # Overlay
# else:
# vmat_file.write('\tF_DETAIL_TEXTURE 1\n') # Mod2X
# if "$detailscale" in mat_props:
# vmat_file.write('\tg_vDetailTexCoordScale "[' + mat_props["$detailscale"] + ' ' + mat_props["$detailscale"] + ']"\n')
# if "$detailblendfactor" in mat_props:
# vmat_file.write('\tg_flDetailBlendFactor "' + mat_props["$detailblendfactor"] + '"\n')

vmat_file.write('}\n')


for mat_name, mat_path, s1_material in s1_materials:
print(f"Parsing {mat_name} material")
kv = valve_utils.KeyValueFile(s1_material, line_parser=normalized_parse_line)
s1_shader = kv[0].key
if s1_shader not in s1_to_s2_shader:
print(f"Skipping {mat_name}, unsupported \"{s1_shader}\" shader")
continue
s2_shader = s1_to_s2_shader[s1_shader]

# base_map: Image.Image = 1
# bump_map: Image.Image = 1
# phong_map: Image.Image = 1
# phong_exp_map: Image.Image = 1
#
# env_map: Image.Image = 1
# illum_map: Image.Image = 1
# ao_map: Image.Image = 1
# detail_map: Image.Image = 1

# collect all textures
for key, value in kv.as_dict[s1_shader].items():
# print(key, value)
if "$basetexture" in key.lower():
base_map = load_vtf(value) or base_map
if "$bumpmap" in key.lower():
bump_map = load_vtf(value) or bump_map
if "$selfillummask" in key.lower():
illum_map = load_vtf(value) or illum_map
if "$phongexponenttexture" in key.lower():
phong_exp_map = load_vtf(value) or phong_exp_map
write_vmat(mat_name, kv.as_dict[s1_shader], mat_path)
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
numpy==1.18.5
Pillow==7.1.2
scipy==1.4.1

0 comments on commit 089e0b9

Please sign in to comment.