|
Code Base
For those of you just tuning in to vbProgramming, the following code
is the existing code base we've been using. If you don't understand
this, please do some of the previous tutorials. For those of you
who've been following my tutorials, you can simply skip this
section. Just be sure you're not rendering anything at all (neither
clsSprite nor Direct3D.Sprite).
'If you didn't put this there, then you'd have to write out the variable names
'For example: "Dim D3DDev as Microsoft.DirectX.Direct3D.Device"
'as opposed to: "Dim D3DDev as Device"
Imports Microsoft.DirectX
Imports Microsoft.DirectX.Direct3D
Imports Microsoft.DirectX.Direct3D.D3DX
Public Class GameClass
'Stores whether game state. If True, then the game will end
Public GameOver As Boolean
Private D3Ddev As Device = Nothing
'Short for PresentationParameters
Private D3Dpp As PresentParameters = Nothing
'Stores Display mode
Private DP As DisplayMode = Nothing
Public Sub Initialize(ByVal TargetForm As Form, ByVal FullScreen As Boolean)
If FullScreen Then
'800x600 Resolution
DP.Width = 800
DP.Height = 600
'R5G6B5 = 24-bit. Visit MSDN to find out what the other ones are.
DP.Format = Format.R5G6B5
Else
'If it's not fullscreen, use the current display mode!
DP = Manager.Adapters.Default.CurrentDisplayMode
End If
D3Dpp = New PresentParameters
'Initialize some stuff for the Presentation parameters.
'The backbuffer is what it draws to first. Then the content of the backbuffer
'is copied to the screen.
D3Dpp.BackBufferFormat = DP.Format
D3Dpp.BackBufferWidth = DP.Width
D3Dpp.BackBufferHeight = DP.Height
'There's flip, copy, and discard - discard seems to be the slowest but the most 'accurate'
'MSDN has more information.
D3Dpp.SwapEffect = SwapEffect.Discard
'Gets the most FPS out of your game. Present the scene immediately
D3Dpp.PresentationInterval = PresentInterval.Immediate
'Set to Fullscreen or Windowed
If FullScreen Then
D3Dpp.Windowed = False
Else
D3Dpp.Windowed = True
End If
'Instantiate the device
D3Ddev = New Device(Manager.Adapters.Default.Adapter, DeviceType.Hardware, TargetForm.Handle, CreateFlags.SoftwareVertexProcessing, D3Dpp)
'Manager.Adapters.Default.Adapter - Use the current display driver: the one that's displaying your desktop right now
'DeviceType.Hardware - If you've worked with GDI+, you'll know how slow software devices are
'TargetForm.Handle - Draw to the form
'CreateFlags.SoftwareVertexProcessing - Processing vertices with Software (Direct3D) is safer than processing it with your hardware
' This is becuase different graphics cards may process them differently. You want it to be
' processed the same universally, so you let Direct3D do the work.
'D3Dpp - Well, use the presentation parameters!
D3Ddev.Transform.View = Matrix.OrthoOffCenterLH(0 - 100, DP.Width - 100, DP.Height - 100, 0 - 100, 1, 10)
'0 - Left side of the view
'DP.Width - Right side of the view. In our case, it's 800
'DP.Height - Bottom side of the view. In our case, it's 600.
'0 - Top side of the view
'0 - Specifies the ZNear plane: The closest our object can be before it's cut-off from the view
'0 - Specifies the ZFar plane: The farthest our object can be before it's cut-off from the view
End Sub
Public Sub RenderScene()
Do While Not GameOver
'Try commenting this out :). You'll see what it does. It might hang your app depending on your graphics card.
'Usually it caueses the screen to flash in multiple colors.
D3Ddev.Clear(ClearFlags.Target, Color.FromArgb(0, 0, 225), 0, 0)
'ClearFlags.Target - Clear the form, our "target"
'Color.FromArgb(0, 0, 225) - Clear it with this color (Dark blue)
'0 - ZDepth. Our app isn't 3D yet, so don't waste memory on ZDepth
'0 - Stencil. I haven't figured out what this is lol.
D3Ddev.BeginScene()
D3Ddev.EndScene()
D3Ddev.Present()
'In a loop, keyboard events are ignored. This means: let them NOT be ignored!
Application.DoEvents()
Loop
Terminate() 'Exit everything!
End Sub
Public Sub Terminate()
'Free up mem
DP = Nothing
D3Dpp = Nothing
D3Ddev.Dispose()
D3Ddev = Nothing
'Exit
Application.Exit()
'FORCE an exit if it didnt exit
System.Environment.Exit(System.Environment.ExitCode)
End Sub
End Class
Now add the following code in form1:
Form1 Globals:
Dim game As New GameClass
Form1_Load:
Me.Show()
game.Initialize(Me, True)
game.RenderScene()
Form1_Keydown:
If e.KeyCode =
Keys.Escape Then game.GameOver = True
And you should
get a blank screen when you run this.
Introduction to the Third Dimension: The
View Matrix
Meh. So far
we've been dealing with 2D, this is your first leap to 3D....
probably your biggest leap since the Object Oriented Programming
tutorial. Fortunately, Direct3D makes 3D seem pretty easy.
First things first. Why do we have this line of code in
GameClass.Initialize:
D3Ddev.Transform.View = Matrix.OrthoOffCenterLH(0 - 100, DP.Width -
100, DP.Height - 100, 0 - 100, 1, 10)
'0
- Left side of the view
'DP.Width
- Right side of the view. In our case, it's 800
'DP.Height
- Bottom side of the view. In our case, it's 600.
'0
- Top side of the view
'0
- Specifies the ZNear plane: The closest our object can be before
it's cut-off from the view
'0
- Specifies the ZFar plane: The farthest our object can be
before it's cut-off from the view
That's
for 2D. We need to have a 3-Dimensional camera. Later in this
tutorial, we're going to be rendering a Cube at the position
(0,0,0).
Remember, the higher the Z value, the further into the screen you
are. Replace the View matrix code with this one:
'Set up
the View matrix. This represents WHAT you can see in the world.
D3Ddev.Transform.View = Matrix.LookAtLH(New Vector3(0, 0, -5), New
Vector3(0, 0, 0), New Vector3(0, 1, 0))
'New Vector3(-1, 3, -2) - The Camera Position. Remember, I placed the cube at position 0,0,0 in my 3d modeling program.
' - Ok, -1 means that the camera is positioned 1 units to the left. 3 means that
' - the camera is positioned up 3 units. And -2 means that the camera is placed BACK a bit
' - so that we're not too close to the cube.
'New Vector3(0, 0, 0) - The Camera Target. Again, the cube is positioned at 0,0,0. And we want to look at the cube, so we
' - simply set the camera target (aka what the camera looks at) to 0,0,0. Simple enough ;P.
'New Vector3(0, 1, 0) - Camera UP direction. In most 3d modeling programs, you work on an XY plane and the Z represents
' - height (up direction). However, in D3D, the convention is for Z to be forwards and backwards
' - (aka depth) and X is left and right. This would make Y the only remaining direction: up direction.
' - So in a D3D shooter game, when you push up, you move forwards.. and you increase
' - the Z value. And when you hit space, you jump, so you increase
' - the Y value. Basically, Y is the up direction, and it's represented by the 1.
'Picture in your head what I said above. The Cube is at 0,0,0 and
you're: Left, Above, and Behind it.
'This means that you can see the Left side, the Top side, and the
back side of the cube.
'Mess around with these values to see different sides of the cube.
Please
excuse the formatting of the indented comments - by changing the
font it would lose the alignment. I've basically explained the view
matrix in the comments.
The reason it says LH in "LookAtLH" is because we're using the
left-handed coordinate system. This basically means that the more Z
you have, the further into the screen you'll be. If you do LookAtRH,
then it'll be the opposite... The more Z you have, the closer you'll
be. The more used convention is LH and that's what I'm sticking with
:).
We're going to be creating a cube at position (0,0,0) pretty soon,
but we've got 1 more matrix to fix!
Introduction to the Third Dimension: The Projection Matrix
Ok, so if the World matrix represents all the objects in the
world, and the View matrix represents what you can see in the
world (the position of your camera), then what does the projection
Matrix represent? It represents how you see the stuff on the
screen. Your next line should be:
'Set the Projection matrix - This represents HOW you see the world.
D3Ddev.Transform.Projection = Matrix.PerspectiveFovLH(Math.PI / 4, DP.Width / DP.Height, 1.0F, 300.0F)
'Math.Pi / 4 - FOV (Field of View) in Radians (this is 45 degrees). This basically
' - means that you can see 45 degrees all around you. If you were to
' - increase this, then you'd be able to see more around you.. sort of like Fish Eyes ;).
'DP.Width / DP.Height - Aspect Ratio. Most TV's use 16:9. It's basically the width to height ratio. The more this
' - number is, the wider your screen will be. For example, with an aspect ratio of 1, a cube would
' - look like a cube. With an aspect ratio of 2, it would look twice as wide (and a lot shorter).
' - If you put .5, then it would be taller but less wide. I like to use screen width / screen height
' - because it provides the picture as it is.
'1.0F - ZNearPlane. Any object that is less than 1 units away from you will be clipped.
' - For mathematical reasons, the zNearPlane cannot equal 0.
'300F - Any object that is more than 300 units away from you will be clipped. This is nice when
' - you're rendering a HUGE landscape, and you don't want to render all of it at once.
Well, time for a bit of explaining, just be sure you read the comments. Just remember that your View matrix and your Projection matrix are completely different.
Say you're in a castle. The king is at (0,0,0) and you're at (0,0,-10) meaning you're behind the king. Say the princess is at (-10,0,0). A little diagram:
(Stop laughing dammit! I suck at drawing - pretend this is 3D also... It's hard enough for me to draw 2D, forget 3D lol)
Say your view matrix is set so that your target is (0,0,0) - meaning you're facing the king (the brown stuff on your face is hair.. because you can only see the back
of your head).
Well, sure you're facing the king, but you can't see the princess! Your field of view is only Math.Pi / 4 in radians (Which is 45 degrees) this means that you can only see
what's in front of you, and 22.5 degrees to the left, and 22.5 degrees to the right.
(The lines represent what you can see on the screen)
Since our FOV (Field of View) is set to 45 (Math.Pi / 4), we can only see the king.
If we increased our FOV to 90 (Math.Pi / 2):
(The lines represent what you can see on the screen)
Etc, etc. You get the point. I'll stop wasting space :p. Here's the
thing though. I read somewhere that a 45 degree FOV was best for
first-person game play, and that
90 degree FOV might be better for 3rd person gameplay.
Now moving on to Aspect Ratio. Aspect ratio is Width : Height. If
our resolution was set to 500x500 (not possible, by the way) - and
we set out aspect ratio to 1 : 1
(or 500 : 500) then we'd get the image as it should appear on the
screen, properly. If we set our aspect ratio of 2 : 1 - then the
image would be twice as wide and would
stretch. So our cube, in this case, would be a rectangular prism.
However there's no such thing as a 500x500 resolution. We're using
800x600 anyways. Since the screen is wider than it is tall
(all monitors are like this), a 1 : 1 aspect
ratio actually wouldn't work out. It would try to stretch the cube
so that it's taller, so that the width = height. However, this isn't
always the best thing to do. We don't want to go around stretching
the object .... etc, unless you have a widescreen monitor or
something. It would be advised that you simply do an aspect ratio of
ScreenWidth :ScreenHeight ratio (like we already have).
The zNear of 1 and the zFar of 300 is pretty self explanatory (it's
explained in the comments above also).
Whew. Enough terminology! Time for the fun stuff!
Rendering a 3D Mesh: An Introduction
Well, finally, we'll
display something on the screen.
To render 3D objects, Direct3D uses something called a Mesh.
Basically, you can create whatever you want in a 3D modeling program
(a cube, a player, a plane, a rock.... whatever you want), and
export it into something called a DirectX Mesh File (also known as
an "X File" because the file extension is ".x").
You've been how poor I do in web design (you're on my site right
now), 2d images (see above)..... you can't really expect me to make
some high-polygon detailed mesh now can you?
As I said before, I created a cube (hey! It's better than nothing
ok?).
Before I continue, let me recommend to you some 3D Modeling
programs:
-Milkshape3D: They always do a 30-day trial .... but then
they release a new version before the trial ends... so in theory you
can keep downloading new versions.
It comes with a built-in DirectX Mesh exporter.
-3D Studio Max: You've got to be incredibly rich to buy this
program. Use Panda DirectX Exporter to export your meshes.
-GMax: This is the free version of 3DS Max. I can't guarantee
that it'll export DirectX files, Google around. I've tried this
program though, it's excellent.
-Anim8or: Extremely lightweight. Free. No installation required (I'm
serious, it's that lightweight!). Save as a 3ds file, and use
a program called CONV3DS to convert
it from 3DS to an X File.
Just remember that in your 3D Modeling program, if you position your
object at... say (200,200,200) and you render it in Direct3D, it
will render at (200,200,200). However, I recommend that you
create all your objects at (0,0,0) and translate the world instead -
this way you'll have more control and you won't have to memorize
where you created your object.
Oh one more thing, this concerns loading animated meshes.
Here's the thing: I used to be able to do this in DirectX 9.0a (the
first version) from a great tutorial in
www.robydx.altervista.org
and in DirectX 9.0b (second version) from the book "Managed DirectX
9 KickStart: Games and Graphics Programming" (another great book by
the way). However, there's 1 slight problem. Microsoft keeps
changing their SDK around: by renaming things (this isn't a
problem), screwing around with the original class hierarchy (this is
the problem). Simply put, I don't know how to animate meshes with
the latest version of DirectX.
Alright, here we go.
The code is slightly confusing so be sure you concentrate or do
whatever you can to pay attention to the explanations given.
Rendering a 3D Mesh: The clsMesh Class
Create a new
class called clsMesh.
Imports
Microsoft.DirectX
Imports Microsoft.DirectX.Direct3D
Imports Microsoft.DirectX.Direct3D.D3DX
Public Class clsMesh
'Our mesh object
Public objMesh As Mesh
'Store a copy of the device in this class to use
Dim d3ddev As Device
End Class
That's pretty basic. We
obviously need a Mesh object.
Time for more explanations. A Mesh is divided into parts called
subsets. When I modeled the cube (in about 2 seconds), I made it
so that the cube only had 1 subset. A subset is just a different
area of the mesh. I could have made each individual face a subset,
so that we could texture each face. But I guess some laziness kicked
in, and I basically... didn't.
Each subset has certain properties: Material, Texture, and
ExtendedMaterial. I'll actually explain these as we move along.
Declare the following variables:
'The material
Dim Mat() As Material
'Texture
Dim MeshTexture() As Texture
'This will be
explained later.
Dim extMaterials() As ExtendedMaterial = Nothing
Now create a
constructor:
'Device, XFile
path, Texture path
Public Sub New(ByVal dev As Device, ByVal filename As String, ByVal
texturefilename As String)
d3ddev = dev
LoadMesh(filename, texturefilename)
End Sub
You'll obviously
get an error on that LoadMesh line. We need to create the LoadMesh
sub:
Public Sub
LoadMesh(ByVal FileName As String, ByVal TextureFileName As String)
'Create our mesh
objMesh = Mesh.FromFile(FileName, MeshFlags.SystemMemory,
d3ddev, extMaterials)
'Filename - Filepath to the mesh
'MeshFlags.SystemMemory - The place where the mesh is stored is on our system.
' - let Direct3D worry about the rest
'd3ddev - Our device
'extMaterials - See explanation below
End Sub
That's pretty basic, and it's explained in the comments. I have to explain the extMaterials argument though.
If you were a good programmer, and typed in the code above (instead of pasting it), you would have realized (with the Intellisense tool tip thing) that the extMaterials
argument is byRef and not byVal.
Not-So-Brief review of byVal versus byRef:
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ByVal:
Public Sub Form1_Load(.....)
Dim ans as Integer = 0
Add(1, 2, ans)
MessageBox.Show(ans)
End Sub
Public Sub Add(byVal number1 as Integer, byVal number2 as Integer, byVal Answer as Integer)
Answer = number1 + number2
End Sub
You'd get a MessageBox saying "0" because the variable "ans" doesn't get affected by the changes in "Answer" argument.
ByRef:
Public Sub Form1_Load(.....)
Dim ans as Integer = 0
Add(1, 2, ans)
MessageBox.Show(ans)
End Sub
Public Sub Add(byVal number1 as Integer, byVal number2 as Integer, byRef Answer as Integer)
Answer = number1 + number2
End Sub
Note the change .... it now says byRef Answer as Integer. This time, you'll get a MessageBox saying "3" because now, whenever you change
the argument Answer, the variable ans gets changed as well. Similarly, extMaterials is a byRef argument, so extMaterials gets changed in some way
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
How does extMaterials get changed? The array size gets ReDimmed to the number of subsets in the mesh. That's all we needed it for, just to retrieve the number of
subsets in the mesh.
So now that we know the number of subsets, we know that we need more Textures and Materials. Obviously a 6-Sided cube needs 6 materials and textures (I know,
I know, I know.... I didn't create a cube with 6 subsets. I created a cube with only 1). Your next line (in LoadMesh) should be this:
'We now know the number of subsets in the mesh
'Each subset has a texture
ReDim MeshTexture(extMaterials.Length)
'Each subset has a material
ReDim Mat(extMaterials.Length)
Again, a brief overview of ReDim (we already covered this in a previous tutorial). ReDim simply resizes the array.
Next thing we have to do is loop through all the subsets and set all the settings:
Dim Subset as Integer
'Loop through each subset
For Subset = 0 To extMaterials.Length - 1
Mat(Subset) = extMaterials(Subset).Material3D
Mat(Subset).Ambient = Mat(Subset).Diffuse
extMaterials(Subset).TextureFilename = TextureFileName
MeshTexture(Subset) = TextureLoader.FromFile(d3ddev, extMaterials(Subset).TextureFilename)
Next
This loop requires a bit of explaining. What we do is we loop through every subset... and make some changes.
I'll go over all the changes
- Mat(Subset) = extMaterials(Subset).Material3D
Apparently, extMaterials not only stores the number of Subsets, but an actual Material effect. See your 3D Modeling program for more details on Materials
- Mat(Subset).Ambient = Mat(Subset).Diffuse
Quick overview of Ambient vs Diffuse.
Ambient lighting is the light around you and Diffuse lighting is the light which is emitted from a particular object.
In science, you learn that the colors which you see are merely a reflection of a particular color of light.
Quick example: "A white T-Shirt isn't really white, the materials inside the shirt reflect white light" - that's an example of Diffuse lighting. You could say that the
T-Shirt's Diffuse color is white.
Ambient lighting is the light that affects the objects.... sorry, vague explanation. Let me clarify.
Say you walk into a room with your white T-Shirt. You're having a party, so you got these crazy disco lights (pretend they're all green). You shirt will appear to be green
(or most likely light green). The Ambient lighting on the shirt is Green, but the Diffuse lighting on the shirt is White.... thus making your shirt look Light-Green.
However, if your shirt was white, and the room was white... your shirt would appear white - this is what we want. So what we do in the that line above is basically make
the ambient color the same as the diffuse color (not the other way around.... if your mesh was green, you want it to stay green, not change according to the Ambient
color.
- extMaterials(Subset).TextureFilename = TextureFileName
Guys you're probably going to hate me for this: In a 3D Modeling program, you can store texture data within the mesh.... but I didn't know how to :(. Normally, this
would be set to extMaterials.TextureFileName - but since I didn't know how to do this in Anim8or, I simply cheaped out and simply set it to whatever texture we want.
- MeshTexture(Subset) = TextureLoader.FromFile(d3ddev, extMaterials(Subset).TextureFilename)
Pretty simple.
Now create your Render sub:
Public Sub Render()
Dim Subset As Integer
'Loop through all subsets
For Subset = 0 To Mat.Length - 1
'Set the Material
d3ddev.Material = Mat(Subset)
'Set the Texture, the 0 represents TextureStage...
'Honestly, I don't know what that means. I think it has
'something to do with "Multipass Texturing". I always put 0 if I'm not sure.... but hey it works for me!
d3ddev.SetTexture(0, MeshTexture(Subset))
'Draw the subsets :).
objMesh.DrawSubset(Subset)
Next Subset
End Sub
Well there you have it. Just remember one thing though. The good thing about Object Oriented Programming is that you now don't even need to worry about how
your Mesh class works you can just use it. But I'd advise you to comment it just in case you need it for reference.
Rendering a 3D Mesh: Making changes to GameClass
Before we continue, we need to make some quick changes to GameClass.
In GameClass.Initialize, right before you instantiate the device you need to make these changes:
'The next 2 lines are for 3d games.
'This one enables Depth Stenciling... meaning that if one object is
'in front of another, then it will render it in front of the other.
'Heh, pretty simple concept, however, if this was set to false
'then what would happen is which ever object is rendered last would appear
'on top.
D3Dpp.EnableAutoDepthStencil = True
'16-bit depth. The greater the bits, the greater the degree of precision.
'I find 16 good enough for our purpouses :).
D3Dpp.AutoDepthStencilFormat = DepthFormat.D16
That's pretty simple.
Now add these lines of code after you instantiate the device, preferably after setting up the matrices (for neatness):
'Set flags that are needed to render the mesh:
'Our app is 3d.
D3Ddev.RenderState.ZBufferEnable = True
'We want our objects to be rendered solid. You can also do Wireframe and Points
D3Ddev.RenderState.FillMode = FillMode.Solid
D3Ddev.RenderState.Lighting = False
Pretty simple actually. ZBufferEnable works like the AutoDepthStencil .... if an object is rendered in front of another, it takes priority. If an object is rendered behind
another... then obviously it will be rendered behind the other. If this was set to False, then whatever object you render last ends up being on top.
Last change we need to make to move on to 3D. In GameClass.RenderScene:
D3Ddev.Clear(ClearFlags.Target Or ClearFlags.ZBuffer, Color.FromArgb(0, 0, 225), 1, 0)
I underlined the changes I made. If you read my BitStringFlicking tutorial, you would know why an "Or" acts as an "And". ZBuffer is the same as what I mentioned
before... its commonly known as a Depth buffer, and renders the "front-most" object in the front and the object in the back... in the back of course.
I underlined the "1" also. If this was set to 0, then the application would be 2D. If this was set to 1, then the application is 3D because it allows us to use the concept
of depth and distance. I think I'm making it more confusing that it should be by explaining it though :p. It's a pretty simple concept anyways. 0 = 2D. 1 = 3D (practically
though).
Rendering a 3D Mesh: ..... Rendering a 3D Mesh!
Now, it's time to use our mesh class!
At the top of GameClass:
Private Car as clsMesh
At the end of GameClass:
Car = New clsMesh(D3Ddev, "cube2.x", "vw_concept.bmp")
You can download the source to get cube2.x and vw_concept.bmp. Follow the link at the very bottom of this tutorial, you can download it at my forum (no registration
required to download source code).
In GameClass.RenderScene, between BeginScene and EndScene:
'Duh.
Car.Render()
Pretty long tutorial huh? The end result is awesome though - run your program and you actually get a 3D model displayed on the screen!
Without further ado, I'll give you the sneak peak to the next tutorial:
Before Car.Render, just type in the following line
D3Ddev.Transform.World = Matrix.Multiply(D3Ddev.Transform.World, Matrix.RotationYawPitchRoll(Math.PI / 10000, Math.PI / 10000, Math.PI / 10000))
Now run your program - how much cooler does that look (Change the Math.Pi / 10000 to Math.Pi / 1000 if it goes too slow for you).
All this stuff will be explained in the next tutorial.
Be prepared with some Trignometry knowledge though (even if you haven't had background in trig, I'll make this a bit easier on you guys by giving a lecture on trig
(no, not a boring lecture).
And yes, I know that the cube is not a car.
Look forward to the next tutorial!
The Source
Code for this tutorial is located here: You can also
locate this by logging in to vbProgramming Forums and going
to: Tutorials > Tutorial Source Code > Source Code
|