P3D File Format - ODOLV4x: Difference between revisions
m (→structMaterial: D3DCOLORVALUE) |
m (→structMaterial) |
||
Line 397: | Line 397: | ||
ulong VertexShaderId;//See enumVertexShaderId | ulong VertexShaderId;//See enumVertexShaderId | ||
ulong Links[2]; | ulong Links[2]; | ||
asciiz | asciiz SurfaceName; | ||
if | ulong MoreLinks[4]; | ||
if SurfaceName nStages=1; | |||
else nStages=1+FunctionOf(PixelShaderId); | |||
StageTexture StageTextures [nStages]; | |||
StageTransform StageTransforms[nStages]; | |||
} | } | ||
</nowiki></code> | </nowiki></code> | ||
:There is """always""" a default Stage as the first entry. | |||
:It is the only entry if a SurfaceName exists. | |||
:Otherwise, nStages is determined by the PixelShaderID (plus 1 for the unconditional default) | |||
:nStages is determined by the PixelShaderID | |||
:This ID references the Material Stages table (see below) to determine how many stages are used for this ID type | :This ID references the Material Stages table (see below) to determine how many stages are used for this ID type | ||
:As of Jan 09, that table is has not been fully checked with latest info and might be obsolete | |||
: As of Jan 09, | |||
==== | ====StageTexture==== | ||
StageTexture | |||
{ | { | ||
ulong TextureFilter; | ulong TextureFilter; | ||
Line 428: | Line 419: | ||
ulong StageID; | ulong StageID; | ||
}; | }; | ||
==== | ====StageTransform==== | ||
StageTransform | |||
{ | { | ||
ulong UVSource; | ulong UVSource; | ||
float Transform[4][3];//a DirectX texture space transform matrix | float Transform[4][3];//a DirectX texture space transform matrix | ||
}; | }; | ||
====D3DCOLORVALUE==== | |||
D3DCOLORVALUE | |||
{ | |||
float r,g,b,a; | |||
} | |||
===structEdge=== | ===structEdge=== |
Revision as of 12:25, 13 January 2009
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, with Mikero trying to keep up with them both.
All accurate data is supplied by the first two individuals. All mistakes, omissions, typos and general misinformation is due to Mikero alone );
General
The general file format of a ArmA ODOL v40 p3d model file is similar to the ODOL v7 format. The major differences are that in ArmA models are
- an optional model.cfg
- resolutions are in reverse numerical order.
The order of resolutions denoted in the header portion of the file is not necessarily the numerical order of the resolutions. (often the 11,000 resolution is the last in the header array) 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.
Legend
Type | Description |
---|---|
byte | 8 bit (1 byte) |
tbool | byte: 0 = false. |
short | 16 bit signed integer (2 bytes) |
ushort | 16 bit unsigned integer (2 bytes) |
int | 32 bit signed integer (4 bytes) |
ulong | 32 bit unsigned integer (4 bytes) |
float | 32 bit signed single precision floating point value (4 bytes) |
asciiz | Null terminated (0x00) variable length ascii string |
Note that 'int' is not used in this documentation for the following reasons:
- an 'int' is machine and compiler and language dependent. It is an arbitrary size SIGNED value.
- with exceptions, BI use floats when requiring negative values.
- almost all references to 'integers' in BI file formats are either positive-only offsets into memory, zero based indexes, and counts
File Format
The following is a mix of pseudo-code and structure references that could be used to describe the file format of ODOL v40. It may or may not be accurate but has do date been used to read ODOL v40 in 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.
Simple
ODOLv40
{
Header;
Model.cfg; (optional)
Resolutions; (reverse numerical order)
}
Detailed
ODOLv40
{
StandardP3DHeader Header;
float HeaderResolutions[Header.NoOfLods];
ModelInfo ModelInfo;
Skeleton Skeleton;
UnknownStruct1 UnknownStruct;
Animations Animations;
ulong StartAdressOfLods[Header.NoOfLods];
ulong EndAdressOfLods [Header.NoOfLods];
tbool LODFaceIndicator [Header.NoOfLods];
structLodFace LodFaces[NoOfFalseLODFaceIndicators];
structResolution Resolutions [Header.NoOfLods]; // The order in which lod's occur
// is descending numerical order.
// eg. Resolution 1.0 will be the last in the file.
}//EndOfFile
- there are only as many LodFaces as there are false LODFaceIndicators
Structures
StandardP3DHeader
StandardP3DHeader
{
char[4] Filetype; // "ODOL"
ulong Version; // 40
ulong NoOfLods; // alias NoOfResolutions;
}
common header structure for all P3D file formats
ModelInfo
ModelInfo { ulong Unknown; float Sphere; byte Unknown[36]; float ViewDensity; byte Unknown[24]; float ModelVertexOffset[3]; //xyz float Unknown[3]; //xyz float ModelCentreOfGravity[3]; //xyz float ModelMassVectors[3][3]; byte AutoCenter, lockAutoCenter, canOcclude, canBeOccluded, allowAnimation; byte Unknown[6]; }
structSkeleton
structSkeleton
{
asciiz SkeletonName;
if (SkeletonName != null)
{
tbool isInherited;
ulong NoOfBones;
structBone[NoOfBones] Bones;
}
}
structBone
structBone { asciiz Bone; asciiz Parent; }
UnknownStruct1
UnknownStruct1 { byte unknown1; tbool Extra; if(Extra) { byte ExtraByte; } bytes unknown3[3]; float ModelMass. ModelMassReciprocal, ModelMassModifier; bytes Unknown16[16]; ulong unknown4; byte unknown5; asciiz ModelString1; asciiz ModelString2; bytes byteArrayUnknown2[5]; }
Animations
Animations { tbool AnimsExist; if (AnimsExist) { ulong NoOfAnimSelections; structAnimation Animations[NoOfAnimSelections]; ulong NoOfResolutions;// same value as Header.NoOfLods Bones2Anims Bones2Anims[NoOfResolutions]; Anims2Bones Anims2Bones[NoOfResolutions][NoOfAnimSelections]; //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 } }
structAnimation
structAnimation
{
ulong AnimTransformType;
asciiz AnimSelection;
asciiz AnimSource;
switch(AnimTransformType)
case 9: //"hide"
{
float minValue;
float maxValue;
float minPhase;
float maxPhase;
ulong sourceAddress;
float hideValue;
}
case 8: //"direct"
{
float minValue;
float maxValue;
float minPhase;
float maxPhase;
ulong sourceAddress;
float axisPos[3];
float axisDir[3];
float angle; //in radians whereas the model.cfg entry is in degrees
float axisOffset;
}
default
{
float minValue;
float maxValue;
float minPhase;
float maxPhase;
ulong sourceAddress;
float angle0/offset0; //depends on animType
float angle1/offset1; //depends on animType
}
}
Bones2Anims
Bones2Anims { ulong NoOfBones; BoneAnims BoneAnims[NoOfBones]; }
BoneAnims
BoneAnims { ulong NoOfAnims; ulong Animation[NoOfAnims]; }
Anims2Bones
Anims2Bones { AnimBones AnimBones[Animations.NoOfAnimSelections]; }
AnimBones
AnimBones { long Bone; /* ** structAnimation.TransformTypes 8 (direct) and 9 (hide) always have a bone associated with them ** but never require axis information. This because the "direct" (type 8) already has axis info in ** it's Animation structure, and "hidden" (type 9) clearly doesn't need it. ** ** All other types may, or may not have an associated bone. ** Bone == -1 when no bone is for this Anim and (obviously?) no axis information follows. */ if (Bone != -1) && (Animations[j].TransformType != 8 && Animations[j].TransformType != 9) { float[3] axisPos; //describes the position of the axis used for this anim float[3] axisDir; } }
structLodFace
//only when the LODFaceIndicator for that lod is false structLodFace { ulong HeaderFaceCount; bytes Unknown[13]; }
structResolution
structResolution { ulong NoOfModelProxies; structProxy[NoOfModelProxies] ModelProxies; ulong nItems; ulong[nItems] Items; StarterStructTwo[...]; PointProperties[...]; // Potentially compressed TextureNames[...]; ulong NoOfMaterials; structMaterial Materials[NoOfMaterials]; structEdge Edge1; structEdge Edge2; PolygonStruct[...]; ulong nSections; structSection Sections[nSections]; ulong nComponents; structComponent Components[nComponents]; ulong nProperties; structProperties Properties[nProperties]; ushort UnknownWord; ushort Flag; if (Flag) byte Unknown[15]; else byte Unknown[17]; structUV UV1; // Potentially compressed ulong nUVs; if (nUVs==2) structUV UV2; // Potentially compressed ulong NoOfVertices; float XZY[NoOfVertices][3]; // Potentially compressed ulong nNormals; tbool DefaultFill; if (DefaultFill) float XZY[3]; else float XZY[nNormals][3]; // Potentially compressed ulong nMinMax; float MinMaxXYZ[nMinMax][2][3];// Potentially compressed
// Note that NoOfVertices == nNormals == nMinMax ulong Count; UnknownResolutionStruct UnknownResolutionStruct[Count]; // Potentially compressed ulong nBytes; byte[nBytes][32] VerticesUnknown2; // Potentially compressed }
UnknownResolutionStruct
UnknownResolutionStruct// Potentially compressed { ulong Index; float Unknown[2];// probably a vertices something }
structProxy
structProxy { asciiz ProxyName; float[12] ModelProxyUnknown1; ulong[4] ModelProxyUnknown2; }
StarterStructTwo
StarterTwo { ulong Count; { ulong NoOf3; ulong Items[NoOf3]; }[Count]; }
PointProperties
PointProperties // into the ResolutionIndex { ushort NoOfPts; ushort Unknown; tbool UseDefault; if (UseDefault) { ulong DefaultValue; } else // =0 { ulong PropertyValues[NoOfPts]; // if NoOfPoints*sizeof(ulong) > 1023 LZH compression. } byte Unknown[8]; float Unknown[10]; }
PropertyValues for NoOfPts are either all the same (UseDefault), or, they are individually declared.
Similar to CompressedStructs of OdolV7, if the amount of data in the array exceeds 1023 bytes, that array is compressed.
TextureNames
TextureNames { ulong NoOfTextures; asciiz Textures[NoOfTextures]; }
structMaterial
//Basically... A direct replication of the information in the given .rvmat file
structMaterial
{
asciiz Material;
ulong Type; // 9 == Arma, 10==VBS2
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
ulong Links[2];
asciiz SurfaceName;
ulong MoreLinks[4];
if SurfaceName nStages=1;
else nStages=1+FunctionOf(PixelShaderId);
StageTexture StageTextures [nStages];
StageTransform StageTransforms[nStages];
}
- There is """always""" a default Stage as the first entry.
- It is the only entry if a SurfaceName exists.
- Otherwise, nStages is determined by the PixelShaderID (plus 1 for the unconditional default)
- This ID references the Material Stages table (see below) to determine how many stages are used for this ID type
- As of Jan 09, that table is has not been fully checked with latest info and might be obsolete
StageTexture
StageTexture { ulong TextureFilter; asciiz Texture; ulong StageID; };
StageTransform
StageTransform { ulong UVSource; float Transform[4][3];//a DirectX texture space transform matrix };
D3DCOLORVALUE
D3DCOLORVALUE { float r,g,b,a; }
structEdge
structEdge { ulong nEdges; ushort Edges[nEdges]; // lzh compressed };
PolygonStruct
PolygonStruct { ulong NoOfPolygons; ulong GrossFaceIndex; // x28 eg ushort Unknown; // 00 00 eg PoyygonVertices { byte NoOfVertices; // 3 or 4 ushort VerticesIndex[NoOfVertices]; // 0-based index into Vertices Arrays }[NoOfPolygons]; }
Note that there are always 3, or 4, vertices.
3 point vertices describe a triangle. 4 point vertices describe a square.
The indices must be transformed as follows 3 point verts : 1st posn, 2nd posn, 0th posn. 4 point verts : 1st, 2nd, 3rd, 0th
structSection
struct structSection { ulong FaceLowerIndex; ulong FaceUpperIndex; //NoOfFaces = (FaceUpperIndex - FaceLowerIndex) / 8 ulong Something1; ulong Something2; ulong UserValue; short TextureIndex; short Something4; byte ZBias; byte Something5; short MaterialIndex; if MaterialIndex ==-1 { byte ExtraByte; } byte Something6[2]; ulong Something7; float Something8; float Something9; }
structComponent
structComponent { asciiz ComponentName; ulong NoOfSelectedFaces; ushort[NoOfSelectedFaces] SelectedFaceIndexes; //NOTE: This array is Compressed if size > 1024. UnKnownComponentStruct UnKnownComponentStruct ulong nSelectedVertices; ushort[nSelectedVertices] SelectedVerticesIndexes; // NOTE: This array is Compressed if size > 1024. ulong nTextureWeights; byte[nTextureWeights] SelectedVerticesWeights; // NOTE: This array is Compressed if size > 1024. }
UnKnownComponentStruct
UnKnownComponentStruct { ulong UnknownInt; tbool IsPresent; ulong NoOfUlongs; if (IsPresent) { ulong[NoOfUlongs] UnknownArray; // possibly subject to compression. none seen so far } }
structProperties
structProperties { asciiz Property; asciiz Value; }
structUV
structUV { ulong nVertices; tbool DefaultFill; if (DefaultFill) float UV[2]; // default fill for all nVertices else float UV[nVertices][2]; // potentially compressed }
The structure either contains a single UV pair of floats. Or, pairs of UV floats for all positions (nVertices)
If a full array is declared (DefaultFill != 0) then that array is compressed if 2 * sizeof(float) * nVertices > 1023
Decompression
In ODOL v40 format files some of the datastructures present in the file are compressed by using a form of LZ 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.
The code that follows is written in C# and may or may not be optimal or correct.
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.
public bool Expand(int ExpectedSize)
{
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);
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);
}
Reference Tables
Note: These are not part of the p3d model file but are reference tables used for processing.
Resolutions
refResolutions
{
float Resolution;
string ResolutionName;
}
Hex-Value | Value | Value | Description |
---|---|---|---|
0x447a0000 | 1.0e3 | 1,000 | View Gunner |
0x44898000 | 1.1e3 | 1,100 | View Pilot |
0x44960000 | 1.2e3 | 1,200 | View Cargo |
0x461c4000 | 1.0e4 | 10,000 | Stencil Shadow |
0x461c6800 | 1.001e4 | 10,010 | Stencil Shadow 2 |
0x462be000 | 1.1e4 | 11000 | Shadow Volume |
0x462c0800 | 1.101e4 | 11010 | Shadow Volume 2 |
0x551184e7 | 1.0e13 | 10,000,000,000,000 | Geometry |
0x58635fa9 | 1.0e15 | 1,000,000,000,000,000 | Memory |
0x58e35fa9 | 2.0e15 | 2,000,000,000,000,000 | Land Contact |
0x592a87bf | 3.0e15 | 3,000,000,000,000,000 | Roadway |
0x59635fa9 | 4.0e15 | 4,000,000,000,000,000 | Paths |
0x598e1bca | 5.0e15 | 5,000,000,000,000,000 | HitPoints |
0x59aa87bf | 6.0e15 | 6,000,000,000,000,000 | View Geometry |
0x59c6f3b4 | 7.0e15 | 7,000,000,000,000,000 | Fire Geometry |
0x59e35fa9 | 8.0e15 | 8,000,000,000,000,000 | View Cargo Geometry |
0x59ffcb9e | 9.0e15 | 9,000,000,000,000,000 | View Cargo Fire Geometry |
0x5a0e1bca | 1.0e16 | 10,000,000,000,000,000 | View Commander |
0x5a1c51c4 | 1.1e16 | 11,000,000,000,000,000 | View Commander Geometry |
0x5a2a87bf | 1.2e16 | 12,000,000,000,000,000 | View Commander Fire Geometry |
0x5a38bdb9 | 1.3e16 | 13,000,000,000,000,000 | View Pilot Geometry |
0x5a46f3b4 | 1.4e16 | 14,000,000,000,000,000 | View Pilot Fire Geometry |
0x5a5529af | 1.5e16 | 15,000,000,000,000,000 | View Gunner Geometry |
0x5a635fa9 | 1.6e16 | 16,000,000,000,000,000 | View Gunner Fire Geometry |
0x5a7195a4 | 1.7e16 | 17,000,000,000,000,000 | Sub Parts |
Note: Hex-Values are provided for convenience, as you can use those in different programming languages 'switch'-statement as opposed to floating point values.
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)