P3D File Format - ODOLV4x
Introduction
Acknowledgements
This body of work is due to Synide's sweat and tears. To whom, all honour and glory. Ably assisted by T_D and Mikero that further detailed the data and gave this article a more general and correct structure.
General
The general format of an ArmA ODOLV4x p3d model is similar to the ODOLV7 format. The major differences are that ArmA models have
- an optional model.cfg, and
- Lods occur in the file from highest to lowest LodType value.
Legend
see Generic FileFormat Data Types
Relative Coordinates
All coordinates are relative to ModelInfo.CentreOfGravity
File Paths
The PrefixRoot\ folder.
Life for modellers would be far less tedious if filenames could also be relative to the p3d they are encountered in. Altering or moving or renaming the pbo (and specifically it's prefix) would not alter the relative location of the paa's it contains.
BI choose to use hard-wired Pbo-Prefix-addressing ONLY.
All hardwired addressing is relative to a built-in-situ (ie virtual) PrefixRoot\ folder
Each and every pbo in Arma contains a unique identity name, a prefix. Irrespective of the name of the pbo, the prefixname is THE name of the pbo from the perspective of the engine. In most cases, the prefixname is, conveniently, the filename. One huge advantage of this mechanism, sorely sorely missed in OFP. is that self-documenting increasing revisions of an addonV123.pbo can be supplied to Arma, with no changes to the mission sqms and other pbos that refer to it.
The PrefixRoot\ folder contains the prefix names of all pbos encountered (almost) ANYWHERE.
Thus the pbos in the Official Addons folder, the Oem Mods\Addons folder(s), the Dta core and bin pbo's, are all examined for their unique prefix names. These prefix names become the dictionary index of where the pbo really is, AND, what filename it actually is.
Thus all filename references in a p3d, *unconditionally* contain a prefixname\someFile\SomeWhere.
In most cases they refer to the very same pbo as the containing p3d and a great pity that the extraneous information could not have been removed by (optional) relative addressing as it requires a great deal of fiddling about when modifying models.
Note also that there is some inconsistency in filename paths. Most do not have a leading \. Some, require it. Both are indeed \hardwired
An Example:
P3dProxyName ="\ca\a10\agm65";
The immediate (and unfortunate) impression is that there is an A10 folder inside the official CA.pbo addon. In fact, the prefix of the A10.pbo = "ca\A10". Thus this reference is to the A10.pbo within which, is a agm65.p3d in it's root folder. (and again, this reference is in fact an extraneous reference to itself since the referring p3d (A10.p3d) is in the same pbo)
Versions
This Document covers ODOL versions:
V40 (Arma1)
- Original Arma1 binarised p3d
V43 Arma2)
• ModelInfo now has a 24 byte thermal profile appended, making it same as vbs2.
• An extra byte at end of Skeleton structure: always 0
- LZSS compression is still used at this level
V47
As per V43 plus:
• all compressed blocks are LZO compressed
• CompressedMinMax block is now nMinMax*8 in size
• CompressedNormals block is now nNormals*4 in size
- LodFrame has 4 extra floats
• UVSet structure changed to:
LodUV { float uvScaling[4]; ulong nVertices; tbool DefaultFill; if (DefaultFill) float UV; // default fill for all nVertices else float UV[nVertices]; // potentially compressed }
V48
As per V47 plus:
• ModelInfo has a 4 byte appendix
V49 (ArmA2:OA)
As per V48. No known differences apart from version number
File Format
ODOLv4x { StandardP3DHeader Header; ModelInfo ModelInfo; // see P3D Model Info Animations Animations; ulong StartAdressOfLods[Header.NoOfLods];// offset relative to start of file. ulong EndAdressOfLods [Header.NoOfLods]; LODFaceDefaults LODFaceDefaults; ODOLv40Lod ODOLv40Lods[Header.NoOfLods]; }//EndOfFile
Structures
StandardP3DHeader
struct { char[4] Filetype; // "ODOL" ulong Version; // 40 ulong NoOfLods; // alias NoOfResolutions; }
common header structure for all P3D file formats
Animations
Animations { tbool AnimsExist; if (AnimsExist) { ulong nAnimationClasses; // eg NoOfAnimSelections; AnimationClass AnimationClasses[nAnimationClasses]; long NoOfResolutions;// is -1 if nAnimationClasses == 0 Bones2Anims Bones2Anims[NoOfResolutions]; Anims2Bones Anims2Bones[NoOfResolutions]; //For every bone there is a list of Animations for each resolution //And, a reversed table of every Animation gets a Bone. //The reversed table optionally appends axis info dependent on the AnimTransformType } }
AnimationClass
AnimationClass
{
ulong AnimTransformType;
asciiz AnimClassName; // "RightDoor"
asciiz AnimSource; // "rotor"
float MinMaxValue[2];
float MinMaxPhase[2];
ulong sourceAddress;
switch(AnimTransformType)
case 0://rotaton
case 1://rotationX
case 2://rotationY
case 3://rotationZ
float angle[2];
break;
case 4://translation
case 5://translationX
case 6://translationY
case 7://translationZ
float offset[2];
break;
case 8: //"direct"
float axisPos[3];
float axisDir[3];
float angle; //in radians whereas the model.cfg entry is in degrees
float axisOffset;
break;
case 9: //"hide"
float hideValue;
break;
}
corresponds to model.cfg class CfgModels { .... class whateverModel: Default { ... class Animations { class RightDoor //AnimClassName { type = "translation";//AnimTransformType source = "rotor"; //AnimSource etc
Bones2Anims
Bones2Anims { ulong NoOfBones; Bone2AnimClassList Bone2AnimClassLists[NoOfBones]; }
Bone2AnimClassList
Bone2AnimClassList { ulong NoOfAnimClasses; ulong AnimationClassIndex[NoOfAnimClasses]; // a (sometimes repeating) list of zero based indexes into above animation classes }
Anims2Bones
Anims2Bones { AnimBones AnimBones[Animations.nAnimationClasses]; }
AnimBones
every lod contains an identical list of animation entries that declare the position and axis of the each animation classes
AnimBones { long SkeletonBoneNameIndex; // zero based index to the SkeletonBoneName name & parentname // equivalent to selection = "LeftDoor"; eg in the model.cfg /* ** SkeletonBoneNameIndex== -1 when no skeleton bone is for this Anim and (obviously?) no axis information follows. */ if (SkeletonBoneNameIndex!= -1) && (AnimationClass.AnimTransformType != 8 || 9) { /* ** AnimationClass.AnimTransformType 8 (direct) and 9 (hide) never require axis information. ** This because the "direct" (type 8) already has axis info in it's AnimationClass structure, ** and "hidden" (type 9) clearly doesn't need it. */ XYZTriplet axisPos; //describes the position of the axis used for this anim XYZTriplet axisDir; } }
LODFaceDefaults
tbool UseDefault[Header.NoOfLods]; FaceData { ulong HeaderFaceCount; bytes Unknown[13]; }[Number of false UseDefault's];
A face data struct only exists for those lods who's UseDefault is zero
ODOLv4xLod
- Lod layout corresponds to Arma1 (type40). The differences in a2 are in the nitty gritty of the structures themselves. Arrowhead(v50) has some changes.
- TrueArma2 == type 47 or greater (lzo compression)
- Type 43 was a preliminary p3d prior to lzo compression. rarely encountered
ODOLv4xLod { ulong nProxies; LodProxy LodProxies[nProxies]; // see P3D Lod Proxies ulong nLodItems; ulong LodItems[nLodItems]; // potentially compressed ulong nBoneLinks; LodBoneLink LodBoneLinks[nBoneLinks]; if v50 ulong LodPointCount; else LodPointFlags LodPointFlags; // Potentially compressed endif float UnknownFloat1; float UnknownFloat2; XYZTriplet MinPos; XYZTriplet MaxPos; XYZTriplet AutoCenterPos; float Sphere; // same as geo or mem values in modelinfo, if this lod is geo or memlod of course ulong NoOfTextures; asciiz LodPaaTextureNames[NoOfTextures]; //"ca\characters\hhl\hhl_01_co.paa" ulong NoOfMaterials; LodMaterial LodMaterials[NoOfMaterials]; LodEdges LodEdges; // compressed see P3D Lod Edges ulong NoOfFaces; ulong OffsetToSectionsStruct; // see below ushort AlwaysZero; LodFace LodFace[NoOfFaces]; // see P3D Lod Faces ulong nSections; LodSection LodSections[nSections]; // see P3D Lod Sections ulong nNamedSelections; LodNamedSelection LodNamedSelections[nNamedSelections]; //See P3D Named Selections potentially compressed ulong nTokens; NamedProperty NamedProperties[nTokens]; ulong nFrames; LodFrame LodFrames[nFrames]; //see P3D Lod Frames ulong IconColor; ulong SelectedColor; ulong UnknownResidue byte UnknownArmaByte; ulong sizeOfVertexTable; //(including these 4 bytes) if (v50) LodPointFlags LodPointFlags; // Potentially compressed endif VertexTable VertexTable; }
VertexTable
all arrays are subject to compression
struct { UvSet DefaultUVset; ulong nUVs; //in error, V47 sometimes sets nUV's as 0 but means 1 UvSet UVSets[nUVs-1]; ulong NoOfPoints; XYZTriplet LodPoints[NoOfPoints]; ulong nNormals; (A2)LodNormals LodNormals[nNormals]; ulong nMinMax; (A2)LodMinMax MinMax[nMinMax]; //optional ulong nProperties; VertProperty VertProperties[nProperties];//optional related to skeleton ulong Count; UnknownVtxStruct UnknownVtxStructs[Count]; //optional }
- All non zero counts counts are the same.
- Points,PointFlags, Normals and UV1 arrays are an integral group, they are either all there, or not specified (RacetK.p3d, a FrameTime lod has no counts at all)
- UV2,MinMax, VertProperties and Unknown are optional in the sense that their counts can individually be zero, else they are the same as the others
- In Odol7 PointFlags are part of this stucture, in Arma, they are a separated table.
CompressedFill Arrays
LodPointFlags, LodUV's and LodNormals arrays are not only subject to the standard 1024 rule compression, but also have a fill byte.
struct { ulong Count; tbool DefaultFill; if (DefaultFill) type Array; // default fill for all Counts else type Array[Count]; // potentially compressed }
The structure either contains a single set of type variables, or, an array of type variables. If a full array is declared (DefaultFill =false) then that array is subject to the 1024 rule as per normal.
UVset
if TrueARMA2 float UVScale[4]; endif (A2)LodUV LodUV;
LodUV
CompressedFill type = UVPair // eg float U,V;
A2LodUV
CompressedFill type = float // eg float UV;
LodNormals
CompressedFill type = XYZTriplet
A2LodNormals
CompressedFill type = CompressedXYZTriplet
CompressedXYZTriplet
contains 3 x 10 bit fields in a 32bit 'integer'
code for converting back to a standard XYZTriplet is:
void DecodeXYZ(ulong CompressedXYZ, XYZTriplet *triplet) { double scaleFactor = -1.0 /511; trp->X=trp->Y=trp->Z=0.0; int x= CompressedXYZ & 0x3FF; int y = (CompressedXYZ>> 10) & 0x3FF; int z = (CompressedXYZ>> 20) & 0x3FF; if (x > 511) x -= 1024; if (y > 511) y -= 1024; if (z > 511) z -= 1024; if (x) trp->X = (float)(x * scaleFactor); if (y) trp->Y = (float)(y * scaleFactor); if (x) trp->Z = (float)(z * scaleFactor); }
LodPointFlags
CompressedFill type = ulong bits
This table is the equivalent of Oxygen's points->properties dialog box. It specifically stores the user values and other flags for that point.
In ODOl7 it was part of the vertex table. In Arma, it is separate.
LodMinMax
CompressedArray { XYZTriplet MinMax[Count][2]; // 2 == min vs max }
A2LodMinMax
CompressedArray { float MinMax[Count][2]; // 2 == min vs max }
VertProperty
CompressedArray { ulong index;// seen range 0..4 ulong V,V; // definate not floats. might be flags, or indices }
UnknownVtxStruct
CompressedArray { ulong Unknown[2][2]; // seems to be arranges as flag,0, 0, 0, }
LodBoneLink
LodBoneLink { ulong NoOfLinks; //range 0..3 ulong Value[NoOfLinks]; //the 'Value' seems to reference the 'LodItems' structure, resulting in a circular-reference. }
LodMaterial
//Basically... A direct replication of the information in the given .rvmat file LodMaterial { asciiz RvMatName; // "ca\characters\data\soldier_captive_hhl.rvmat" ulong Type; // 9 == Arma, 10==VBS2,11==Arrowhead D3DCOLORVALUE Emissive; D3DCOLORVALUE Ambient; D3DCOLORVALUE Diffuse; D3DCOLORVALUE forcedDiffuse; D3DCOLORVALUE Specular; D3DCOLORVALUE Unknown; //Usually same as Specular float SpecularPower; ulong PixelShaderId; //See enumPixelShaderId ulong VertexShaderId;//See enumVertexShaderId LongBool NoAlphaWrite; //mostly 1 otherwise 0 ulong AnIndex; //0,1 or 2 asciiz BiSurfaceName; // "ca\data\Penetration\plastic.bisurf" LongBool Mostly0x01; // rarely zero ulong aCount; //Generally 0 ulong nTextures; ulong nTransforms; // always same as nTextures LodStageTexture StageTextures [nTextures]; LodStageTransform StageTransforms[nTransforms]; if type>=10//vbs2/arma2 LodStageTexture DummyStageTexture; if type==11//arrowhead byte Always0; endif endif }
- If there are *any* lodmaterialse, then there is always one default Texture and Transform as the first entry.
- It is the only entry if a SurfaceName exists.
D3DCOLORVALUE
D3DCOLORVALUE { float r,g,b,a; }
LodStageTexture
LodStageTexture { ulong TextureFilter; // see below asciiz PaaTexture; // "ca\characters\data\civil_tvreport_body_as.paa // alternatively "#(argb,8,8,3)color(0,0,0,1,CO)" (eg) ulong TransformIndex; // zero based, see below };
- The first stageTexture is a dummy entry. For N humanly readable stage classes, there are (at least) 1+N LodStageTextures
- The TransformIndex is generally iterative (linear sequential). 1st entry is 0, 2nd 1, 3rd 2, etc. It refers to the nTH Transform Matrix
Later p3d formats (VBS2, Operation Arrowhead) append one or more LodStageTextures in addition to above, the latter are generally dummy, and generally refer to the first Transform matrix (TransformIndex=0) (also a dummy)
- TextureFilter maybe 1 of the following values.
- 0: Point // sometimes
- 1: Linear // rarely
- 2: TriLinear // not seen
- 3: Anisotropic (default)
LodStageTransform
LodStageTransform { ulong UVSource; float Transform[4][3];//a DirectX texture space transform matrix };
NamedProperty
struct { Asciiz Property;// "noshadow" = "1" eg Asciiz Value; }
Decompression
see Compressed LZSS File Format
see Compressed LZO File Format
In ODOL v40 and v43 format files, some of the data structures present in the file are compressed by using LZSS compression.
ODOL v47 and v48 use LZO compression.
Unlike pbo compression, in ArmA model files, one only knows the number of items to decompress, the expected output size (in bytes) and the expected checksum. With this information and the size of a given data item one has the necessary information to expand the data to it's original format and size.
Note:- Data structures that are identified as being compressible will only be compressed if the 'expectedSize' is >= 1024 bytes.
As an example if one was expanding the array of vertices positions...
- A vertex is described by it's x,y,z coordinates which are floats. A float is a 32bit (4 byte) number.
- If we were processing 1968 vertices then our expected output size would be 1968 * (3 * 4) = 23,616 bytes.
This 'expectedSize' is the only necessary information one would need to pass to a processing sub-routine or function.
Reference Tables
Material Stages
The number of material stages is dependant on the type of Shader that is used to process the material by the ArmA game engine. A reference table is used when processing materials where depending on the shader specified the given number of stages should be processed.
refShaderStages
{
int PixelShaderId;
int NoOfStages;
};
ID (Hex/Decimal) | Name | Description | NoOfStages |
---|---|---|---|
0x00, 0 | Normal | diffuse color modulate, alpha replicate | 0 |
0x01, 1 | NormalDXTA | diffuse color modulate, alpha replicate, DXT alpha correction | 0 |
0x02, 2 | NormalMap | normal map shader | 3 |
0x03, 3 | NormalMapThrough | normal map shader - through lighting | 3 |
0x04, 4 | NormalMapSpecularDIMap | ? | 2 |
0x05, 5 | NormalMapDiffuse | ? | 2 |
0x06, 6 | Detail | ? | 1 |
0x07, 7 | ? | ? | ? |
0x08, 8 | Water | sea water | 2 |
0x09, 9 | ? | ? | ? |
0x0A, 10 | White | ? | 0 |
0x0B, 11 | ? | ? | ? |
0x0C, 12 | AlphaShadow | shadow alpha write | 0 |
0x0D, 13 | AlphaNoShadow | shadow alpha (no shadow) write | 0 |
0x0E, 14 | ? | ? | ? |
0x0F, 15 | DetailMacroAS | ? | 3 |
0x10, 16 | ? | ? | ? |
0x11, 17 | ? | ? | ? |
0x12, 18 | NormalMapSpecularMap | ? | 2 |
0x13, 19 | NormalMapDetailSpecularMap | Similar to NormalMapDiffuse | 3 |
0x14, 20 | NormalMapMacroASSpecularMap | ? | 4 |
0x15, 21 | NormalMapDetailMacroASSpecularMap | ? | 5 |
0x16, 22 | NormalMapSpecularDIMap | Same as NormalMapSpecularMap, but uses _SMDI texture | 2 |
0x17, 23 | NormalMapDetailSpecularDIMap | ? | 3 |
0x18, 24 | NormalMapMacroASSpecularDIMap | ? | 4 |
0x19, 25 | NormalMapDetailMacroASSpecularDIMap | ? | 5 |
0x38, 56 | Glass | ? | 2 |
0x3A, 58 | NormalMapSpecularThrough | ? | 3 |
0x3B, 59 | Grass | Special shader to allow volumetric shadows to be cast on grass clutter | 0 |
0x3C, 60 | NormalMapThroughSimple | ? | 0 |
Enums
int enum PixelShaderId
{
Normal = 0x00,
NormalMap = 0x02,
NormalMapDiffuse = 0x05,
NormalMapMacroASSpecularMap = 0x14,
NormalMapSpecularDIMap = 0x16,
NormalMapMacroASSpecularDIMap = 0x18,
AlphaShadow = 0x0C,
AlphaNoShadow = 0x0D,
Glass = 0x38,
Detail = 0x06,
NormalMapSpecularMap = 0x12
}
int enum VertexShaderId
{
Basic = 0x00,
NormalMap = 0x01,
NormalMapAS = 0x0F
}
Links
Article Author - Sy (Synide) -- Sy 17:16, 11 August 2007 (CEST)
Original ODOLv40 Article detailed by Bxbx (Biki'd by Mikero)