POF data structure
The following information was originally from the Descent Developer's Network pof specs page with additional clarity edits and fixes.
Introduction
These files are used to hold the ship model data. The data for the ship is basically a BSP tree that doesn't split polygons across the planes (since we're using zbuffering) and the planes are created by halving the largest dimension of each bounding box recursively. Due to the vastness of some of our models, this recursively split structure allows our collision detection and dynamic lighting to do quick out's on the model. For example, to check if a vector interects the model, I check against the bounding box. If it hits, I then check each of the two boxes it is split into. Then I recursively check each subdivided box as long as the vector is still interesecting.
We create the POF file by exporting a 3DS Max file with a special plugin tool to generate a ".P3D" file, which is essentially a 3d studio file only with texture uv's stored for each face vertex rather than for each vertex. We then run the .P3D file through an in-house Win32 program called 'BSPGEN' which creates the POF file. Depending on the model size, BSPGEN takes a few minutes or so.
Each ship is made up of a lot of subobjects where a subobject is a collection of polygons. For instance, each detail level of a hull of a ship is a subobject. A radar dish is a subobject. A piece of debris that the ship explodes into is a subobject. Each subobject can then have children subobjects. To draw the highest detail model of a ship, draw the subobject identified as detail level 0 and draw all it's children.
This document was written by John Slagel from Volition Inc., with revisions and bugfixing from Garry Knudson. FreeSpace 2 additions were made by Dave Baranec (Volition Inc.), Garry Knudson and Francis "Pastel" Avila.
POF block order list for retail FS2 models
Data types
This uses standard Intel data types.
(int) == 4 bytes, signed (uint) == 4 bytes, unsigned (short) == 2 bytes, signed (ushort) == 2 bytes, unsigned (char) == 1 byte, signed (ubyte) == 1 byte, unsigned (float) == 4 bytes, signed (vector) == 3 floats, 12 bytes total (string) == an int specifying length of string and char[length] for the string itself (note: must be null-terminated and 4 byte aligned, but PCS2-created .pofs do not adhere to this)
Basic file structure
The file format is a binary file using standard Intel data types. The header consists of a signature and the file version number:
char[4] file_id // must be 'PSPO' int version // Major*100+Minor
Notes:
- PSPO stood for Parallax Software Polygon Object at one time. Conflict, Descent: FreeSpace shipped at POF version 20.14, so version would be equal to 2014 in most POF files (not all). FreeSpace 2 shipped at POF version 21.17, some files are 21.16. POF version 22 was added in June 2021; this version ensures data alignment and SLC2 chunk presence. Versions 21.18 and 22.01 add support for gun submodel offset angles.
- The rest of the file is a bunch of chunks. Each chunk is:
char[4] chunk_id // see below for available chunk types int length // length of the chunk.
Note: Contrary to BSP_Data structure, in pof data structure "Length" does not considers the first 8 bytes of the chunk (chunk id+length), so in order to go from the begining of the chunk to the next one you need to do lenght+8. Media:Pof_chunk.png
Chunk specs
Here is a breakdown of each of the chunk types:
- 'OHDR' (FreeSpace 1) and 'HDR2' (FreeSpace 2) - Object header info
#ifdef version2116orhigher // FreeSpace 2 float max_radius // maximum radius of entire ship int obj_flags // object flags. Bit 1 = Textures contain tiling int num_subobjects // number of subobjects #else // FreeSpace 1 int num_subobjects // number of subobjects float max_radius // maximum radius of entire ship int obj_flags // object flags. Bit 1 = Textures contain tiling #endif vector min_bounding // min bounding box point vector max_bounding // max bounding box point int num_detaillevels // number of detail levels for each detail_level, i { int sobj_detailevels[i] // subobject number for detail level I, 0 being highest. } int num_debris // number of debris pieces that model explodes into for each debris piece, i int sobj_debris[i] // subobject number for debris piece i #ifdef version1903orhigher float mass // see notes below vector mass_center // center of mass float[3][3] moment_inertia // moment of inertia #endif #ifdef version2014orhigher int num_cross_sections // number of cross sections (used for exploding ship) (*) for each cross_section, i { float depth[i] float radius[i] } #endif // these are currently unused by the engine #ifdef version2007orhigher int num_lights // number of precalculated muzzle flash lights for each light { vector location int light_type // type of light } #endif
- Notes:
for version<2009, mass is a volume mass for version>=2009, mass is an area mass conversion: area_mass=4.65*(vol_mass^2/3); also scale moment_inertia by vol_mass/area_mass (*) if there is no cross_section data, num_cross_sections is -1 instead of 0, as one would expect.
- 'TXTR' - A list of textures used on this ship. The order they appear here is the number that a face uses to reference a particular texture.
int num_textures for each texture,i int tex_filename_length[i] string tex_filename[i] // texture filename
- 'PINF' - Miscellaneous info about the POF file, command line, etc.
Contains a block of NULL-terminated strings. Just read chunk size bytes and stuff it into a string.
- 'PATH' - Paths for docking and AI ships to follow
int num_paths for each path, p { int name_length string name[p] // name int parent_length string parent[p] // parent's name int num_verts[p] for each vert, v { vector pos[p,v] float radius[p,v] int num_turrets[p,v] // the following data is unused by the engine for each turret, t { int sobj_number[p,v,t] // subobject number } } }
- 'SPCL' - Data for special points
int num_special_points for each special_point, p { int name_length[p] string name[p] int properties_length[p] string properties[p] vector point[p] float radius[p] }
- 'SHLD' - Data for the shield mesh
int num_vertices for each vertex, v { vector position[v] } int num faces for each face, f { vector face_normal[f] int[3] face_vertices // indexed into vertex list int[3] neighbors // indexed into face list }
- 'EYE ' - Data for eye points (Where pilot looks in 1st person views). Note the space after EYE.
int num_eye_positions for each eye_position, e { int sobj_number[e] // subobject number this eye is attached to vector sobj_offset[e] // offset from subobject vector normal[e] }
- 'GPNT' and 'MPNT' - Gun and Missile firing points
int num_banks for each bank, s { int num_guns[s] for each gun, g { vector point[g,s] vector norm[g,s] #ifdef version2201orhigher float offset[g,s] #endif } }
- Notes:
A "bank" is what you see in the loadout screen. Primaries have a max of 2 and secondaries of 3 for player-flyable ships. "Guns" are the actual number of gunpoints and hence projectiles you'll get when you press the trigger. There is likely no practical max. "Offset" refers to external_model_angle_offset which allows weapon models mounted on this gunpoint to be rotated from the ship's orientation. This was added in 21.18 and 22.01.
- 'TGUN' and 'TMIS' - Turret Gun and Turret Missile firing points.
int num_turrets for each turret, t { int sobj_base[t] // the base subobject of this turret (*) int sobj_gun[t] // the gun subobject, or barrels of this turret(*) vector turret_normal[t] int num_firing_points[t] for each firing_point { vector position[t,f] } }
- Notes:
For multipart turrets, sobj_gun is the "barrel" of a turret, and the firing points will be in this sobj's axial frame. sobj_base is the "base" of a turret, which the barrel will rotate with. For single-part turrets, sobj_base == sobj_gun.
- 'DOCK' - Data for docking points
int num_docks for each dock, d { int properties_length string properties[d] // see notes below int num_spline_paths[d] // although multiple paths can be associated to the docking point, only the first will be used by the engine for each spline_path, p { int path_number[d,p] } int num_points for each point, d { vector position vector normal } }
- Note: Properties… if $name= found, then this is name. If name is cargo then this is a cargo bay.
- 'FUEL' - Data for engine thruster glows
int num_thruster_banks for each thruster_bank,t { int num_glows[t] #ifdef version2117orhigher // FreeSpace 2 change int properties_length string properties #endif for each glow { vector pos[t,g] vector norm[t,g] // used to tell if behind glow float radius[t,g] } }
- 'SOBJ' (FreeSpace 1) and 'OBJ2' (FreeSpace 2) - Data for a subobject. Contains some info and a bunch of vertices and polygons in the form of a BSP tree or Octree depending on how you look at it.
int submodel_number // What submodel number this is. #ifdef version2116orhigher // FreeSpace 2 float radius // radius of this subobject int submodel_parent // What submodel is this model's parent. Equal to -1 if none. vector offset // Offset to from parent object <- Added 09/10/98 #else // FreeSpace 1 int submodel_parent // What submodel is this model's parent. Equal to -1 if none. vector offset // Offset to from parent object <- Added 09/10/98 float radius // radius of this subobject #endif vector geometric_center vector bounding_box_min_point vector bounding_box_max_point int submodel_name_length string submodel_name int properites_length string properites int movement_type int movement_axis int reserved // must be 0 int bsp_data_size // number of bytes now following char[bsp_data_size] bsp_data // contains actual polygons, etc.
- Note: bsp_data is explained here
- 'INSG' - Squad logo/Insignia data chunk (FreeSpace 2 only)
int num_insignias for each insignia, i { int detail_level // ship detail level int num_faces int num_vertices for each vertice, j { vector vertex_position } vector offset // offset of the insignia in model coords for each face, j { for 0 to 2, k { int vertex_index // vertex index for this face float u_texture_coordinate float v_texture_coordinate } } }
- 'ACEN' - Auto-Centering info (FreeSpace 2 only)
vector point // autocentering point for the entire model
- The autocentering point was basically just a little convenient extra data we stuck in models where the pivot point of the model wasn't at the center. Since we rotate ships in the tech room by rotating around the pivot point, it looked dumb when the colossus' rear end was in the middle of the screen and it was spinning on it. So the autocenter point was just a point pretty much near the model we could use to push an extra matrix onto our transform stack and have it show up centered around it. The point itself is in model coordinates.
- 'GLOW' - Glowpoint info (FS2_Open only)
int num_glowbanks for each glowbank, g { int disp_time // displacement time for blinking (e.g. for Orion runway lights) int on_time int off_time int obj_parent // parent subobject number int LOD // Should be 0 int type int num_glowpoints // Number of glowpoints in this bank int properties_length string properties // Texture name, no file path or extension, like: "$glow_texture=..." for each glowpoint, p { vector point // location vector vector norm // (0,0,0) is omnidirectional float radius } }
- 'SLDC' - Shield mesh collision tree info (FS2_Open only)
uint tree_size for each node, n { ubyte type // 0 = SPLIT, 1 = LEAF/polylist uint size if !type { // SPLIT vector bound_min vector bound_max uint front_offset uint back_offset } else { // LEAF vector bound_min vector bound_max uint num_polygons for each polygon, p { uint polygon_addr // indexed into shield mesh face list? } } }
- The shield mesh collision tree speeds up collision calculation by adding bounding boxes around each polygon in the shield mesh. It works basically like a BSP tree.
- The first byte of each node is a size 1 char, this makes all the following data to be in unaligned memory access positions, thus making the SLDC system, as it is, incompatible with ARM arch.
- 'SLC2' - Revised version of Shield mesh collision tree info (FS2_Open and POF version 2118 only)
uint tree_size for each node, n { int type // 0 = SPLIT, 1 = LEAF/polylist uint size if !type { // SPLIT vector bound_min vector bound_max uint front_offset uint back_offset } else { // LEAF vector bound_min vector bound_max uint num_polygons for each polygon, p { uint polygon_addr // indexed into shield mesh face list? } } }
- The shield mesh collision tree speeds up collision calculation by adding bounding boxes around each polygon in the shield mesh. It works basically like a BSP tree.
- Revised version of SLDC chunk, now with a int for type to ensure data alignment.