|
3D Engine 3 - Direct-X and .3DS format
Hi people !
Today i promised to tell you something about 3d-studio and DirectX. We will go over directs
at first. You will need module for work with DirectX.
It runs with directx 3.0 and better. (in next parts of serial i'm using dx7 - version of this
module, but i keep this 3.0 for people working on old computers ...)
To add this module into your project it's necessary to copy both files Directx.cpp and
Directx.h into project directory, add them into the project and then in menu Project
>> Settings (Visual C ++ 6.0) under Link bookmark simply add
text "ddraw.lib" into field Object / library modules (maybe you'll have to go to
downloads section and download libraries for directX7 (works well with this module
and in fact require directx 3 only)) :
And of course you'll have to add this line : #include "directx.h" somewhere into
file where you want to use it.
There are plenty of functions in this module :
HRESULT InitDirectX(HWND hwnd, int WIDTH, int HEIGHT);
void TerminateDirectX(void );
int InitVideoBuffer(unsigned __int32 **pole);
void FlipVideoBuffer(void );
char * err_to_string(HRESULT h);
|
InitDirectX
|
Quite self-explanatory. Just pass window handle and desired resolution ...
|
TerminateDirctX
|
Terminates directx context.
|
InitVideoBuffer
|
Returns true when success, returns pointer where you can copy images ...
|
FlipVideoBuffer
|
Flips buffers and displays results of your drawing.
|
err_to_string
|
Translates errors to strings ... czech :-( but we won't need that.
|
See, it didn't even pain ... It shouldn't be hard now to convert our simple engine to
directx, see it pretty in fullscreen and get rid of blinking when redrawing ...
For those who don't use MSVC: __int32 type is simply 32-bit long so replace it.
Just a little sample how to use it :
#include <windows.h>
#include <malloc.h>
#include <stdio.h>
#include <math.h>
#include "directx.h"
#define APPNAME "DX_Window"
#define WNDNAME "DX_Window"
#define width 400
#define height 300
unsigned __int32 *video;
int WINAPI WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance, PSTR szCmdline, int nCmdShow)
{
static char szAppName[] = APPNAME;
HWND hwnd;
MSG msg;
WNDCLASSEX wndclass;
wndclass.cbSize = sizeof (wndclass);
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = Wokno;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(hInstance, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszMenuName = 0;
wndclass.lpszClassName = szAppName;
wndclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
RegisterClassEx(&wndclass);
hwnd = CreateWindow(szAppName,
WNDNAME,
WS_POPUP,
CW_USEDEFAULT, CW_USEDEFAULT,
width, height,
NULL,
NULL,
hInstance,
NULL);
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
ShowCursor(FALSE);
InitDirectX(hwnd, width, height);
SetTimer(hwnd, 0, 40, NULL);
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK Wokno (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
switch (iMsg)
{
case WM_KEYDOWN:
if (wParam == 27){
ShowCursor(TRUE);
SendMessage(hwnd, WM_CLOSE, 0, 0);
return 0;
}
return 0;
case WM_TIMER:
if (InitVideoBuffer(&video)){
memset((char *)video, 0, sizeof (unsigned __int32 ) *
width * height);
FlipVideoBuffer();
}
return 0;
case WM_DESTROY:
TerminateDirectX();
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, iMsg, wParam, lParam);
}
|
DirectX example
It's really simple. Just create window, go fullscreen - and wait for escape to quit.
Yes, readers who are paying attention noticed nothing is drawn. But the better pieces are
on their way ...
Let's get to 3d-studio. You can download 3ds_loader module (thanks to author of page www.the-labs.com
for nice table of 3DS tags), inside there are two files again - load_3ds.cpp and load_3ds.h.
Again add to project and include header. When you open it, you'll see this :
#ifndef __3DS_INCLUDED
#define __3DS_INCLUDED
struct Plane {
float a, b, c, d;
};
struct Vector {
float x, y, z;
};
struct Vertex {
float x, y, z;
float u, v;
Vector normal;
};
typedef float Matrix[4][4];
struct Material {
int n_id;
char *name;
};
struct Light {
int n_id;
char *name;
int b_spot;
int b_off;
int b_cast_shadows;
Vector v_position;
Vector v_target;
float f_hotspot;
float f_faloff;
float f_inner_range;
float f_outer_range;
float f_multiplier;
Vector v_color;
int r, g, b;
};
struct Face {
int n_id, n_used;
int n_material;
Vertex *vertex[3];
Plane normal;
};
struct Object {
int n_id;
char *name;
int n_vertex_num;
int n_face_num;
Face *face;
Vertex *vertex;
Matrix m_object;
int n_parent;
int n_pos_num;
int n_rot_num;
float *pt, *px, *py, *pz;
float *qt, *qw, *qx, *qy, *qz;
};
struct Camera {
int n_id;
char *name;
Matrix m_camera;
float f_pos_x, f_pos_y, f_pos_z;
float f_tar_x, f_tar_y, f_tar_z;
float f_bank_angle;
float f_focus;
int n_target_num;
int n_position_num;
float *tt, *tx, *ty, *tz;
float *pt, *px, *py, *pz;
};
struct World {
int n_start_frame;
int n_end_frame;
int n_object_num;
int n_camera_num;
int n_light_num;
int n_material_num;
Object *object;
Camera *camera;
Light *light;
Material *material;
};
World *Load_3DS(char *file_name);
#endif
|
We have some structures Plane is plane, defined by it's prependicular (= normal) vector and distance from origin along that vector.
Vector is vector in 3-space. Vertex is our vertex with position and u, v coordinates,
telling us position of texture in that vertex. We aren't interested in it now. Next our well known matrix, Material - structure,
describing texture, Light - light with it's id, name, position, color
and a few next properties, but we will be interested in lights in long long time.
Next is Face - mainly three pointers to it's vertices, material and plane it's lying on.
Object is just list of faces and their vertices + some animating stuff.
Camera is camera, for now just it's matrix and finally the
World structure connects it all togather.
The function loads world from file, that should be obvious ... Now we could write our
wire-frame spinning box (or more likely potato). To make it really cool we include
Backface culling i'm reminding you again and again. But how does that work ?
You should already know the plane equation :
A * x + B * y + C * z + D = 0
Where A, B and C are coordinates of normal vector (vector, prependicular to that plane),
and D is distance from origin O ( O = [0 0 0]) along that vector.
You can compute common plane from three points, lying on that using cross product :
struct Plane {
float a, b, c, d;
};
struct Vector {
float x, y, z;
};
#define _Len(A) ((float )sqrt(A.x * A.x + A.y * A.y + A.z * A.z))
#define _Normalize(A) {float s = _Len(A); A.x /= s; A.y /= s; A.z /= s; }
#define _Dot(A,B) (B.x * A.x + A.y * B.y)
#define _Cross(A,B,C) C.x = B.y * A.z - A.y * B.z; \
C.y = B.z * A.x - A.z * B.x; \
C.z = B.x * A.y - A.x * B.y;
Vector u, v, w;
u.x = c.x - a.x;
u.y = c.y - a.y;
u.z = c.z - a.z;
v.x = b.x - a.x;
v.y = b.y - a.y;
v.z = b.z - a.z;
_Cross(u, v, w);
_Normalize(w);
p_plane->a = w.x;
p_plane->b = w.y;
p_plane->c = w.z;
p_plane->d = -(p_plane->a * a1 + p_plane->b * a2 + p_plane->c * a3);
|
A bit explaining ... Plane is plane, given by its normal vector [ a b c] and
prependicular distance from origin d. Vector is 3-space vector.
_Len is length of vector. _Normalize makes vector to be unit (direction is the same,
but the length is 1). And eventually _Cross computes cross-product, ie. vector w will be prependicular to both vectors u
and v. ( u and v lies in our plane, they mustn't be parallel !) It should be clear now.
Well - back to our plane equation : x, y and z are point coordinates. The equation is truth - this point lies on the plane.
But also if the result is positive, the point is lying in front of plane, negative - behind the plane.
So you can tell where the point is relatively to plane. Did it hit you already ?
We can tell when our camera is lying in front of face to see it ! There is simple trick
that doesn't require to transform the plane. You simply invert transformation matrix
and it's position will be eye position in objectspace. You can tell that matrix inversion
costs more CPU time than plane transformation, but you inverse one matrix per object
or one plane per face (and object will be certainly more than one face) Let's see the code :
void DrawObject(Matrix *m_object, Matrix *m_camera,
Object *p_object, unsigned __int32 *vram)
{
float x, y, z, tx, ty, tz;
Vertex vtx[3];
Matrix m, im;
int i, j;
Face fac;
fac.vertex[0] = &vtx[0];
fac.vertex[1] = &vtx[1];
fac.vertex[2] = &vtx[2];
Matrix_Inverse(m_camera, &m);
Matrix_Multiply(&m, m_object, &m);
Matrix_Inverse(&m, &im);
for (i = 0; i < p_object->n_face_num; i ++) {
if (im[3][0] * p_object->face[i].normal.a +
im[3][1] * p_object->face[i].normal.b +
im[3][2] * p_object->face[i].normal.c +
p_object->face[i].normal.d > 0.1)
continue ;
for (j = 0; j < 3; j ++) {
x = p_object->face[i].vertex[j]->x;
y = p_object->face[i].vertex[j]->y;
z = p_object->face[i].vertex[j]->z;
tx = x * m[0][0] + y * m[1][0] +
z * m[2][0] + m[3][0];
ty = x * m[0][1] + y * m[1][1] +
z * m[2][1] + m[3][1];
tz = x * m[0][2] + y * m[1][2] +
z * m[2][2] + m[3][2];
if (tz > 0.1){
tx = n_Width / 2 + (tx * z_delta) / tz;
ty = n_Height / 2 + (ty * z_delta) / tz;
fac.vertex[j]->x = tx;
fac.vertex[j]->y = ty;
fac.vertex[j]->z = tz;
}
}
Wire_Polygon(&fac, vram);
}
}
|
You may notice i'm no longer checking for off-screen vertices. When you work with faces,
it's done quite another way. You put every face trough the clipping pyramid
(that's quad of planes, forming field of view ;-)) And when the face is only partialy
visible it gets cut to polygon. (or completely discarded when not visible) So i'm
not checking for offscreen points, because there won't be such.
I'll put here simple image for better imagination
Polygon, made by cutting face by pyramid can have maximally seven vertices. Can someone of
you send me that image ? (just for excercise, of course i know it !)
We just need contents of DrawPolygon(); It will simpl draw three lines. How to draw line ?
Simplest and possibly fastest way to do it is Brensham's algorithm :
void Line(int xa, int ya, int xb, int yb, unsigned __int32 *vram)
{
float len, stepx, stepy, x, y;
int i, xlen, ylen;
x = (float )xa;
y = (float )ya;
xlen = xa - xb;
ylen = ya - yb;
len = (float )fabs(((fabs(ylen) > fabs(xlen))? ylen : xlen));
stepx = xlen / len;
stepy = ylen / len;
for (i = 0; i < (int )len; i ++){
if (x > 0 && x < n_Width && y > 0 && y < n_Height)
vram[(int )x + n_Width * (int )y] = RGB(150, 255, 100);
x -= stepx;
y -= stepy;
}
}
|
And we are complete ! I will stop torturing your mind with these terrible 3-space imaginations,
you can download your example :
Wire-frame example
Valid HTML 4.01
|