Working with materials

One of A6's most powerful features is its ability to use materials. The difference between A5 and A6 might not appear too obvious when it comes to materials, but it certainly is big. Take a look at the screenshot below:

Can you believe that all the models above are clones of the "normal" crystal, but are using different materials? This means that I have used the same model over and over, without touching its skin and I have changed its appearance by setting the values of a few variables. Sky is the limit, really! Now here's the other good news: the project allows you to create any type of material you can think of.

Let's see what this demo can do for us; open materials.wmp, build it and run it using materials.wdl. You will see the picture with the four crystal models; I think that the author of the model is TMW, but he didn't include a readme file with the model, so I can't be too sure about that. Well, the first "normal" model hasn't got any material assigned to it, the second model uses Conitec's "smaragd" material, and I have created the other two materials ("lava" and "marble").

Take a good look at those models, and then press the "R" (rotate) key once to move on:

You can see the same "normal" model (without using a material), a "shiny" model and a "dark" model. Once again, I have used the same model with the same skin and all these dramatic changes are nothing more than different materials used for the model. Ready for more fun? Press the "R" key again.

This time you can see the "normal" model and two predefined materials ("metal" and "unlit"). Press "R" one more time to see our "material" creation lab:

This is where you will want to place your model in order to create the perfect material for it, by adjusting all the parameters on the panel. Click every arrow to see the changes; some of them will produce noticeable effects only under certain circumstances, but we'll be getting there soon.

Some of you might have noticed the black model surrounded by the yellow circle in the upper left corner of the screen. I drew the yellow circle to make the model more visible in my picture; the model is used for a dynamic light that can be dragged around the screen.

Most of the arrows that appeared to be useless show their power in the presence of the light coming from the sun or from a regular dynamic light. You can see that the position of the mouse pointer and that of the light don't match; I have decided to use this method because I didn't want you (or me) to lose the light entity by moving it behind the panel or outside the boundaries of the screen.

What is it with these materials, after all? They are nothing more than a collection of parameters that control the light reflection properties of a model, sprite, terrain or surface. The light that affects the entity can be received from the level or can be generated by the entity itself.

The introduction is over now, so get ready for some easy to understand code. Let me show you the panel definition first:

panel material_pan
{
bmap = menu_pcx;
pos_x = 0;
pos_y = 0;
layer = 10;

button = 90, 12, up_pcx, up_pcx, up_pcx, change_material, null, null;
button = 105, 12, down_pcx, down_pcx, down_pcx, change_material, null, null;
button = 90, 37, up_pcx, up_pcx, up_pcx, change_material, null, null;
button = 105, 37, down_pcx, down_pcx, down_pcx, change_material, null, null;
button = 90, 62, up_pcx, up_pcx, up_pcx, change_material, null, null;
button = 105, 62, down_pcx, down_pcx, down_pcx, change_material, null, null;

...................................

digits = 125, 12, 3, arial_font, 1, e_red; // emissive_red
digits = 125, 37, 3, arial_font, 1, e_blue; // emissive_blue
digits = 125, 63, 3, arial_font, 1, e_green; // emissive_green

....................................

flags = overlay, refresh, visible;
}

The panel uses a button for each black arrow, so we've got 30 buttons and 15 digits that show the values for every material parameter. Please note that when we click a button (any button) we run a single function: change_material().

Function main is simple:

function main()
{
fps_max = 30;
sun_light = 1;
level_load (materials_wmb);
mouse_map = pointer_pcx;
mouse_mode = 2;
while (1)
{
mouse_pos.x = pointer.x;
mouse_pos.y = pointer.y;
wait (1);
}
}
The function above limits the frame rate to 30 fps, enables the sun light and then loads the materials_wmb level. We set pointer_pcx as the mouse pointer, we show the pointer and we allow it to move by setting its mouse_pos.x and mouse_pos.y coordinates in a "while" loop.

You can be sure that materials.wdl contains definitions for many materials, but we will discuss only a few of them. The reason is simple: they use similar definitions and different numerical values. Let's start with the material used for the lava:

material mat_lava
{
emissive_red = 200;
emissive_green = 70;
emissive_blue = 20;
ambient_red = 255;
ambient_green = 150;
ambient_blue = 50;
diffuse_red = 200;
diffuse_green = 150;
diffuse_blue = 50;
specular_red = 255;
specular_green = 255;
specular_blue = 255;
alpha = 60;
albedo = 70;
power = 10;
}

The first 3 (emissive) parameters set the power and the color of the light that is generated by the entity itself. You can be sure that the "shiny" material in one of the pictures above was created by setting the emissive parameters to their maximal values (255), while the "dark" material was obtained by setting the emissive parameters to the smallest value (0).

The next set of parameters (ambient) set the color and the strength of the light that will be received by the entity from the static lights inside the level, its own "ambient" or the "ambient" of the camera. Let's pretend that your game takes place on Mars and most of the objects, including those precious crystals, ought to look red. So your sun is red, the ground is painted in red, but those crystals aren't quite red. You can use those 3 "ambient" parameters to create a material that absorbs more red from the static light sources in the level; take a look at what I've got by playing with the "ambient" values:

You can notice that this material isn't using its emissive parameters, because it isn't generating light; it simply receives more red from the white, static light sources. Of course that you can (and you should) combine proper "emissive" and good "ambient" values.

The next set of parameters (diffuse) sets the influence of the dynamic lights (including the sun light). Let's make a little experiment: run materials.wdl, set diffuse_green to 255 and take a good look at the "user defined" crystal:

I couldn't see any change, no matter how hard I tried! Now let's click the black model (the dynamic light) and let's drag it close to the crystal model:

You can see that my white dynamic light casts green light on the crystal model; the material ignores the red and blue components of the white light because diffuse_red and diffuse_blue were set to zero. The last set of parameters (specular) is similar to "diffuse", but this time the effect depends on the camera angles.

This time I have set specular_blue to 255. The result is obvious: only a part of the crystal model is affected by the dynamic light; the big specular_blue value colors the crystal in blue, but you can see the effect only when the camera has a certain pan angle. I encourage you to press the "zero" key while the engine is running; this way you will be able to move and rotate inside the level. One more thing: you can click the "user defined" crystal to start / stop its rotation around the pan angle.

The last 3 parameters ("power", "alpha" and "albedo") are pretty straightforward: they set the power / sharpness of the effect (0...10), the opacity of the material (0...100) and the influence of the sun light (0...100).

You will notice that some of the material definitions inside material.wdl don't include all the parameters, because sometimes we won't need them all. Ok, so now we know how to define a material. How do we use it?

action test_lava
{
my.material = mat_lava;
..................................
}

It's really that simple, isn't it? We create the action that will be attached to our entity, and then we use a single line of code to assign our previously defined material to the entity. The engine knows how to work with several predefined materials like mat_flat, mat_shaded, mat_metal and so on; please note that you can change the values for them too. Here's the action attached to the "metal" crystal in the demo:

action test_metal
{
my.metal = on;
mat_metal.emissive_red = 50;
mat_metal.emissive_green = 50;
mat_metal.emissive_blue = 80;
}

The first line of code sets metal = on for my crystal; this way mat_metal is set for my model automatically. Now I can control all the parameters of the predefined mat_metal material; I chose to make it a little more metallic by adding a bit more light (especially blue light) to it. Let's take a look at the action attached to the dynamic light now:

action dynamic_light
{
my.lightrange = 80;
my.red = 100;
my.green = 100;
my.blue = 100;
my.enable_click = on;
my.event = move_me;
}

The light has a range of 80 quants and it generates white light because its red, green and blue components have the same value (100). The entity is sensitive to clicking; click it once and its move_me event function will run:

function move_me()
{
while (mouse_left == on)
{
temp.x = pointer.x;
temp.y = pointer.y;
temp.z = 100;
vec_for_screen (temp, camera);
my.x = temp.x;
my.z = temp.z;
wait (1);
}
}
This function moves the entity that generates light for as long as the left mouse button is kept pressed. Take a look at the picture below to see the area that can be covered by the light emitter for temp.z = 100; change that value if you want to cover a different area. The coordinates stored in temp are converted to 3D world coordinates using vec_for_screen, and the entity is moved to its new position.

Now here's the code for the action attached to the "user defined" model:

action test_user
{
my.material = mat_user;
my.enable_click = on;
my.event = rotate_me;
}

function rotate_me()
{
rotation += 1;
while ((rotation % 2) == 1)
{
my.pan += 5 * time;
wait (1);
}
}

The model uses the mat_user material, is sensitive to clicks and runs its event function rotate_me when you click it using the left mouse button. The model will rotate around its pan angle for as long as rotation has an odd value: 1, 3, 5, 7, etc.

Do you remember that huge panel definition we've talked about at the beginning of the article? Every button (arrow) on the panel runs the same function when we click it:

function change_material(button_number)
{
if (button_number == 1)
{
while (mouse_left == 1)
{
e_red += 1;
e_red = min(max(e_red, 0), 255);
wait (1);
}
wait (1);
}
if (button_number == 2)
{
while (mouse_left == 1)
{
e_red -= 1;
e_red = min(max(e_red, 0), 255);
wait (1);
}
wait (1);
}

......................
}

This function is pretty big because it is used by all those 30 buttons (arrows) on the panel. I have decided to show you the code for the first two buttons here because the rest of the code is similar.

If you are an avid AUM reader you have learned that function change_material returns "1" when we press the first button on the panel, "2" when we press the second button on the panel and so on. The first two buttons increase and decrease e_red, a variable that controls the emissive_red parameter.

If we press the first button (arrow) on the panel, we keep incrementing e_red for as long as the left mouse button is pressed. The line of code below:

e_red = min(max(e_red, 0), 255);

makes sure that the value for e_red is kept in this range: 0...255. If we press the second button (arrow) on the panel, e_red is decremented for as long as the left mouse button remains pressed.

Take a look at our "user defined" material definition:

material mat_user
{
emissive_red = 0;
emissive_green = 0;
emissive_blue = 0;
ambient_red = 0;
ambient_green = 0;
ambient_blue = 0;
diffuse_red = 0;
diffuse_green = 0;
diffuse_blue = 0;
specular_red = 0;
specular_green = 0;
specular_blue = 0;
power = 0;
}

All these mat_user parameters are controlled by a starter function:

starter set_material()
{
while (1)
{
mat_user.emissive_red = e_red;
mat_user.emissive_green = e_green;
mat_user.emissive_blue = e_blue;
mat_user.ambient_red = a_red;
mat_user.ambient_green = a_green;
mat_user.ambient_blue = a_blue;
mat_user.diffuse_red = d_red;
mat_user.diffuse_green = d_green;
mat_user.diffuse_blue = d_blue;
mat_user.specular_red = s_red;
mat_user.specular_green = s_green;
mat_user.specular_blue = s_blue;
mat_user.power = m_power;
mat_user.alpha = m_alpha;
mat_user.albedo = m_albedo;
wait (1);
}
}

The while loop above updates all the material parameters using the values of the variables that are set on the panel. The last function is the one that rotates the camera with 90 degrees every time we press the "R" key:

function rotate_camera()
{
camera.pan -= 90;
}

on_r = rotate_camera;

That's all, folks! Now go and replace the "user defined" model with your own models, push some buttons and amaze the world with your materials!