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 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
- Lods occur in the file from highest to lowest LodType value.
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) |
long | 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
XYZTriplet
XYZTriplet { float x,y,z; }
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)
File Format
ODOLv40 { StandardP3DHeader Header; float LodTypes[Header.NoOfLods];// alias resolutions ModelInfo ModelInfo; Skeleton Skeleton; UnknownStruct1 UnknownStruct; Animations Animations; ulong StartAdressOfLods[Header.NoOfLods]; ulong EndAdressOfLods [Header.NoOfLods]; tbool LODFaceIndicator [Header.NoOfLods]; LodFace LodFaces[NoOfFalseLODFaceIndicators];//there are only as many LodFaces as there are false LODFaceIndicators ODOLv40Lod ODOLv40Lods[Header.NoOfLods]; }//EndOfFile
LodTypes
The order in which lod's occur is descending resolutions. The lowest resolution (eg. Resolution 1.0) will be the last in the file.
- LodTypes is an unsorted list of all these resolutions. To make it usable (eg to specifically access the 'memory lod') the list must be sorted from highest to lowest value.
This then gives the order of occurrence of lods.
- The order, has significance to various lod offsets mentioned below.
- For a list of 'lod types' see bottom of document.
LodTypes are resolutions. Most of them have humanly readable context such as the 'memory' lod. And are selected as such in Oxygen. The are consequenlty referred to here as LodTypes, since the floating point values, although relevant to the engine, are not as immediately apparent to a human.
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]; XYZTriplet ModelVertexOffset; XYZTriplet Unknown; XYZTriplet ModelCentreOfGravity; XYZTriplet ModelMassVectors[3]; byte AutoCenter, lockAutoCenter, canOcclude, canBeOccluded, allowAnimation; byte Unknown[6]; }
structSkeleton
structSkeleton { asciiz SkeletonName; //"A10Skeleton" if (SkeletonName != null) { tbool isInherited; ulong NoOfBoneNames; SkeletonBoneName SkeletonBoneNames[NoOfBoneNames]; } }
SkeletonBoneName
SkeletonBoneName { asciiz BoneName; //"3dhud" or "Gearlocks" or "Fuel" or ... asciiz ParentBoneName; // "Aeileron_1" }
corresponds to model.cfg
class cfgSkeletons { class SkeletonClassname: Default { skeletonBones[]= { "RightDoor1","", "RightDoor2","", "LeftDoor1","", "LeftDoor2","RightDoor2", etc" }; };
UnknownStruct1
UnknownStruct1 { byte UnknownByte; tbool Extra; if(Extra) { byte ExtraByte; } byte UnknownBytes[3]; float ModelMass. ModelMassReciprocal, ModelMassModifier; byte UnknownBytes[16]; ulong UnknownLong; byte UnknownByte; asciiz ClassType; //class="" or "House" or... asciiz DestructType; //damage="" or "Tent" or "Building",,,, byte UnknownBytes[5]; }
Animations
Animations { tbool AnimsExist; if (AnimsExist) { ulong nAnimationClasses; // eg NoOfAnimSelections; AnimationClass AnimationClasses[nAnimationClasses]; ulong NoOfResolutions;// same value as Header.NoOfLods 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; } }
LodFace
//only when the LODFaceIndicator for that lod is false LodFace { ulong HeaderFaceCount; bytes Unknown[13]; }
ODOLv40Lod
ODOLv40Lod { ulong nProxies; LodModelProxy LodModelProxies[nProxies]; ulong nLodItems; ulong LodItems[nLodItems]; // potentially compressed ulong nUsedBones; LodUsedBone LodUsedBones[nUsedBones]; LodPointProperties[...]; // Potentially compressed byte UnknownBytes[8]; float UnknownFloats[10]; ulong NoOfTextures; asciiz LodPaaTextureNames[NoOfTextures]; //"ca\characters\hhl\hhl_01_co.paa" ulong NoOfMaterials; LodMaterial LodMaterials[NoOfMaterials]; LodEdge LodEdge1; // potentially compressed LodEdge LodEdge2; // potentially compressed LodPolygons[...]; ulong nSections; LodSection LodSections[nSections]; ulong nNamedSelections; LodNamedSelection LodNamedSelections[nNamedSelections]; // potentially compressed ulong nTokens; LodTokenPair LodTokenPairs[nTokens]; ulong nFrames; LodKeyFrame LodKeyFrames[nFrames]; byte Unknown[17]; { LodUV LodUV1; // Potentially compressed ulong nUVs; if (nUVs==2) LodUV LodUV2; // Potentially compressed ulong NoOfVertices; XYZTriplet LodXZY[NoOfVertices]; // Potentially compressed ulong nNormals; LodNormals LodNormals[nNormals]; // Potentially compressed } // Note that nUV1's== nNormals == NoOfVertices ulong nMinMax; XYZTriplet LodMinMaxXYZ[nMinMax][2]; // Potentially compressed ulong Count; LodUnknownStruct UnknownLodStruct[Count]; // Potentially compressed ulong nBytes; byte UnknownBytes[nBytes][32]; // Potentially compressed }
LodModelProxy
LodModelProxy { asciiz P3dProxyName; //"\ca\a10\agm65" (.p3d is implied) <<note the leading filename backslash XYZTriplet RotationMatrix[3]; XYZTriplet Translation; ulong Indices[4]; }
This structure is (almost) identical to ODOL7 except it has 4 indices rather than 2.
LodUsedBone
LodUsedBone { ulong nIDs; //range 0..3 ulong BoneID[nIDs]; }
This struct seems to assign unique iterative IDs (starting from zero) to some bones or their anims. Quite weird way of doing that, so it is probably not the whole truth.
LodPointProperties
LodPointProperties { ulong NoOfPts; tbool UseDefault; if (UseDefault) ulong DefaultValue; else // =0 ulong PropertyValues[NoOfPts]; // potentially compressed }
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.
The use of a) potential compression and b) a default fill, is endemic to many ODOLV40 type packets.
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 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 BoolFlag; //mostly 1 otherwise 0 ulong AnIndex; //0,1 or 2 asciiz BiSurfaceName; // "ca\data\Penetration\plastic.bisurf" ulong Always0x01; ulong aCount; //Generally 0 ulong nTextures; ulong nTransforms; // always same as nTextures LodStageTexture StageTextures [nTextures]; LodStageTransform StageTransforms[nTransforms]; }
- 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; // mostly 3, sometimes 0 asciiz PaaTexture; // "ca\characters\data\civil_tvreport_body_as.paa // alternatively "#(argb,8,8,3)color(0,0,0,1,CO)" (eg) ulong StageID; // zero based };
- The StageID is iterative (linear sequential). 1st entry is 0, 2nd 1, 3rd 2, etc.
- TextureFilter maybe 1 of the following values.
- 0: Point
- 1: Linear
- 2: TriLinear
- 3: Anisotropic (default)
LodStageTransform
LodStageTransform { ulong UVSource; float Transform[4][3];//a DirectX texture space transform matrix };
LodEdge
LodEdge { ulong nEdges; ushort Edges[nEdges]; // potentially compressed };
LodPolygons
LodPolygons { ulong NoOfPolygons; ulong OffsetToSectionsStruct; // see below ushort AlwaysZero; PolygonVertice { byte NoOfVertices; // 3 or 4 ushort VerticesIndex[NoOfVertices]; // 0-based index into Vertices Arrays }PolygonVertices[NoOfPolygons]; }
Note that there are always 3, or 4, vertices.
- 3 point vertices describe a triangle.
- 4 point vertices describe a rectangle.
The indices must be transformed as follows
- triangles : 1st posn, 2nd posn, 0th posn.
- quadrangles : 1st, 2nd, 3rd, 0th
- Because of the variable amount of vertices in this struct (3 or 4), OffsetToSectionsStruct is used to skip the block. It's value is relative to the first PolygonVertice and is computed as follows
OffsetToSectionsStruct= NoOfPolygons * (SizeofEach (PolygonVertice));
Each PolygonVertice is
nOfVertices *sizeof(ushort) + sizeof(ushort); // always 8 or 10
This, is in fact, an ERROR because the type size of the NoOfVertices is byte, NOT ushort. Hence the REAL offset is
RealOffsetToSectionsStruct = OffsetToSectionsStruct - NoOfPolygons *(sizeof(ushort)- sizeof(byte) );
or, to put it more simply
RealOffsetToSectionsStruct = OffsetToSectionsStruct - NoOfPolygons;
NOTE: See discussions for actual nature of this value
LodSection
LodSection { 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; }
LodNamedSelection
LodNamedSelection { asciiz NamedSelectionName; // "rightleg" or "neck" eg ulong NoOfSelectedFaces; ushort SelectedFaceIndexes[NoOfSelectedFaces]; //NOTE: This array is Compressed if size > 1024. ulong Always0; tbool IsSectional; //Appears in the sections[]= list of a model.cfg ulong NoOfUlongs; ulong UnknownArray[NoOfUlongs]; // possibly subject to compression. none seen so far ulong nSelectedVertices; ushort SelectedVerticesIndexes[nSelectedVertices]; // NOTE: This array is Compressed if size > 1024. ulong nTextureWeights; byte SelectedVerticesWeights[nTextureWeights]; // NOTE: This array is Compressed if size > 1024. }
LodTokenPair
LodTokenPair { asciiz Property;// "noshadow" = "1" eg asciiz Value; }
LodKeyFrame
LodKeyFrame { float FrameTime; ulong NoOfFramePoints; XYZTriplet LodFramePoints[NoOfFramePoints]; }
LodUV
LodUV { 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
LodNormals
LodNormals { tbool DefaultFill; if (DefaultFill) XYZTriplet XZY; else XYZTriplet XZY[nNormals]; // Potentially compressed }
LodUnknownStruct
LodUnknownStruct // potentially compressed { ulong Index; float Unknown[2]; // probably a vertices something }
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)