P3D File Format - ODOLV4x: Difference between revisions

From Bohemia Interactive Community
m (Text replacement - "\[\[Category:BIS( |_)File( |_)Formats\]\]" to "Category:Real Virtuality File Formats")
 
(239 intermediate revisions by 6 users not shown)
Line 1: Line 1:
== ODOL v40 File Format ==
{{TOC|side}}
{{Feature|UnsupportedDoc}}
<!--


{{unsupported-doc}}
Wiki Editors: DO NOT DOCUMENT ANYTHING AFTER ARMA 1


=== Introduction ===
-->
== Introduction ==


The general file format of a ArmA ODOL v40 p3d model file is similar to the ODOL v7 format.
=== Acknowledgements ===
The major differences are that in ArmA models there is now included (but not always) a model.cfg and the resolutions are ordered in the file in reverse numerical order.


The order of resolutions denoted in the header portion of the file is <b>not</b> necessarily the numerical order of the resolutions. (often the 11,000 resolution is the last in the header array)
This body of work is due to Synide's sweat and tears. To whom, all honour and glory.
The header resolutions need to be sorted in descending order. The resultant sorted array of resolutions is the order in which they appear in the file.
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


<b>''NOTE:- As at Aug, 13th 2007, this file format is not conclusive and final. It may or may not fit the actual model file format of an Armed Assault ODOL v40 p3d model.''</b>
*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 [[P3D Model Info|ModelInfo]].CentreOfGravity


==== Legend ====
==== File Paths ====
 
'''All''' file references are absolute to the Linux \ (root).


{| border="0"
As a convenience A drive letter is used (such as P:\) for sanity when modelling. The engine knows nothing about C:\my documents or even P:
!width="100"|Type
!width="300" align="left"|Description
|-
|-
|align="middle"|byte||align="left"| 8 bit (1 byte)
|-
|align="middle"|ushort||align="left"| 16 bit unsigned short (2 bytes)
|-
|align="middle"|int||align="left"| 32 bit signed integer (4 bytes)
|-
|align="middle"|float||align="left"| 32 bit signed single precision floating point value (4 bytes)
|-
|align="middle"|asciiz||align="left"| Null terminated (0x00) variable length ascii string
|-
|-
|}


=== Enums ===
The leading \ is optional.


<code><nowiki>
\my_project\data\some.paa AND
int enum PixelShaderId
my_project\data\some.paa
{
  Normal = 0x00,
  NormalMap = 0x02,
  NormalMapDiffuse = 0x05,
  NormalMapMacroASSpecularMap = 0x14,
  NormalMapSpecularDIMap = 0x16,
  NormalMapMacroASSpecularDIMap = 0x18,
  AlphaShadow = 0x0C,
  AlphaNoShadow = 0x0D,
  Glass = 0x38,
  Detail = 0x06,
  NormalMapSpecularMap = 0x12
}
</nowiki></code>


<code><nowiki>
point to the same file.
int enum VertexShaderId
{
  Basic = 0x00,
  NormalMap = 0x01,
  NormalMapAS = 0x0F
}
</nowiki></code>


=== Structures ===
=== Versions ===


==== structP3DHeader ====
This Document covers ODOL versions:
<code><nowiki>
struct structP3DHeader
{
  asciiz                  Filetype; //eg. ODOL
  int                      Version;    //eg. 0x2800 0000 = 40
  int                      NoOfResolutions;
  float[NoOfResolutions]  HeaderResolutions;
}
</nowiki></code>


==== structBone ====
==== V40 ({{arma1}}) ====
<code><nowiki>
  structBone
  {
    asciiz Bone;
    asciiz Parent;
  }
</nowiki></code>


==== structSkeleton ====
* Original {{arma1}} binarised p3d
<code><nowiki>
  structSkeleton
  {
    asciiz                  SkeletonName;
    if (SkeletonName != null)
    {
      bool                    isInherited;
      int                    NoOfBones;
      structBone[NoOfBones]  Bones;
    }
  }
</nowiki></code>


==== structAnimation ====
<code><nowiki>
  structAnimation
  {
    int        AnimTransformType;
    asciiz      AnimSelection;
    asciiz      AnimSource;
    if (AnimTransformType == 9)
      {
        float[6] Transforms;
      }
      else
      {
        float[7] Transforms;
      }
  }
</nowiki></code>


==== structProxy ====
== File Format ==
<code><nowiki>
  structProxy
  {
    asciiz      ProxyName;
    float[12]  ModelProxyUnknown1;
    int[4]      ModelProxyUnknown2;
  }
</nowiki></code>


==== structStage ====
<syntaxhighlight lang="cpp">
<code><nowiki>
ODOLv4x
  structStage
{
  {
StandardP3DHeader Header;
    asciiz  StageTexture;
     struct ModelInfo;
     int      Stage;
Animations Animations;
    int      UVSource;
ulong StartAddressOfLods[Header.NoOfLods]; // offset relative to start of file.
    float[3] aside;
ulong EndAddressOfLods  [Header.NoOfLods];
    float[3] up;
LODFaceDefaults LODFaceDefaults;
    float[3] dir;
ODOLv40Lod ODOLv40Lods[Header.NoOfLods];
    float[3] pos;
} // EndOfFile
  }
</syntaxhighlight>
</nowiki></code>


==== structMaterial ====
<code><nowiki>
  structMaterial
  {
    asciiz        Material;
    float[4]      Emissive;
    float[4]      Ambient;
    float[4]      Diffuse;
    float[4]      forcedDiffuse;
    float[4]      Specular;
    float          SpecularPower;
    int            PixelShaderId;
    int            VertexShaderId;
    structStage[]  Stages;
  }
</nowiki></code>


<code><nowiki>
== Structures ==
    //Basically... A direct replication of the information in the given .rvmat file
    for (int i = 0; i < NoOfMaterials; i++)
    {
      asciiz Material;
      byte[4] byteArrayMaterialUnknown1;
      float[4] Emissive;
      float[4] Ambient;
      float[4] Diffuse;
      float[4] forcedDiffuse;
      float[4] Specular;
      float    SpecularPower;
      int      PixelShaderId; //See enumPixelShaderId
      int      VertexShaderId; //See enumVertexShaderId
      //Based on the enumPixelShaderId that matches this PixelShaderId process a variable 'NoOfStages'
      //by default one should probably process 2 stages as this seems the most common amount
      if (NoOfStages > 0)
        {
          byte[34]  byteArrayMaterialUnknown2;
          for (int i = 0; i < NoOfStages; i++)
          {
            byte[4]  byteArrayMaterialUnknown3;
            asciiz    StageTexture;
            int      StageNumber;
          }
          for (int i = 0; i < NoOfStages; i++)
          {
            int      UVSource;
            float[3] aside;
            float[3] up;
            float[3] dir;
            float[3] pos;
          }
          byte[52] byteArrayMaterialUnknown4; //Possibly default values for a stage as same struct size
        }
        else
        {
          byte[86] byteArrayMaterialUnknown5;
        }
     
    }//EndOfMaterials
</nowiki></code>


=== StandardP3DHeader ===


==== structPolygons ====
Common header structure for all P3D file formats.
<code><nowiki>
  structPolygons
  {
    byte NoOfVertices; // 3 or 4
    ushort[NoOfVertices] VerticesIndex; // 0-based index into Vertices Arrays
  }
</nowiki></code>


<syntaxhighlight lang="cpp">
struct
{
char[4] Filetype; // "ODOL"
ulong Version;
ulong NoOfLods; // alias NoOfResolutions;
}
</syntaxhighlight>


==== structComponent ====
[[P3D Model Info|ModelInfo]]
<code><nowiki>
  structComponent
  {
    asciiz ComponentName;


    //Selected Faces
=== Animations ===
    int NoOfSelectedFaces;
    ushort[NoOfSelectedFaces] SelectedFaceIndexes;


    int intComponentUnknown1;
<syntaxhighlight lang="cpp">
    bool bComponentUnknown1;
Animations
    if (bComponentUnknown1)
{
    {
tbool AnimsExist;
      int NoOf;
if (AnimsExist)
      int[NoOf] intArrayComponentUnknown1;
{
    }
ulong nAnimationClasses; // eg NoOfAnimSelections;
    else
AnimationClass AnimationClasses[nAnimationClasses];
    {
      int intComponentUnknown2;
    }


    // Selected Vertices
long NoOfResolutions; // is -1 if nAnimationClasses == 0
    int NoOfSelectedVertices;
Bones2Anims Bones2Anims[NoOfResolutions];
    ushort[NoOfSelectedVertices] SelectedVerticesIndexes; // Zero based array of index values into
Anims2Bones Anims2Bones[NoOfResolutions];
                                                          // the array of Vertices.
// For every bone there is a list of Animations for each resolution
                                                          // </nowiki><b>''Note:-''</b><nowiki> If expectedSize >= 1024 bytes this array is
// And, a reversed table of every Animation gets a Bone.
                                                                      compressed.
// The reversed table optionally appends axis info dependent on the AnimTransformType
}
}
</syntaxhighlight>


    // Selected Vertices Properties
=== AnimationClass ===
    int NoOfSelectedVertices;
    byte[NoOfSelectedVertices] SelectedVerticesProperties; // Zero based array of index values into
                                                          // the array of Vertices.
                                                          // </nowiki><b>''Note:-''</b><nowiki> If expectedSize >= 1024 bytes this array is
                                                                      compressed.
  }
</nowiki></code>


<syntaxhighlight lang="cpp">
AnimationClass
{
ulong AnimTransformType;
asciiz AnimClassName; // "RightDoor"
asciiz AnimSource; // "rotor"
float MinMaxValue[2];
float MinMaxPhase[2];
ulong junk; // used to be sourceAddress, no longer, always 953267991
IF ARMA3
ulong Always0; // no idea what this is used for
ulong sourceAddress; // this is the actual source address, 0 = clamp, 1 = mirror, 2 = loop
endif


==== structProperties ====
switch(AnimTransformType)
<code><nowiki>
case 0://rotaton
  structProperties
case 1://rotationX
  {
case 2://rotationY
    asciiz Property;
case 3://rotationZ
    asciiz Value;
float angle0;
  }
float angle1;
</nowiki></code>
break;
case 4://translation
case 5://translationX
case 6://translationY
case 7://translationZ
float offset0;
float offset1;
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;
}




==== structUV ====
// corresponds to model.cfg
<code><nowiki>
class CfgModels
  structUV
{
  {
// ...
    float u;
    float v;
  }
</nowiki></code>


class whateverModel : Default
{
// ...
class Animations
{
class RightDoor // AnimClassName
{
type = "translation"; // AnimTransformType
source = "rotor"; // AnimSource
// etc
};
};
};
</syntaxhighlight>


==== structVerticesPosition ====
==== Bones2Anims ====
<code><nowiki>
Bones2Anims
   structVerticesPosition
{
   {
   ulong        NoOfBones;
    float x;
   Bone2AnimClassList  Bone2AnimClassLists[NoOfBones];
    float z;
}
    float y;
==== Bone2AnimClassList ====
   }
Bone2AnimClassList
</nowiki></code>
{
  ulong NoOfAnimClasses;
   ulong AnimationClassIndex[NoOfAnimClasses]; // a (sometimes repeating) list of zero based indexes into above animation classes
}


==== Anims2Bones ====
Anims2Bones
{
  AnimBones AnimBones[Animations.nAnimationClasses];
}


==== structVerticesNormal ====
==== AnimBones ====
<code><nowiki>
  structVerticesNormal
  {
    float x;
    float z;
    float y;
  }
</nowiki></code>


every lod contains an identical list of animation entries that declare the position and axis of the each animation classes


==== structVerticesMinMax ====
AnimBones
<code><nowiki>
{
   structVerticesMinMax
  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)
   {
   {
    float x1;
  /*
    float z1;
  ** AnimationClass.AnimTransformType 8 (direct) and 9 (hide) never require axis information.
    float y1;
  ** This because the "direct" (type 8) already has axis info in it is AnimationClass structure,
    float x2;
  ** and "hidden" (type 9) clearly doesn't need it.
     float z2;
  */
     float y2;
     XYZTriplet axisPos; //describes the position of the axis used for this anim
     XYZTriplet axisDir;
   }
   }
</nowiki></code>
}


=== LODFaceDefaults ===


==== structVerticesUnknown1 ====
  tbool            UseDefault[Header.NoOfLods];
<code><nowiki>
   FaceData
   structVerticesUnknown1
   {
   {
    ushort One;
  ulong  HeaderFaceCount;
    ushort Two;
  ulong  aDefaultLong;     //ffffffff or 6f 7a 80 fa eg
    ushort Three;
  byte    UnknownByte;      //generally zero
     ushort Four;
  byte    aFlag;           // zero or one
    ushort Five;
  bytes  Zeroes[7];
    ushort Six;
   }[Number of false UseDefault's];
   }
 
</nowiki></code>
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.


==== structResolution (simple) ====
   ODOLv4xLod
<code><nowiki>
   structResolution
   {
   {
    NoOfVertices;
    ulong                        nProxies;
    <space>
    LodProxy                      LodProxies[nProxies];              // see [[P3D Lod Proxies]]
    NoOfTextures;
    ulong                        nLodItems;
    Textures;
    ulong                        LodItems[nLodItems];               // potentially compressed, except for v64 and later
    NoOfMaterials;
    ulong                        nBoneLinks;
    Materials;
    LodBoneLink                  LodBoneLinks[nBoneLinks];
    <space>
    float                        UnknownFloat1;
    NoOfPolygons;
    float                        UnknownFloat2;
    <space>
    XYZTriplet                    MinPos;
    Polygons;
    XYZTriplet                    MaxPos;
    <space>
    XYZTriplet                    AutoCenterPos;
    NoOfComponents;
    float                        Sphere;                           // same as geo or mem values in modelinfo, if this lod is geo or memlod of course
    Components;
    ulong                        NoOfTextures;
    NoOfProperties;
    asciiz                        LodPaaTextureNames[NoOfTextures]; //"ca\characters\hhl\hhl_01_co.paa"
    Properties;
    ulong                        NoOfMaterials;
    <space>
    LodMaterial                  LodMaterials[NoOfMaterials];
    NoOfVertices;
    LodEdges                      LodEdges;                         // compressed see [[P3D Lod Edges]]
    VerticesUVSet1;
    ulong                        NoOfFaces;
    NoOfVertices;
    ulong                        OffsetToSectionsStruct;           // see below
    VerticesUVSet2;
    ushort                        AlwaysZero;
    NoOfVertices;
    LodFace                      LodFace[NoOfFaces];               // see [[P3D Lod Faces]]
    VerticesPositions;
    ulong                        nSections;
    NoOfVertices;
    LodSection                    LodSections[nSections];           // see [[P3D Lod Sections]]
    VerticesNormals;
    ulong                        nNamedSelections;
    NoOfVertices;
    LodNamedSelection            LodNamedSelections[nNamedSelections]; //See [[P3D Named Selections]] potentially compressed
    VerticesMinMax; //Looks like Min/Max info.
    ulong                        nTokens;
    NoOfVertices;
    NamedProperty                NamedProperties[nTokens];         //See [[Named Properties]]
    VerticesUnknown1; //Looks like per vertex properties
    ulong                        nFrames;
    NoOfVertices;
    LodFrame                      LodFrames[nFrames];               //see [[P3D Lod Frames]]
    VerticesUnknown2; //hmmmm...
    ulong                        IconColor;
    if(pointer<filesize)
    ulong                        SelectedColor;
    {
    ulong                        special; // IsAlpha|IsTransparent|IsAnimated|OnSurface
        NoOfProxies;
    byte                          vertexBoneRefIsSimple;
        Proxies;
    ulong                        sizeOfVertexTable;                //(including these 4 bytes)
        <space>
    if (v5x)
    }
    LodPointFlags                LodPointFlags;                    // Potentially compressed
    NoOf;
    endif
    IntermittentUnknownData; // As at article date 12-Aug-2007. This data is not in every lod
    VertexTable                  VertexTable;
                              // it is intermittent. Currently, structure is unknown.
                              // Can be bypassed by manual intervention to start of next resolution.
                              // Most likely is Texture-2-Face/Vertex mappings.
                              //This is a 'show-stopper' for continuous processing.
   }
   }
</nowiki></code>


==== VertexTable ====
all arrays are subject to compression


==== structResolution (detailed) ====
struct
<code><nowiki>
{
  structResolution
     UvSet                        DefaultUVset;
  {
     ulong                        nUVs;
     int        NoOfVertices;
     UvSet                        UVSets[nUVs-1];
     byte        byteResUnknown1;
     ulong                        NoOfPoints;
     byte        byteResUnknown2;
     XYZTriplet                    LodPoints[NoOfPoints];
     switch (byteResUnknown2)
    ulong                        nNormals;
     {
    (A2)LodNormals                LodNormals[nNormals];
        case 0x00: { byte[40] byteArrayResUnknown1; break; }
    ulong                        nMinMax;
        case 0x20: { byte[45] byteArrayResUnknown1; break; }
    (A2)LodMinMax                MinMax[nMinMax];                   //optional
        case 0x30: { byte[45] byteArrayResUnknown1; break; }
     ulong                        nProperties;
        case 0xFF: { byte[45] byteArrayResUnknown1; break; }
     VertProperty                  VertProperties[nProperties];       //optional related to skeleton
        case 0x3F: { byte[51] byteArrayResUnknown1; break; }
     ulong                        Count;
     }
     VertexNeighborInfo            neighborBoneRef[Count];         //optional
    int                            NoOfTextures;
}
     asciiz[NoOfTextures]           Textures;
     int                            NoOfMaterials;
     structMaterial[NoOfMaterials]   Materials;


    byte[8] byteArrayResUnknown2;
* All non zero counts counts are the same.
    int NoOfPolygons;
* Points,PointFlags, Normals and UV1 arrays are an integral group, they are either all there, or not specified (RacetK.p3d, a [[P3D Lod Frames|FrameTime]] lod has no counts at all)
    byte[6] byteArrayResUnknown3;
* 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
    structPolygons[NoOfPolygons] Polygons;
* In Odol7 PointFlags are part of this stucture, in Arma, they are a separated table.
   
    //The following is an Unknown structure, however this code snippet iterates over it
    int NoOf;
    for (int i = 0; i < NoOf; i++)
    {
      byte[26] byteArrayResUnknown4;
      byte byteResUnknown3;
      if (byteResUnknown3 == 0xFF)
      {
        byte[16] byteArrayResUnknown5;
      }
      else
      {
        byte[15] byteArrayResUnknown5;
      }
    }//EndOfUnknownStructure


==== CompressedFill Arrays ====


    int NoOfComponents;
LodPointFlags, LodUV's and LodNormals arrays are not only subject to the standard 1024 rule compression, but also have a fill byte.
    structComponent[NoOfComponents] Components;


    int NoOfProperties;
struct
    structProperties[NoOfProperties] Properties;
{
  ulong                        Count;
  tbool                        DefaultFill;
  if (DefaultFill)
  type                        Array;          // default fill for all Counts
  else
  type                        Array[Count];   // potentially compressed
}


    ushort usUnknown1;
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.
    ushort usUnknown2;
==== UVset ====
    if (usUnknown2 == 0)
if TrueARMA2
     {
     float                        UVScale[4];
      byte[17] byteArrayResUnknown6;
endif
    }
     (A2)LodUV                    LodUV;
     else
    {
      byte[15] byteArrayResUnknown6;
    }


    int NoOfVertices;
==== LodUV ====
    byte bUV
CompressedFill type = UVPair // eg float U,V;
    if (bUV == 0x00)
==== A2LodUV ====
    {
  CompressedFill type = float // eg float UV;
        structUV[NoOfVertices2ndUV] VerticesUVSet1;        // </nowiki><b>''Note:-''</b><nowiki> If expectedSize >= 1024 bytes this array is
==== LodNormals ====
                                                                      compressed.
  CompressedFill type = XYZTriplet
      int NoOfVertices2ndUV;
==== A2LodNormals ====
    }
  CompressedFill type = CompressedXYZTriplet
    else
    {
      byte[12] byteArrayResUnknown7;
    }


    if (NoOfVertices2ndUV == 2)
===== CompressedXYZTriplet =====
    {
      int NoOfVertices2ndUV;
      byte bUV;
      if (bUV == 0x00)
      {
        structUV[NoOfVertices2ndUV] VerticesUVSet2;        //  </nowiki><b>''Note:-''</b><nowiki> If expectedSize >= 1024 bytes this array is
                                                                      compressed.
      }
      else
      {
        byte[8] byteArrayResUnknown8;
      }
    }


    int NoOfVertices;
contains 3x 10 bit fields in a 32bit 'integer'
    structVerticesPosition[NoOfVertices] VerticesPositions;        // </nowiki><b>''Note:-''</b><nowiki> If expectedSize >= 1024 bytes this array is
                                                                      compressed.
    int NoOfVertices;
    byte bNormal;
    if (bNormal == 0x00)
    {
      structVerticesNormal[NoOfVertices] VerticesNormals;  // </nowiki><b>''Note:-''</b><nowiki> If expectedSize >= 1024 bytes this array is
    }                                                                compressed.
    else
    {
        byte[12] byteArrayResUnknown9;
    }


    int NoOfVertices;
code for converting back to a standard XYZTriplet is:
    if (NoOfVertices != 0)
    {
      structVerticesMinMax[NoOfVertices] VerticesMinMax;  // </nowiki><b>''Note:-''</b><nowiki> If expectedSize >= 1024 bytes this array is
    }                                                                compressed.


     int NoOfVertices;
void DecodeXYZ(ulong CompressedXYZ, XYZTriplet *triplet)
     if (NoOfVertices != 0)
{
     {
     double scaleFactor = -1.0 /511;
      structVerticesUnknown1[NoOfVertices] VerticesUnknown1;   // </nowiki><b>''Note:-''</b><nowiki> If expectedSize >= 1024 bytes this array is
     trp->X=trp->Y=trp->Z=0.0;
      int NoOfVertices;                                               compressed.
     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);
}


    if (NoOfVertices != 0)
==== LodPointFlags ====
    {
CompressedFill type = ulong bits
      byte[32][NoOfVertices] VerticesUnknown2;  // </nowiki><b>''Note:-''</b><nowiki> If expectedSize >= 1024 bytes this array is
    }                                                                compressed.


    if (Pointer < Filesize)
This table is the equivalent of Oxygen's points->properties dialog box. It specifically stores the user values and other flags for that point.
    {
      int NoOfProxies;
      if (ProxyCount != 0)
      {
          structProxy[NoOfProxies] Proxies;
      }
      if (Pointer < Filesize)
      {
        int NoOf;
        int[NoOf] intArrayResUnknown1;
        int NoOf;
        if (NoOf > 0)
        {
          for (int i = 0; i < NoOf; i++)
          {
              int NoOf2;
              int[NoOf2] intArrayResUnknown2;
          }
        }
        else
        {
            int intResUnknown1;
        }
      }
    }


  // Show stopper...
In ODOl7 it was part of the vertex table. In ArmA, it is separate.
  // At this point there is a unknown, intermittent, variable length structure.
  // Requires manual intervention to move to next resolution.
  // possibly Face-2-Texture mappings.


  byte[VariableLength] IntermittentUnknownData; //Only present intermitently.
See [[P3D Point and Face Flags]]


==== LodMinMax ====
CompressedArray
{
  XYZTriplet    MinMax[Count][2]; // 2 == min vs max
}


   //EndOfResolution
==== A2LodMinMax ====
  }
CompressedArray
</nowiki></code>
{
   float        MinMax[Count][2]; // 2 == min vs max
}


=== File Format ===
==== VertProperty ====
CompressedArray
{
  ulong  index;// seen range 0..4
  ulong  a,b; // definite not floats. might be flags, or indices
}


==== VertexNeighborInfo ====
CompressedArray
{
  ushort  vertexIndex _posA;
  AnimationRTWeight _rtwA;
  ushort  vertexIndex _posB;
  AnimationRTWeight _rtwB;
}


The following is a mix of ''pseudo-code'' and structure references that could be used to discribe the file format of ODOL v40.
==== LodBoneLink ====
It may or may not be accurate but has do date been used to read ODOL v40 is some cases without manual intervention. As at the writing of this article in most cases though, manual intervention is required to complete navigation throughout the given p3d file as there is some unkonwn data that prevents continuous processing.


LodBoneLink
{
  ulong NoOfLinks;        //range 0..3
  ulong Value[NoOfLinks];  //the 'Value' seems to reference the 'LodItems' structure, resulting in a circular-reference.
}


==== Simple ====
==== LodMaterials ====
Basically... A direct replication of the information in the given .rvmat file


<code><nowiki>
The stages in the p3d include a default stage and a TI stage that are not normally listed in the rvmat.
  ODOLv40
:The first stage (in the p3d) is unconditionally the default stage. It is defaulted to empty (RvMatName=""), unless, specified in the rvmat
  {
:The last stage is the TI stage, and is also defaulted empty, unless specified in the rvmat.
    Header;
::TI Stages were introduced for operation arrowhead. Lod Material Types 9 and 10 (Arma1 and Arma2) do not have a TI stage at all.
    Model.cfg; (optional)
Neither of these two special, hidden, stage types use uvsets. The transform matrix for them is defaulted empty (so-called 'TexGen0').
    Resolutions; (reverse numerical order)
  }
</nowiki></code>


When specified in the rvmat (class Stage0 and StageTI respectively), no  class uvTransform is declared for them. It is assumed default empty.


==== Detailed ====
In an rvmat, uvTransforms are ordinarily declared within each stage body.


<code><nowiki>
In a P3D, identical UVTransforms are declared once, and multiple 'stages' refer to them. There is, always, a default UVSet0 Transform as the 1st entry. (IE some stages dont require uvsets)


  ODOLv40
This P3D style can, if preferred, be used in rvmat syntax as
  {
class TexGenX
    structP3DHeader Header;
{
    byte[155] Unknown;
  .......
    structSkeleton Skeleton;
};
    byte unknown1;
    byte unknown2;
    if(unknown2== 0x00)
    {
      byte[31] byteArrayUnknown1;
    }
    else
    {
      byte[32] byteArrayUnknown1;
    }
    int unknown4;
    byte unknown5;
    asciiz ModelString1;
    asciiz ModelString2;
    byte[5] byteArrayUnknown2;
    byte AnimsExist;
    if (AnimsExist == 0x01)
      {
        int NoOfAnimSelections;
        structAnimation[NoOfAnimSelections] Animations;


        //Basically... for each bone there is a list of Animations and this array structure
class StageZ
        //            is stored on a per resolution basis.
{
        int NoOfResolutions;
  .........
        for(int i=0; i<NoOfResolutions; i++)
  Texgen=X;
        {
};
          int NoOfBones;
          for(int ii=0; ii<NoOfResolutions; ii++)
          {
            int NoOfAnims;
            if (NoOfAnims > 0)
              {
                for(int iii=0; iii<NoOfAnims; iii++)
                {
                  int Animation;
                }
              }
          }
        }


        //Unknown Anim info...
where X and Z are numbers
        //Basically... for each Animation if the TransformType !=9 then there
        // is a 6 x float of positional info.
        for(int i=0; i<NoOfResolutions; i++)
        {
          int Anim;
          if (Anim != -1)
            {
              if (Animations[Anim].TransformType != 9)
              {
                  float[6] UnknownAnimInfo;
              }
            }
        }
      }//AnimExist
    byte[Header.NoOfResolutions * 8] Unknown8;
    bool[Header.NoOfResolutions] ResolutionFaceIndicator;


    //Basically...For each Resolution if the LODFaceIndicator is true
    //there is a int FaceCount + 13 bytes
    //I think this 'indicator' may serve other areas but at the very least it indicates
    //the following structure
    for (int i = 0; i < Header.NoOfResolutions; i++)
    {
        if (LODFaceIndicator[i])
        {
            int HeaderFaceCount;
            byte[13] Unknown9;
        }
    }


     int NoOfModelProxies;
====LodMaterial====
     if (NoOfModelProxies != 0)
  LodMaterial
     {
  {
      structProxy[NoOfModelProxies] ModelProxies;
     asciiz            RvMatName;     // "ca\characters\data\soldier_captive_hhl.rvmat"
     }
     ulong            Type;          // 9 == ArmA
    D3DCOLORVALUE    Emissive;
    D3DCOLORVALUE    Ambient;
    D3DCOLORVALUE    Diffuse;
    D3DCOLORVALUE    forcedDiffuse;
    D3DCOLORVALUE    Specular;
    D3DCOLORVALUE    Specular2;      //Usually same as Specular
    float            SpecularPower;  //
    ulong            PixelShaderId;  //See enumPixelShaderId
    ulong            VertexShaderId;  //See enumVertexShaderId
    LongBool          mainLight;    // 1 or zero
    ulong            ul_FogMode;    /// 0..4
     Asciiz            BiSurfaceName;  // "ca\data\Penetration\plastic.bisurf"
    LongBool          Arma1Mostly1;    //rarely zero
    ulong            RenderFlags;    //Generally 0
    ulong            nTextures;
    ulong            nTransforms;    // always same as nTextures
    LodStageTexture  StageTextures  [nTextures];
     LodStageTransform StageTransforms[nTransforms];
  }


:Each lodmaterial entry contains a default StageTexture and StageTransform as the first entry. It is not shown in the rvmat file and has no PaaTexture
:It is the only entry if a SurfaceName exists.


    structResolution[Header.NoOfResolutions]//Note:- Remember, the order in which lod's
=== D3DCOLORVALUE ===
                                              //      occur is descending numerical order.
D3DCOLORVALUE
                                              //      eg. Resolution 1.0 will be the last in
{
                                              //          the file.
  float r,g,b,a;
  }
===== RenderFlags =====
:*Bit0:AlwaysInShadow (A1 only)
:*Bit1:NoZWrite
:*Bit4:NoColorWrite
:*Bit5:NoAlphaWrite
:*Bit6:AddBlend
:*Bit7:AlphaTest (clutter)
:*Bit8:AlphaTest64 (clutter)
:*Bit19:Road      (a1only)
:*Bit11:NoTiWrite


    //EndOfFile
===== 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
};


</nowiki></code>
:The first stageTexture is a dummy entry. For N humanly readable stage classes, there are 1+N LodStageTextures


=== Decompression ===
:TextureFilter maybe 1 of the following values.
:*0: Point // sometimes
:*1: Linear  // rarely
:*2: TriLinear // not seen
:*3: Anisotropic (default)


In ODOL v40 format files some of the datastructures present in the file are compressed by using a form of LZ compression.
===== LodStageTransform =====
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.
  LodStageTransform
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.
  {
    ulong UVSource;
    float Transform[4][3];//a DirectX texture space transform matrix
  };


:UVSource corresponds to the 8 possible uvsets available


<b>''Note:- Data structures that are identified as being compressible will only be compressed if the 'expectedSize' is >= 1024 bytes.''</b>
:*0 "None"
:*1 "Tex" default
:*2: "Tex2"
:*........
:*8:"Tex8"


:*Tex1..8 cannot be taken literally as uvsource 1..8. They can mean anything, according to the template and are scarcely encountered


<b>''The code that follows is written in C# and may or may not be optimal or correct.''</b>
==== NamedProperty ====
  struct
  {
    Asciiz Property;// "noshadow" = "1" eg
    Asciiz Value;
  }


*See [[Named Properties]]


== Decompression ==


As an example if one was expanding the array of vertices positions...
see [[Compressed LZSS File Format]]


* A vertex is described by it's x,y,z coordinates which are floats. A float is a 32bit (4 byte) number.
see [[Compressed LZO File Format]]
* 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.


In ODOL v40 format files, some of the data structures present in the file are compressed by using LZSS compression.


<code><nowiki>
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.
  public bool Expand(int ExpectedSize)
With this information and the size of a given data item one has the necessary information to expand the data to it is original format and size.
  {
      byte PacketFlagsByte; //packet flags
      byte WIPByte;
      BitVector32 BV;
      msLZ = new MemoryStream(ExpectedSize);
      BinaryWriter bwLZ = new BinaryWriter(msLZ);
      byte[] Buffer = new byte[ExpectedSize + 15];
      bool[] BitFlags = new bool[8];
      int i = 0, PointerRef = 0, ndx = 0, CalculatedCRC = 0, ReadCRC = 0, rPos, rLen, CurrentPointerRef = 0, Count = 0;
      int Bit0 = BitVector32.CreateMask();
      int Bit1 = BitVector32.CreateMask(Bit0);
      int Bit2 = BitVector32.CreateMask(Bit1);
      int Bit3 = BitVector32.CreateMask(Bit2);
      int Bit4 = BitVector32.CreateMask(Bit3);
      int Bit5 = BitVector32.CreateMask(Bit4);
      int Bit6 = BitVector32.CreateMask(Bit5);
      int Bit7 = BitVector32.CreateMask(Bit6);


{{Feature|informative|Data structures that are identified as being compressible will only be compressed if the 'expectedSize' is &ge; 1024 bytes.}}


      PacketFlagsByte = br.ReadByte();
      do
      {
          BV = new BitVector32(PacketFlagsByte);
          BitFlags[0] = BV[Bit0];
          BitFlags[1] = BV[Bit1];
          BitFlags[2] = BV[Bit2];
          BitFlags[3] = BV[Bit3];
          BitFlags[4] = BV[Bit4];
          BitFlags[5] = BV[Bit5];
          BitFlags[6] = BV[Bit6];
          BitFlags[7] = BV[Bit7];
          i = 0;
          do
          {
              if ((int)bwLZ.BaseStream.Position >= ExpectedSize) { break; }
              if (BitFlags[i++]) //Direct Output
              {
                  WIPByte = br.ReadByte();
                  bwLZ.Write(WIPByte);
                  Buffer[PointerRef++] = WIPByte;
                  CalculatedCRC += WIPByte;
              }
              else //Get from previous 4k
              {
                  rPos = (int)(br.ReadByte());
                  rLen = (int)(br.ReadByte());
                  rPos |= (rLen & 0xF0) << 4;
                  rLen = (rLen & 0x0F) + 2;
                  CurrentPointerRef = PointerRef;
                  if ((CurrentPointerRef - (rPos + rLen)) > 0)
                  {
                      //Case of wholly within the buffer, partially within the end of the buffer or wholly outside the end of the buffer
                      for (Count = 0; Count <= rLen; Count++)
                      {
                          ndx = (CurrentPointerRef - rPos) + Count;
                              if (ndx < 0)
                              {
                                  //Beyond the start of the buffer
                                  WIPByte = 0x20;
                              }
                              else
                              {
                                  //Within the buffer
                                  WIPByte = Buffer[ndx];
                              }
                          //}
                          bwLZ.Write(WIPByte);
                          Buffer[PointerRef++] = WIPByte;
                          CalculatedCRC += WIPByte;
                      }
                  }
                  else
                  {
                      //Case of wholly or partially beyond the start of the buffer.
                      for (Count = 0; Count <= rLen; Count++)
                      {
                          ndx = (CurrentPointerRef - rPos) + Count;
                          if (ndx < 0)
                          {
                              //Beyond the start of the buffer
                              WIPByte = 0x20;
                          }
                          else
                          {
                              //Within the buffer
                              WIPByte = Buffer[ndx];
                          }
                          bwLZ.Write(WIPByte);
                          Buffer[PointerRef++] = WIPByte;
                          CalculatedCRC += WIPByte;
                      }
                  }
              }
          }
          while ((i < 8) & (bwLZ.BaseStream.Position < ExpectedSize));
          if (bwLZ.BaseStream.Position < ExpectedSize) { PacketFlagsByte = br.ReadByte(); }
      }
      while (bwLZ.BaseStream.Position < ExpectedSize);
      ReadCRC = br.ReadInt32();
      return (ReadCRC == CalculatedCRC);
  }
</nowiki></code>


=== Reference Tables ===
== Reference Tables ==


Note: These are not part of the p3d model file but are reference tables used for processing.
=== Material Stages ===


==== Resolutions ====
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.


<code><nowiki>
<code style="display: block"><nowiki>
  refResolutions
  refShaderStages
  {
  {
   float  Resolution;
   int PixelShaderId;
   string ResolutionName;
   int NoOfStages;
  }
  };
</nowiki></code>
</nowiki></code>


{| border="0"
{| class="wikitable" border="0"
!width="50" align="left"|Value
!align="left"|ID (Hex/Decimal)
!width="150" align="left"|Value
!width="250" align="left"|Name
!width="300" align="left"|Description
!width="350" align="left"|Description
!width="50" align="left"|NoOfStages
|-
|-
|-
|-
|align="left"|1.0e3||align="left"|1,000||align="left"|View Gunner
|align="left"|0x00, 0||align="left"|Normal||align="left"|diffuse color modulate, alpha replicate||align="left"|0
|-
|-
|align="left"|1.1e3||align="left"|1,100||align="left"|View Pilot
|align="left"|0x01, 1||align="left"|NormalDXTA||align="left"|diffuse color modulate, alpha replicate, DXT alpha correction||align="left"|0
|-
|-
|align="left"|1.2e3||align="left"|1,200||align="left"|View Cargo
|align="left"|0x02, 2||align="left"|NormalMap||align="left"|normal map shader||align="left"|3
|-
|-
|align="left"|1.0e4||align="left"|10,000||align="left"|Stencil Shadow
|align="left"|0x03, 3||align="left"|NormalMapThrough||align="left"|normal map shader - through lighting||align="left"|3
|-
|-
|align="left"|1.001e4||align="left"|10,010||align="left"|Stencil Shadow 2
|align="left"|0x04, 4||align="left"|NormalMapSpecularDIMap||align="left"|VBS2 only||align="left"|2
|-
|-
|align="left"|1.1e4||align="left"|11000||align="left"|Shadow Volume
|align="left"|0x05, 5||align="left"|NormalMapDiffuse||align="left"|?||align="left"|2
|-
|-
|align="left"|1.101e4||align="left"|11010||align="left"|Shadow Volume 2
|align="left"|0x06, 6||align="left"|Detail||align="left"|?||align="left"|1
|-
|-
|align="left"|1.0e13||align="left"|10,000,000,000,000||align="left"|Geometry
|align="left"|0x07, 7||align="left"|?||align="left"|?||align="left"|?
|-
|-
|align="left"|1.0e15||align="left"|1,000,000,000,000,000||align="left"|Memory
|align="left"|0x08, 8||align="left"|Water||align="left"|A1 only sea water||align="left"|2
|-
|-
|align="left"|2.0e15||align="left"|2,000,000,000,000,000||align="left"|Land Contact
|align="left"|0x09, 9||align="left"|?||align="left"|vbs2||align="left"|?
|-
|-
|align="left"|3.0e15||align="left"|3,000,000,000,000,000||align="left"|Roadway
|align="left"|0x0A, 10||align="left"|White||align="left"|A1 only||align="left"|0
|-
|-
|align="left"|4.0e15||align="left"|4,000,000,000,000,000||align="left"|Paths
|align="left"|0x0B, 11||align="left"|?||align="left"|vbs2||align="left"|?
|-
|-
|align="left"|5.0e15||align="left"|5,000,000,000,000,000||align="left"|HitPoints
|align="left"|0x0C, 12||align="left"|AlphaShadow||align="left"|shadow alpha write||align="left"|0
|-
|-
|align="left"|6.0e15||align="left"|6,000,000,000,000,000||align="left"|View Geometry
|align="left"|0x0D, 13||align="left"|AlphaNoShadow||align="left"|shadow alpha (no shadow) write||align="left"|0
|-
|-
|align="left"|7.0e15||align="left"|7,000,000,000,000,000||align="left"|Fire Geometry
|align="left"|0x0E, 14||align="left"|?||align="left"|vbs2||align="left"|?
|-
|-
|align="left"|8.0e15||align="left"|8,000,000,000,000,000||align="left"|View Cargo Geometry
|align="left"|0x0F, 15||align="left"|DetailMacroAS||align="left"|?||align="left"|3
|-
|-
|align="left"|9.0e15||align="left"|9,000,000,000,000,000||align="left"|View Cargo Fire Geometry
|align="left"|0x10, 16||align="left"|?||align="left"|vbs2||align="left"|?
|-
|-
|align="left"|1.0e16||align="left"|10,000,000,000,000,000||align="left"|View Commander
|align="left"|0x11, 17||align="left"|?||align="left"|vbs2||align="left"|?
|-
|-
|align="left"|1.1e16||align="left"|11,000,000,000,000,000||align="left"|View Commander Geometry
|align="left"|0x12, 18||align="left"|NormalMapSpecularMap||align="left"|?||align="left"|2
|-
|-
|align="left"|1.2e16||align="left"|12,000,000,000,000,000||align="left"|View Commander Fire Geometry
|align="left"|0x13, 19||align="left"|NormalMapDetailSpecularMap||align="left"|Similar to NormalMapDiffuse||align="left"|3
|-
|-
|align="left"|1.3e16||align="left"|13,000,000,000,000,000||align="left"|View Pilot Geometry
|align="left"|0x14, 20||align="left"|NormalMapMacroASSpecularMap||align="left"|?||align="left"|4
|-
|-
|align="left"|1.4e16||align="left"|14,000,000,000,000,000||align="left"|View Pilot Fire Geometry
|align="left"|0x15, 21||align="left"|NormalMapDetailMacroASSpecularMap||align="left"|?||align="left"|5
|-
|-
|align="left"|1.5e16||align="left"|15,000,000,000,000,000||align="left"|View Gunner Geometry
|align="left"|0x16, 22||align="left"|NormalMapSpecularDIMap||align="left"|Same as NormalMapSpecularMap, but uses _SMDI texture||align="left"|2
|-
|-
|align="left"|1.6e16||align="left"|16,000,000,000,000,000||align="left"|View Gunner Fire Geometry
|align="left"|0x17, 23||align="left"|NormalMapDetailSpecularDIMap||align="left"|?||align="left"|3
|-
|-
|}
 
 
==== 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.
 
<code><nowiki>
refShaderStages
{
  int PixelShaderId;
  int NoOfStages;
};
</nowiki></code>
 
{| border="0"
!width="350" align="left"|PixelShaderId enum
!width="50" align="left"|NoOfStages
|-
|-
|align="left"|0x18, 24||align="left"|NormalMapMacroASSpecularDIMap||align="left"|?||align="left"|4
|-
|-
|align="left"|PixelShaderId.Normal||align="left"|0
|align="left"|0x19, 25||align="left"|NormalMapDetailMacroASSpecularDIMap||align="left"|?||align="left"|5
|-
|-
|align="left"|PixelShaderId.NormalMapSpecularDIMap||align="left"|2
|align="left"|0x38, 56||align="left"|Glass||align="left"|?||align="left"|2
|-=
|align="left"|PixelShaderId.NormalMapDiffuse ||align="left"|2
|-
|-
|align="left"|PixelShaderId.AlphaNoShadow||align="left"|0
|align="left"|0x3A, 58||align="left"|NormalMapSpecularThrough||align="left"|?||align="left"|3
|-
|-
|align="left"|PixelShaderId.AlphaShadow||align="left"|0
|align="left"|0x3B, 59||align="left"|Grass||align="left"|Special shader to allow volumetric shadows to be cast on grass clutter||align="left"|0
|-
|-
|align="left"|PixelShaderId.NormalMapMacroASSpecularDIMap||align="left"|4
|align="left"|0x3C, 60||align="left"|NormalMapThroughSimple||align="left"|?||align="left"|0
|-
|-
|align="left"|PixelShaderId.Glass||align="left"|2
|align="left"|0xxx, 102||align="left"|Super||align="left"|Arrowhead||align="left"|0
|-
|-
|align="left"|PixelShaderId.Detail||align="left"|1
|align="left"|0xxx, 103||align="left"|Multi||align="left"|Arrowhead||align="left"|0
|-
|-
|align="left"|PixelShaderId.NormalMap||align="left"|3
|align="left"|0xxx, 107||align="left"|Tree||align="left"|Arrowhead||align="left"|0
|-
|-
|align="left"|PixelShaderId.NormalMapMacroASSpecularMap||align="left"|4
|align="left"|0xxx, 110||align="left"|Skin||align="left"|Arrowhead||align="left"|0
|-
|-
|align="left"|PixelShaderId.NormalMapSpecularMap||align="left"|2
|align="left"|0x6F, 111||align="left"|CalmWater||align="left"|Arrowhead||align="left"|7
|-
|-
|align="left"|0xxx, 114||align="left"|TreeAdv||align="left"|Arrowhead||align="left"|0
|-
|-
|align="left"|0xxx, 116||align="left"|TreeAdvTrunk||align="left"|Arrowhead||align="left"|0
|}
|}
== Enums ==
<code style="display: block"><nowiki>
int enum VertexShaderId
{
case 0: return "Basic";
case 1: return "NormalMap";
case 2: return "NormalMapDiffuse";
case 3: return "Grass";
case 8: return "Water";
case 11: return  "NormalMapThrough";
case 15: return "NormalMapAS";
case 14: return "BasicAS";
case 17: return "Glass";
case 18: return "NormalMapSpecularThrough";
case 19: return "NormalMapThroughNoFade";
case 20: return "NormalMapSpecularThroughNoFade";
case 23: return "Super";
case 24: return "Multi";
case 25: return "Tree";
case 30: return "CalmWater";
case 26: return "TreeNoFade";
case 29: return "Skin";
case 31: return "TreeAdv";
case 32: return "TreeAdvTrunk";
}
</nowiki></code>


== Links ==
== Links ==
Line 895: Line 649:
[[User:Sy|Article Author - Sy (Synide)]] -- [[User:Sy|Sy]] 17:16, 11 August 2007 (CEST)
[[User:Sy|Article Author - Sy (Synide)]] -- [[User:Sy|Sy]] 17:16, 11 August 2007 (CEST)


[[P3D File Format - ODOLV40|Original ODOLv40 Biki Article detailed by Bxbx (Biki'd by Mikero)]]
 
[[P3D File Format - ODOLV40|Original ODOLv40 Article detailed by Bxbx (Biki'd by Mikero)]]
[[Category:Real Virtuality File Formats]]
{{GameCategory|arma1|File Formats}}

Latest revision as of 13:27, 8 May 2025

bi symbol white.png
Disclaimer: This page describes internal undocumented structures of Bohemia Interactive software.

This page contains unofficial information.

Some usage of this information may constitute a violation of the rights of Bohemia Interactive and is in no way endorsed or recommended by Bohemia Interactive.
Bohemia Interactive is not willing to tolerate use of such tools if it contravenes any general licenses granted to end users of this community wiki or BI products.

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

All file references are absolute to the Linux \ (root).

As a convenience A drive letter is used (such as P:\) for sanity when modelling. The engine knows nothing about C:\my documents or even P:

The leading \ is optional.

\my_project\data\some.paa AND my_project\data\some.paa

point to the same file.

Versions

This Document covers ODOL versions:

V40 (Armed Assault)

  • Original Armed Assault binarised p3d


File Format

ODOLv4x
{
	StandardP3DHeader Header;
    struct ModelInfo;
	Animations		Animations;
	ulong			StartAddressOfLods[Header.NoOfLods]; // offset relative to start of file.
	ulong			EndAddressOfLods  [Header.NoOfLods];
	LODFaceDefaults	LODFaceDefaults;
	ODOLv40Lod		ODOLv40Lods[Header.NoOfLods];
} // EndOfFile


Structures

StandardP3DHeader

Common header structure for all P3D file formats.

struct
{
	char[4]	Filetype;		// "ODOL"
	ulong	Version;
	ulong	NoOfLods;		// alias NoOfResolutions;
}

ModelInfo

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	junk; // used to be sourceAddress, no longer, always 953267991
		IF ARMA3
			ulong	Always0;		// no idea what this is used for
			ulong	sourceAddress;	// this is the actual source address, 0 = clamp, 1 = mirror, 2 = loop
		endif

		switch(AnimTransformType)
		case 0://rotaton
		case 1://rotationX
		case 2://rotationY
		case 3://rotationZ
			float angle0;
			float angle1;
			break;
		case 4://translation
		case 5://translationX
		case 6://translationY
		case 7://translationZ
			float offset0;
			float offset1;
			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 is 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;
  ulong   aDefaultLong;     //ffffffff or 6f 7a 80 fa eg
  byte    UnknownByte;      //generally zero
  byte    aFlag;            // zero or one
  bytes   Zeroes[7];
 }[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.
 ODOLv4xLod
 {
   ulong                         nProxies;
   LodProxy                      LodProxies[nProxies];              // see P3D Lod Proxies
   ulong                         nLodItems;
   ulong                         LodItems[nLodItems];               // potentially compressed, except for v64 and later
   ulong                         nBoneLinks;
   LodBoneLink                   LodBoneLinks[nBoneLinks];
   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];          //See Named Properties
   ulong                         nFrames;
   LodFrame                      LodFrames[nFrames];                //see P3D Lod Frames
   ulong                         IconColor;
   ulong                         SelectedColor;
   ulong                         special; // IsAlpha|IsTransparent|IsAnimated|OnSurface
   byte                          vertexBoneRefIsSimple;
   ulong                         sizeOfVertexTable;                 //(including these 4 bytes)
   if (v5x)
   LodPointFlags                 LodPointFlags;                     // Potentially compressed
   endif
   VertexTable                   VertexTable;
 }

VertexTable

all arrays are subject to compression

struct
{
   UvSet                         DefaultUVset;
   ulong                         nUVs;
   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;
   VertexNeighborInfo            neighborBoneRef[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 3x 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.

See P3D Point and Face Flags

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  a,b; // definite not floats. might be flags, or indices
}

VertexNeighborInfo

CompressedArray
{
 ushort  vertexIndex _posA;
 AnimationRTWeight _rtwA;
 ushort  vertexIndex _posB;
 AnimationRTWeight _rtwB;
}

LodBoneLink

LodBoneLink
{
  ulong NoOfLinks;         //range 0..3
  ulong Value[NoOfLinks];  //the 'Value' seems to reference the 'LodItems' structure, resulting in a circular-reference.
}

LodMaterials

Basically... A direct replication of the information in the given .rvmat file

The stages in the p3d include a default stage and a TI stage that are not normally listed in the rvmat.

The first stage (in the p3d) is unconditionally the default stage. It is defaulted to empty (RvMatName=""), unless, specified in the rvmat
The last stage is the TI stage, and is also defaulted empty, unless specified in the rvmat.
TI Stages were introduced for operation arrowhead. Lod Material Types 9 and 10 (Arma1 and Arma2) do not have a TI stage at all.

Neither of these two special, hidden, stage types use uvsets. The transform matrix for them is defaulted empty (so-called 'TexGen0').

When specified in the rvmat (class Stage0 and StageTI respectively), no class uvTransform is declared for them. It is assumed default empty.

In an rvmat, uvTransforms are ordinarily declared within each stage body.

In a P3D, identical UVTransforms are declared once, and multiple 'stages' refer to them. There is, always, a default UVSet0 Transform as the 1st entry. (IE some stages dont require uvsets)

This P3D style can, if preferred, be used in rvmat syntax as

class TexGenX
{
  .......
};
class StageZ
{
  .........
  Texgen=X;
};

where X and Z are numbers


LodMaterial

 LodMaterial
 {
   asciiz            RvMatName;     // "ca\characters\data\soldier_captive_hhl.rvmat"
   ulong             Type;          // 9 == ArmA
   D3DCOLORVALUE     Emissive;
   D3DCOLORVALUE     Ambient;
   D3DCOLORVALUE     Diffuse;
   D3DCOLORVALUE     forcedDiffuse;
   D3DCOLORVALUE     Specular;
   D3DCOLORVALUE     Specular2;       //Usually same as Specular
   float             SpecularPower;   // 
   ulong            PixelShaderId;   //See enumPixelShaderId
   ulong            VertexShaderId;  //See enumVertexShaderId
   LongBool          mainLight;     // 1 or zero
   ulong             ul_FogMode;    /// 0..4
   Asciiz            BiSurfaceName;   // "ca\data\Penetration\plastic.bisurf"
   LongBool          Arma1Mostly1;    //rarely zero
   ulong             RenderFlags;     //Generally 0
   ulong             nTextures;
   ulong             nTransforms;     // always same as nTextures
   LodStageTexture   StageTextures  [nTextures];
   LodStageTransform StageTransforms[nTransforms];
 }  
Each lodmaterial entry contains a default StageTexture and StageTransform as the first entry. It is not shown in the rvmat file and has no PaaTexture
It is the only entry if a SurfaceName exists.

D3DCOLORVALUE

D3DCOLORVALUE
{
  float r,g,b,a;
}
RenderFlags
  • Bit0:AlwaysInShadow (A1 only)
  • Bit1:NoZWrite
  • Bit4:NoColorWrite
  • Bit5:NoAlphaWrite
  • Bit6:AddBlend
  • Bit7:AlphaTest (clutter)
  • Bit8:AlphaTest64 (clutter)
  • Bit19:Road (a1only)
  • Bit11:NoTiWrite
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 1+N LodStageTextures
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
  };
UVSource corresponds to the 8 possible uvsets available
  • 0 "None"
  • 1 "Tex" default
  • 2: "Tex2"
  • ........
  • 8:"Tex8"
  • Tex1..8 cannot be taken literally as uvsource 1..8. They can mean anything, according to the template and are scarcely encountered

NamedProperty

 struct
 {
    Asciiz Property;// "noshadow" = "1" eg
    Asciiz Value;
 }

Decompression

see Compressed LZSS File Format

see Compressed LZO File Format


In ODOL v40 format files, some of the data structures present in the file are compressed by using LZSS 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 is original format and size.

Data structures that are identified as being compressible will only be compressed if the 'expectedSize' is ≥ 1024 bytes.


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 VBS2 only 2
0x05, 5 NormalMapDiffuse ? 2
0x06, 6 Detail ? 1
0x07, 7 ? ? ?
0x08, 8 Water A1 only sea water 2
0x09, 9 ? vbs2 ?
0x0A, 10 White A1 only 0
0x0B, 11 ? vbs2 ?
0x0C, 12 AlphaShadow shadow alpha write 0
0x0D, 13 AlphaNoShadow shadow alpha (no shadow) write 0
0x0E, 14 ? vbs2 ?
0x0F, 15 DetailMacroAS ? 3
0x10, 16 ? vbs2 ?
0x11, 17 ? vbs2 ?
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
0xxx, 102 Super Arrowhead 0
0xxx, 103 Multi Arrowhead 0
0xxx, 107 Tree Arrowhead 0
0xxx, 110 Skin Arrowhead 0
0x6F, 111 CalmWater Arrowhead 7
0xxx, 114 TreeAdv Arrowhead 0
0xxx, 116 TreeAdvTrunk Arrowhead 0


Enums

int enum VertexShaderId { case 0: return "Basic"; case 1: return "NormalMap"; case 2: return "NormalMapDiffuse"; case 3: return "Grass"; case 8: return "Water"; case 11: return "NormalMapThrough"; case 15: return "NormalMapAS"; case 14: return "BasicAS"; case 17: return "Glass"; case 18: return "NormalMapSpecularThrough"; case 19: return "NormalMapThroughNoFade"; case 20: return "NormalMapSpecularThroughNoFade"; case 23: return "Super"; case 24: return "Multi"; case 25: return "Tree"; case 30: return "CalmWater"; case 26: return "TreeNoFade"; case 29: return "Skin"; case 31: return "TreeAdv"; case 32: return "TreeAdvTrunk"; }

Links

Article Author - Sy (Synide) -- Sy 17:16, 11 August 2007 (CEST)


Original ODOLv40 Article detailed by Bxbx (Biki'd by Mikero)