P3D File Format - ODOLV4x: Difference between revisions

From Bohemia Interactive Community
Jump to navigation Jump to search
m (→‎structMaterial: D3DCOLORVALUE)
Line 397: Line 397:
     ulong          VertexShaderId;//See enumVertexShaderId
     ulong          VertexShaderId;//See enumVertexShaderId
     ulong          Links[2];
     ulong          Links[2];
     asciiz        BiSurface;
     asciiz        SurfaceName;
     if BiSurf != 0
    ulong          MoreLinks[4];
      byte   bytes[77];
     if SurfaceName nStages=1;
     else
    else          nStages=1+FunctionOf(PixelShaderId);
    structNoBisurf  NoBisurf;
    StageTexture  StageTextures  [nStages];
     StageTransform StageTransforms[nStages];  
   }   
   }   
</nowiki></code>
</nowiki></code>
====D3DCOLORVALUE====
 
D3DCOLORVALUE
:There is """always""" a default Stage as the first entry.
{
:It is the only entry if a SurfaceName exists.
  float r,g,b,a;
:Otherwise, nStages is determined by the PixelShaderID (plus 1 for the unconditional default)
}
====structNoBisurf====
struct structNoBisurf
{
  ulong[4]                        MoreLinks;
  structStageTexture  [nStages+1] StageTextures;
  structStageTransform[nStages+1] StageTransforms;
}
: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
:There is always the default, sometimes dummy 1st stage entry so actual nStages is +1
:As of Jan 09, that table is has not been fully checked with latest info and might be obsolete
: As of Jan 09, the table is has not been fully checked and might be obsolete


=====structStageTexture=====
====StageTexture====
  struct StageTexture
  StageTexture
  {
  {
   ulong  TextureFilter;
   ulong  TextureFilter;
Line 428: Line 419:
   ulong  StageID;
   ulong  StageID;
  };
  };
=====structStageTransform=====
====StageTransform====
   struct structStageTransform
   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

Template:unsupported-doc

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)