Compare commits

...

13 Commits

Author SHA1 Message Date
gsemaj
3aabb35f33 Add terrain mesh extractor 2023-07-13 16:18:09 -04:00
gsemaj
7b750db9f9 Update README 2022-08-11 15:56:07 -04:00
gsemaj
aa564926a0 Fix CameraPos translation 2022-08-11 15:46:58 -04:00
gsemaj
2496f04987 Add semantic to f2a struct 2022-08-11 13:21:33 -04:00
gsemaj
916857edc3 Unity 3 fixes 2022-08-11 12:20:51 -04:00
gsemaj
3791e889c8 Add support for multiple subprograms 2022-08-11 11:53:38 -04:00
gsemaj
d0e67d55c9 Standalone support for basic fragment shaders 2022-08-11 04:06:03 -04:00
gsemaj
869d5b1976 Add support for SetTexture 2022-08-10 21:03:56 -04:00
gsemaj
8725dd1e4e Entry points and arg fix 2022-08-10 21:01:51 -04:00
gsemaj
8fbe59e5a1 Fix rcp bug 2022-08-10 20:09:59 -04:00
gsemaj
a53fb21621 Add .gitignore 2022-08-10 20:09:22 -04:00
gsemaj
9dd5db86eb Update README 2022-08-10 20:06:02 -04:00
gsemaj
4ace5f065f Add dx2cg 2022-08-10 19:47:13 -04:00
7 changed files with 580 additions and 0 deletions

2
dx2cg/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
__pycache__

11
dx2cg/README.md Normal file
View File

@@ -0,0 +1,11 @@
# dx2cg
Tools for converting d3d9 shader assembly to HLSL/Cg.
- `disassembler.py`: Takes in d3d9 assembly and gives back the HLSL equivalent.
- `swapper.py`: Searches a shader file for d3d9 assembly and calls the disassembler to replace it with HLSL.
- `main.py`: Executes the swapper on every file in a path, writing the changes to new files.
## Known issues
- Only vertex shaders with profile `vs_1_1` are supported
- Only fragment shaders with profile `ps_2_0` are supported
- Only a limited set of instructions (those used by FF and Unity 2.6) are supported
- Properties that don't begin with an underscore do not get captured as locals

360
dx2cg/disassembler.py Normal file
View File

@@ -0,0 +1,360 @@
#!/usr/bin/env python
# coding: utf-8
# d3d9 to cg crude dissassembler
# ycc 08/08/2022
import re
import sys
legacy = False # True for 2.6
reserved = {
"_Time",
"_SinTime",
"_CosTime",
"_ProjectionParams",
"_PPLAmbient",
"_ObjectSpaceCameraPos",
"_ObjectSpaceLightPos0",
"_ModelLightColor0",
"_SpecularLightColor0",
"_Light2World0", "_World2Light0", "_Object2World", "_World2Object", "_Object2Light0",
"_LightDirectionBias",
"_LightPositionRange",
}
decls = {
"dcl_position": "float4 {0} = vdat.vertex;",
"dcl_normal": "float4 {0} = float4(vdat.normal, 0);",
"dcl_texcoord0": "float4 {0} = vdat.texcoord;",
"dcl_texcoord1": "float4 {0} = vdat.texcoord1;",
"dcl_color": "float4 {0} = vdat.color;",
"dcl_2d": "; // no operation",
"dcl": "float4 {0[0]}{0[1]} = pdat.{0[0]}{0[1]};",
"def": "const float4 {0} = float4({1}, {2}, {3}, {4});",
}
ops = {
"mov": "{0} = {1};",
"add": "{0} = {1} + {2};",
"mul": "{0} = {1} * {2};",
"mad": "{0} = {1} * {2} + {3};",
"dp4": "{0} = dot((float4){1}, (float4){2});",
"dp3": "{0} = dot((float3){1}, (float3){2});",
"min": "{0} = min({1}, {2});",
"max": "{0} = max({1}, {2});",
"rsq": "{0} = rsqrt({1});",
"frc": "{0} = float4({1}.x - (float)floor({1}.x), {1}.y - (float)floor({1}.y), {1}.z - (float)floor({1}.z), {1}.w - (float)floor({1}.w));",
"slt": "{0} = float4(({1}.x < {2}.x) ? 1.0f : 0.0f, ({1}.y < {2}.y) ? 1.0f : 0.0f, ({1}.z < {2}.z) ? 1.0f : 0.0f, ({1}.w < {2}.w) ? 1.0f : 0.0f);",
"sge": "{0} = float4(({1}.x >= {2}.x) ? 1.0f : 0.0f, ({1}.y >= {2}.y) ? 1.0f : 0.0f, ({1}.z >= {2}.z) ? 1.0f : 0.0f, ({1}.w >= {2}.w) ? 1.0f : 0.0f);",
"rcp": "{0} = ({1} == 0.0f) ? FLT_MAX : (({1} == 1.0f) ? {1} : (1 / {1}));",
"texld": "{0} = tex2D({2}, (float2){1});",
}
struct_a2v = """struct a2v {
\tfloat4 vertex : POSITION;
\tfloat3 normal : NORMAL;
\tfloat4 texcoord : TEXCOORD0;
\tfloat4 texcoord1 : TEXCOORD1;
\tfloat4 tangent : TANGENT;
\tfloat4 color : COLOR;
};
"""
v2f_postype = "POSITION" if legacy else "SV_POSITION"
struct_v2f = f"""struct v2f {{
\tfloat4 pos : {v2f_postype};
\tfloat4 t0 : TEXCOORD0;
\tfloat4 t1 : TEXCOORD1;
\tfloat4 t2 : TEXCOORD2;
\tfloat4 t3 : TEXCOORD3;
\tfloat fog : FOG;
\tfloat4 d0 : COLOR0;
\tfloat4 d1 : COLOR1;
}};
"""
struct_f2a = """struct f2a {
\tfloat4 c0 : COLOR0;
};
"""
cg_header = """CGPROGRAM
#include "UnityCG.cginc"
#pragma exclude_renderers xbox360 ps3 gles
"""
cg_footer = """ENDCG"""
vertex_func = """v2f vert(a2v vdat) {{
\tfloat4 r0, r1, r2, r3, r4;
\tfloat4 tmp;
\tv2f o;
{0}
\treturn o;
}}
"""
fragment_func = """f2a frag(v2f pdat) {{
\tfloat4 r0, r1, r2, r3, r4;
\tfloat4 tmp;
\tf2a o;
{0}
\treturn o;
}}
"""
def process_header(prog):
keywords = []
loctab = {}
locdecl = []
binds = []
i = 0
lighting = False
textures = 0
while i < len(prog):
line = prog[i]
if line.startswith("Keywords"):
keywords = re.findall("\"[\w\d]+\"", line)
del prog[i]
i = i - 1
elif line.startswith("Bind"):
binds.append(line)
del prog[i]
i = i - 1
elif line.startswith("Local") or line.startswith("Matrix"):
dec = line.split(' ')
key = int(dec[1][:-1])
if dec[2][0] == '[':
# singleton
val = dec[2][1:-1]
if val[0] == '_' and val not in reserved:
loctype = "float4" if dec[0] == "Local" else "float4x4"
locdecl.append(f"{loctype} {val};")
elif dec[2][0] == '(':
#components
vals = dec[2][1:-1].split(',')
for j, v in enumerate(vals):
if v[0] == '[':
vals[j] = v[1:-1]
if vals[j][0] == '_' and vals[j] not in reserved:
locdecl.append(f"float {vals[j]};")
val = f"float4({vals[0]},{vals[1]},{vals[2]},{vals[3]})"
lightval = re.match("glstate_light(\d)_([a-zA-Z]+)", val)
if lightval:
val = f"glstate.light[{lightval[1]}].{lightval[2]}"
lighting = True
elif val == "_ObjectSpaceCameraPos" and not legacy:
val = "mul(_World2Object, float4(_WorldSpaceCameraPos, 1.0f))"
elif val == "_ObjectSpaceLightPos0" and not legacy:
val = "mul(_World2Object, _WorldSpaceLightPos0)"
lighting = True
elif val == "glstate_lightmodel_ambient":
val = "glstate.lightmodel.ambient"
lighting = True
elif val.startswith("glstate_matrix_texture"):
val = f"glstate.matrix.texture[{val[-1]}]" if legacy else f"UNITY_MATRIX_TEXTURE{val[-1]}"
elif val == "glstate_matrix_mvp":
val = "glstate.matrix.mvp" if legacy else "UNITY_MATRIX_MVP"
elif val == "glstate_matrix_modelview0":
val = "glstate.matrix.modelview[0]" if legacy else "UNITY_MATRIX_MV"
elif val == "glstate_matrix_transpose_modelview0":
val = "glstate.matrix.transpose.modelview[0]" if legacy else "UNITY_MATRIX_T_MV"
elif val == "glstate_matrix_invtrans_modelview0":
val = "glstate.matrix.invtrans.modelview[0]" if legacy else "UNITY_MATRIX_IT_MV"
elif val.startswith("glstate"):
raise ValueError(f"Unrecognized glstate: {val}")
if dec[0] == "Local":
loctab[f"c{key}"] = val
elif dec[0] == "Matrix":
for offset in range(0,4):
loctab[f"c{key + offset}"] = f"{val}[{offset}]"
del prog[i]
i = i - 1
elif line.startswith("SetTexture"):
dec = line.split(' ')
if dec[2] != "{2D}":
raise ValueError(f"Unknown texture type {dec[2]}")
key = f"s{textures}"
val = dec[1][1:-1]
loctab[key] = val
locdecl.append(f"sampler2D {val};")
textures = textures + 1
del prog[i]
i = i - 1
i = i + 1
# print(loctab)
return (keywords, loctab, locdecl, binds, lighting)
def resolve_args(args, loctab, consts):
for a in range(0, len(args)):
arg = args[a]
neg = ""
if arg[0] == '-':
arg = arg[1:]
neg = "-"
# save swizzler!
dot = arg.find(".")
if dot > -1:
swiz = arg[dot:]
arg = arg[:dot]
else:
swiz = ""
if arg[0] == 'r':
pass
elif arg[0] == 'v':
pass
elif arg[0] == 't':
pass
elif arg[0] == 'c':
if arg not in consts:
arg = loctab[arg]
elif arg[0] == 's':
arg = loctab[arg]
elif arg[0] == 'o':
arg = f"o.{arg[1:].lower()}"
elif re.match("[+-]?([0-9]*[.])?[0-9]+", arg):
pass
else:
raise ValueError(f"Unknown arg {arg}")
args[a] = neg + arg + swiz
def decode(code, args):
if code in decls:
return [decls[code].format(*args)]
elif code in ops:
target = args[0]
if target == "o.fog":
return [ops[code].format(*args)]
dot = re.search("\.[xyzw]+", target)
if dot:
swiz = target[dot.start()+1:]
target = target[:dot.start()]
else:
swiz = "xyzw"
lines = [ops[code].format("tmp", *args[1:])]
for c in swiz:
lines.append(f"{target}.{c} = tmp.{c};")
return lines
else:
raise ValueError(f"Unknown opcode {code}")
def process_asm(asm, loctab):
shadertype = ""
if asm[0] == "\"vs_1_1":
shadertype = "vertex"
elif asm[0] == "\"ps_2_0":
shadertype = "fragment"
else:
raise ValueError(f"Unsupported shader type: {asm[0][1:]}")
consts = set()
translated = []
i = 1
while i < len(asm):
instruction = asm[i]
if instruction == "\"":
break
space = instruction.find(" ")
if space == -1:
code = instruction
args = []
else:
code = instruction[:space]
args = instruction[space+1:].split(", ")
if code == "def":
consts.add(args[0])
pp = code.find("_pp")
if pp > -1:
code = code[:pp]
resolve_args(args, loctab, consts)
disasm = decode(code, args)
# print(f"{instruction} \t==>\t{disasm}")
disasm.insert(0, f"// {instruction}")
translated.extend(disasm)
i = i + 1
return (shadertype, translated)
def disassemble(blocks):
shaders = {}
keywords = set()
locdecl = set()
binds = set()
lighting = False
for block in blocks:
asm = block.split('\n')[1:-1]
(kw, ltab, ldecl, bds, light) = process_header(asm)
keywords.update(kw)
locdecl.update(ldecl)
binds.update(bds)
lighting |= light
(shadertype, disasm) = process_asm(asm, ltab)
shaders[shadertype] = disasm
text = ""
if len(binds) > 0:
text += "BindChannels {\n"
for b in binds:
text += f"\t{b}\n"
text += "}\n"
if lighting:
text += "Lighting On\n"
text += cg_header
if len(keywords) > 0:
text += "#pragma multi_compile " + " ".join(keywords)
if "vertex" in shaders:
text += "#pragma vertex vert\n"
if "fragment" in shaders:
text += "#pragma fragment frag\n"
text += "\n"
if "vertex" in shaders:
text += struct_a2v + "\n"
text += struct_v2f + "\n"
if "fragment" in shaders:
text += struct_f2a + "\n"
text += "\n".join(locdecl) + "\n"
if "vertex" in shaders:
text += "\n" + vertex_func.format("\t" + "\n\t".join(shaders["vertex"]))
if "fragment" in shaders:
text += "\n" + fragment_func.format("\t" + "\n\t".join(shaders["fragment"]))
text += cg_footer
return text
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: disassembler.py <filename>")
else:
with open(sys.argv[1], "r") as fi:
buf = fi.read()
disasm = disassemble(buf.split('~'))
print(disasm)

37
dx2cg/main.py Normal file
View File

@@ -0,0 +1,37 @@
#!/usr/bin/env python
# coding: utf-8
import os
import sys
from swapper import process
def process_file(filename, suffix):
dot = filename.rfind(".")
if dot > -1:
outfile_name = filename[:dot] + suffix + filename[dot:]
else:
outfile_name = filename + suffix
return process(filename, outfile_name)
def process_batch(path, suffix="_hlsl"):
files = os.listdir(path)
for f in files:
if os.path.isdir(f):
process_batch(f"{path}/{f}")
else:
try:
if process_file(f"{path}/{f}", suffix):
print(f"Processed {f}")
else:
print(f"Skipping {f}")
except ValueError as err:
print(f"Failed to process {f}: {err}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: main.py <folder> [outfile-suffix]")
elif len(sys.argv) == 2:
process_batch(sys.argv[1])
else:
process_batch(*sys.argv[1:3])

77
dx2cg/swapper.py Normal file
View File

@@ -0,0 +1,77 @@
#!/usr/bin/env python
# coding: utf-8
# parser for replacing d3d9 subprograms in shaderlab files with HLSL/CG
# ycc 08/08/2022
import re
import sys
from disassembler import disassemble
tabs = 3
def indent(block):
lines = block.split('\n')
for i in range(0, len(lines)-1):
lines[i] = tabs * "\t" + lines[i]
return "\n".join(lines)
def find_closing_bracket(block, i):
count = 0
while i < len(block):
if block[i] == '{':
count = count + 1
if block[i] == '}':
count = count - 1
if count == 0:
return i
i = i + 1
raise ValueError(f"Block at {i} has no closing bracket")
def process_program(prog):
# print("processing:\n" + prog)
subprogs = []
subprog_index = prog.find("SubProgram \"d3d9")
while subprog_index > -1:
subprog_end_index = find_closing_bracket(prog, subprog_index)
subprog = prog[subprog_index:subprog_end_index+1]
subprogs.append(subprog)
prog = prog[subprog_end_index+1:]
subprog_index = prog.find("SubProgram \"d3d9")
if len(subprogs) < 1:
raise ValueError(f"Program has no d3d9 subprograms")
processed = disassemble(subprogs) + "\n"
return indent(processed)
def process_shader(shader):
buf = shader
processed = ''
program_index = buf.find("Program \"\"")
while program_index > -1:
processed = processed + buf[:program_index]
buf = buf[program_index:]
line = re.search("#LINE [0-9]+\n", buf)
if not line:
raise ValueError(f"Program at {program_index} has no #LINE marker")
end_index = line.end() + 1
program_section = buf[:end_index+1]
processed = processed + process_program(program_section)
buf = buf[end_index+1:]
program_index = buf.find("Program \"\"")
processed = processed + buf
return processed
def process(fn_in, fn_out):
with open(fn_in, "r") as fi:
buf = fi.read()
processed = process_shader(buf)
if buf != processed:
with open(fn_out, "w") as fo:
fo.write(processed)
return True
return False
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: swapper.py <file-in> <file-out>")
else:
process(*sys.argv[1:3])

View File

@@ -0,0 +1,88 @@
from unitypackff.asset import Asset
from unitypackff.environment import UnityEnvironment
import bpy
import bmesh
import os
dongpath = r'C:\Users\gents\AppData\LocalLow\Unity\Web Player\Cache\Fusionfall'
env = UnityEnvironment(base_path=dongpath)
outpath = r'C:\Users\gents\3D Objects\FFTerrainMeshes'
def rip_terrain_mesh(f, outpath):
dong = Asset.from_file(f, environment=env)
for k, v in dong.objects.items():
if v.type == 'TerrainData':
terrainData = dong.objects[k].read()
terrain_width = terrainData['m_Heightmap']['m_Width'] - 1
terrain_height = terrainData['m_Heightmap']['m_Height'] - 1
# create the terrain
bpy.ops.mesh.primitive_grid_add(x_subdivisions=terrain_width, y_subdivisions=terrain_height, size=40, enter_editmode=True, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
context = bpy.context
grid = context.edit_object
# apply triangulate modifier
mod = grid.modifiers.new("Triangulate", 'TRIANGULATE')
mod.quad_method = 'FIXED' # triangle orientation
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.modifier_apply(modifier="Triangulate")
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(context.edit_object.data)
bm.verts.ensure_lookup_table()
for index, height in enumerate(terrainData['m_Heightmap']['m_Heights']):
height = height / terrainData['m_Heightmap']['m_Scale']['y']
bm.verts[index].co.z = height
indices = []
shift_amt = abs(bm.verts[0].co.x - bm.verts[1].co.x)
# gather m_Shifts positions
for shift in terrainData['m_Heightmap']['m_Shifts']:
shift_index = shift['y'] + shift['x'] * 129
indices.append(shift_index)
v = bm.verts[shift_index]
flags = shift['flags'] # bits: +X -X +Y -Y
if flags & 0b1000: # +X
v.co.x += shift_amt
if flags & 0b0100: # -X
v.co.x -= shift_amt
if flags & 0b0010: # +Y
v.co.y += shift_amt
if flags & 0b0001: # -Y
v.co.y -= shift_amt
# flip to correct orientation
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action='SELECT')
bpy.ops.transform.mirror(orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(False, True, False))
outfile = f"{k}.fbx"
bpy.ops.export_scene.fbx(filepath=os.path.join(outpath, outfile))
# select modified vertices
#bpy.ops.object.mode_set(mode="EDIT")
#bm = bmesh.from_edit_mesh(context.edit_object.data)
#bm.verts.ensure_lookup_table()
#for v in bm.verts:
# v.select = False
#for shift_index in indices:
# v = bm.verts[shift_index]
# v.select = True
# clear the scene
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
dongs = os.listdir(dongpath)
for dongname in dongs:
if not dongname.endswith("resourceFile"):
continue
assets = os.listdir(os.path.join(dongpath, dongname))
for assetname in assets:
if not assetname.startswith("CustomAssetBundle"):
continue
with open(os.path.join(dongpath, dongname, assetname), "rb") as f:
outdir = os.path.join(outpath, dongname, assetname)
os.makedirs(outdir, exist_ok=True)
rip_terrain_mesh(f, outdir)

View File

@@ -0,0 +1,5 @@
# Terrain Mesh Extractor
Blender + UPFF script to import terrain data as a mesh into Blender, then apply the shifts property to applicable vertices.
- Exports as FBX
- The fbx filenames are the index of the TerrainData object within the asset file
- Folders for asset bundles that had no terrain objects will be empty