Main Site Links Resources Tutorials
News VB Gaming Code Downloads DirectX 7
Contact Webmaster VB Programming Product Reviews DirectX 8
  General Multimedia Articles DirectX 9
      Miscellaneous

Immediate Mode: Loading Objects
By: Jack Hoxley (With initial help from 'Corre')
Written: November 2000

Download: IM_ASEObjects.Zip (12Kb)


Anyone who makes their game in full 3D will require objects to populate the world, they are as essential to 3D as sprites are to 2D. But unfortunately they aren't as simple as you might think. The main problem is that you'll need compatability with a common 3D package (3D Studio Max or Lightwave) and probably a 3D modeller/Artist - unless you're very good anyway. There is support in DirectX for X-Files (a Microsoft format), but they have some nasty limitations.

  1. It's difficult to get them to work with textures
  2. It's not very fast, as it uses retained mode to load/process them initilially.
  3. They only work with D3DVERTEX - not D3DLVERTEX; unless you play around with them....

So the only option is to make your own format, or decipher an existing one.

This tutorial will show you how to read, compile and load a 3D scene from an ASE file; this is featured in the 3D Studio Max series, as well as several other major programs. The reason I chose this format is that it's extremely simple to understand; you can open it up in notepad and read it! By the end of this tutorial you'll have made a class module that will work all the time, every time - a class module that you need only import into a new project and it's ready to use...

This tutorial is divided into the following sections:

  1. Understanding the ASE format
  2. Reading it into a program and processing it
  3. Saving it from our compiler
  4. Loading it into a class
  5. Manipulating the object
  6. Rendering the object
  7. Using the Class

1: Understanding the ASE format.
If you just look at this simple ASE file:

*3DSMAX_ASCIIEXPORT 200
*COMMENT "AsciiExport Version  2.00 - Thu Nov 09 21:39:57 2000"
*SCENE {
*SCENE_FILENAME ""
*SCENE_FIRSTFRAME 0
*SCENE_LASTFRAME 100
*SCENE_FRAMESPEED 30
*SCENE_TICKSPERFRAME 160
*SCENE_BACKGROUND_STATIC 0.0000 0.0000 0.0000
*SCENE_AMBIENT_STATIC 0.0431 0.0431 0.0431
}
*MATERIAL_LIST {
*MATERIAL_COUNT 1
*MATERIAL 0 {
*MATERIAL_NAME "Material #1"
*MATERIAL_CLASS "Standard"
*MATERIAL_AMBIENT 0.1791 0.0654 0.0654
*MATERIAL_DIFFUSE 0.5373 0.1961 0.1961
*MATERIAL_SPECULAR 0.9000 0.9000 0.9000
*MATERIAL_SHINE 0.2500
*MATERIAL_SHINESTRENGTH 0.0500
*MATERIAL_TRANSPARENCY 0.0000
*MATERIAL_WIRESIZE 1.0000
*MATERIAL_SHADING Blinn
*MATERIAL_XP_FALLOFF 0.0000
*MATERIAL_SELFILLUM 0.0000
*MATERIAL_FALLOFF In
*MATERIAL_SOFTEN
*MATERIAL_XP_TYPE Filter
*MAP_DIFFUSE {
*MAP_NAME "Map #2"
*MAP_CLASS "Bitmap"
*MAP_SUBNO 1
*MAP_AMOUNT 1.0000
*BITMAP "C:\GRAPHICS\3DSMAX2\MAPS\AGRAGATE.JPG"
*MAP_TYPE Spherical
*UVW_U_OFFSET 0.0000
*UVW_V_OFFSET 0.0000
*UVW_U_TILING 1.0000
*UVW_V_TILING 1.0000
*UVW_ANGLE 0.0000
*UVW_BLUR 1.0000
*UVW_BLUR_OFFSET 0.0000
*UVW_NOUSE_AMT 1.0000
*UVW_NOISE_SIZE 1.0000
*UVW_NOISE_LEVEL 1
*UVW_NOISE_PHASE 0.0000
*BITMAP_FILTER Pyramidal
}
}
}
*GEOMOBJECT {
*NODE_NAME "Box01"
*NODE_TM {
*NODE_NAME "Box01"
*INHERIT_POS 0 0 0
*INHERIT_ROT 0 0 0
*INHERIT_SCL 0 0 0
*TM_ROW0 1.0000 0.0000 0.0000
*TM_ROW1 0.0000 1.0000 0.0000
*TM_ROW2 0.0000 0.0000 1.0000
*TM_ROW3 0.0000 0.0000 -5.0000
*TM_POS 0.0000 0.0000 -5.0000
*TM_ROTAXIS 0.0000 0.0000 0.0000
*TM_ROTANGLE 0.0000
*TM_SCALE 1.0000 1.0000 1.0000
*TM_SCALEAXIS 0.0000 0.0000 0.0000
*TM_SCALEAXISANG 0.0000
}
*MESH {
*TIMEVALUE 0
*MESH_NUMVERTEX 8
*MESH_NUMFACES 12
*MESH_VERTEX_LIST {
*MESH_VERTEX    0 -5.0000 -5.0000 -5.0000
*MESH_VERTEX    1 5.0000 -5.0000 -5.0000
*MESH_VERTEX    2 -5.0000 5.0000 -5.0000
*MESH_VERTEX    3 5.0000 5.0000 -5.0000
*MESH_VERTEX    4 -5.0000 -5.0000 5.0000
*MESH_VERTEX    5 5.0000 -5.0000 5.0000
*MESH_VERTEX    6 -5.0000 5.0000 5.0000
*MESH_VERTEX    7 5.0000 5.0000 5.0000
}
*MESH_FACE_LIST {
*MESH_FACE    0:    A:    0 B:    2 C:    3 AB:    1 BC:    1 CA:    0
*MESH_FACE    1:    A:    3 B:    1 C:    0 AB:    1 BC:    1 CA:    0
*MESH_FACE    2:    A:    4 B:    5 C:    7 AB:    1 BC:    1 CA:    0
*MESH_FACE    3:    A:    7 B:    6 C:    4 AB:    1 BC:    1 CA:    0
*MESH_FACE    4:    A:    0 B:    1 C:    5 AB:    1 BC:    1 CA:    0
*MESH_FACE    5:    A:    5 B:    4 C:    0 AB:    1 BC:    1 CA:    0
*MESH_FACE    6:    A:    1 B:    3 C:    7 AB:    1 BC:    1 CA:    0
*MESH_FACE    7:    A:    7 B:    5 C:    1 AB:    1 BC:    1 CA:    0
*MESH_FACE    8:    A:    3 B:    2 C:    6 AB:    1 BC:    1 CA:    0
*MESH_FACE    9:    A:    6 B:    7 C:    3 AB:    1 BC:    1 CA:    0
*MESH_FACE   10:    A:    2 B:    0 C:    4 AB:    1 BC:    1 CA:    0
*MESH_FACE   11:    A:    4 B:    6 C:    2 AB:    1 BC:    1 CA:    0
}
*MESH_NUMTVERTEX 12
*MESH_TVERTLIST {
*MESH_TVERT 0 0.0000 0.0000 0.0000
*MESH_TVERT 1 1.0000 0.0000 0.0000
*MESH_TVERT 2 0.0000 1.0000 0.0000
*MESH_TVERT 3 1.0000 1.0000 0.0000
*MESH_TVERT 4 0.0000 0.0000 0.0000
*MESH_TVERT 5 1.0000 0.0000 0.0000
*MESH_TVERT 6 0.0000 1.0000 0.0000
*MESH_TVERT 7 1.0000 1.0000 0.0000
*MESH_TVERT 8 0.0000 0.0000 0.0000
*MESH_TVERT 9 1.0000 0.0000 0.0000
*MESH_TVERT 10 0.0000 1.0000 0.0000
*MESH_TVERT 11 1.0000 1.0000 0.0000
}
*MESH_NUMTVFACES 12
*MESH_TFACELIST {
*MESH_TFACE 0 9 11 10
*MESH_TFACE 1 10 8 9
*MESH_TFACE 2 8 9 11
*MESH_TFACE 3 11 10 8
*MESH_TFACE 4 4 5 7
*MESH_TFACE 5 7 6 4
*MESH_TFACE 6 0 1 3
*MESH_TFACE 7 3 2 0
*MESH_TFACE 8 4 5 7
*MESH_TFACE 9 7 6 4
*MESH_TFACE 10 0 1 3
*MESH_TFACE 11 3 2 0
}
}
*PROP_MOTIONBLUR 0
*PROP_CASTSHADOW 1
*PROP_RECVSHADOW 1
*MATERIAL_REF 0
}

This was created, as you can see, by 3D Studio Max 2. If you have this program to create an ASE file you go File=>Export and select ASE file; then choose the following options : Mesh Definition, Materials, Mapping Coordinates, Geometric (under object type).

Now we have this data we can manually go through it and remove what we dont need. Depending on what your engine is capable of you may want different bits of information; but in general you can cut it all back to just the mesh definition (with texture coords) and the texture information. Like so:

MATERIAL_COUNT 1 //How many materials were used in the scene - materials in this instance are textures
MATERIAL 0 { //there'll be more of these depending on how many were used
BITMAP "C:\GRAPHICS\3DSMAX2\MAPS\AGRAGATE.JPG" //the file name - this is the only part of the material definition we kept
}
}
GEOMOBJECT { //Beginning of a new object - a useful reference point for when we have multiple objects
MESH_NUMVERTEX 8 //How many vertices
MESH_NUMFACES 12 //How many faces
MESH_VERTEX_LIST { //this lists the number of vertices in this object
MESH_VERTEX 0 -5.0000 -5.0000 -5.0000
MESH_VERTEX 1 5.0000 -5.0000 -5.0000
MESH_VERTEX 2 -5.0000 5.0000 -5.0000
MESH_VERTEX 3 5.0000 5.0000 -5.0000
MESH_VERTEX 4 -5.0000 -5.0000 5.0000
MESH_VERTEX 5 5.0000 -5.0000 5.0000
MESH_VERTEX 6 -5.0000 5.0000 5.0000
MESH_VERTEX 7 5.0000 5.0000 5.0000
}
MESH_FACE_LIST { //A face is defined as a triangle - of 3 vertices
MESH_FACE 0: A: 0 B: 2 C: 3 //Face 0 is made up of the points 023 which are given in the list above.
MESH_FACE 1: A: 3 B: 1 C: 0
MESH_FACE 2: A: 4 B: 5 C: 7
MESH_FACE 3: A: 7 B: 6 C: 4
MESH_FACE 4: A: 0 B: 1 C: 5
MESH_FACE 5: A: 5 B: 4 C: 0
MESH_FACE 6: A: 1 B: 3 C: 7
MESH_FACE 7: A: 7 B: 5 C: 1
MESH_FACE 8: A: 3 B: 2 C: 6
MESH_FACE 9: A: 6 B: 7 C: 3
MESH_FACE 10: A: 2 B: 0 C: 4
MESH_FACE 11: A: 4 B: 6 C: 2
}
MESH_NUMTVERTEX 12 //This rarely equals the number of vertices.
MESH_TVERTLIST {
MESH_TVERT 0 0.0000 0.0000 //You may have noticed that the original had three values
MESH_TVERT 1 1.0000 0.0000 //these were UVW coordinates; D3D doesn't use W coordinates, so we can ignore them.
MESH_TVERT 2 0.0000 1.0000
MESH_TVERT 3 1.0000 1.0000
MESH_TVERT 4 0.0000 0.0000
MESH_TVERT 5 1.0000 0.0000
MESH_TVERT 6 0.0000 1.0000
MESH_TVERT 7 1.0000 1.0000
MESH_TVERT 8 0.0000 0.0000
MESH_TVERT 9 1.0000 0.0000
MESH_TVERT 10 0.0000 1.0000
MESH_TVERT 11 1.0000 1.0000
}
MESH_NUMTVFACES 12 //This should equal the number of faces in the model - if not we're a little stuck.
MESH_TFACE 0 9 11 10 //This is the same as the face list earlier - face 0 has the texture coordinates of vertex 9,11,10
MESH_TFACE 1 10 8 9
MESH_TFACE 2 8 9 11
MESH_TFACE 3 11 10 8
MESH_TFACE 4 4 5 7
MESH_TFACE 5 7 6 4
MESH_TFACE 6 0 1 3
MESH_TFACE 7 3 2 0
MESH_TFACE 8 4 5 7
MESH_TFACE 9 7 6 4
MESH_TFACE 10 0 1 3
MESH_TFACE 11 3 2 0
}
}
MATERIAL_REF 0 //References the list at the beginning of the file
}

That's quite a lot of information we've cut out now. The only parts that I've cut out that are likely to be of any interest to you are the material definitions. I've also add ed some comments about what the information means. I've also dropped the formatting - as we dont really need the Tab indents.

Now we have the data in simple form we can just parse it into arrays and then store it in our own file format for later use.

2: Reading it into a program and processing it
We're now going to design our first class. This class will take a raw ASE file and output a custom format 3D file with only the information that we require. There are various methods you can use to process the data - usually a variation of "All in one go" or in several passes. I prefer to do it with several passes. First we read the data in, then we remove any junk that we dont like, then we compile it into information we need - in arrays, then we store it in a file.

First I'll list which functions we'll be creating and their prototypes; then we'll see what they do.

Private Function CompileRetrievedData() As Boolean //This saves the data to a file that we can understand
Private Function DumpToFile(Filename As String) As Boolean //Creates an intermediate file
//These 5 functions take the raw ASE data and alter it so it only contains stuff we need...
Private Function FormatBITMAPline(Line As String) As String
Private Function FormatFACEline(Line As String) As String
Private Function FormatTCOORDline(Line As String) As String
Private Function FormatTFACEline(Line As String) As String
Private Function FormatVERTEXline(Line As String) As String
//These next 4 functions retrieve information from formatted lines and reads them into arrays
Private Function GetASEFaceDataFromLine(Line As String, ObjNum As Integer) As tASEFace
Private Function GetASETCoordDataFromLine(Line As String) As tASETexCoord
Private Function GetASETFaceDataFromLine(Line As String, ObjNum As Integer) As tASEFace
Private Function GetASEVertexFromLine(Line As String) As tASEVertex
//This next function starts the whole thing rolling...
Public Function LOAD(ASEfilename As String) As Boolean
//This just appends information to a log that you can specify..
Private Sub LogText(Str As String)
//This is the first step - it reads the data into an array of lines
Private Function ReadDataIntoArray(Filename As String) As Boolean
//This is one of the biggest sections - removing stuff we decide isn't useful...
Private Function RemoveUnwantedData() As Boolean
//This is done after we remove the junk, and at the end of it our arrays will hold all the info we might need.
Private Function RetrieveRawData() As Boolean

One note on these functions, first you'll notice that all of them are functions, no "sub"s. Alot of them would need to return things anyway but there are others that normally wouldn't. All the ones that return a boolean datatype are functions that need to be checked for errors - if the function returns false we need to stop before trying to do other things. Consider this function template:

Private Function Something() as Boolean
On Error goto ErrHand:

//Do stuff now

Something=True
Exit function
ErrHand:
Something=False
End Function

If you use this model for all the procedures you normally declare as a "sub" you can check if it succeeded or failed. We can now use this following code quite easily:

if Not(SomeFunction()=True) then goto ErrHand:

If the function fails we go straight to the parents error handler and cease exececution - before any other errors occur.

Now we'll go through each function.

2a. Declarations.
Before we write any of the functions we will want some declarations; remember that we're designing a class module, so alot of this will be private (hidden) from the rest of our project.

'////////////////////////////////////////////////////////////////////////////////////
'////                                                   
'////       clsASECompiler : Used to import ASE data from 3DSMax and   
'////                              compile it into a format that the game      
'////                              'The Man With The Digital Gun' can read    
'////                                                                            
'////       Written : November 7th 2000                                       
'////       By : Jack Hoxley                                                   
'////                                                                                 
'////       Contact: Jollyjeffers@Greenonions.netscapeonline.co.uk       
'////                    www.vbexplorer.com/DirectX4vb/                   
'////                    www.CloneSoftware.Cjb.Net                               
'//////////////////////////////////////////////////////////////////////////////////

'//November 06 2000
    'Class created. Data read from file and uneeded data removed
'//November 07 2000
    'Data formated, read into correct arrays
'//November 08 2000
    'Added support for log files
    'Final Data-Array copying finished
    'Fixed bug with high-number vertices.
    'Now collects Texture Filenames and references them against each object...
    'The Class now exports a final file listing all the required information...
    
    'COMPLETE


Option Explicit '//Removes any Aliasing...

Private Declare Function GetTickCount Lib "kernel32" () As Long '//For statistical Timing Purposes..

Public LogBox As TextBox '//Where we stick log entries
Public ExportTo As String '//The destination for the compiled file...

Private ASEData() As String             'Open ended array for the length of the file...
Private ASEData_NumLines As Long   'How many lines are in the ASEData() array

'//CORRE's suggestions....

Private Type tASEVertex
    X As Single
    Y As Single
    Z As Single
End Type
 
Private Type tASETexCoord
    u As Single
    v As Single
End Type
 
Private Type tASEFace
    p1 As tASEVertex '//Triangle Vertices
    p2 As tASEVertex
    p3 As tASEVertex
    n1 As tASEVertex '//Vertex Normals - Not used.
    n2 As tASEVertex
    n3 As tASEVertex
    t1 As tASETexCoord '//Texture Coordinates
    t2 As tASETexCoord
    t3 As tASETexCoord
End Type


'//Designed so that num objects and num vertices/faces can be redefined later...

Private Type ASEObject
    StartLine As Long 'Which line in memory does this object start @
    Texture As String '//What file we want to use...
    TextureReference As Integer 'What number it is...
    numVertices As Long 'How many vertices we need to deal with
    numTVertices As Long 'How many TVertices we have
    numFaces As Long 'How many faces we have to deal with...
    ASE_Vertices() As tASEVertex '//Temporary arrays
    ASE_TVertices() As tASETexCoord
    ASE_Faces() As tASEFace '//The final results for the model
End Type
'Only the ASE_Faces() and the Texture are to be exported...

Private NumObjectsInFile As Long '//How many objects do we have to deal with?
Private OBJECT() As ASEObject '//Open ended....

Private numTextureNames As Integer '//How many textures we require...
Private TextureNames() As String '//Open Ended array for the textures used in the model..

note the header at the top - this is actually a class that I designed for use in my upcoming game. Aren't I nice sharing this with you :)

2b: LOAD()
This is a very simple function, and uses the above error handling techniques in order to return a sucess/fail code to it's parent.

Public Function LOAD(ASEfilename As String) As Boolean
'//This is the only publicly visible class...

Dim ProcessTime As Long
ProcessTime = GetTickCount() '//For statistics

'//1. Get the data from the file
    'This part just reads the data in, line by line, into memory

    Call LogText("Retrieving ASE data from File {" & ASEfilename & "}")
If Not (ReadDataIntoArray(ASEfilename) = True) Then GoTo FATAL:

'//2. Remove junk
    'Now we need to remove all the parts that aren't needed.

    Call LogText("Removing Unwanted Data from File....")
If Not (RemoveUnwantedData() = True) Then GoTo FATAL:

'//2a. Dump the data to a file should you need to look at it
    Call LogText("Dumping Data to file")
If Not (DumpToFile(ASEfilename & "_Intermediate.txt") = True) Then GoTo FATAL:

'//3. Retrieve the first set of data
    'We now get the information from the formatted data - in the same format

    Call LogText("Collecting Information On Objects...")
If Not (RetrieveRawData() = True) Then GoTo FATAL:

'//4. Final pass through the data
    'We now compile all this stuff into information that we want to use

    Call LogText("Compiling and Saving...")
If Not (CompileRetrievedData() = True) Then GoTo FATAL:

ProcessTime = GetTickCount() - ProcessTime '//Finalise the statistical information.
Call LogText("")
Call LogText("Model Compiled Successfully in  " & Format$(ProcessTime / 1000, "0.000") & "s")

LOAD = True '//If we're here we have loaded the data successfully
Exit Function
FATAL:
LOAD = False '//If we're here we had an error somewhere....
'Might as well explain why:
Call LogText("")
Call LogText("ERROR OCCURED")
Call LogText("   Number:  " & Err.Number)
Call LogText("   Description:  " & Err.Description)
End Function

2c. Retrieving the data from the file.
Before we can start storing the data and processing it we need to get it into our program. I've decided to load each line into an individual element of an array. We can then examine each individual line - which usually only contain one thing.

Private Function ReadDataIntoArray(Filename As String) As Boolean
On Error GoTo ERRHAND:

Dim FileNum As Integer, Dummy As String
FileNum = FreeFile '//Get us a file port to use.


Open Filename For Input As #FileNum

Do While Not EOF(FileNum)
'Increment the number of lines
    ASEData_NumLines = ASEData_NumLines + 1
'resize the array to suit the number of lines
    ReDim Preserve ASEData(ASEData_NumLines) As String
'read the line from the file
    Line Input #FileNum, Dummy
'add the new line into the array
    ASEData(ASEData_NumLines) = Dummy
Loop

Close #FileNum

Call LogText("   Number of lines Read: " & ASEData_NumLines) '//This can range from 100 to 8000+...


ReadDataIntoArray = True
Exit Function
ERRHAND:
ReadDataIntoArray = False
End Function

Nothing greatly complicated there, but what we get is the basis of everything else we're doing - so it's probably the most important function in the class.

2d. Removing the Junk.
This is where it gets fun. You can use various forms of data processing/manipulation but the code shown here works fine... Also bare in mind that this uses several smaller functions that I'll explain a little later. This function also formats the data, as well as removing stuff we dont need.

'//This loops through the file removing any data we dont need...
Private Function RemoveUnwantedData() As Boolean
On Local Error Resume Next:

'//Variables
Dim i As Long                       'Main Loops
Dim i2 As Long                     'Loop for clearing the *SCENE header
Dim LinesRemoved As Long       'Statistical information

'//Main Loop

For i = 0 To ASEData_NumLines

'//Strip all TAB chars from the lines
    'Maximum of 3 Tabs at the start of a line...

    If Left(ASEData(i), 1) = Chr$(9) Then
        ASEData(i) = Right(ASEData(i), Len(ASEData(i)) - 1)
    End If
    If Left(ASEData(i), 1) = Chr$(9) Then
        ASEData(i) = Right(ASEData(i), Len(ASEData(i)) - 1)
    End If
    If Left(ASEData(i), 1) = Chr$(9) Then
        ASEData(i) = Right(ASEData(i), Len(ASEData(i)) - 1)
    End If

'//First two header lines...
If UCase(Left(ASEData(i), 5)) = "*3DSM" Then '//first line header...
    ASEData(i) = "" '//Clear this entry
ElseIf UCase(Left(ASEData(i), 5)) = "*COMM" Then '//Second line comment
    ASEData(i) = "" '//Clear this entry
End If

'//Scene Description
If UCase(Left(ASEData(i), 5)) = "*SCEN" Then '//Beginning of a scene block...
    For i2 = i To i + 30 '//scan 30 lines ahead for the end of the SCENE block
        If UCase(Left(ASEData(i2), 1)) = "}" Then '//End found
            ASEData(i2) = ""
            GoTo FINISHEDSCENEHEADER: '//about 5 lines below here...
        Else    '//End hasn't been found yet...
            ASEData(i2) = ""
        End If
    Next i2
FINISHEDSCENEHEADER:
End If

'//GEOMOBJECT HEADERS
    'We'll keep the actual GEOMOBJECT parts... Just lose the rest.

If UCase(Left(ASEData(i), 10)) = "*NODE_NAME" Then ASEData(i) = ""

If UCase(Left(ASEData(i), 8)) = "*NODE_TM" Then '//Beginning of a Node block...
i2 = 0 'variable has already been used, so clear it first
    For i2 = i To i + 30 '//scan 30 lines ahead for the end of the Node block
        If Left(ASEData(i2), 2) = Chr$(9) & "}" Then '//End found
            ASEData(i2) = ""
            GoTo FINISHEDNODEHEADER: '//about 5 lines below here...
        Else  '//End hasn't been found yet...
            ASEData(i2) = ""
        End If
    Next i2
FINISHEDNODEHEADER:
End If

If UCase(Left(ASEData(i), 19)) = "*MESH_VERTEX_LIST {" Then '//Beginning of a MESH block...
ASEData(i) = "" '//Remove the "*MESH {" part
i2 = 0 'variable has already been used, so clear it first
    For i2 = i To ASEData_NumLines '//Vertex lists can be quite large
        If Left(ASEData(i2), 3) = Chr$(9) & Chr$(9) & "}" Then '//End found
            ASEData(i2) = ""
            GoTo FINISHEDVLISTHEADER: '//about 5 lines below here...
        End If
    Next i2
FINISHEDVLISTHEADER:
End If

If UCase(Left(ASEData(i), 17)) = "*MESH_FACE_LIST {" Then '//Beginning of a MESH block...
ASEData(i) = "" '//Remove the "*MESH_FACE_LIST {" part
i2 = 0 'variable has already been used, so clear it first
    For i2 = i To ASEData_NumLines 'Face lists can be enourmous
        If Left(ASEData(i2), 3) = Chr$(9) & Chr$(9) & "}" Then '//End found
            ASEData(i2) = ""
            GoTo FINISHEDFACEHEADER: '//about 5 lines below here...
        End If
    Next i2
FINISHEDFACEHEADER:
End If

If UCase(Left(ASEData(i), 17)) = "*MESH_TVERTLIST {" Then '//Beginning of a TVert block...
ASEData(i) = "" '//Remove the "*MESH_TVERTLIST {" part
i2 = 0 'variable has already been used, so clear it first
    For i2 = i To ASEData_NumLines 'TVERT lists can be enourmous
        If Left(ASEData(i2), 3) = Chr$(9) & Chr$(9) & "}" Then '//End found
            ASEData(i2) = ""
            GoTo FINISHEDTVERTHEADER: '//about 5 lines below here...
        End If
    Next i2
FINISHEDTVERTHEADER:
End If

If UCase(Left(ASEData(i), 17)) = "*MESH_TFACELIST {" Then '//Beginning of a TFACE block...
ASEData(i) = "" '//Remove the "*MESH_TFACELIST {" part
i2 = 0 'variable has already been used, so clear it first
    For i2 = i To ASEData_NumLines 'TFACE lists can be enourmous
        If Left(ASEData(i2), 3) = Chr$(9) & Chr$(9) & "}" Then '//End found
            ASEData(i2) = ""
            GoTo FINISHEDTFACEHEADER: '//about 5 lines below here...
        End If
    Next i2
FINISHEDTFACEHEADER:
End If

If UCase(Left(ASEData(i), 7)) = "*MESH {" Then '//Beginning of a TFACE block...
ASEData(i) = "" '//Remove the "*MESH {" part
i2 = 0 'variable has already been used, so clear it first
    For i2 = i To ASEData_NumLines 'MESH headers can be enourmous
        If Left(ASEData(i2), 2) = Chr$(9) & "}" Then '//End found
            ASEData(i2) = ""
            GoTo FINISHEDMESHHEADER: '//about 5 lines below here...
        End If
    Next i2
FINISHEDMESHHEADER:
End If


'//PROPERTY VALUES
If UCase(Left(ASEData(i), 6)) = "*PROP_" Then ASEData(i) = ""  '//Clear the line should we find it
'//Time indices

If UCase(Left(ASEData(i), 10)) = "*TIMEVALUE" Then ASEData(i) = ""  '//Clear the line should we find it
If UCase(Left(ASEData(i), 4)) = "*UVW" Then ASEData(i) = ""  '//Clear the line should we find it
If UCase(Left(ASEData(i), 13)) = "*MATERIAL_REF" Then ASEData(i) = "*OBJECT_TEXTURE=" & Right(ASEData(i), Len(ASEData(i)) - 13)
If UCase(Left(ASEData(i), 10)) = "*MATERIAL " Then ASEData(i) = "*TEXTURE " & Right(ASEData(i), Len(ASEData(i)) - 10)
If UCase(Left(ASEData(i), 10)) = "*MATERIAL_" Then ASEData(i) = ""  '//Clear the line should we find it
If UCase(Left(ASEData(i), 5)) = "*MAP_" Then ASEData(i) = ""  '//Clear the line should we find it
If UCase(Left(ASEData(i), 8)) = "*BITMAP_" Then ASEData(i) = ""  '//Clear the line should we find it

'//Remove things that were, but no longer are needed
If UCase(Left(ASEData(i), 1)) = "}" Then ASEData(i) = ""  '//Clear the line should we find it
If UCase(Left(ASEData(i), 1)) = "*" Then ASEData(i) = Right(ASEData(i), Len(ASEData(i)) - 1) '//Clear the line should we find it

'//Convert useful lines to shorter versions...
If UCase(Left(ASEData(i), 11)) = "MESH_VERTEX" Then
    ASEData(i) = "VERTEX" & Right(ASEData(i), Len(ASEData(i)) - 11)
    ASEData(i) = FormatVERTEXline(ASEData(i))
End If

If UCase(Left(ASEData(i), 9)) = "MESH_FACE" Then
    ASEData(i) = "FACE" & Right(ASEData(i), Len(ASEData(i)) - 9)
    ASEData(i) = FormatFACEline(ASEData(i))
End If

If UCase(Left(ASEData(i), 10)) = "MESH_TVERT" Then
    ASEData(i) = "TCOORD" & Right(ASEData(i), Len(ASEData(i)) - 10)
    ASEData(i) = FormatTCOORDline(ASEData(i))
End If

If UCase(Left(ASEData(i), 10)) = "MESH_TFACE" Then
    ASEData(i) = "TFACE" & Right(ASEData(i), Len(ASEData(i)) - 10)
    ASEData(i) = FormatTFACEline(ASEData(i))
End If

If UCase(Left(ASEData(i), 6)) = "BITMAP" Then
    ASEData(i) = FormatBITMAPline(ASEData(i))
End If

If Not (ASEData(i) = "") Then
    LinesRemoved = LinesRemoved + 1 '//Statistical information
End If

DoEvents
Next i

Debug.Print "File reduced from " & ASEData_NumLines & " to " & LinesRemoved & "  " _ 
& Format$(100 - ((LinesRemoved / ASEData_NumLines) * 100), "0.000") & "% reduction in size"

RemoveUnwantedData = True
Exit Function
ERRHAND:
RemoveUnwantedData = False
End Function

Again, not greatly complicated, and a lot of it is very similiar - but repeated. I mentioned above that we need several "helper" functions for this function to operate. Here they are:

'//Nothing complicated about this one.
Private Function FormatBITMAPline(Line As String) As String
Dim i As Integer, Temp As String, FoundSlashAt As Integer

'Remove the Line Title
FormatBITMAPline = Right(Line, Len(Line) - 7)

'Remove the " " 's
FormatBITMAPline = Right(FormatBITMAPline, Len(FormatBITMAPline) - 1)
FormatBITMAPline = Left(FormatBITMAPline, Len(FormatBITMAPline) - 1)

'Now remove everything but the actual file name
For i = Len(FormatBITMAPline) To 1 Step -1 'In reverse order
    Temp = Mid$(FormatBITMAPline, i, 1)
    If Temp = "\" Then FoundSlashAt = i: Exit For
Next i
FormatBITMAPline = Right(FormatBITMAPline, Len(FormatBITMAPline) - FoundSlashAt)
End Function




'//This is probably the least 'clean' of the lot... but it works.
Private Function FormatFACEline(Line As String) As String
'//We only need to remove anything from "AB:" onwards
Dim i As Integer, Temp As String, Compiled


For i = 1 To Len(Line) - 1 '//Run through each character
    Temp = Mid$(Line, i, 2)
    If Temp = "AB" Then
    Compiled = Left(Line, i - 1)
    End If
Next i

'//The string now only contains information upto what we need...

Compiled = Trim(Compiled)

For i = 1 To Len(Compiled)
    Temp = Mid$(Compiled, i, 1)
    If Temp = "A" Then
        If i > 4 Then FormatFACEline = FormatFACEline & "" Else FormatFACEline = FormatFACEline & Temp
    ElseIf Temp = "B" Then
        If i > 4 Then FormatFACEline = FormatFACEline & "" Else FormatFACEline = FormatFACEline & Temp
    ElseIf Temp = "C" Then
        If i > 4 Then FormatFACEline = FormatFACEline & "" Else FormatFACEline = FormatFACEline & Temp
    ElseIf Temp = ":" Then
        If i > 4 Then FormatFACEline = FormatFACEline & "" Else FormatFACEline = FormatFACEline & Temp
    ElseIf Temp = " " Then
        FormatFACEline = FormatFACEline & ""
    Else
        FormatFACEline = FormatFACEline & Temp
    End If
Next i

FormatFACEline = Replace(FormatFACEline, "", ",")
FormatFACEline = Replace(FormatFACEline, "", ",")
FormatFACEline = Replace(FormatFACEline, "", ",")
FormatFACEline = Replace(FormatFACEline, "", ",")
FormatFACEline = Replace(FormatFACEline, "", ",")
FormatFACEline = Replace(FormatFACEline, "", ",")
FormatFACEline = Replace(FormatFACEline, "", ",")
FormatFACEline = Replace(FormatFACEline, "", ",")
FormatFACEline = Replace(FormatFACEline, "", ",")
FormatFACEline = Replace(FormatFACEline, "", "")
End Function



'//A nice simple one next.
Private Function FormatTCOORDline(Line As String) As String
'//We need to replace all TABs with comma's....
Dim i As Integer, Temp As String

For i = 1 To Len(Line)
    Temp = Mid$(Line, i, 1)
    If Temp = Chr$(9) Then
        FormatTCOORDline = FormatTCOORDline & ","
    Else
        FormatTCOORDline = FormatTCOORDline & Temp
    End If
Next i
End Function


'//Pretty much identical to the above function.
Private Function FormatTFACEline(Line As String) As String
'//We need to replace all TABs with comma's....
Dim i As Integer, Temp As String

For i = 1 To Len(Line)
    Temp = Mid$(Line, i, 1)
    If Temp = Chr$(9) Then
        FormatTFACEline = FormatTFACEline & ","
    Else
        FormatTFACEline = FormatTFACEline & Temp
    End If
Next i
End Function



'//What a surprise :-) it's the same as the previous two.
Private Function FormatVERTEXline(Line As String) As String
'//We need to replace all TABs with comma's....
Dim i As Integer, Temp As String

For i = 1 To Len(Line)
    Temp = Mid$(Line, i, 1)
    If Temp = Chr$(9) Then
        FormatVERTEXline = FormatVERTEXline & ","
    Else
        FormatVERTEXline = FormatVERTEXline & Temp
    End If
Next i

End Function

From this point we should be able to read all the data in and remove any junk, as well as formatting it so it's easier to use later on. At this point the program dumps an intermediate file - not important at all, but sometimes can be useful if you think it's going wrong.

'//Not greatly difficult...

Private Function DumpToFile(Filename As String) As Boolean
On Error GoTo ERRHAND:

Dim Free As Integer, i As Long
Free = FreeFile

Open Filename For Output As #Free
    For i = 0 To ASEData_NumLines
        If ASEData(i) <> "" Then
            Print #Free, ASEData(i)
        End If
    Next i
Close #Free

DumpToFile = True
Exit Function

ERRHAND:
DumpToFile = False
End Function

2e. This is another very important function - before we execute this we'll have a list of useful data, at the end of it we'll have several structures that hold all the information needed to create an object in a Direct3D environment. It's a rather long function as well; and the majority of the processing time is spent here.

'//This function will get all the information that we need...
Private Function RetrieveRawData() As Boolean
'On Error GoTo ERRHAND:

'//We want to loop through the file and get all the information on each object
'//Object Starting Positions
'//Texture Coords
'//Vertices
'//Faces
'//Textured Faces
'//Material ID...



'//1. Variables required...
    Dim i As Long       'For Main Loop
    Dim Ic As Long      'For counting things...
    Dim Temp As String 'Temporary data holder
    Dim tNumber As Long 'Temporary Number...
    Dim TotVerts As Long 'Total Number of Vertices
    Dim TotFaces As Long 'Total Number of Faces
    Dim tFace As tASEFace 'Temporary Face Object
    
'//2. Count objects
    'First we need to count the number of objects
    'and bookmark their positions in the array...

    For i = 0 To ASEData_NumLines
    
'Search for the string: GEOMOBJECT
        If UCase(Left(ASEData(i), 10)) = "GEOMOBJECT" Then
            NumObjectsInFile = NumObjectsInFile + 1
            ReDim Preserve OBJECT(NumObjectsInFile) As ASEObject
            OBJECT(NumObjectsInFile).StartLine = i
        End If
    
    Next i
    
    Call LogText("Objects Found In File: " & NumObjectsInFile)
    
    'This stops the program getting an "Subscript Out of Bounds" error when
    'scanning between two objects.... see later on...

    ReDim Preserve OBJECT(NumObjectsInFile + 1) As ASEObject
    OBJECT(NumObjectsInFile + 1).StartLine = ASEData_NumLines
    
'//Just some simple Statistical Information...
    Debug.Print "Number of Objects Found in File = " & NumObjectsInFile
    

'//3. We now loop through every Object
    
    i = 0 'Variable has been used, reset it...
    
    For i = 1 To NumObjectsInFile
        Call LogText("") '//Insert A Blank Line
        Call LogText("Processing Object " & i & " of " & NumObjectsInFile)
    
        '3a. Calculate how many vertices we have....
                'starts with MESH_NUMVERTEX

                Call LogText("   Information:")
                For Ic = OBJECT(i).StartLine To OBJECT(i + 1).StartLine 'Scan just the text for this object
                    If UCase(Left(ASEData(Ic), 14)) = "MESH_NUMVERTEX" Then
'we now need to parse this string and get the value at the end...
                        Temp = Right(ASEData(Ic), Len(ASEData(Ic)) - 14)
                        Temp = Trim(Temp)
                        OBJECT(i).numVertices = Val(Temp) 'Convert the string into a number
                    End If
                Next Ic
                Call LogText("     Vertices in Model: " & OBJECT(i).numVertices)
                
        '3b. How many TVertices we have...
                'starts with MESH_NUMTVERTEX

                For Ic = OBJECT(i).StartLine To OBJECT(i + 1).StartLine 'Scan just the text for this object
                    If UCase(Left(ASEData(Ic), 15)) = "MESH_NUMTVERTEX" Then
'we now need to parse this string and get the value at the end...
                        Temp = Right(ASEData(Ic), Len(ASEData(Ic)) - 15)
                        Temp = Trim(Temp)
                        OBJECT(i).numTVertices = Val(Temp)  'Convert the string into a number
                    End If
                Next Ic
                
        '3c. How many Faces we have...
                'starts with MESH_NUMFACES

                For Ic = OBJECT(i).StartLine To OBJECT(i + 1).StartLine 'Scan just the text for this object
                    If UCase(Left(ASEData(Ic), 13)) = "MESH_NUMFACES" Then
'we now need to parse this string and get the value at the end...
                        Temp = Right(ASEData(Ic), Len(ASEData(Ic)) - 13)
                        Temp = Trim(Temp)
                        OBJECT(i).numFaces = Val(Temp) 'Convert the string into a number
                    End If
                Next Ic
                Call LogText("     Triangles in Model: " & OBJECT(i).numFaces)
                
'3d. Get the Texture Reference Index
'starts with OBJECT_TEXTURE=
                For Ic = OBJECT(i).StartLine To OBJECT(i + 1).StartLine 'Scan just the text for this object
                    If UCase(Left(ASEData(Ic), 15)) = "OBJECT_TEXTURE=" Then
'we now need to parse this string and get the value at the end...
                        Temp = Right(ASEData(Ic), Len(ASEData(Ic)) - 15)
                        Temp = Trim(Temp)
                        OBJECT(i).TextureReference = Val(Temp)  'Convert the string into a number
                    End If
                Next Ic
                
'3e. Get the Vertex coordinates
                Call LogText("   Progress:")
                Call LogText("     Collecting Vertex Information")
                For Ic = OBJECT(i).StartLine To OBJECT(i + 1).StartLine 'Scan just the text for this object
                    If UCase(Left(ASEData(Ic), 6)) = "VERTEX" Then
'We've found a vertex; lets get some more data...
                        Temp = Right(ASEData(Ic), Len(ASEData(Ic)) - 6)
                        Temp = Trim(Temp)
                        tNumber = Val(Temp) 'This now holds the vertex number
                        ReDim Preserve OBJECT(i).ASE_Vertices(tNumber) As tASEVertex 'resize the array to suit 
'the number of vertices in our model...
                        OBJECT(i).ASE_Vertices(tNumber) = GetASEVertexFromLine(Temp)
                    End If
                Next Ic
'3f. Get the face indices and compile a face list...
                Call LogText("     Compiling Face List")
                For Ic = OBJECT(i).StartLine To OBJECT(i + 1).StartLine 'Scan just the text for this object
                    If UCase(Left(ASEData(Ic), 4)) = "FACE" Then
'We've found a vertex; lets get some more data...
                        Temp = Right(ASEData(Ic), Len(ASEData(Ic)) - 4)
                        If Left(Temp, 1) = "," Then Temp = Right(Temp, Len(Temp) - 1)
                        Temp = Trim(Temp)
                        tNumber = Val(Temp) 'This now holds the Face number
                        ReDim Preserve OBJECT(i).ASE_Faces(tNumber) As tASEFace 'resize the array to suit the number of faces....
                        OBJECT(i).ASE_Faces(tNumber) = GetASEFaceDataFromLine(Temp, CInt(i))
                    End If
                Next Ic
                
'3g. Get the Textured Vertex Values
                Call LogText("     Collecting Texture Coordinates")
                For Ic = OBJECT(i).StartLine To OBJECT(i + 1).StartLine 'Scan just the text for this object
                    If UCase(Left(ASEData(Ic), 6)) = "TCOORD" Then
'We've found a vertex; lets get some more data...
                        Temp = Right(ASEData(Ic), Len(ASEData(Ic)) - 6)
                        Temp = Trim(Temp)
                        tNumber = Val(Temp) 'This now holds the Face number
                        ReDim Preserve OBJECT(i).ASE_TVertices(tNumber) As tASETexCoord 'Resize the array...
                        OBJECT(i).ASE_TVertices(tNumber) = GetASETCoordDataFromLine(Temp)
                    End If
                Next Ic
                
                
'3h. Get the TFace values and compile texture coordinates
                Call LogText("     Compiling Texture Coordinate Information")
                For Ic = OBJECT(i).StartLine To OBJECT(i + 1).StartLine 'Scan just the text for this object
                    If UCase(Left(ASEData(Ic), 5)) = "TFACE" Then
'We've found a vertex; lets get some more data...
                        Temp = Right(ASEData(Ic), Len(ASEData(Ic)) - 5)
                        Temp = Trim(Temp)
                        tNumber = Val(Temp) 'This now holds the Face number
                        '//To Solve bug where the Vertex Coordinates are lost we must use
                        '//A temporary variable, then copy the TCoords from this...

                        tFace = GetASETFaceDataFromLine(Temp, CInt(i))
                        
                        OBJECT(i).ASE_Faces(tNumber).t1.u = tFace.t1.u
                        OBJECT(i).ASE_Faces(tNumber).t1.v = tFace.t1.v
                        
                        OBJECT(i).ASE_Faces(tNumber).t2.u = tFace.t2.u
                        OBJECT(i).ASE_Faces(tNumber).t2.v = tFace.t2.v
                        
                        OBJECT(i).ASE_Faces(tNumber).t3.u = tFace.t3.u
                        OBJECT(i).ASE_Faces(tNumber).t3.v = tFace.t3.v
                    End If
                Next Ic
                Call LogText("     Object Compiled Successfully.")
                
'3i. Get some statistical Information...
                TotVerts = TotVerts + OBJECT(i).numVertices
                TotFaces = TotFaces + OBJECT(i).numFaces
    Next i

Call LogText("")
Call LogText("All Objects Compiled Successfully")
Call LogText("   Total Number of Vertices in Model: " & TotVerts)
Call LogText("   Total Number of Triangles in Model: " & TotFaces)
Call LogText("")

'////////////////////////////////////////////////////////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
'|||||| AT THIS POINT OBJECT(I).ASE_Faces() HOLDS ALL THE INFORMATION WE NEED |||||||||||||
'\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\////////////////////////////////////////////////////////

'4. We now need to extract the texture names that we are using.
    'we have the reference, now we need the string...

    Dim J As Long
i = 0 'Reset variables

For i = 0 To ASEData_NumLines 'Scan the whole file
    If UCase(Left(ASEData(i), 3)) = "TEX" Then
'//We've found a Texture Line....
        tNumber = Val(Trim(Right(ASEData(i), Len(ASEData(i)) - 7)))
        If tNumber > numTextureNames Then numTextureNames = tNumber  'Record the maximum Size...
        ReDim Preserve TextureNames(numTextureNames) As String
'//We now need to scan until the next non-empty line
        For J = i + 1 To i + 75 'Scan 75 lines beyond...
            If ASEData(J) <> "" Then
                TextureNames(tNumber) = ASEData(J)
                Exit For
            End If
        Next J
    End If
Next i

'//Now copy this data into the object variable
For i = 1 To NumObjectsInFile
    OBJECT(i).Texture = TextureNames(OBJECT(i).TextureReference)
Next i

'//Information for the user.
Call LogText("Textures Found: " & numTextureNames + 1)
For i = 0 To numTextureNames
    Call LogText("     " & TextureNames(i))
Next i

Call LogText("") '//Insert a blank line...

RetrieveRawData = True
Exit Function
ERRHAND:
RetrieveRawData = False
End Function

There are several parts in there that you may well need to change, should you use more of the material information than just the bitmap line. Otherwise, this function should work pretty much perfectly.

3: Saving it from our compiler
At this point we have all the information we need to create an object in Direct3D - but it's not in the easiest format to understand. So we'll now format the data and save it to a file. The method we'll use to save it will be similiar to the original ASE format - Ascii. You could save it using Binary access or databases, but I felt that was a little unnecessary - there's no great speed advantage in this case and no ones likely to steal any secrets from reading the file...

Private Function CompileRetrievedData() As Boolean
On Error GoTo ERRHAND:

'//We now format the data into a medium from which we can save it...
'//Convert them all into the faces...

Dim FFile As Integer
Dim i As Integer 'Object Loop
Dim J As Long 'Inner Loop
Dim Compiled As String 'The line we want to write..


FFile = FreeFile '//Get a free file number

Open ExportTo For Output As #FFile
    Print #FFile, "HEADER: The Man With The Digital Gun 3D MODEL {STATIC}"
    For i = 1 To NumObjectsInFile
        LogText ("   Saving Object " & i)
            Print #FFile, "TEX: " & OBJECT(i).Texture '//Write the Texture
        For J = 0 To OBJECT(i).numFaces - 1
'compiled=x,y,z, x,y,z, x,y,z, t1,t2,t3
'Point1:
                Compiled = OBJECT(i).ASE_Faces(J).p1.X & "," & OBJECT(i).ASE_Faces(J).p1.Y & "," _
                              & OBJECT(i).ASE_Faces(J).p1.Z & ","
'Point2:
                Compiled = Compiled & OBJECT(i).ASE_Faces(J).p2.X & "," & _
                              OBJECT(i).ASE_Faces(J).p2.Y & "," & OBJECT(i).ASE_Faces(J).p2.Z & ","
'Point3:
                Compiled = Compiled & OBJECT(i).ASE_Faces(J).p3.X & "," & OBJECT(i).ASE_Faces(J).p3.Y & "," _
                              & OBJECT(i).ASE_Faces(J).p3.Z & ","
'TexCoord1:
                Compiled = Compiled & OBJECT(i).ASE_Faces(J).t1.u & "," & OBJECT(i).ASE_Faces(J).t1.v & ","
'TexCoord2:
                Compiled = Compiled & OBJECT(i).ASE_Faces(J).t2.u & "," & OBJECT(i).ASE_Faces(J).t2.v & ","
'TexCoord3:
                Compiled = Compiled & OBJECT(i).ASE_Faces(J).t3.u & "," & OBJECT(i).ASE_Faces(J).t3.v & ","
                
            Print #FFile, Compiled '//Finally, Stick the line in the file...
        Next J
    Next i
Close #FFile


CompileRetrievedData = True
Exit Function
ERRHAND:
CompileRetrievedData = False
End Function

As you can see, especially if you open the final file in notepad, is that it is divided into blocks defined by their textures. Each line defines a triangle which can be rendered by Direct3D - as we'll see later on. An example of the final format would be something like this:

HEADER: The Man With The Digital Gun 3D MODEL {STATIC}
TEX: Bubing_2.bmp
-5,-5,-5,-5,5,-5,5,5,-5,1,0,1,1,0,1,
5,5,-5,5,-5,-5,-5,-5,-5,0,1,0,0,1,0,
-5,-5,5,5,-5,5,5,5,5,0,0,1,0,1,1,
5,5,5,-5,5,5,-5,-5,5,1,1,0,1,0,0,
-5,-5,-5,5,-5,-5,5,-5,5,0,0,1,0,1,1,
5,-5,5,-5,-5,5,-5,-5,-5,1,1,0,1,0,0,
5,-5,-5,5,5,-5,5,5,5,0,0,1,0,1,1,
5,5,5,5,-5,5,5,-5,-5,1,1,0,1,0,0,
5,5,-5,-5,5,-5,-5,5,5,0,0,1,0,1,1,
-5,5,5,5,5,5,5,5,-5,1,1,0,1,0,0,
-5,5,-5,-5,-5,-5,-5,-5,5,0,0,1,0,1,1,
-5,-5,5,-5,5,5,-5,5,-5,1,1,0,1,0,0,

The above would render a cube using the texture "Bubing_2.bmp"...

4: Loading it into a class for use with Direct3D.
Now it gets interesting. Fine, we can save all the data to a file as a long list of numbers - but as of yet we can't see anything for our efforts. So we'll now design a second class that reads in this data and renders it in Direct3D. The added advantage of using a class system is that once we've created it we can create any number of objects - all with the same code. We'll also [later] add some functions so the parent application can set the rotation, scale and position of an object....

4a. The Declarations

'////////////////////////////////////////////////////////////////////////////////////
'////                                                                  
'////       clsGameModel: Used to load pre-compiled .Dat Models into   
'////                           a Direct3D7 application                
'////                                                                  
'////       Written : November 8th 2000                                
'////       By : Jack Hoxley                                           
'////                                                                  
'////       Contact: Jollyjeffers@Greenonions.netscapeonline.co.uk     
'////                    www.vbexplorer.com/DirectX4vb/                
'////                    www.CloneSoftware.Cjb.Net                     
'//////////////////////////////////////////////////////////////////////////////////

'//November 08 2000
        'Class Created.
        'Basic Loading And Object Detection Code
'//November 09 2000
        'Loading Finished
        'Texture Loading Complete
        'Object Rendering Complete

            
                
        
Option Explicit

Public Dx As DirectX7 '//We need the graphics engine to pass a valid Reference...
Public DD As DirectDraw7 '//We need another reference for the DirectDraw Object...
Public Device As Direct3DDevice7 '//A reference to the Graphics Engine Renderer...
Public TexturePrefix As String '//A pointer to the folder where the textures are stored...

'//Mathematics Constants
Private Const Pi As Single = 3.14159265358979 '4*atn(1)
Private Const Rad As Single = Pi / 180 '//Multiply Degrees by this to get Radians
Private Const Deg As Single = 180 / Pi '//Multiply Radians by this to get Degrees

Enum RETURNPARAM
        retX = 0
        retY = 1
        retZ = 2
End Enum

Private Type OBJECT3D
    StartLine As Long 'Whereabouts in the file this object starts...
    Texture As String           'Filename
    TextureIndex As Integer  'Internal
    Vertices() As D3DLVERTEX 'Open ended array of Lit vertices
End Type

Private NumModels As Integer
Private ObjMdl() As OBJECT3D 'Open ended array of models...

Private NumLines As Long 'How many lines are in the array & file
Private FileData() As String 'The actual Lines

Private NumTextures As Long 'How many Textures we require
Private oTexture() As DirectDrawSurface7 'The actual Textures...

'//Model Transformations
Private vPosition As D3DVECTOR 'Where the object is
Private vRotation As D3DVECTOR 'Where is it facing
Private vScale As D3DVECTOR 'How big it is...

Note that you'll need to have your project reference the DirectX7 type library for this class to compile/work. A lot of the declarations here are similiar to the compiler classes; as the data we're loading is the same [to a certain extent]. Also notice that there are three variables at the bottom for rotation, Scale and position - we'll get these working a little later on.

4b. The function prototypes
Fortunately the function prototypes are much simpler than in the previous class:

//These functions can be seen from *outside*
Public Function LOAD(Model As String) As Boolean
Public Sub RENDER() 'Pretty Obvious this one...

//these functions help with the loading of the data
Private Function GetDataFromLine(Line As String, nEntry As Integer) As D3DLVERTEX
Private Function CreateTexture(sFile As String, pWidth As Long, pHeight As Long, Optional ColKey As Integer = 0) As DirectDrawSurface7

//These next 3 functions set the world-space information for our object:
Public Sub SetPosition(X As Single, Y As Single, Z As Single)
Public Sub SetRotation(X As Single, Y As Single, Z As Single)
Public Sub SetScale(X As Single, Y As Single, Z As Single)

//Then these 3 allow the host to get the current world-space information
Public Function GetPosition(Param As RETURNPARAM) As Single
Public Function Getrotation(Param As RETURNPARAM) As Single
Public Function GetScale(Param As RETURNPARAM) As Single

//Then there are two helper functions for the Matrix Maths
Private Sub ScaleMatrix(pMatrix As D3DMATRIX, pVector As D3DVECTOR)
Private Sub TranslateMatrix(pMatrix As D3DMATRIX, pVector As D3DVECTOR)

4c. Loading the data into the class.
The first thing that we need to do is get the data from the DAT file, we then need to read off the relevent information - which is easy because we compiled it to be. So off we go:

Public Function LOAD(Model As String) As Boolean
On Error GoTo FATAL:

Debug.Print "Model to Load: " & Model

'//0. Variables
Dim FFile As Integer
Dim Temp As String
Dim I As Long
Dim J As Long
Dim nVerts As Long
Dim ArrayI As Long 'Where we are in the Vertices() array
Dim tArg As String 'Temporarary Argument String


'//1. Setup Variables, Error Check...

FFile = FreeFile '//Choose a free file place

'//2. Read in the data

Open Model For Input As #FFile

'2a. Check the file header
        Line Input #FFile, Temp
'//Remember to change this line should you change the header...
        If Not (Temp = "HEADER: The Man With The Digital Gun 3D MODEL {STATIC}") Then GoTo FATAL:
        
'2b. Read in the data
        Do While Not EOF(FFile)
'Increment the number of lines
            NumLines = NumLines + 1
'resize the array to suit the number of lines
            ReDim Preserve FileData(NumLines) As String
'read the line from the file
            Line Input #FFile, Temp
'add the new line into the array
            FileData(NumLines) = Temp
        Loop
Close #FFile
'Some statistical Information
        Debug.Print "Loaded " & NumLines & " lines from model File"
        
'//3. Now we calculate the number of objects in the file
       For I = 0 To NumLines
            Temp = Left(FileData(I), 3)
            If UCase(Temp) = "TEX" Then
'We've found a texture Heading. Must be a new object....
                NumModels = NumModels + 1
                ReDim Preserve ObjMdl(NumModels) As OBJECT3D
                ObjMdl(NumModels).StartLine = I
                ObjMdl(NumModels).Texture = Trim(Right(FileData(I), Len(FileData(I)) - 4))
                Debug.Print "Object " & NumModels & " starts on line " & ObjMdl(NumModels).StartLine & " and has '" _
& ObjMdl(NumModels).Texture & "' As a texture"
            End If
       Next I
        
'To Avoid Subscript errors we'll create a final - empty - object...
        ReDim Preserve ObjMdl(NumModels + 1) As OBJECT3D
        ObjMdl(NumModels + 1).StartLine = NumLines + 1
        
'//4. Now we need to work out how many triangles/Vertices there are in our Object
    For I = 1 To NumModels
        nVerts = (ObjMdl(I + 1).StartLine - 1) - ObjMdl(I).StartLine
            Debug.Print "Object " & I & " has " & nVerts & " faces"
        nVerts = nVerts * 3 'Each line has 1 triangle = 3 vertices
            Debug.Print "Object " & I & " has " & nVerts & " vertices"
        ReDim Preserve ObjMdl(I).Vertices(nVerts) As D3DLVERTEX
    Next I

'//5. Now we fill out our ready-sized array with the data from the file...
For I = 1 To NumModels
    ArrayI = 0 'Reset the array marker
    For J = ObjMdl(I).StartLine To ObjMdl(I + 1).StartLine - 1
'We're now scanning through the data for object I
        If Not (Left(FileData(J), 3) = "TEX") Then
            'We're not on a Texture Header
            'So we can format this line...
                'We need to fill out three vertices per Face...

                tArg = FileData(J)
            ObjMdl(I).Vertices(ArrayI) = GetDataFromLine(tArg, 0)
                ArrayI = ArrayI + 1
                
                tArg = FileData(J)
            ObjMdl(I).Vertices(ArrayI) = GetDataFromLine(tArg, 1)
                ArrayI = ArrayI + 1
                
                tArg = FileData(J)
            ObjMdl(I).Vertices(ArrayI) = GetDataFromLine(tArg, 2)
                ArrayI = ArrayI + 1
        End If
    Next J
Next I

'//6. We now need to Create The relevent Textures...
    NumTextures = NumModels 'There should be 1 texture per model.
    ReDim Preserve oTexture(1 To NumTextures) As DirectDrawSurface7
    
    For I = 1 To NumTextures
        Set oTexture(I) = CreateTexture(TexturePrefix & ObjMdl(I).Texture, 0, 0, 1)
        ObjMdl(I).TextureIndex = I '//So we know which texture to use when rendering...
    Next I
    
'//7. Set the Default Values
    vScale.X = 1: vScale.Y = 1: vScale.Z = 1 'otherwise the object wont appear
    
LOAD = True '//We succeeded
Exit Function
FATAL:
LOAD = False '//We didn't succeed
End Function

A note on Texture usage in this class. At the moment the class just loads a new texture for each entry it finds in a file. This has the obvious downside of loading multiple copies of the texture should it be used twice in a model file - even worse if the texture is big. You could easily get around this by checking if the texture is already loaded, and if it is just reference that one. But there is another disadvantage - what if we create 10 objects, all from the same DAT file - or all 10 objects use the same texture? you'd then have 10 copies of the same texture in memory - not a good idea. A possible optimisation would be to create a third class that holds a global set of textures and each object class references the textures there... that part is up to you.

The CreateTexture function is in the downloadable archive, and if you've done any work with textures/other tutorials from this site you'll have seen it before anyway. At the end of this procedure we'll have all the information that we need to render the object. But first we need to allow the host to set/get the world information...

5: Manipulating the object
This part is extremely easy, it just involves creating a few interfaces from which the host can set the rotation/scale/position of the object; depending on your engine you may well want to improve this to add other features...

'//THESE NEXT THREE ALLOW THE HOST TO SET HOW THE MODEL
'//IS REPRESENTED IN 3D SPACE

Public Sub SetRotation(X As Single, Y As Single, Z As Single)
    vRotation.X = X: vRotation.Y = Y: vRotation.Z = Z
    If vRotation.X > 360 Then vRotation.X = vRotation.X - 360
    If vRotation.Y > 360 Then vRotation.Y = vRotation.Y - 360
    If vRotation.Z > 360 Then vRotation.Z = vRotation.Z - 360
    If vRotation.X < 0 Then vRotation.X = vRotation.X + 360
    If vRotation.Y < 0 Then vRotation.Y = vRotation.Y + 360
    If vRotation.Z < 0 Then vRotation.Z = vRotation.Z + 360
End Sub

Public Sub SetPosition(X As Single, Y As Single, Z As Single)
    vPosition.X = X: vPosition.Y = Y: vPosition.Z = Z
End Sub

Public Sub SetScale(X As Single, Y As Single, Z As Single)
    vScale.X = X: vScale.Y = Y: vScale.Z = Z
End Sub



'//THESE NEXT THREE FUNCTIONS RETURN INFORMATION ABOUT
'//THE POSITION,ROTATION & SCALE OF THE OBJECT

Public Function Getrotation(Param As RETURNPARAM) As Single
If Param = retX Then
    Getrotation = vRotation.X
ElseIf Param = retY Then
    Getrotation = vRotation.Y
ElseIf Param = retZ Then
    Getrotation = vRotation.Z
End If
End Function

Public Function GetPosition(Param As RETURNPARAM) As Single
If Param = retX Then
    GetPosition = vPosition.X
ElseIf Param = retY Then
    GetPosition = vPosition.Y
ElseIf Param = retZ Then
    GetPosition = vPosition.Z
End If
End Function

Public Function GetScale(Param As RETURNPARAM) As Single
If Param = retX Then
    GetScale = vScale.X
ElseIf Param = retY Then
    GetScale = vScale.Y
ElseIf Param = retZ Then
    GetScale = vScale.Z
End If
End Function

6: Rendering the object
Now we have all the information we require to render the object - we've loaded the data and formatted it, we've allowed the host to set any world-space information, so now we need to make it appear on screen. We'll assume that the host application has provided us with a valid reference to a rendering device, and that the call is between a "BeginScene...EndScene" block - if either of these two aren't okay you'll get errors...

Public Sub RENDER()
Dim I As Integer, Temp As D3DMATRIX, Final As D3DMATRIX
'//0. Setup the relevent Matrix...
'a. Rotate
        Dx.RotateXMatrix Final, vRotation.X * Rad
        
        Dx.RotateYMatrix Temp, vRotation.Y * Rad
        Dx.MatrixMultiply Final, Final, Temp
        
        Dx.RotateZMatrix Temp, vRotation.Z * Rad
        Dx.MatrixMultiply Final, Final, Temp
        
        
'b. Scale
        ScaleMatrix Temp, vScale
        Dx.MatrixMultiply Final, Final, Temp
        
'c. Translate
        TranslateMatrix Temp, vPosition
        Dx.MatrixMultiply Final, Final, Temp
        
'd. Set the Matrix
        Device.SetTransform D3DTRANSFORMSTATE_WORLD, Final

'//1. Render
For I = 1 To NumModels 'Go through each part...
    Device.SetTexture 0, oTexture(I) 
    Device.DrawPrimitive D3DPT_TRIANGLELIST, D3DFVF_LVERTEX, ObjMdl(I).Vertices(0), UBound(ObjMdl(I).Vertices()) + 1, D3DDP_DEFAULT
Next I
End Sub

Again, not very difficult at all; you can render an entire object with one call - and if you have only 3-4 objects in the model you can have the whole thing done pretty easily....

7: Using the Class.
This is the final part that we have to do. There are several things that we must bare in mind if we to use this class in a real-world application. The following code will initialise the object:

'Create the Object
Set CPlayer = New clsGameModel

'Add the references
Set CPlayer.DD = DD
Set CPlayer.Device = Device
Set CPlayer.Dx = Dx
CPlayer.TexturePrefix = App.Path & "\textures\"

'Load the object and output the result...
Debug.Print "Load Model returned: ", CPlayer.LOAD(App.Path & "\mole.dat")

You'll need to customise this slightly for your own needs, the main things to bare in mind are:
1. The object is Late Bound, if you're using it alot (as you will be) you should change this to be early bound:
Dim CPlayer as clsGameModel
Set CPlayer = New clsGameModel

2. You'll need to make the references valid - this means only creating an object after you have initialised the rest of your Direct3D application - see the other tutorials for this. Ideally you should alreadys know this though.

3. You might need to change the path to the textures.

Other than that you're done. Download the completed classes from the top of the page, or from the Downloads Page. And well done - you've reached the end of this rather large tutorial... In fact, I wonder if anyone's still reading..... :)

If you have time, check out my site for "The Man With The Digital Gun" - this class was designed for that game, and there wasn't really any reason other than my kindness to make it open source... Go Here

DirectX 4 VB 2000 Jack Hoxley. All rights reserved.
Reproduction of this site and it's contents, in whole or in part, is prohibited,
except where explicitly stated otherwise.
Design by Mateo
Contact Webmaster