BRender – The Real-Time 3D Renderer

by Maria Ingold

Tank

The Brender Power Rendering System is a real-time 3D rendering package from Argonaut Technologies Limited in London, England. (Note: BRender is pronounced like the Br sound in Brenda.) This technology has been benchmarked to handle over 120,000 polygons per second on a standard 100 MHz 486 system with local bus graphics. BRender became available to OS/2 Warp games developers in the OS/2 Entertainment Tools that we released in Volume 8 of The Developer Connection for OS/2 (the Entertainment Tools are part of the Developer’s Toolkit for OS/2 Warp).

A Bit About BRender

When you create a game, each scene typically has characters, scenery, and objects that somehow interact with each other. The character might walk through a dank dungeon corridor, see a flail laying on the ground, pick it up and use it in an attack on an approaching monster. The character, the walls, the floor and ceiling of the corridor, the flail, and the monster are all actors in this scene. (An actor is the building block of the BRender World; it is represented by the br_actor data type.) The other more subtle actors are the camera that views the scene, and the lights that illuminate the other actors. All these actors are used to create the BRender World.

As shown in Figure 1, the BRender World is a tree structure:

BRender World tree structure

Figure 1. The BRender World tree structure

  • At the root of the tree structure is the none node; its only purpose is to provide structure for the world database. The none node is represented as BR_ACTOR_NONE.
  • The actors are composed of models, cameras, and lights, represented as BR_ACTOR_MODEL, BR_ACTOR_CAMERA, and BR_ACTOR_LIGHT, respectively.
  • A model is a physical object – a grouping of polygons created from triangular meshes. The aforementioned flail, character walls, and monster are models. Models become more realistic by applying materials and texture maps to them. You can generate models using a product such as AutoDesk’s 3D Studio, or any other product whose output can be converted to a .3DS format. The .3DS file can be converted into a BRender-usable format by using BRender’s 3DS2BR tool. BRender stores the model as an array of vertices and an array of faces. These vertices store the color information and the faces store the surface normals, material pointers, and information about smoothing groups. The model type is referred to as br_model.
  • A material describes the surface characteristic of the object. This includes information about the reflectivity, transparency, color, and specular (KS), ambient (KA), and diffuse (KD) lighting. Flat and Gouraud shading are the only types of shading currently available. The material drives the style of shading, texture mapping algorithms, and light effects used by the rendering engine. The material type is denoted by br_material.
  • A texture map is a bitmapped image that can be layered onto a polygon. This type is also used to store information about Z-buffers, rendering buffers, and shade tables. This type is called .

Due to the way BRender resolves bindings, the texture maps should be loaded first, followed by the materials, and then the models. Thus, if the texture maps have already been loaded, then the material that references a texture map can find it easily by name and point to it. Similarly, when the model is loaded, the materials that cover each face can be named and pointed to.

The Registry maintains and supports the actors, models, materials, and textures in the BRender World database, and provides the following functions:

  • Memory allocation and de-allocation
  • Loading and saving items
  • Item preparation and update
  • Enumeration through a callback function
  • Search for items by textual name
  • Counting items

BRender can use different rendering engines. Its current rendering engine uses the Z-buffer rendering algorithm. The rendering engine also has support for perspective and non-perspective texture mapping: 8-, 16-, or 24-bit color; and includes a full lighting model, including colored light, directional lights, point lights, and spot lights.

This article describes how you can take advantage of BRender and OS/2 Warp to develop 3D OS/2 Warp games.

Writing Your Own BRender Application – The Basics

The first part of creating any game is developing the story line and designing the characters. (The specific example code listed here demonstrates my experimentation with a tank.) BRender lets you model your characters using AutoDesk’s 3D Studio program. Once the models are created, and the lights and cameras and materials are set, you use BRender’s 3DS2BR program to convert your .3DS file into a format that is usable by BRender. The 3DS2BR program separates out the model, material, and actor information. This data can be placed in separate files (for example, TANKMOD.DAT, TANKMAT.DAT, and TANKACT.DAT) or concatenated into one file.

Once the object models are in the BRender format, the actual code needs to written to use these models in an interactive format. Every piece of BRender code is composed of four basic parts:

  • The initialization
  • Setting up the BRender World database
  • The main loop, which performs the rendering and modifications to the BRender World database (usually from user input)
  • The termination and cleanup code

Initialization and Termination

BRender is initialized by a call to BrBegin. This function must be called before any other calls to BRender can be made. Once this call has been made, the Z-buffer renderer must be initialized by calling BrZbBegin. The parameters to BrZbBegin specify both the color depth of the bitmap (pixelmap) being rendered into, as well as the depth of the Z-buffer. For example, if the color depth of the bitmap being rendered into is 8 bits or 256 colors, and the Z-buffer is 16 bits or 65,536 colors, then BR_PMT_INDEX8 and BR_PMT_DEPTH_16, respectively would be specified. BRender is terminated and cleaned up simply by making calls to BrZbEnd and BrEnd.

During the creation of the window procedure, you should also set up the palette. BRender provides and OS/2 palette called OS2PAL.PAL. Use BrPixelmapLoad to load the palette and get the palette entries. BRender’s RGB layout is the same as OS/2’s.

There are three different ways this palette can be set: The Graphical Programming Interface (GPI), the Direct Interface Video Extensions (DIVE), or the graphics engine (GRE). However, DIVE only sets the logical palette. GPI sets the physical palette, but is slowed down because it maps the palette entries. The fastest way to set the middle 236 entries of the physical palette is by using the GRE. (Note: The following GRE calls are undocumented. However, you can find information about them in the DIVE.H file in the Developer’s Toolkit for OS/2 Warp.) Use GpiQueryRealColors to get the original top and bottom 10 entries of the palette (so the desktop isn’t completely destroyed). Then execute the following:

HPS hps;
HDC hdc;
ULONG PaletteEntries[256];

GpiCreateLogColorTable( hps,
                        LCOL_PURECOLOR | LCOL_REALIZABLE,
                        LCOLF_CONSECRGB,
                        0,
                        256,
                        (PLONG)PaletteEntries );
Gre32EntrY3( hdc,
             0L,
             0x000060C6L );
WinInvalidateRect( HWND_DESKTOP,
                   (PRECTL)NULL,
                   TRUE );
WinRealesePS( hps );

Release the palette pixelmap by using BrPixelmapFree.

When terminating the application, restore the palette with the following:

hps = WinGetPS( HWND_DESKTOP );
hdc = GpiQueryDevice( hps );

Gre32EntrY3( hdc, 0L, 0x000060C7L );
WinInvalidateRect( HWND_DESKTOP, (PRECTL)NULL, TRUE );
WinReleasePS( hps );

Setting Up the BRender World Database

The next step in developing your game is to create the 3D world. This includes setting up actors (the models, camera, and lights). The world information can be stored in a global data structure as shown in Figure 2:

typedef struct _TANKWORLD
{
	struct br_actor *root;
	struct br_actor *camera;
	struct br_actor *camera_pivot;
	struct br_actor *light;
	struct br_actor *actor;
} TANKWORLD;
typedef TANKWORLD * PTANKWORLD;

Figure 2. World data structure

Allocate the world data structure by using BrMemAllocate to allocate the memory and BrActorAllocate to allocate the root of the actor tree (the root is specified by BR_ACTOR_NONE).

Next, any models, actors, and materials have to be loaded. The models are loaded by specifying BrModelLoadMany and BrModelAddMany, as follows:

br_model *models[50]; 	/* Pointer to array of model data */
int nmodels;            /* Number of models               */

nmodels = BrModelLoadMany( "tankmod.dat",
                           models,
                           BR_ASIZE(models) );
BrModelAddMany( models, nmodels );

The actors are added to the tree by calling BrActorAdd and BrActorLoad, as follows:

PTANKWORLD pWorld;   /* Pointer to Tank World data structure */
br_actor *a;         /* Pointer to tank actor hierarchy      */

pWorld->actor = BrActorAdd( pWorld->root,
                               BrActorAllocate( BR_ACTOR_NONE, NULL ) );
a = BrActorLoad( "tankact.dat" );
BrActorAdd( pWorld->actor, a );

Once all the actors are in their places, it’s time for “Lights, camera, and…action!” Setting up the camera and lights requires adding them to the world with BrActorAdd and positioning them. The current position of an actor in 3D space is held in a matrix. BRender uses 3×4 and 4×4 matrices represented as br_matrix34 and br_matrix4, respectively. You specify the positioning through a sequence of manipulations that are made upon the matrix. These manipulations include scalings, shears, translations, and rotations. The order in which the positioning is done is important (because matrix multiplication is non-commutative).

There are three basic types of matrix calls: these include the normal base call and a “Pre” and a “Post” version of the base call. These calls specify the order in which the manipulations take place. Imagine a “pipeline” of transforms. The “Pre” call adds this transform to the beginning of the pipeline, and the “Post” call adds it to the end (for example, BrMatrix34RotateX, BrMatrix34PreRotateX, and BrMatrix34PostRotateX). BrMatrix34RotateX would generate a matrix that represents a rotation about the X axis. BrMatrix34PreRotateX would pre-multiply a matrix by a matrix representing a rotation around the X axis. BrMatrix34PostRotateX would post-multiply a matrix by a matrix representing a rotation about the X axis.

Camera data is specified by the br_camera data type, and light data is specified by br_light. The camera view can be shown in perspective or in parallel. The camera also has control over the aspect ratio of the view. The light definition includes the type of light (omnidirectional, directed, spot), the color, the intensity, and the attenuation (intensity falloff). The light is enabled by specifying BrLightEnable.

Getting It to the Screen

While BRender handles all of the 3D-specific manipulations, BRender does not do the actual blitting of the bits to the screen. The method used to display the bits on the screen is up to you. You can use custom code, the OS/2 Graphical Programming Interface (GPI), or the Direct Interface Video Extensions (DIVE). DIVE provides fast blit access as well as direct access to video RAM (VRAM). In the OS/2 Entertainment Toos (available as of DevCon Volume 8), a full-screen support .DLL (GAMESRVR.DLL) is available that lets you access the screen in Mode 13 (320 x 200 x 256 colors).

DIVE is fast and simplifies buffer access. To allocate the buffer for the image to be displayed to the screen, use DiveAllocImageBuffer; use DiveBeginImageBufferAccess to allow the buffer to be used. The buffer is used by BrPixelMapAllocate as in the following call:

struct br_pixelmap *color_buffer;  /* Rendering buffer for color */
br_uint_16 width;                  /* Width of image (USHORT)    */
br_uint_16 height;                 /* Height of image (USHORT)   */
PBYTE pbBuffer;                    /* Buffer from DIVE           */

color_buffer = BrPixelMapAllocate( BR_PMT_INDEX_8,
                                   width,
                                   height,
                                   pbBuffer,
                                   BR_PMAF_INVERTED );

Once the color buffer is allocated, end the image buffer access with a call to DiveEndImageBufferAccess. (Remember to call DiveFreeImageBuffer when the buffer is no longer being used.)

Next, set up the depth buffer and fill it:

struct br_pixelmap *depth_buffer;  /* Rendering buffer for depth */

depth_buffer = BrPixelmapMatch( color_buffer,
                                BR_PMMATCH_DEPTH_16 );
BrPixelmapFill( depth_buffer,
                0xFFFF );         /* A happy little color       */

Rendering the buffer is fairly simple. If you want to customize actions performed during rendering, you can use BrZbSetRenderBoundsCallback to set a callback function. For example, you might want to log the dirty rectangles of the color buffer. To render the buffer, do the following:

BrZbSceneRender( pTankWorld->root,
                 pTankWorld->camera,
                 color_buffer,
                 depth_buffer );

Finally, if you’re using DIVE, a call to DiveBlitImage will display the image on the screen.

And…Action!

Up until this point, the code I’ve shown you has just gotten the 3D object displayed properly on the screen. Actually moving through the 3D world requires a bit more work.

There are several ways to manipulate the scene. Using the tank as an example, the tank could be stationary and the camera could move around it, or the camera could remain still and the tank could move, or both the tank and camera could move. Because both the camera and the tank are of type br_actor, the manipulation for each is similar.

One method you can use to move the camera is to track mouse input and use the location of the pointer to determine the angle of the camera. (Optimally, a joystick could be used. A set of joystick APIs is now available in the Entertainment Tools.) For example, clicking the left mouse button could rotate the camera, the right button could zoom, and pressing both buttons at the same time could do translation. This would require that you check which button was pressed and track the mouse movement. Once the values are stored, they can be used at render time to pivot the camera angle, as follows:

#define MOUSETRACK_LEFT  0x01;
#define MOUSETRACK_RIGHT 0x02;

PTANKWORLD pWorld; /* Pointer to Tank World data structure */

pWorld->camera_pivot->t.t.euler.e.a =
     TrackingValues[MOUSETRACK_LEFT].y * 130;

pWorld->camera_pivot->t.t.euler.e.b =
     TrackingValues[MOUSETRACK_LEFT].x * -130;

pWorld->camera->t.t.translate.t.v[2]=
     BR_SCALER(1700.0) +
     BrIntToScaler( TrackingValues[MOUSETRACK_RIGHT].y * 10 );

pWorld->camera->t.t.translate.t.v[0]=
     BrIntToScaler( TrackingValues[MOUSETRACK_LEFT | MOUSETRACK_RIGHT].x * 10 );

pWorld->camera->t.t.translate.t.v[1]=
     BrIntToScaler( TrackingValues[MOUSETRACK_LEFT | MOUSETRACK_RIGHT].y * 10 );

Cleaning Up Those Dirty Rectangles

At this point, the view of the tank can be manipulated, but the dirty rectangles still need to be cleaned up. Cleaning up requires that you introduce a callback function at render time to handle the dirty rectangles. Before the call to BrZbSceneRender, store the old callback value and set the function to be executed (in this case, TankBoundsCallback). The new function is of type br_renderbounds_cbfn.

br_renderbounds_cbfn *old_cb; /* Old callback */

old_cb = BrZbSetRenderBoundsCallback( TankBoundsCallback );

After the BrZbSceneRender call, set the original callback:

BrZbSetRenderBoundsCallback( old_cb );

TankBoundsCallback simply adds the current bounding rectangle to a list of dirty rectangles. The actual clearing of the dirty rectangles is done right before rendering the scene by calling BrPixelmapRectangleFill with both the color buffer and the depth buffer.

The End of the Beginning

I’ve shown you how to get 3D objects to your game screen using BRender coupled with DIVE and the graphics engine. These APIs are only a subset of BRender’s full capabilities, but they should give you a starting point for your 3D OS/2 Warp game.

Happy gaming!

 

Glossary:

3D – Three dimensional. A point in three-dimensional space is uniquely described by three coordinates: x, y, and z.

actor – Can be thought of as an indexing system. An actor actually only contains pointers to resources or other data. Actors are the building blocks of a BRender world. A complete BRender world description is a tree of actors which reference models; the models reference materials, and the materials reference texture maps.

Autodesk 3D Studio – A 3D rendering package for the PC. It allows the artist to create object models, materials, and textures, and assign light sources, specify camera angels, select palettes, and store this data in a .3DS output file that can be translated to a BRender .DAT file using BRender’s 3DS2BR tool.

BRender – 3D technology provided by Argonaut Technologoies Limited.

dirty rectangle – A rectangular area that has been written to in the color buffer.

DIVE – Direct Interface Video Extensions.

material – A data type that describes the surface characteristics of an object, such as the reflectivity, the color, the texture map (if any) that is applied to the surface, and even options for the rendering process.

models – The descriptors for the physical objects in a BRender environment. They consist of triangle meshes with a material or a default material affixed to each face.

texture map – A bitmap with an image that can be layered onto a polygon.

Z-buffer – This algorithm is an image space view (2D pixel view) of the hidden surface removal of a 3D scene. The Z-buffer is a screen buffer that is the same size as the video screen.