DirectXGraphics:
Tips and Tricks
Author:
Jack Hoxley
Written: 8th May 2001
Contact: [EMail]
Contents
of this lesson
1. Introduction
2. Maths
3. Resources
4. Everything Else
5. Quick Snippets
1.
Introduction
wow! I
think we've pretty much learnt all of
the basics for Direct3D8 now - there's
plenty more to play with, but I'm fairly
confident in saying that you could,
having learnt everything in this series,
make a perfectly capable 3D engine for
your game. Whilst you're not going to go
straight into writing the next Unreal
Tournament, the possibilities are almost
endless from here - you can, as I said,
just use what I've covered; or you can
branch out into more complex areas of 3D
game design - 75% of what you'll need
for a decent 3D game cannot be covered
by learning Direct3D, maths and
techniques are the key to it all.
Having
said that you could go make a 3D engine,
could you make a good one? I don't
know... but there area many little bits
that I keep repeating to people via
email, things that once you know you
don't forget - and you'll use for
everything after that day. The simple
statements and facts that you can build
your engine around to make it go faster,
look smoother and be much more capable.
The following 4 sections are all about
these things...
2.
Maths
As I'm
very sure you're aware, mathematics
plays a massive role in 3D programming -
if your not up to scratch on the basic
maths and algebra then your going to
find things pretty tough. You will tend
to find that computer science degrees
require mathematics qualifications for
this reason (well they do in the UK!),
if you've found many things in this
series, and other articles you've read
hard then you may want to dig out that
old maths book...
Tip
1: Simplify
Whilst trying to scratch every free
processor cycle is rather pointless in
VB (too high level), there's no reason
to be foolish - some of the most
processor hungry parts of the Visual
Basic language are the maths functions.
Therefore it makes sense not to overdo
it. Wherever possible use \ / * + -
instead of other maths functions, those
are the fastest possible. Another key
point, if you've derived a very clever
maths function to perform a certain job,
tidy it up on paper - see if any
operations cancel out ( (n \ a) * a =
n), see if there are any special cases,
see if you can convert it to integer
maths...
Tip
2: Datatypes
The Long datatype is fastest, if your
only using whole numbers then use this
datatype - sure, it takes up twice as
much memory as an integer - but unless
your storing millions of them it's worth
the extra space. Also, if your dividing
two integer datatypes (Byte, Integer,
Long) use the "\" operator
instead of the "/" operator -
the former is an integer divide and will
be much faster, if you don't want a
decimal number then use it. When doing
floating point maths use the
"single" datatype, as with
Long's, they are 32 bits in size, all
current processors are optimised for 32
bit processing. Only use double's if you
need the extra resolutions. AND PLEASE -
NO VARIANTS!! variants are large, slow
and horrible - and not good coding
practise. Avoid them like the plague.
Tip
3: Don't use complex functions
Tan( ), Atn( ), Sin( ), Cos( ), Log( ),
Sqr( ), rnd - are all pretty
useful at times, but they are also the
slowest maths functions. Wherever
possible restrict the usage of these
functions, removing 10 of these from
your main frame loop will often see a
good speed increase. Also note that
things like Log to the base 10 is
derived from two log( ) calls, therefore
it's going to be twice as slow. Also
note that there are several maths
functions (sgn and int are good
examples) that return variants - which
are bad and slow. Work around these
functions if possible. Using your own
logic tree will be faster than sgn() and
\1 will be much faster than int().
Tip
4: Cache values
As already mentioned, maths functions
are slow - so if you can pre-calculate
them and store them somewhere you'll
feel better for it. For example, the
hermite spline formula uses v*v and
v*v*v many times, if you calculated v*v
and v*v*v and stored them in a single
datatype (v_Squared and v_Cubed for
example), you can then replace all the
parts in the formula with v_Squared and
v_Cubed - in the case of the hermite
spline formula you can get rid of quite
a lot of multiplies. Connected to this,
use constants, in some cases, the VB
compiler will place the number in the
raw code - meaning that you dont need
any storage space or any calculations
whatsoever. Particularly common, and
complicated values such as PI should be
stored this way (as I have in all my
tutorials), calculated 4*atn(1) each
time you need PI will not be pretty.
Finally, whilst not so popular now, but
still an interesting optimisation -
lookup tables. If you know your going to
need the values of Cos( ) and Sin( ) for
0 - 360 degrees all the time,
pre-calculate them and stick them in an
array - and then just read out of the
array. It's much faster than calculating
it each time, and much easier than
typing out 720 constants....
Tip
5: Integer Maths is best
Whilst the newer generation of
processors (Athlon, Pentium 4) are
getting faster and faster as far as
floating point (decimal number)
calculations, they are still often
slower than using integers -
particularly 32bit integers (Long's in
VB). Where possible use integer maths,
but dont keep converting from floating
point to integer as that's just as
bad... start with integers and convert
to floats at the very end, or vice
versa...
3.
Resources
Data
driven games are the norm these days -
levels are all loaded from disk, scripts
are loading from files, textures,
movies, sounds are all loaded from disk,
configuration, object information and
all the rest is often loaded in at
runtime. There is no doubt that this is
a good thing, but being sensible about
it is another...
Tip
1: Texture Sizes
I'm sure you'll be aware that texture
sizes should go in 2^n sizes
(32,64,128,256...) and you would be wise
to keep them that way, even if the
hardware supports non-2^n texture sizes.
You may also be aware that you can, on
some hardware load textures into the
2048x2048 range. This is not good -
rarely will you need a texture that
size, 3D cards will run much slower that
way, very little hardware supports it. I
have a fast 3D card (I like to think
so!) - A GeForce 256, whilst it handles
textures in most formats, up to
2048x2048, I never use textures larger
than 256x256; why? it's the most optimal
size to speed ratio, AND the hardware
I'm developing games and programs for
may not support large textures - take
the Voodoo3 card, It's immensely popular
and was in many shop-built computers -
so assume that quite a few of your end
users will have one, yet the largest
textures they can handle is 256x256...
It is also much faster to store multiple
textures in one texture (like a tile
set), and square textures are the
fastest...
Tip
2: Less Textures = Good
The less textures you use in a scene the
better - texture swapping isn't the
quickest thing around... Also, keep them
to a reasonable size for what your going
to be rendering. a small stone that will
only ever occupy a 100x100 area on the
screen when finally rendered does not
warrant a 512x512 32 bit texture - you
could keep it to a 128 x 128 x 16
texture and not notice much difference
except the go-faster part!
Tip
3: Less data in memory
Whilst not so much relevant to Direct3D
itself, the more free memory there is
the faster the computer as a whole goes.
Requiring a 64mb array will kill all but
the most hardy super computers around...
Keep things simple and small, I have
288mb of Ram in this computer, and I
make the most of it - but I try to keep
the end programs memory consumption
below 32mb; even on a 64mb RAM machine
you're unlikely to get more than 32mb of
Free ram to play with - windows loves to
chew it all up for you!
4.
Everything else
Everything
else, what a great title. The following
few tips cover some general tips that
don't apply to any particular area, but
are still important.
Tip
1: Less Lights
Lighting is fast, but don't get cocky -
especially if you have a Transform and
Lighting enabled 3D card, use the
minimum number of lights possible, and
keep them as small as possible (so that
they don't light too many triangles);
try to keep to below 8 lights where
possible - if you require more, check
the hardware - my GeForce card doesn't
like more than 8 lights. When choosing
the type of light, use Ambient lighting
to increase the overall brightness,
Directional lights to a similiar effect
- lighting a large area, but not
necessarily equally. Point lights for
particular sources, and spot lights only
if you have to. Specular lighting
doubles the processing time for lights,
so use it sparingly.
Tip 2:
Less rendering
This one is incredibly obvious, but
somehow, people often miss it. The rule
is simple: Render only what you must,
and think hard about how you render what
you must render! More complex models
require more processing which brings the
overall speed of your game/app down.
Bare in mind that it's quicker to render
100 triangles in 1 call than rendering
100 triangles in 10 or 100 calls; where
possible group your DrawPrimative()
calls to a minimum, and for any large
amount of geometry stick it in a vertex
buffer and/or index buffer. When using
vertex and/or index buffers try to
create them in video memory - it's much
much faster.
Tip 3:
Simpler models
Whilst this holds the same truth as
rendering less geometry, models have the
added problem of often requiring
animation - sometimes complex
algorithms; reduce the detail of your
models and you can see a substantial
speed improvement. Also, with regards to
storage of model geometry, if you know
your going to have 100
big_ugly_green_Alien™ models you could
only store 1 copy of the geometry, and
then a smaller set of individual
frame/state information...
Tip
4: State Changes
Calling SetRenderState( ) or
SetTextureStageState( ) functions often
requires Direct3D to recalculate
internal structures - which can cause
slow down; keep calls to these and
similar functions to a minimum. Also,
changing the World, View and Projection
matrices causes internal recalculations
- whilst it's often difficult to greatly
reduce the number of World matrix
changes it is possible to cut down on
the view matrix recalculations - make
the view matrix an identity matrix
(D3DXMatrixIdentity) and apply it to the
device - and leave it that way. To
change the camera position multiply the
world matrix by the view matrix you want
(just before committing the world matrix
to the device), this will have the same
effect, but cause less internal
calculations - on TnL cards it often
works out much faster... Look into using
State blocks if you often set the same
block of render states multiple times...
Tip
5: Clearing
Depending on how much your application
renders, you can often get away without
clearing the render target before the
frame is redrawn - it's a good idea to
always clear the Z buffer and/or Stencil
buffer though... Not clearing the frame
buffer (D3DCLEAR_TARGET) can give you a
reasonable speed increase.
5.
Quick Snippets
There
are quite a few things I'd like to have
been able to cover, but time has run out
- and often, the little things don't
really warrant an entire article about
them, just a brief explanation and the
code... so here we go:
Tip 1:
Fogging
fogging is great! It allows you to draw
attention to the foreground, and to mask
the end of the draw distance by fading
into clouds and such... and some clever
special effects as well...
'//These lines go in the initialisation section;
D3DDevice.SetRenderState D3DRS_FOGENABLE, 1 'set to 0 to disable
D3DDevice.SetRenderState D3DRS_FOGTABLEMODE, D3DFOG_NONE 'dont use table fog
D3DDevice.SetRenderState D3DRS_FOGVERTEXMODE, D3DFOG_LINEAR 'use standard linear fog
D3DDevice.SetRenderState D3DRS_RANGEFOGENABLE, 0 'enable range based fog, hw dependent
D3DDevice.SetRenderState D3DRS_FOGSTART, FloatToDWord(Start_Distance)
D3DDevice.SetRenderState D3DRS_FOGEND, FloatToDWord(Finish_Distance)
'//And the function the above uses:
Function FloatToDWord(f As Single) As Long
'this function packs a 32bit floating point number
'into a 32bit integer number; quite slow - dont overuse.
'DXCopyMemory or CopyMemory() (win32 api) would
'probably be faster...
Dim buf As D3DXBuffer
Dim l As Long
Set buf = D3DX.CreateBuffer(4)
D3DX.BufferSetData buf, 0, 4, 1, f
D3DX.BufferGetData buf, 0, 4, 1, l
FloatToDWord = l
End Function
'//To check for Range Based Fog support (it looks better!!)
Public Function CheckForRangeBasedFog(adapter As Byte) As Boolean
On Local Error Resume Next
Dim DX As New DirectX8
Dim D3D As Direct3D8
Dim Caps As D3DCAPS8
Set D3D = DX.Direct3DCreate
D3D.GetDeviceCaps adapter - 1, D3DDEVTYPE_HAL, Caps
If Caps.RasterCaps And D3DPRASTERCAPS_FOGRANGE Then
CheckForRangeBasedFog = True
Else
CheckForRangeBasedFog = False
End If
End Function
|
|
|
|
Tip 2:
Anti-Aliasing
Anti-Aliasing, when it's done without
crippling the frame rate makes the final
image something special indeed - gone
are those jaggy lines around polygons
and textures symbolic of the low
resolutions used, instead we have silky
smooth edges and textures...
Unfortunately it's hardware dependent,
and short of the top-of-the-line
graphics cards your not going to find
much support for it... yet.
'//These lines replace the relevant lines in the D3DPRESENT_PARAMETERS used during
'//device creation...
D3DWindow.SwapEffect = D3DSWAPEFFECT_DISCARD
D3DWindow.MultiSampleType = D3DMULTISAMPLE_2_SAMPLES
'//Set this render state after device creation, all subsequent rendering will be anti-aliased.
D3DDevice.SetRenderState D3DRS_MULTISAMPLE_ANTIALIAS, 1
'//To Check for FSAA support:
Public Function CheckForFSAA(adapter As Byte, DispModeFormat As CONST_D3DFORMAT) As Boolean
'//0. Any variables
Dim DX As New DirectX8
Dim D3D As Direct3D8
'//1. Get the data
Set D3D = DX.Direct3DCreate
If D3D.CheckDeviceMultiSampleType(adapter - 1, D3DDEVTYPE_HAL, DispModeFormat, False, _
D3DMULTISAMPLE_2_SAMPLES) >= 0 Then
CheckForFSAA = True
Exit Function
Else
CheckForFSAA = False
Exit Function
End If
End Function
|
|
|
|
Tip 3:
Compressed Textures
Compressed textures are a gift when it
comes to a heavily texture intensive
program - using level 1 compression you
can, with minimal quality loss, compress
with a 6:1 ratio (6mb becomes 1mb). Even
if your not heavily using textures you
can, as Unreal Tournament does, use a
special set of high detail textures -
which will look better, and fit into the
same, if not less space... As with all
cool things, it's hardware dependent...
'//To check for hardware support:
If D3D.CheckDeviceFormat(0, D3DDEVTYPE_HAL, DispMode.Format, 0, D3DRTYPE_TEXTURE, D3DFMT_DXT1) = D3D_OK Then
Debug.Print "DXT1 Textures are supported"
End If
If D3D.CheckDeviceFormat(0, D3DDEVTYPE_HAL, DispMode.Format, 0, D3DRTYPE_TEXTURE, D3DFMT_DXT2) = D3D_OK Then
Debug.Print "DXT2 Textures are supported"
End If
If D3D.CheckDeviceFormat(0, D3DDEVTYPE_HAL, DispMode.Format, 0, D3DRTYPE_TEXTURE, D3DFMT_DXT3) = D3D_OK Then
Debug.Print "DXT3 Textures are supported"
End If
If D3D.CheckDeviceFormat(0, D3DDEVTYPE_HAL, DispMode.Format, 0, D3DRTYPE_TEXTURE, D3DFMT_DXT4) = D3D_OK Then
Debug.Print "DXT4 Textures are supported"
End If
If D3D.CheckDeviceFormat(0, D3DDEVTYPE_HAL, DispMode.Format, 0, D3DRTYPE_TEXTURE, D3DFMT_DXT5) = D3D_OK Then
Debug.Print "DXT5 Textures are supported"
End If
'//To create a texture, use the CreateTextureFromFileEx( ) call, except use one of the following 5 values in the texture format parameter
D3DFMT_DXT1 - Opaque / 1 bit transparent colour
D3DFMT_DXT2 - Explicit Alpha (Alpha Premultiplied)
D3DFMT_DXT3 - Explicit Alpha
D3DFMT_DXT4 - Interpolated Alpha (Alpha Premultiplied)
D3DFMT_DXT5 - Interpolated Alpha
'//For standard textures DXT1 is the best option...
|
|
|
|
Tip 4:
Gamma Correction
Gamma correction is an interesting
feature, it allows you to alter the way
the video card displays a colour,
effectively you can control how much
red, green or blue there is in the final
image - without altering the existing
textures or surfaces. I've written a
tutorial here
about gamma correction in DirectDraw7 -
should you want to learn more. Do not
assume gamma correction support in Dx8
if it was there in Dx7.
'//To Check for support, and to apply new settings use the following code:
Dim gRamp As D3DGAMMARAMP, Caps As D3DCAPS8
D3DDevice.GetDeviceCaps Caps
If (Caps.Caps2 And D3DCAPS2_CANCALIBRATEGAMMA) Then
For I = 0 To 255
gRamp.red(I) = Interpolate(0, CSng(GammaRedVal), I / 255)
gRamp.green(I) = Interpolate(0, CSng(GammaGreenVal), I / 255)
gRamp.blue(I) = Interpolate(0, CSng(GammaBlueVal), I / 255)
Next I
'if following line does not work, replace flags with "D3DSGR_CALIBRATE"
D3DDevice.SetGammaRamp D3DSGR_NO_CALIBRATION, gRamp
End If
|
|
|
|
Tip 5: Multiple Viewports
Now this is a clever one, everyone will
of seen the way that 3D renderers and
level editors offer 3-4 different views
of the world/level - usually arranged in
a 2x2 grid. Want to do that? here's
how... Also, using this method of
switching render targets, you can get
Direct3D to render onto a texture -
which can then be used when rendering
other geometry, great for mirrors and
other strange special effects...
'//Global Objects/Variables required:
Dim Swap(0 To 1) As Direct3DSwapChain8 'represents our additional windows
Dim D3DWin(0 To 1) As D3DPRESENT_PARAMETERS 'creation information on the additional windows
Dim DepthBufferSurf As Direct3DSurface8 'the global Depth buffer
Dim RenderSurface(0 To 2) As Direct3DSurface8 'the 3 windows...
'//1. Create Device as normal
'-make sure it's in windowed mode, make it use the first picture box (Picture1.hWnd)
'//2. Create additional swap chains - we're going to create 2 more (total of 3).
'-In the main initialisation function, after creating the device
'-Each swap chain represents an additional viewport
D3D.GetAdapterDisplayMode 0, DispMode
D3DWin(0).Windowed = 1 '//Tell it we're using Windowed Mode
D3DWin(0).SwapEffect = D3DSWAPEFFECT_COPY_VSYNC '//We'll refresh when the monitor does
D3DWin(0).BackBufferFormat = DispMode.Format '//We'll use the format we just retrieved...
D3DWin(0).EnableAutoDepthStencil = 1
D3DWin(0).hDeviceWindow = frmMain.Picture2.hWnd
Set Swap(0) = D3DDevice.CreateAdditionalSwapChain(D3DWin(0))
D3DWin(1).Windowed = 1 '//Tell it we're using Windowed Mode
D3DWin(1).SwapEffect = D3DSWAPEFFECT_COPY_VSYNC '//We'll refresh when the monitor does
D3DWin(1).BackBufferFormat = DispMode.Format '//We'll use the format we just retrieved...
D3DWin(1).EnableAutoDepthStencil = 1
D3DWin(1).hDeviceWindow = frmMain.Picture3.hWnd
Set Swap(1) = D3DDevice.CreateAdditionalSwapChain(D3DWin(1))
'//3. Retrieve pointers to all the relevant surfaces.
'-We recycle the same depth buffer, but should you want separate ones you can create a surface of the correct size
'-and make it of format D3DFMT_D16 (or any other valid depth buffer format).
Set RenderSurface(0) = D3DDevice.GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO)
Set DepthBufferSurf = D3DDevice.GetDepthStencilSurface()
Set RenderSurface(1) = Swap(0).GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO)
Set RenderSurface(2) = Swap(1).GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO)
'//4. Restructure our rendering loop.
'-note that we're rendering 3 times, which is effectively 3 frames, expect frame rate to drop...
'##START RENDERING OF FIRST WINDOW##
D3DDevice.SetRenderTarget RenderSurface(0), DepthBufferSurf, 0
'All subsequent calls for rendering will affect the first window
D3DDevice.Clear 0, ByVal 0, D3DCLEAR_TARGET Or D3DCLEAR_ZBUFFER, &HFF, 1#, 0
D3DDevice.BeginScene
'ALL RENDERING FOR FIRST WINDOW IN HERE!!
D3DDevice.EndScene
'display the first window.
D3DDevice.Present ByVal 0, ByVal 0, 0, ByVal 0
'##START RENDERING OF SECOND WINDOW##
D3DDevice.SetRenderTarget RenderSurface(1), DepthBufferSurf, 0
'All subsequent calls for rendering will affect the first window
D3DDevice.Clear 0, ByVal 0, D3DCLEAR_TARGET Or D3DCLEAR_ZBUFFER, &HFF, 1#, 0
D3DDevice.BeginScene
'ALL RENDERING FOR FIRST WINDOW IN HERE!!
D3DDevice.EndScene
'display the first window.
Swap(0).Present ByVal 0, ByVal 0, 0, ByVal 0
'##START RENDERING OF THIRD WINDOW##
D3DDevice.SetRenderTarget RenderSurface(2), DepthBufferSurf, 0
'All subsequent calls for rendering will affect the first window
D3DDevice.Clear 0, ByVal 0, D3DCLEAR_TARGET Or D3DCLEAR_ZBUFFER, &HFF, 1#, 0
D3DDevice.BeginScene
'ALL RENDERING FOR FIRST WINDOW IN HERE!!
D3DDevice.EndScene
'display the first window.
Swap(1).Present ByVal 0, ByVal 0, 0, ByVal 0
'//IMPORTANT NOTE:
'ALL SURFACES MUST BE THE SAME SIZE, WHEN USING PICTURE BOXES (AS WE HAVE)
'ALL OF THEM MUST BE THE SAME SIZE, OR IN DESCENDING ORDER (1=>2=>3=>4), IF
'NOT, YOU'LL GET A FATAL ERROR!!
' - ALSO -
'YOU CAN HAVE ONLY 1 VIEWPORT IN FULLSCREEN MODE AT A TIME.
|
|
|
|
There! I begun
by saying that we'd covered almost everything
that you needed to write a 3D engine in Direct3D8
and Visual Basic; now I can [hopefully] say that
you can make the best possible 3D engine - fast,
efficient and full of features. Despite what I've
covered I know that there are 100's, if not 1000's
of clever tricks and features that I haven't covered
yet - go searching the web if your still hungry
for more. Click here
to continue to the grand finale of the series!!
|