Skip to content

Instantly share code, notes, and snippets.

@warmist
Last active January 9, 2022 08:25
Show Gist options
  • Save warmist/8cd30ea72388385041836a872ed5470e to your computer and use it in GitHub Desktop.
Save warmist/8cd30ea72388385041836a872ed5470e to your computer and use it in GitHub Desktop.
ImHex pattern and lua file for reading titanfall2 navmesh file format
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
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] @ $;
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)
# 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]))
dtLink u32 at the end (might be padding, but also some sort of ids?)
dtPolyDetail u16 at the end (also maybe padding)
dtDetailTris u8 at the end (same)
dtPoly unk2 and pos. Unk2 could be padding, pos might be center of the poly or sth...
dtOffmeshconnection there is vec3 (float*3) and another float at the end with unk use
link array (u32 each) count is maxLinkCount*polyCount. Looks like some sort quicker link lookup? can't find use places
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment