DirectXGraphics:
Accessing Texture Memory
Author:
Jack Hoxley
Written: 22nd May 2001
Contact: [EMail]
Download: Graph_14.Zip
(122 kb)
Contents
of this lesson
1. Introduction
2. Copying from Texture to Texture
3. Gaining access to Texture memory
4. Gaining access to the back-buffer
and front-buffer
5. Doing something with the memory
1.
Introduction
Welcome
to the last in the series of tutorials on advanced
texturing. By the end of this article you'll probably
have enough knowledge to do most things related
to textures - if not, you'll be able to pick them
up fairly quickly. Accessing texture memory is
the last major thing to cover.
Up
till this point all we've done with texturing
is load them, render them and blend them - there
will be times when you need a little bit more
functionality. Maybe you want to combine lots
of little textures onto one larger texture, maybe
you want to create a texture using a mathematical
filter (noise for example), or maybe you need
to draw a copy of the world map onto a texture
for use in the user interface... This is what
this tutorial is about.
2.
Copying from Texture to Texture
Before
I launch into the main core of this tutorial I
want to cover the copying of one texture to another.
This was a very common thing to do in DirectDraw7
- many people migrating from version 7 to version
8 of DirectX will be scratching their heads wondering
where they're Blt( ) and BltFast( ) calls had
been hidden...
A
typical use of this method is to load in lots
of small tiles (Grass, Stone, Sand, Water etc...)
and stick them all on one large palette like texture.
Particularly useful if you only want to store
1 copy of each tile's texture, and you don't know
during development what textures will be used...
Direct3D
only allows us to copy between surfaces (Direct3DSurface8),
not textures (Direct3DTexture8) - so the first
step to copying between the textures is to convert
them to a surface. Strictly speaking we're not
going to convert them or copy them to surfaces,
we're going to make a surface point at a specific
level in the texture, identical in theory to pointers
in C/C++ (if your familiar with them). These surfaces
can then be used in copy operations, and the results
will be directly mirrored onto the texture - which
we can then render from. Using this method of
referencing a texture through a surface can also
be used to make a texture a render target (for
special effects). Here's the complete Code:
'## DECLARATIONS ##
'//Our renderable textures
Dim TexSource As Direct3DTexture8
Dim TexDest As Direct3DTexture8
Dim TexComb As Direct3DTexture8
'//The surfaces that will point to them.
Dim SurfSource As Direct3DSurface8
Dim SurfDest As Direct3DSurface8
Dim SurfComb As Direct3DSurface8
'## INITIALISATION ##
'//Create Our TEXTURE objects
Set TexSource = D3DX.CreateTextureFromFileEx(D3DDevice, App.Path & "\texsource.bmp", _
128, 128, 1, 0, D3DFMT_R5G6B5, D3DPOOL_MANAGED, _
D3DX_FILTER_LINEAR, D3DX_FILTER_LINEAR, 0, ByVal 0, _
ByVal 0)
Set TexDest = D3DX.CreateTextureFromFileEx(D3DDevice, App.Path & "\texdest.bmp", _
128, 128, 1, 0, D3DFMT_R5G6B5, D3DPOOL_MANAGED, _
D3DX_FILTER_LINEAR, D3DX_FILTER_LINEAR, 0, ByVal 0, _
ByVal 0)
Set TexComb = D3DX.CreateTexture(D3DDevice, 128, 128, 1, 0, D3DFMT_R5G6B5, D3DPOOL_MANAGED)
'## RENDER LOOP ##
'//When Rendering, We make a pointer to the texture:
Set SurfSource = TexSource.GetSurfaceLevel(0)
Set SurfDest = TexDest.GetSurfaceLevel(0)
Set SurfComb = TexComb.GetSurfaceLevel(0)
'//We then copy 1/2 of SurfSource and 1/2 of SurfDest to SurfComb
rctSource.Top = 0: rctSource.Left = 0: rctSource.Right = 64: rctSource.bottom = 128
ptDest.X = 0: ptDest.Y = 0
D3DDevice.CopyRects SurfSource, rctSource, 1, SurfComb, ptDest
rctSource.Top = 0: rctSource.Left = 64: rctSource.Right = 128: rctSource.bottom = 128
ptDest.X = 64: ptDest.Y = 0
D3DDevice.CopyRects SurfDest, rctSource, 1, SurfComb, ptDest
|
|
|
|
Not
too complicated really, but a few things to be noted.
Firstly, when creating the textures they must all
be of the same format - CopyRects will not convert
between formats, and will just do nothing (maybe
an error if your lucky!). This is why I've used
CreateTextureFromFileEx( ) - so I can explicitly
specify what type of textures I want used. The third
texture is just a blank one - we're going to be
filling this texture with parts of the other two
textures. Then there's the part about retrieving
pointers to the textures - You may well have seen
references to it already, if not used in previous
D3D's, Mip Mapping (Much in Little), which is too
big to go into now, involves generating progressively
smaller textures (1/2 the size each time), and stores
them in a series of levels, 0 is the main texture,
1 is 1/2 the size of that, 2 is half the size of
that and so on... down to however many levels you
specify in the CreateTextureFromFileEx call (0 indicates
a full chain down to 2x2 or as close as possible).
We want the main texture, so we can just put a 0
in here, but if your dealing with mipmaps then you
may want to alter this value.
Finally
there is the CopyRects( ) call. This is the basis
of everything we want to do - it takes multiple
Rect's (Rectangles) defined by the structure RECT,
and copies them from the destination to a given
point on the destination surface. Anyone familiar
with DirectDraw7 will be at home straight away
here - it's basically a BltFast( ) call. The only
difference being that you can specify an array
of Rectangles. The most important thing to note
is that you must make sure that the rectangles
are valid, as in, they cannot overlap the edge
of a surface - D3D will not clip the rectangle
to fit - it'll just refuse to do it. Also note
that we're now using pixels as coordinates and
measurements - not the standard texture addressing
0.0 to 1.0 scale...
3.
Gaining access to Texture memory
Now
we move onto the real meat of this tutorial -
accessing individual pixels. BUT, this is complicated
- we're going down as far as binary manipulation
of memory, if you're not too sure with your ANDs,
ORs, Bits, Shifting and so on then you may well
get out of your depth here; I'll make it as easy
as possible - but if you do get lost then you'll
need to go looking through some guides elsewhere
(many better articles than I could write exist).
The
first stage is about getting access to the memory
- how to play with it, and how it all works is
going to be explained in a minute. First off,
why do you you need to know about this?
Well,
this sort of thing comes up in a variety of forms
- traditionally it's been the realm of the advanced
DirectDraw programmer looking for some special
effects (Alpha Blending, Colour Blending, Particle
Effects, Lighting etc...), and to a certain degree
it still is going to be a toy of the advanced
programmer, but it has it's uses to the simple
program. Take the map that your world is based
on - you could, during level creation, save a
picture of it (for an in game map), but using
this method you can read the data from the file
and generate an appropriate texture on the fly
as you need it - especially useful if you want
multiple zoom levels, or the level itself is very
large (and saving a .bmp is not appropriate).
Alternatively, should you want to play around
with your own image format, unless you're using
the DXTex tool (included with the SDK) it can
be a pain in the back side getting an alpha channel
embedded into your texture - using this method
you could make a tool that takes two images (the
RGB channel and the A channel) and combines them,
saves them to a custom format, then when your
game loads it reads this data, creates the relevant
texture type and writes the pixel data straight
in. I'm sure you'll find some use for it...
Dim pData As D3DLOCKED_RECT, pxArr() As Byte
TexDMA.LockRect 0, pData, ByVal 0, 0
'we can now play around with the stuff in pData
ReDim pxArr(pData.Pitch * 128) As Byte 'enough bytes, we're not using integers or longs because we have
'to mess around with the signed bit in them (what makes it a -n)
DXCopyMemory pxArr(0), pData.pBits, pData.Pitch * 128 'where 128 is the height of the surface
'//At this point in time, pxArr() holds a copy of all the texture's pixel data
DXCopyMemory ByVal pData.pBits, pxArr(0), pData.Pitch * 128 'thanks to MetalWarrior for helping sort a bug with this!
TexDMA.UnlockRect 0
|
|
|
|
The
above piece of code locks the texture, allocates
enough memory to store the data, then copies it
to this array, then copies the data back again before
unlocking the texture again. If you're familiar
with DirectDraw surface locking then most of that
will make sense to you (it's not too dissimilar
to the GetLockedArray( ) call), if it's gone straight
over your head... read on.
Locking
- this allows your program to gain access to a
portion of the textures memory; due to DirectX8's
complex memory management functions it's a little
difficult to know where the texture's memory will
be, when you lock the resource (it's not only
textures that you can lock) Direct3D will stick
it in a place where it's easy to get at (usually
system memory) and then tell us where we can go
find it. Direct3D returns, as the pBits member,
a pointer to the first bit of texture memory,
we therefore need to read a certain amount of
data from this point onwards; Visual Basic has
no native support for pointers, so we have to
copy the memory at the location pointed to by
the pointer to a more permanent place that we
can access; this is what the DXCopyMemory( ) call
does for us (it's a wrapper for the CopyMemory
API call). We then use the same function to copy
the data back again when we're finished - whatever
we do to the array in between these calls will
be mirrored on the next frame update. Finally
we unlock the texture and let Direct3D go on about
it's business...
A
few important things to note:
1. Invalid Data - we're using CopyMemory
here, no formatting is applied - this can be a
good thing, but it can also lead to bad things!
Namely the fabled "Blue Screen Of Death"
- on several occasions here I've managed to lose
count of quite how many blue screens I provoked...
2. Speed - the actual locking and copying
is pretty fast, thats of no great problem (usually);
BUT as we'll see later on, to process each pixel
it can take some quite complicated maths and logic
- multiply the time this takes by the number of
pixels in the texture (16,384 in a small 128x128
texture) and suddenly you have a lot of processing
to do...
3. Clever things - dont try them during
the lock. Locking messes around with intenal windows
functions, effectively stopping programs accessing
the memory in some cases, as well as suspending
other applications; the point being that you shouldn't
try doing anything major with the Win32 API or
DirectX during the lock.
4.
Gaining access to the back-buffer and front-buffer
This
is just a quick extension of the previous section;
but I thought I may as well include it as there
are some situations where accessing the flipping
chain directly is required. This sort of thing
will be familiar to the seasoned DirectDraw7 programmer,
but for those new to all this stuff the back-buffer
is where the scene is rendered and composed, and
the front-buffer represents the screen (and holds
the final image). The present (flip in DD7) function
called at the end of every frame swaps the addresses
for the front and back buffer.
Dim FrontBuffer As Direct3DSurface8 Dim BackBuffer As Direct3DSurface8
D3DDevice.GetFrontBuffer FrontBuffer Set BackBuffer = D3DDevice.GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO)
'you can now lock as per normal: FrontBuffer.LockRect pData, rct, 0 BackBuffer.LockRect pData, rct, 0 'etc...
|
|
|
|
5.
Doing something with the memory
okay,
now things start to heat up. conveniently this
is also where those not upto scratch with binary
manipulation will keel over dead.
I'm
going to show you how to manipulate two pixel
formats, one extremely easy, one moderately difficult.
Having said that though, as soon as you've done
one worked example this all becomes extremely
simple and you'll easily be able to apply it to
all 30 something pixel formats that Direct3D8
supports. First off then, whats a pixel format?
8 bit, 16 bit, 24 bit and 32 bit are all basic
pixel formats, they tell you that (in order) there
are 8, 16, 24 or 32 bits of memory allocated to
store the colour of every pixel. 8 bit is rarely
used anymore with almost complete support for
at least 16 bit rendering now. 24 and 32 bit modes
are the easiest possible formats, 16 bit is a
bit of a pain in the back side. You will so far
of seen the enumeration flags "D3DFMT_X8R8G8B8"
and "D3DFMT_R5G6B5" in these tutorials
- these are what describe the pixel formats, normally
you only specify them in the CreateTextureFromFileEx(
) calls. The former is a 32 bit mode, 8 bits are
unused, 8 bits are red, 8 bits are green and 8
bits are blue. The latter is a 16 bit mode, it
tells us that there are 5 bit for red, 6 bits
for green and 5 bits for blue (There is more blue
because our eyes are more sensitive to the green
spectrum).
As
you've seen already (when locking the surface)
we copy all the data to an array of bytes. each
byte is made up of 8 bits. We could copy them
straight to 32bit or 16bit integers, but this
makes things very difficult later on because of
the signed bit (the part that makes the number
+ or - ), so we're going to stay away from them.
First off, for those sharp people you'll have
noticed that 8 bits = 1 byte, data stored in 1
byte increments, D3DFMT_X8R8G8B8 indicates that
each colour component is stored in a byte. Well
done, you've just worked out how to decode the
24 and 32 bit colour modes (24 bit is 32 bit but
without the alpha, A, or unused, X, channel).
TexDMA2.LockRect 0, pData, ByVal 0, 0 'we can now play around with the stuff in pData ReDim pxArr(pData.Pitch * 128) As Byte 'enough bytes If Not (DXCopyMemory(pxArr(0), ByVal pData.pBits, pData.Pitch * 128) = D3D_OK) Then 'handle errors here if unable to copy data. End If 'Should be XRGB format, instead, in BGRX format... For x = 0 To (pData.Pitch * 128) - 1 Step 4 'unused = pxArr(x + 3) bRed = pxArr(x + 2) bGreen = pxArr(x + 1) bBlue = pxArr(x + 0) bRed = 0 bGreen = 0 bBlue = 255 pxArr(x + 2) = bRed pxArr(x + 1) = bGreen pxArr(x + 0) = bBlue Next x If Not (DXCopyMemory(ByVal pData.pBits, pxArr(0), pData.Pitch * 128) = D3D_OK) Then 'handle error for bad copy here... End If TexDMA2.UnlockRect 0
|
|
|
|
The
previous piece of code was for the 32 bit mode,
to read out the colours all we need to do is read
the bytes int he correct order. The original array
is a single dimension, stored as BGRXBGRXBGRXBGRXBGRX
so we need the "Step 4" in the main
loop, we would also need to use a conversion formula
should we want to get the X/Y coordinates for
the current pixel we're dealing with. Also note
that it's stored backwards! instead of RGBX format
we find that it's actually stored as BGRX format.
The reasons are a little complicated, and not
really that important here - but look into big/little
endian formats if you're interested. The above
piece of code will make the whole texture perfect-blue,
simply because we're changing the bRed, bGreen
and bBlue values to (0,0,255).
A
quick note on pitch, you'll have seen the pData.Pitch
member being used in several parameters so far;
this value represents the real width of the texture
- in memory. it is extremely important. As we've
just seen, it takes 4 bytes to store the 32 bit
colour value; which means that we must have 4
bytes representing each pixel in memory. Which
therefore means that the width of the texture
may be 128x128 pixels, but in memory it's going
to be more like 512x128 bytes - each row requires
4x the number of pixels in bytes. whilst the streamlines
nature of DirectX8 allows you to be fairly confident
that you can assume it'll be 512 bytes per row
it's a good idea to check first.
Right,
I'm bored with 32 bit mode - it's too easy
<grin>
Whilst
32 bit modes are already the prefered mode they
are by no means the most common modes - all of
the new cards tend to have full 32 bit support,
but many of the popular 3D cards of last year
(the Voodoo3 in particular) only support 16 bit
mode. This means that you'll need to support both
modes for locking/writing. It's 16 bit mode that
makes things fun.
I'm
going to show you how to manipulate a 16 bit 565
RGB value - extract the 16 bit value, read out
the values, change them, put them back into the
16 bit value. I'll admit now that this one had
me stumped for a week or so - the theory that
I scribbled worked perfectly on paper, and there
was no logical reason why it shouldn't of worked
in code, but it refused. Then I sat down and ran
a couple of very simple tests, check the results,
and *!!Click!!* I realised that I'd been combining
the bytes in the wrong order, and suddenly my
perfect piece of theory worked a treat. Expect
many similiar situations...
[RRRRRGGG]
[GGGBBBBB] (where [] signifies a byte)
above
is what our 16 bit value will look like in memory,
and in our array it will be split into 2 bytes
(the square brackets). Straight away you can see
the problem - the red and green are mixed together,
and so is the green and blue - in fact, the green
is in both bytes! On closer inspection you'll
see that you cant actually read out the values
directly anyway (even if they aren't mixed) -
RRRRR000 is very different from 000RRRRR (248
for the former, and 31 for the latter). The process
for extracting the colour channels looks like
this:
1.
Take the two bytes, combine them into one straight
16 bit line
2. Mask out the green and blue channels, shift
right 11 bits = Red
3. Mask out the red and blue channels, shift right
5 bits = Green
4. Mask out the red and green channels = Blue
- Manipulate colours here -
5. Shift the Red left by 11 bits, shift the green
across by 5 bits
6. Combine the Red, Green and Blue values into
a 16 bit long
7. Mask out the lowest 8 bits, shift right 8 bits
= second byte
8. Mask out the highest 8 bits = first byte.
If
you understand all of that then you're on a roll
- read on. For those of you that look at it as
though it was chinese (assuming chinese isn't
your first language!), here's a quick guide.
Bit Shifting, this either goes left or
right, take the binary value 0011100, shift it
left by 1 = 0111000 and shift it right by 1 =
0001110, this is the same as multiplying by 2^n
(n bits) for going left, and dividing by 2^n (n
bits) to go right. Be careful when shifting left
that you dont multiply the value out of range
(easy to do!).
Masking, you use a given value and using
the AND logical operator you can remove certain
parts of the chain, making it easy to extract
only the part that you want.
In
More detail.
Step 1: Combining
Take the array of locked data, and two bytes,
bFirst and bSecond; and a Long to store the result,
lRes. Why a long? and not an integer. Whilst an
integer is a perfect fit (16 bit) we'll have to
mess with the signed bit - which I like to avoid
wherever possible. a 32 bit long gives us a 15
bit cushion between the data we're interested
in and the signed bit.
bFirst = pxArr(x) bSecond = pxArr(x + 1) lRes = (bSecond * 2 ^ 8) Or bFirst
|
|
|
|
The
above piece of code takes the two bytes out of
the array and logically combines them using the
OR operator. If you're not sure of how the OR
operator works take the following truth table:
A
|
B
|
A
Or B
|
0
|
0
|
0
|
1
|
0
|
1
|
0
|
1
|
1
|
1
|
1
|
1
|
Basically,
if either of the two bits are true (1) then the
output will be true (1). We can therefore combine
the two values:
1111111100000000
= bSecond shifted left 8 bits
0000000011111111 = bFirst
---------------------
1111111111111111 = lRes
Step
2: Extracting the Red component
In the 16 bit value, 1111100000000000 are the
bits that the red channel occupies. We can use
AND to extract these values. Take the following
truth table for the AND operator:
A
|
B
|
A
And B
|
0
|
0
|
0
|
1
|
0
|
0
|
0
|
1
|
0
|
1
|
1
|
1
|
AND
only outputs a true value (1) if both of it's
inputs are 1 (makes sense really). Therefore if
we take the decimal value for 1111100000000000,
63488, and logically AND it with the complete
16 bit chain, the output will be only the red
bits, but shifted left 11 bits (so we undo this
later by shifting right by 11 bits).
1011011101100111
= 16 bit chain
1111100000000000 = Red mask, 63488
---------------------
1011000000000000 = output, red shifted left by
11 bits
0000000000010110 = Red correctly shifted right
by 11 bits.
bRed = (lRes And 63488) / 2 ^ 11 bRed = (255 / 31) * bRed 'to convert to the familiar 0-255 range.
|
|
|
|
The
above piece of code is what the sample uses to
extract the Red component, notice that it also
converts it to the familiar 0-255 range; if you've
done any work with graphics in a paint package
you'll probably have use these values. It's not
an important step (as long as you remember to
do the opposite later on).
Step
3: Extracting the Green component
This is almost identical to extracting the
red component, but we use a different mask.
The green bits occupy this section: 0000011111100000,
which is 2016 in decimal, which is our mask.
To
extract the green component we AND the 16 bit
value with 2016, and then shift it right by 5
bits to result in the correct value:
bGreen = (lRes And 2016) / 2 ^ 5 bGreen = (255 / 63) * bGreen 'to convert it to 0-255 range
|
|
|
|
Again,
we convert it to the 0-255 range.
Step
4: Extracting the Blue component
Now, guess what we do here - almost exactly
what we've done in the last 2 steps; with the
only exception that we dont need to do any bit
shifting (The blue bits are already in the correct
place). Blue occupies 0000000000011111 in the
16 bit chain, which is 31 in decimal.
bBlue
= lRes And 31
bBlue = (255 / 31) *
bBlue
'convert it to 0-255
range |
|
|
|
That
wasn't too nasty was it. in theory all the bit
shifting, masking and so may look horrible - but
you can get your head around it pretty quickly.
Step
5 & 6: Preparing the bytes again.
We're now at the stage that we've read the
colour channels, messed around with them, and
we now want to stick them back into the texture/surface.
The first step is to convert the channels back
into valid numbers.
Note
that we converted from 5 bit accuracy to 8 bit
accuracy (0-255 range), so we now need to convert
from 8 bit back to 5 bit. This will result in
a loss of precision - see the note at the end
about accuracies.
'//Convert RED bRed = Int((31 / 255) * bRed) 'convert it back to the 0-31 scale lRed = bRed * 2 ^ 11 '//Convert GREEN bGreen = Int((63 / 255) * bGreen) 'convert it back to the 0-63 scale lGreen = bGreen * 2 ^ 5 '//Convert BLUE bBlue = Int((31 / 255) * bBlue) 'convert back to the 0-31 scale lBlue = bBlue '//Assemble Complete Long lRes = lRed Or lGreen Or lBlue
|
|
|
|
Above
is the complete code to combine the bRed, bGreen
and bBlue values back to the lRes value. First
we convert from 8 bit to 4 bit (or 6 bit for green)
and we then use the OR logical operator to put
them into one long 16 bit chain.
Step
7 & 8: Splitting the 16 bit value
At this point in time we've gotten back to
the 16 bit long, but to store it in our array
we'll need it to be in two bytes.
To
extract the second byte we mask out the highest
8 bits, then shift it right by 8 bits.
To extract the first byte we just need to mask
out the lowest 8 bits.
'bSecond
is the highest 8 bits
bSecond = (lRes And
65280) / 2 ^ 8
'bfirst
is the lowest 8 bits
bFirst = lRes
And 255
pxArr(x)
= bFirst
pxArr(x + 1) = bSecond
|
|
|
|
As
shown in the above example; the final two lines
putting the bytes back into their correct places
in the array...
Finally,
some notes that you may find useful:
1. Hex values; in almost every case so
far it would have been easier to use hexidecimal
notation, such as &HFF&... I didn't want
to use them because VB hasa tendency to change
them around and muck them up; it seemed simpler
just to keep them as decimals. You can store them
in constants if you prefer.
2. Accuracies - an 8 bit colour has 256
possible colours (2^8 = 256); whereas 5 bit only
has 32 shades and 6 bit has 64 shades. In the
16 bit mode I just demonstrated, there are 32
shades or red, 64 shades of green and 32 shades
of blue. This isn't too much of a problem (beyond
it not looking so pretty), but when we convert
it to 8 bit and/or back again we'll lose some
accuracy. 5 bit to 8 bit implies that every 8
values in 8 bit are represented by one of the
32 shades; and similiar with 6 bit (each shade
represents 4 colours). This is of particular importance
when you go from 8 to 5 (or 6) bit accuracy; you'll
need a change of 4 or 8 for it to be reflected
in the 16 bit version; anything less will not
show up. The method of conversion used above is
probably extremely primative compared with what
the photoshops and paint-shop-pro's use (it's
unlikely they're algorithms are suitable for games),
but you may want to look into a slightly better
conversion.
3. Other Formats - there are at least 8
texture formats that you're likely to use; ranging
from alpha to no alpha, 16 bit to 32 bit... whilst
you can be fairly confident how it works, 3 simple
tests can be applied. 3 textures, one full red,
one full green, one full blue. Load them all in
and log the lRes value (the complete 16 bit chain)
and scribble it down on paper in binary - if the
pattern is a little strange 1110000000000111 instead
of 11111100000 then you know you've got the two
bytes the wrong way around. Simple things like
that will make it all easy...
4. Using the calculator - the little program
"Calc.exe" built into windows can sort
out all your binary-decimal conversion if you're
too lazy (or cant) to do it by hand; stick it
in scientific mode and type in a decimal number,
then change it to binary mode - and out comes
the binary equivelent of your number; and vice-versa.
Well,
another massive tutorial completed. I hope it's
been of use to you - I've seen many 100's of posts
on message boards about this sort of thing, it
seems to confuse quite a lot of people! including
me at times... On to Lesson
15: Billboarding for special effects.
|