Czech Welcome Guff Coding 3D - Engine Guestbook Links Downloads About author Mail me Mailform
box_en 3D Engine 3 - Direct-X and .3DS format box_en

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)) :

ddraw lib settings

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);
    // initialize direct x

    SetTimer(hwnd, 0, 40, NULL);
    // timer with period of 40 msec

    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);

            // add your drawing code here
    
            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))
// computes length of vector

#define _Normalize(A) {float s = _Len(A); A.x /= s; A.y /= s; A.z /= s; }
// creates unit vector (length = 1)

#define _Dot(A,B) (B.x * A.x + A.y * B.y)
// dot product (2 * cos(angle vectors are holding))

#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;
// cross product (vector C, prependicular to both A, B)

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;
// 2 vectors, lying in the plane
    
_Cross(u, v, w);
_Normalize(w);
// compute normal vector of plane

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);
// prependicular distance D

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];
    // vertices for our temporal face

    Matrix_Inverse(m_camera, &m);
    Matrix_Multiply(&m, m_object, &m);
    Matrix_Inverse(&m, &im);
    // transformation matrix

    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;
        // backface culling

        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];
            // transform vertices

            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;
            }
            // perspective correction
        }
        
        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


pyramid


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);
        // matrix green
        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-engine

 Wire-frame example

Just to be sure : module for DirectX 7 (must link with dxguid.lib and ddraw.lib)

    -tHE SWINe-
back


Valid HTML 4.01!
Valid HTML 4.01