Last active
January 9, 2022 08:25
-
-
Save warmist/8cd30ea72388385041836a872ed5470e to your computer and use it in GitHub Desktop.
ImHex pattern and lua file for reading titanfall2 navmesh file format
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Hulls extracted from the game: | |
180b4be60 hulldef[6] | |
hulldef[0] | |
name=HULL_HUMAN | |
field1_0x8=0x1 | |
bbox_min={-16,-16,0} | |
bbox_max={16,16,72} | |
walk_height=18.0 //not 100% sure... | |
field5_0x28=0.5 | |
field6_0x2c={60.0f,250.0f,400.0f} | |
navmesh_id=0 | |
flags=0x2000B | |
hulldef[1] | |
name=HULL_MEDIUM | |
field1_0x8=0x2 | |
bbox_min={-48,-48,0} | |
bbox_max={48,48,150} | |
walk_height=32.0 | |
field5_0x28=0.5 | |
field6_0x2c={256,256,512} | |
navmesh_id=2 | |
flags=0x2000B | |
hulldef[2] | |
name=HULL_FLYING_VEHICLE | |
field1_0x8=0x4 | |
bbox_min={-200,-200,0} | |
bbox_max={200,200,200} | |
walk_height=80.0 | |
field5_0x28=0.5 | |
field6_0x2c={0,0,0} | |
navmesh_id=3 | |
flags=0x2000B | |
hulldef[3] | |
name=HULL_SMALL | |
field1_0x8=0x8 | |
bbox_min={-16,-16,0} | |
bbox_max={16,16,32} | |
walk_height=18.0 | |
field5_0x28=0.5 | |
field6_0x2c={0,0,0} | |
navmesh_id=0 | |
flags=0x2000B | |
hulldef[4] | |
name=HULL_TITAN | |
field1_0x8=0x10 | |
bbox_min={-60,-60,0} | |
bbox_max={60,60,235} | |
walk_height=80.0 | |
field5_0x28=1.0 | |
field6_0x2c={60,512,1024} | |
navmesh_id=3 | |
flags=0x20000 | |
hulldef[5] | |
name=HULL_PROWLER | |
field1_0x8=0x20 | |
bbox_min={-40,-40,0} | |
bbox_max={40,40,72} | |
walk_height=18.0 | |
field5_0x28=0.5 | |
field6_0x2c={60,250,400} | |
navmesh_id=1 | |
flags=0x2000B |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct header_t { | |
u32 magic; | |
u32 version; | |
u32 tile_count; | |
float x,y,z; | |
float tile_w,tile_h; | |
u32 max_tiles,max_polys; | |
//UNKNOWN PARTS | |
//bit table allocated by server.dll+0x3e87f0 | |
u32 disjoint_poly_group_count; | |
u32 reachability_table_size; //actually ((count_sth+31)/32)*count_sth*4 | |
u32 reachability_table_count; //small.nm has 4,others 1. Indexed by hulls | |
}; | |
header_t header @ 0; | |
struct tile_header_t { | |
u32 ref; | |
u32 size; | |
}; | |
fn header_select(u32 num) | |
{ | |
u64 cur=sizeof(header); | |
for(u32 i=0,i<num,i=i+1) | |
{ | |
u32 offset; | |
offset=std::mem::read_unsigned(cur+4,4); | |
cur=cur+offset+8; | |
} | |
return cur; | |
}; | |
tile_header_t th0 @ header_select(0); | |
struct dtMeshHeader { | |
u32 magic; //OK | |
s32 version; //OK | |
s32 x,y,layer; //OK | |
u32 user_id; //OK | |
s32 polyCount; //OK | |
s32 maxLinkCount; //OK | |
s32 vertCount; //OK | |
s32 actualLinkCount; //OK? | |
s32 detailMeshCount; //OK? | |
s32 detailVertsCount; //OK | |
s32 detailTrisCount; //OK | |
s32 bvNodeCount; //LOOK OK | |
s32 offMeshConCount; //OK | |
s32 offMeshBase; //OK | |
float walkableHeight; //OK | |
float walkableRadius; //OK | |
float walkableClimb; //OK | |
float bmin[3]; //OK | |
float bmax[3]; //OK | |
/// The bounding volume quantization factor. | |
float bvQuantFactor; //OK | |
}; | |
struct dtPoly{ | |
u32 firstLink; //NOT LOOKED INTO | |
u16 verts[6]; //OK | |
u16 neis[6]; //links but havent looked into it | |
u16 flags; //NO IDEA | |
u8 count; //OK | |
u8 areaAndType; //STUPID but TYPE works | |
u16 disjoint_set_id; //OK (1 looks like special group) | |
u16 unk2; //NO IDEA | |
float pos[3]; //NO IDEA | |
}; | |
struct dtLink | |
{ | |
u32 ref; | |
u32 next; | |
u8 edge; ///< Index of the polygon edge that owns this link. | |
u8 side; ///< If a boundary link, defines on which side the link is. | |
u8 bmin; ///< If a boundary link, defines the minimum sub-edge area. | |
u8 bmax; ///< If a boundary link, defines the maximum sub-edge area. | |
u32 unk; | |
}; | |
struct vert | |
{ | |
float x,y,z; | |
}; | |
struct dtPolyDetail | |
{ | |
///< The tile's detail sub-meshes. [Size: dtMeshHeader::detailMeshCount] | |
u32 vertBase; ///< The offset of the vertices in the dtMeshTile::detailVerts array. | |
u32 triBase; ///< The offset of the triangles in the dtMeshTile::detailTris array. | |
u8 vertCount; ///< The number of vertices in the sub-mesh. | |
u8 triCount; ///< The number of triangles in the sub-mesh. | |
u16 unk; | |
// u16 unk[6]; | |
}; | |
struct detailVert | |
{ | |
/// The detail mesh's unique vertices. [(x, y, z) * dtMeshHeader::detailVertCount] | |
float x,y,z; | |
}; | |
struct dtDetailTris | |
{ | |
/// The detail mesh's triangles. [(vertA, vertB, vertC) * dtMeshHeader::detailTriCount] | |
u8 verts[3]; | |
u8 unk; | |
}; | |
struct bvNode | |
{ | |
u16 bmin[3]; | |
u16 bmax[3]; | |
s32 id; | |
}; | |
dtMeshHeader mh0 @ $; | |
vert verts[mh0.vertCount] @ $; //OK | |
dtPoly polys[mh0.polyCount] @ $; //see above | |
u32 links[mh0.maxLinkCount*mh0.polyCount] @ $; //NOT LOOKED INTO | |
dtLink dlinks[mh0.actualLinkCount] @ $; //NOT LOOKED INTO | |
dtPolyDetail detailMeshes[mh0.detailMeshCount]@$; //NOT LOOKED INTO | |
detailVert detailVerts[mh0.detailVertsCount] @$ ; //NOT LOOKED INTO | |
dtDetailTris detailTris[mh0.detailTrisCount] @$ ; //NOT LOOKED INTO | |
bvNode bvnodes[mh0.bvNodeCount] @ $; //Looks ok! | |
struct dtOffMeshConnection | |
{ | |
float pos[6]; | |
float radius; | |
u16 poly; | |
u8 flags; | |
u8 side; | |
u32 userId; | |
float pos_unk[3]; | |
float float_unk; | |
}; | |
dtOffMeshConnection offconns[mh0.offMeshConCount] @ $; //NO IDEA | |
//probably indexed by | |
u32 sth_array[header.disjoint_poly_group_count] @ header_select(header.tile_count); | |
//indexed by ((header.reachability_table_size+31)/32) * poly1.disjoint_set_id+poly2.disjoint_set_id>>5. resulting | |
// u32 is then >> (poly2.disjoin_set_id & 0x1f) | |
// finnally first bit indicates if p1 is reachable by p2 | |
u32 reachability_tables[(header.reachability_table_size/4)*header.reachability_table_count] @ $; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local fname="sp_skyway_v1_large.nm" | |
local print_stuff=false | |
--[[ current struct layouts ]] | |
local header_t={ | |
{"u32","magic"}, | |
{"u32","version"}, | |
{"u32","tile_count"}, | |
{"float","x"}, | |
{"float","y"}, | |
{"float","z"}, | |
{"float","tile_w"}, | |
{"float","tile_h"}, | |
{"u32","max_tiles"}, | |
{"u32","max_polys"}, | |
{"u32","disjoint_poly_group_count"}, | |
{"u32","reachability_table_size"}, | |
{"u32","reachability_table_count"}, | |
} | |
local mesh_pre_header_t={ | |
{"u32","ref"}, | |
{"u32","size"}, | |
} | |
local dtMeshHeader={ | |
{"u32","magic"}, | |
{"s32","version"}, | |
{"s32","x"}, | |
{"s32","y"}, | |
{"s32","layer"}, | |
{"u32","user_id"}, | |
{"s32","polyCount"}, | |
{"s32","maxLinkCount"}, | |
{"s32","vertCount"}, | |
{"s32","actualLinkCount"}, | |
{"s32","detailMeshCount"}, | |
{"s32","detailVertsCount"}, | |
{"s32","detailTrisCount"}, | |
{"s32","bvNodeCount"}, | |
{"s32","offMeshConCount"}, | |
{"s32","offMeshBase"}, | |
{"float","walkableHeight"}, | |
{"float","walkableRadius"}, | |
{"float","walkableClimb"}, | |
{"float","bmin_x"}, | |
{"float","bmin_y"}, | |
{"float","bmin_z"}, | |
{"float","bmax_x"}, | |
{"float","bmax_y"}, | |
{"float","bmax_z"}, | |
{"float","bvQuantFactor"}, | |
} | |
local vert_t={ | |
{"float","x"}, | |
{"float","y"}, | |
{"float","z"}, | |
} | |
local dtLink= | |
{ | |
{"u32", "ref"}, | |
{"u32", "next"}, | |
{"u8", "edge"}, | |
{"u8", "side"}, | |
{"u8", "bmin"}, | |
{"u8", "bmax"}, | |
{"u32", "unk"}, | |
} | |
local dtPolyDetail= | |
{ | |
{"u32","vertBase"}, | |
{"u32","triBase"}, | |
{"u8","vertCount"}, | |
{"u8","triCount"}, | |
{"u16","unk"}, | |
} | |
local dtDetailTris= | |
{ | |
{"u8","verts1"}, | |
{"u8","verts2"}, | |
{"u8","verts3"}, | |
{"u8","unk"}, | |
}; | |
local bvNode= | |
{ | |
{"u16","bmin_x"}, | |
{"u16","bmin_y"}, | |
{"u16","bmin_z"}, | |
{"u16","bmax_x"}, | |
{"u16","bmax_y"}, | |
{"u16","bmax_z"}, | |
{"s32","id"}, | |
}; | |
--needs arrays inside so just function-load-it | |
function load_dtPoly( s,cp ) | |
local ret={} | |
ret.firstLink,cp=load_typename(s,cp,"u32") | |
ret.verts,cp=load_array(s,cp,"u16",6) | |
ret.neis,cp=load_array(s,cp,"u16",6) | |
ret.flags,cp=load_typename(s,cp,"u16") | |
ret.count,cp=load_typename(s,cp,"u8") | |
ret.areaAndType,cp=load_typename(s,cp,"u8") | |
ret.disjoint_set_id,cp=load_typename(s,cp,"u16") | |
ret.unk2,cp=load_typename(s,cp,"u16") | |
ret.pos,cp=load_typename(s,cp,vert_t) | |
return ret,cp | |
end | |
function load_dtOffMeshConnection( s,cp ) | |
local ret={} | |
ret.pos,cp=load_array(s,cp,"float",6) | |
ret.radius,cp=load_typename(s,cp,"float") | |
ret.poly,cp=load_typename(s,cp,"u16") | |
ret.flags,cp=load_typename(s,cp,"u8") | |
ret.side,cp=load_typename(s,cp,"u8") | |
ret.userId,cp=load_typename(s,cp,"u32") | |
ret.pos_unk,cp=load_array(s,cp,"float",3) | |
ret.float_unk,cp=load_typename(s,cp,"float") | |
return ret,cp | |
end | |
--lua string.(un)pack uses short names | |
local type_transforms={ | |
u32="I4", | |
u16="I2", | |
u8="I1", | |
s32="i4", | |
s16="i2", | |
s8="i1", | |
float="f", | |
double="d", | |
} | |
function opt_print( ... ) | |
if print_stuff then | |
print(...) | |
end | |
end | |
function byte_array_tohex( barr ) | |
local ret="" | |
for i=1,#barr do | |
ret=ret..string.format("%02x",barr[i]) | |
end | |
return ret | |
end | |
--prints typename | |
function print_tname( struct,tname,fname ) | |
if type(tname)=="string" then | |
opt_print(string.format("\t%20s\t%s",fname or "",tostring(struct))) | |
return | |
end | |
for i,v in ipairs(tname) do | |
opt_print(string.format("\t%20s\t%s",v[2],tostring(struct[v[2]]))) | |
end | |
end | |
--loads typename from a string | |
function load_typename(s,cp,tname) | |
local ret={} | |
if type(tname)=="string" then | |
local tformat=type_transforms[tname] | |
ret,cp=string.unpack(tformat,s,cp) | |
return ret,cp | |
end | |
for i,v in ipairs(tname) do | |
local tformat=type_transforms[v[1]] | |
ret[v[2]],cp=string.unpack(tformat,s,cp) | |
end | |
return ret,cp | |
end | |
--loads array of typename from a string | |
function load_array( s,cp,tname,count ) | |
local ret={} | |
for i=1,count do | |
ret[i],cp=load_typename(s,cp,tname) | |
end | |
return ret,cp | |
end | |
--[[ Actually load stuff ]] | |
function load_mesh(s,cp) | |
local ph,mesh_head | |
ph,cp=load_typename(s,cp,mesh_pre_header_t) | |
local mesh_start=cp | |
mesh_head,cp=load_typename(s,cp,dtMeshHeader) | |
assert(mesh_head.magic==1145979222) | |
assert(mesh_head.version==13) | |
opt_print("pre-mesh-header:") | |
print_tname(ph,mesh_pre_header_t) | |
opt_print("dtMeshHeader:") | |
print_tname(mesh_head,dtMeshHeader) | |
local data={} | |
data.verts,cp=load_array(s,cp,vert_t,mesh_head.vertCount) | |
--opt_print("vert1:") | |
--print_tname(data.verts[1],vert_t) | |
data.polys={} | |
for i=1,mesh_head.polyCount do | |
data.polys[i],cp=load_dtPoly(s,cp) | |
end | |
--opt_print("polys[1]:") | |
--print_tname(data.polys[1].firstLink,"u32","ref") | |
--print_tname(data.polys[1].pos,vert_t,"pos") | |
data.links,cp=load_array(s,cp,"u32",mesh_head.maxLinkCount*mesh_head.polyCount) | |
--opt_print("links[1]:") | |
--print_tname(data.links[1],"u32","link") | |
data.dlinks,cp=load_array(s,cp,dtLink,mesh_head.actualLinkCount) | |
--opt_print("dlinks1:") | |
--print_tname(data.dlinks[1],dtLink) | |
data.detailMeshes,cp=load_array(s,cp,dtPolyDetail,mesh_head.detailMeshCount) | |
--opt_print("detailMeshes1:") | |
--print_tname(data.detailMeshes[1],dtPolyDetail) | |
data.detailVert,cp=load_array(s,cp,vert_t,mesh_head.detailVertsCount) | |
data.detailTris,cp=load_array(s,cp,dtDetailTris,mesh_head.detailTrisCount) | |
data.bvNodes,cp=load_array(s,cp,bvNode,mesh_head.bvNodeCount) | |
--data.unk_stuff,cp=load_array(s,cp,"u8",mesh_head.unk2*52) | |
data.offMeshConnections={} | |
for i=1,mesh_head.offMeshConCount do | |
data.offMeshConnections[i],cp=load_dtOffMeshConnection(s,cp) | |
end | |
assert(cp-mesh_start==ph.size) | |
return {ph=ph,mesh_head=mesh_head,data=data},cp | |
end | |
function load_navmesh( fname ) | |
local f=io.open(fname,"rb") | |
local s=f:read("a") | |
f:close() | |
local cp=1 | |
local header | |
header,cp=load_typename(s,cp,header_t) | |
print(string.format("Loading:\t%20s\t=========================================",fname)) | |
opt_print("header:") | |
print_tname(header,header_t) | |
local meshes={} | |
for i=1,header.tile_count do | |
meshes[i],cp=load_mesh(s,cp) | |
end | |
local table_header | |
table_header,cp=load_array(s,cp,"u32",header.disjoint_poly_group_count) | |
local reachability_tables={} | |
for i=1,header.reachability_table_count do | |
reachability_tables[i],cp=load_array(s,cp,"u32",header.reachability_table_count//4) | |
end | |
return {header=header,meshes=meshes,table_header=table_header,reachability_tables=reachability_tables} | |
end | |
local navmesh=load_navmesh(fname) | |
function save_ply_polygons(nm,fname) | |
local ply_header= | |
[[ | |
ply | |
format ascii 1.0 | |
element vertex %d | |
property float x | |
property float y | |
property float z | |
element face %d | |
property list uchar int vertex_index | |
end_header | |
]] | |
local vert_out="" | |
local poly_out="" | |
local vert_count=0 | |
local poly_count=0 | |
for i,v in ipairs(nm.meshes) do | |
for i,v in ipairs(v.data.verts) do | |
vert_out=vert_out..string.format("%g %g %g\n",v.x,v.y,v.z) | |
end | |
for i,v in ipairs(v.data.polys) do | |
if v.areaAndType<0x40 then | |
poly_out=poly_out..v.count | |
for i=1,v.count do | |
poly_out=poly_out.." "..tostring(vert_count+v.verts[i]) | |
end | |
poly_out=poly_out.."\n" | |
poly_count=poly_count+1 | |
end | |
end | |
vert_count=vert_count+#v.data.verts | |
end | |
local ply_data=ply_header:format(vert_count,poly_count)..vert_out..poly_out | |
print(string.format("Writing:\t%20s\t=========================================",fname)) | |
local f_out=io.open (fname,"w") | |
f_out:write(ply_data) | |
f_out:close() | |
end | |
save_ply_polygons(navmesh,fname:sub(1,-3).."ply") | |
function save_ply_details(nm,fname) | |
local ply_header= | |
[[ | |
ply | |
format ascii 1.0 | |
element vertex %d | |
property float x | |
property float y | |
property float z | |
element face %d | |
property list uchar int vertex_index | |
end_header | |
]] | |
local vert_out="" | |
local poly_out="" | |
local vert_count=0 | |
local poly_count=0 | |
for mesh_id,tile in ipairs(nm.meshes) do | |
for i,poly in ipairs(tile.data.polys) do | |
if poly.areaAndType<0x40 then | |
local pd=tile.data.detailMeshes[i] | |
for j=1,pd.triCount do | |
local tt=tile.data.detailTris[(pd.triBase+j-1)+1] | |
local t={tt.verts1,tt.verts2,tt.verts3} | |
local vv={} | |
for k=1,3 do | |
if t[k]<poly.count then | |
vv[k]=tile.data.verts[poly.verts[ t[k]+1 ]+1] | |
else | |
vv[k]=tile.data.detailVert[(pd.vertBase+(t[k]-poly.count))+1] | |
end | |
vert_out=vert_out..string.format("%g %g %g\n",vv[k].x,vv[k].y,vv[k].z) | |
end | |
poly_out=poly_out..string.format("3 %d %d %d\n",vert_count,vert_count+1,vert_count+2) | |
vert_count=vert_count+3 | |
poly_count=poly_count+1 | |
end | |
end | |
end | |
end | |
local ply_data=ply_header:format(vert_count,poly_count)..vert_out..poly_out | |
print(string.format("Writing:\t%20s\t=========================================",fname)) | |
local f_out=io.open (fname,"w") | |
f_out:write(ply_data) | |
f_out:close() | |
end | |
save_ply_details(navmesh,fname:sub(1,-4).."_detail.ply") | |
function cross_prod(a,b) | |
local ret={ | |
a[2]*b[3]-a[3]*b[2], | |
a[3]*b[1]-a[1]*b[3], | |
a[1]*b[2]-a[2]*b[1] | |
} | |
return ret | |
end | |
function length( v ) | |
return math.sqrt(v[1]*v[1]+v[2]*v[2]+v[3]*v[3]) | |
end | |
function normalize( v ) | |
local l=length(v) | |
return {v[1]/l,v[2]/l,v[3]/l} | |
end | |
function add_pt(a,b) | |
return {a[1]+b[1],a[2]+b[2],a[3]+b[3]} | |
end | |
function mult_pt( a,scalar) | |
return {a[1]*scalar,a[2]*scalar,a[3]*scalar} | |
end | |
--save tubes that show where offmesh cons are | |
function save_ply_offmesh_cons( nm,fname ) | |
local ply_header= | |
[[ | |
ply | |
format ascii 1.0 | |
element vertex %d | |
property float x | |
property float y | |
property float z | |
element face %d | |
property list uchar int vertex_index | |
end_header | |
]] | |
local vert_out="" | |
local poly_out="" | |
local vert_count=0 | |
local poly_count=0 | |
for mesh_id,tile in ipairs(nm.meshes) do | |
for i=tile.mesh_head.offMeshBase,tile.mesh_head.offMeshBase+tile.mesh_head.offMeshConCount-1 do | |
opt_print("Tile:",mesh_id-1,"Poly:",i,"Con:",i-tile.mesh_head.offMeshBase) | |
local poly=tile.data.polys[i+1] | |
if poly.areaAndType>0x40 then | |
local pd=tile.data.offMeshConnections[i-tile.mesh_head.offMeshBase+1] | |
local dx={-1,-1,1,1} | |
local dz={1,-1,-1,1} | |
local s_pt={pd.pos[1],pd.pos[2],pd.pos[3]} | |
local e_pt={pd.pos[4],pd.pos[5],pd.pos[6]} | |
local con_d=add_pt(s_pt,mult_pt(e_pt,-1)) | |
local cd=normalize(con_d) | |
local ovec1=cross_prod(cd,{0,0,1}) | |
local ovec2=cross_prod(cd,ovec1) | |
local rad=pd.radius | |
local vert_count_start=vert_count | |
for i=1,4 do | |
local v=add_pt(s_pt,add_pt(mult_pt(ovec1,dx[i]*rad),mult_pt(ovec2,dz[i]*rad))) | |
vert_out=vert_out..string.format("%g %g %g\n",v[1],v[2],v[3]) | |
vert_count=vert_count+1 | |
end | |
for i=1,4 do | |
local v=add_pt(e_pt,add_pt(mult_pt(ovec1,dx[i]*rad),mult_pt(ovec2,dz[i]*rad))) | |
vert_out=vert_out..string.format("%g %g %g\n",v[1],v[2],v[3]) | |
vert_count=vert_count+1 | |
end | |
local tris={ | |
{0,1,5}, | |
{0,5,4}, | |
{1,2,5}, | |
{2,6,5}, | |
{3,2,6}, | |
{3,6,7}, | |
{0,3,4}, | |
{3,7,4} | |
} | |
for i,v in ipairs(tris) do | |
poly_out=poly_out..string.format("3 %d %d %d\n",v[1]+vert_count_start,v[2]+vert_count_start,v[3]+vert_count_start) | |
poly_count=poly_count+1 | |
end | |
else | |
error("Didn't expect a non-offmesh tile") | |
end | |
end | |
end | |
local ply_data=ply_header:format(vert_count,poly_count)..vert_out..poly_out | |
print(string.format("Writing:\t%20s\t=========================================",fname)) | |
local f_out=io.open (fname,"w") | |
f_out:write(ply_data) | |
f_out:close() | |
end | |
save_ply_offmesh_cons(navmesh,fname:sub(1,-4).."_offmeshcon.ply") | |
function save_ppm_reachability_table( nm,fname,id ) | |
local w=math.floor((nm.header.disjoint_poly_group_count+31)/32) | |
local h=nm.header.disjoint_poly_group_count | |
--print(w,h,w*h*4,nm.header.reachability_table_size) | |
local img_w=w*32 | |
local img_h=h | |
--print(img_w,img_h) | |
local ppm_data=string.format("P1\n%d %d\n",img_w,img_h) | |
local strings={ppm_data} | |
local big_tbl=nm.reachability_tables[id] | |
for y=0,h-1 do | |
for x=0,w-1 do | |
local v=big_tbl[y*w+x+1] | |
if v==nil then | |
print(x,y,y*w+x) | |
end | |
for bit=0,31 do | |
if bit32.extract(v,bit)==1 then | |
table.insert(strings," 1") | |
else | |
table.insert(strings," 0") | |
end | |
end | |
end | |
table.insert(strings,"\n") | |
end | |
ppm_data=table.concat(strings) | |
print(string.format("Writing:\t%20s\t=========================================",fname)) | |
local f_out=io.open (fname,"w") | |
f_out:write(ppm_data) | |
f_out:close() | |
end | |
--save_ppm_unk_tables(navmesh,fname:sub(1,-4).."_tbl0.ppm",0) | |
function is_poly_reachable(nm,p1,p2,hull_id) | |
if p1==p2 then | |
return true | |
end | |
if hull_id==-1 then | |
return p1.disjoint_set_id==p2.disjoint_set_id | |
end | |
local tbl=nm.tables[hull_id] | |
local w=math.floor((nm.header.count_sth+31)/32) | |
return bit32.band(bit32.rshift(tbl[w*p1.disjoint_set_id+bit32.rshift(p2.disjoint_set_id, 5)],bit32.band(p2.disjoint_set_id,0x1f)),1) ~=0 | |
end | |
function collect_group_ids(nm) | |
local id_counts={} | |
for mesh_id,tile in ipairs(nm.meshes) do | |
for i,poly in ipairs(tile.data.polys) do | |
local lidx=poly.disjoint_set_id | |
if id_counts[lidx]==nil then id_counts[lidx]=1 else id_counts[lidx]=id_counts[lidx]+1 end | |
end | |
end | |
return id_counts | |
end | |
function sort_group_ids( tbl ) | |
local tbl_sorted={} | |
for i,v in pairs(tbl) do | |
table.insert(tbl_sorted,{i,v}) | |
end | |
table.sort(tbl_sorted,function ( a,b ) | |
return a[2]>b[2] | |
end) | |
return tbl_sorted | |
end | |
function save_ply_polygons_by_group_id(nm,fname,id) | |
local ply_header= | |
[[ | |
ply | |
format ascii 1.0 | |
element vertex %d | |
property float x | |
property float y | |
property float z | |
element face %d | |
property list uchar int vertex_index | |
end_header | |
]] | |
local vert_out="" | |
local poly_out="" | |
local vert_count=0 | |
local poly_count=0 | |
for i,v in ipairs(nm.meshes) do | |
for i,v in ipairs(v.data.verts) do | |
vert_out=vert_out..string.format("%g %g %g\n",v.x,v.y,v.z) | |
end | |
for i,v in ipairs(v.data.polys) do | |
if v.areaAndType<0x40 and v.disjoint_set_id==id then | |
poly_out=poly_out..v.count | |
for i=1,v.count do | |
poly_out=poly_out.." "..tostring(vert_count+v.verts[i]) | |
end | |
poly_out=poly_out.."\n" | |
poly_count=poly_count+1 | |
end | |
end | |
vert_count=vert_count+#v.data.verts | |
end | |
local ply_data=ply_header:format(vert_count,poly_count)..vert_out..poly_out | |
print(string.format("Writing:\t%20s\t=========================================",fname)) | |
local f_out=io.open (fname,"w") | |
f_out:write(ply_data) | |
f_out:close() | |
end | |
function save_biggest_poly_groups(navmesh,fname_prefix,count) | |
local ids=collect_group_ids(navmesh) | |
ids=sort_group_ids(ids) | |
for i=1,math.min(count,#ids) do | |
local idx=ids[i][1] | |
save_ply_polygons_by_group_id(navmesh,fname_prefix..fname:sub(1,-4).."_group_"..idx..".ply",idx) | |
end | |
end | |
save_biggest_poly_groups(navmesh,"big_dump/",10) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# McSimps Titanfall Map Exporter Tool | |
# Website: https://will.io/ | |
import struct | |
from enum import Enum | |
import os | |
map_name = 'sp_sewers1' | |
map_path_prefix='C:\\Users\\warmi\\Downloads\\ttmp\\maps\\' | |
map_path = map_path_prefix + map_name + '.bsp' | |
dump_base = map_path_prefix+"out\\" | |
def read_null_string(f): | |
chars = [] | |
while True: | |
c = f.read(1).decode('ascii') | |
if c == chr(0): | |
return ''.join(chars) | |
chars.append(c) | |
class LumpElement: | |
@staticmethod | |
def get_size(): | |
raise NotImplementedError() | |
class TextureData(LumpElement): | |
def __init__(self, data): | |
self.string_table_index = struct.unpack_from('<I', data, 12)[0] | |
@staticmethod | |
def get_size(): | |
return 36 | |
class BumpLitVertex(LumpElement): | |
def __init__(self, data): | |
self.vertex_pos_index = struct.unpack_from('<I', data, 0)[0] | |
self.vertex_normal_index = struct.unpack_from('<I', data, 4)[0] | |
self.texcoord0 = struct.unpack_from('<ff', data, 8) # coord into albedo, normal, gloss, spec | |
self.texcoord5 = struct.unpack_from('<ff', data, 20) # coord into lightmap | |
@staticmethod | |
def get_size(): | |
return 44 | |
class UnlitVertex(LumpElement): | |
def __init__(self, data): | |
self.vertex_pos_index = struct.unpack_from('<I', data, 0)[0] | |
self.vertex_normal_index = struct.unpack_from('<I', data, 4)[0] | |
self.texcoord0 = struct.unpack_from('<ff', data, 8) | |
@staticmethod | |
def get_size(): | |
return 20 | |
class UnlitTSVertex(LumpElement): | |
def __init__(self, data): | |
self.vertex_pos_index = struct.unpack_from('<I', data, 0)[0] | |
self.vertex_normal_index = struct.unpack_from('<I', data, 4)[0] | |
self.texcoord0 = struct.unpack_from('<ff', data, 8) | |
@staticmethod | |
def get_size(): | |
return 28 | |
class MaterialSortElement(LumpElement): | |
def __init__(self, data): | |
self.texture_index = struct.unpack_from('<H', data, 0)[0] | |
self.vertex_start_index = struct.unpack_from('<I', data, 8)[0] | |
@staticmethod | |
def get_size(): | |
return 12 | |
class VertexType(Enum): | |
LIT_FLAT = 0 | |
UNLIT = 1 | |
LIT_BUMP = 2 | |
UNLIT_TS = 3 | |
class MeshElement(LumpElement): | |
def __init__(self, data): | |
self.indices_start_index = struct.unpack_from('<I', data, 0)[0] | |
self.num_triangles = struct.unpack_from('<H', data, 4)[0] | |
self.material_sort_index = struct.unpack_from('<H', data, 22)[0] | |
self.flags = struct.unpack_from('<I', data, 24)[0] | |
def get_vertex_type(self): | |
temp = 0 | |
if self.flags & 0x400: | |
temp |= 1 | |
if self.flags & 0x200: | |
temp |= 2 | |
return VertexType(temp) | |
@staticmethod | |
def get_size(): | |
return 28 | |
# Read all vertex position data | |
print("Reading vertex position data...") | |
with open(map_path + '.0003.bsp_lump', 'rb') as f: | |
data = f.read() | |
vertex_positions = [struct.unpack_from('<fff', data, i * 12) for i in range(len(data) // 12)] | |
# Read all vertex normals | |
print("Reading vertex normal data...") | |
with open(map_path + '.001e.bsp_lump', 'rb') as f: | |
data = f.read() | |
vertex_normals = [struct.unpack_from('<fff', data, i * 12) for i in range(len(data) // 12)] | |
# Read indices | |
print("Reading indices...") | |
with open(map_path + '.004f.bsp_lump', 'rb') as f: | |
data = f.read() | |
indices = [struct.unpack_from('<H', data, i * 2)[0] for i in range(len(data) // 2)] | |
# Read texture information | |
print("Reading texture information...") | |
with open(map_path + '.002c.bsp_lump', 'rb') as f: | |
data = f.read() | |
texture_string_offets = [struct.unpack_from('<I', data, i * 4)[0] for i in range(len(data) // 4)] | |
with open(map_path + '.002b.bsp_lump', 'rb') as f: | |
texture_strings = [] | |
for offset in texture_string_offets: | |
f.seek(offset) | |
texture_strings.append(read_null_string(f)) | |
textures = [] | |
with open(map_path + '.0002.bsp_lump', 'rb') as f: | |
data = f.read() | |
elem_size = TextureData.get_size() | |
for i in range(len(data) // elem_size): | |
textures.append(TextureData(data[i*elem_size:(i+1)*elem_size])) | |
# Read bump lit vertices | |
print("Reading bump lit vertices...") | |
bump_lit_vertices = [] | |
with open(map_path + '.0049.bsp_lump', 'rb') as f: | |
data = f.read() | |
elem_size = BumpLitVertex.get_size() | |
for i in range(len(data) // elem_size): | |
bump_lit_vertices.append(BumpLitVertex(data[i*elem_size:(i+1)*elem_size])) | |
# Read unlit vertices | |
print("Reading unlit vertices...") | |
unlit_vertices = [] | |
with open(map_path + '.0047.bsp_lump', 'rb') as f: | |
data = f.read() | |
elem_size = UnlitVertex.get_size() | |
for i in range(len(data) // elem_size): | |
unlit_vertices.append(UnlitVertex(data[i*elem_size:(i+1)*elem_size])) | |
# Read unlit TS vertices | |
print("Reading unlit TS vertices...") | |
unlit_ts_vertices = [] | |
with open(map_path + '.004a.bsp_lump', 'rb') as f: | |
data = f.read() | |
elem_size = UnlitTSVertex.get_size() | |
for i in range(len(data) // elem_size): | |
unlit_ts_vertices.append(UnlitTSVertex(data[i*elem_size:(i+1)*elem_size])) | |
vertex_arrays = [ | |
[], | |
unlit_vertices, | |
bump_lit_vertices, | |
unlit_ts_vertices | |
] | |
# Read the material sort data | |
print("Reading material sort data...") | |
material_sorts = [] | |
with open(map_path + '.0052.bsp_lump', 'rb') as f: | |
data = f.read() | |
elem_size = MaterialSortElement.get_size() | |
for i in range(len(data) // elem_size): | |
material_sorts.append(MaterialSortElement(data[i*elem_size:(i+1)*elem_size])) | |
# Read mesh information | |
print("Reading mesh data...") | |
meshes = [] | |
with open(map_path + '.0050.bsp_lump', 'rb') as f: | |
data = f.read() | |
elem_size = MeshElement.get_size() | |
for i in range(len(data) // elem_size): | |
meshes.append(MeshElement(data[i*elem_size:(i+1)*elem_size])) | |
# Build combined model data | |
print("Building combined model data...") | |
combined_uvs = [] | |
mesh_faces = [] | |
texture_set = set() | |
for mesh_index in range(len(meshes)): | |
faces = [] | |
mesh = meshes[mesh_index] | |
if mesh.get_vertex_type().value==1 or mesh.get_vertex_type().value==3: | |
continue | |
mat = material_sorts[mesh.material_sort_index] | |
texture_set.add(texture_strings[textures[mat.texture_index].string_table_index]) | |
for i in range(mesh.num_triangles * 3): | |
vertex = vertex_arrays[mesh.get_vertex_type().value][mat.vertex_start_index + indices[mesh.indices_start_index + i]] | |
combined_uvs.append(vertex.texcoord0) | |
uv_idx = len(combined_uvs) - 1 | |
faces.append((vertex.vertex_pos_index + 1, uv_idx + 1, vertex.vertex_normal_index + 1)) | |
mesh_faces.append(faces) | |
# Build material files | |
print('Building material files...') | |
for i in range(len(textures)): | |
texture_string = texture_strings[textures[i].string_table_index] | |
# Work out the path to the actual texture | |
if os.path.isfile(dump_base + texture_string + '.png'): | |
path = dump_base + texture_string + '.png' | |
elif os.path.isfile(dump_base + texture_string + '_col.png'): | |
path = dump_base + texture_string + '_col.png' | |
else: | |
print('[!] Failed to find texture file for {}'.format(texture_string)) | |
path = 'error.png' | |
# # Write the material file | |
# with open('{}\\tex{}.mtl'.format(map_name, i), 'w') as f: | |
# f.write('newmtl tex{}\n'.format(i)) | |
# f.write('illum 1\n') | |
# f.write('Ka 1.0000 1.0000 1.0000\n') | |
# f.write('Kd 1.0000 1.0000 1.0000\n') | |
# f.write('map_Ka {}\n'.format(path)) | |
# f.write('map_Kd {}\n'.format(path)) | |
# Create obj file | |
print("Writing output file...") | |
with open(map_name+".obj", 'w') as f: | |
f.write('o {}\n'.format(map_name)) | |
for i in range(len(textures)): | |
f.write('mtllib tex{}.mtl\n'.format(i)) | |
for v in vertex_positions: | |
f.write('v {} {} {}\n'.format(*v)) | |
for v in vertex_normals: | |
f.write('vn {} {} {}\n'.format(*v)) | |
for v in combined_uvs: | |
f.write('vt {} {}\n'.format(*v)) | |
for i in range(len(mesh_faces)): | |
f.write('g {}\n'.format(i)) | |
f.write('usemtl tex{}\n'.format(material_sorts[meshes[i].material_sort_index].texture_index)) | |
faces = mesh_faces[i] | |
for i in range(len(faces) // 3): | |
f.write('f {}/{}/{} {}/{}/{} {}/{}/{}\n'.format(*faces[i*3], *faces[(i*3) + 1], *faces[(i*3) + 2])) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment