10 Materials Basics

I know I said last time in our discussion about textures, that I’d go on to talk about meshes. But I lied.

Actually, I made a mistake. What happened was, I started writing about meshes and realized that using meshes and textures together at the same time is a bit much for an introduction. So I decided that we should talk about materials, which will give us a nice gentle way to get started with meshes. After we talk about zbuffers, too, of course.

We’ve now talked about two ways to get objects in our scenes to have some color: we can either use a vertex format that has color information, or we can use a texture to warp a bitmap onto a triangle or set of triangles. But it turns out there is at least one more way: we can set up a material.

A material is a bunch of global settings that tells the Device, “Hey, the next time you draw a triangle, I’d like it to look like this.” The settings are captured in a structure of type Material, and are associated with the Device by assigning to its property of the same name. It goes something like this:

protected void SetupMaterials() { Material mat = new Material(); // Set the properties of the material // (Code omitted for the moment) device.Material = mat; }

What could be easier?

The Material struct only has a few properties, let’s go through them one at a time.

Material.Diffuse: sets the diffuse color of the material. You’ll recall the definition of diffuse from our talk about lighting. To recap, it’s what you’d think of as the “normal” color of an object.

Material.Ambient: sets the ambient color of the material. This is the color that the object is when interacting with the ambient – or global – lighting. We also covered this during the lighting introduction. The ambient color will usually be the same as the diffuse color.

Material.Emissive: sets the emissive color of the material. The emissive color is the component of the color of an object that’s the same no matter how much light is shining on it. Unlike diffuse and ambient colors – which contribute to the color of the object only in proportion to the amount of diffuse or ambient light hitting the object – the emissive color will be present even if the object is in complete darkness. It tends to make the object look like it’s glowing, although it doesn’t actually cast any light. For this reason, you won’t see emissive color used very often. It is, however, occasionally useful for special effects, such as rendering a sun or a light bulb, although we would generally always put a light source inside the object as well. We’ll set it to black in our examples.

Material.Specular: sets the specular color of the material. Whether you know it or not, you’re familiar with specularity. Specularity is an effect caused by microscopic imperfections in the surface of an object. It generally manifests itself as a shiny spot on the surface of an object. Think about a picture of an apple – it’s red, but there’s a little white bit where the light hits it. That’s called a specular highlight. If you set the specular color to white, you’ll wind up with a white specular highlight (assuming white lighting), which will tend to make an object look like it’s made out of plastic. Setting the specular color to a color more similar to the diffuse color will tend to make an object look like it’s made out of metal. Here are a couple of pictures that demonstrate this difference:

The first image looks more plastic, and the second more metallic, although the effect is subtle, especially without reflection (a topic for another time).

Material.SpecularSharpness: controls how tight the specular highlight is. A lower number will cause the specular highlight to be more spread out, making the object look duller. A higher number will make the specular highlight tighter, making the object look shiny. Here’s an example of an object with two different specular sharpnesses:

The first image has a specular sharpness of 50. The second has a specular sharpness of 10. You can see how much more spread out the specular highlight is. We might also want to make the specular color a bit darker in this case to enhance the dullness of the object.

The specular highlight is generated based on the same surface normals that the lighting model works from, and has the same limitations – a smoother object will look more realistic because the specular highlight will be smoother, but will require more triangles and will therefore be slower to render.

Given that we understand now what the various fields of the Material struct do, we can finish filling out our SetupMaterials function. Maybe something like this:

protected void SetupMaterials() { Material mat = new Material(); // Set the properties of the material // The object itself will be blue mat.Diffuse = Color.Blue; // We want it to look slightly dull, so maybe a grey // wide highlight mat.Specular = Color.LightGray; mat.SpecularSharpness = 15.0F; device.Material = mat; // Very important - without this there is no specularity device.RenderState.SpecularEnable = true; }

Note the all-important call to set the SpecularEnable RenderState to true.

Once we call this method, every triangle rendered from that point on will be blue and somewhat shiny. If we want to render some blue and some yellow triangles, we’d have to first set Device.Material to the blue material, render the blue triangles, then set Device.Material to the yellow material and render the yellow triangles: the Material is a global setting!

The complete code appears below. Most of it is what we’re used to by now. I’m using a cylinder instead of a simple triangle because specular highlights are most visible with curved objects. I’ve also added the call to SetupMaterials in Render. Technically, I could have done it in InitializeGraphics, since the material never changes, but you get the idea.

Next time, we’ll deal with one other issue I’d like to get out of the way before hitting meshes: z-buffers.

The Code

Update:

If you're using the October 2004 SDK, you'll need to replace the SetPosition and SetNormal methodcalls below with assignments to the Position and Normal properties.

Also, you'll need to change the call to Commit in SetupLights with a call to Update. This is due to the changes the DirectX team made to the SDK in the October 2004 release.

using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; namespace Craig.DirectX.Direct3D { public class Game : System.Windows.Forms.Form { private Device device; private VertexBuffer vertices; static void Main() { Game app = new Game(); app.Width = 800; app.Height = 600; app.InitializeGraphics(); app.Show(); while (app.Created) { app.Render(); Application.DoEvents(); } app.DisposeGraphics(); } protected bool InitializeGraphics() { PresentParameters pres = new PresentParameters(); pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; pres.EnableAutoDepthStencil = true; pres.AutoDepthStencilFormat = DepthFormat.D16; device = new Device( 0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres ); device.RenderState.CullMode = Cull.None; vertices = CreateVertexBuffer(device); return true; } protected void PopulateVertexBuffer(VertexBuffer vertices) { CustomVertex.PositionNormal[] verts = (CustomVertex.PositionNormal[]) vertices.Lock(0, 0); // Create a cylinder out of a bunch of triangles for (int i = 0; i < 50; i++) { float theta = (float)(2 * Math.PI * i) / 49; verts[2 * i].Position = ( new Vector3( (float)Math.Sin(theta), -1, (float)Math.Cos(theta) ) ); verts[2 * i].Normal = ( new Vector3( (float)Math.Sin(theta), 0, (float)Math.Cos(theta) ) ); verts[2 * i + 1].Position = ( new Vector3( (float)Math.Sin(theta), 1, (float)Math.Cos(theta) ) ); verts[2 * i + 1].Normal = ( new Vector3( (float)Math.Sin(theta), 0, (float)Math.Cos(theta) ) ); } vertices.Unlock(); } protected VertexBuffer CreateVertexBuffer(Device device) { VertexBuffer buf = new VertexBuffer(typeof(CustomVertex.PositionNormal), 100, device, 0, CustomVertex.PositionNormal.Format, Pool.Default); PopulateVertexBuffer(buf); return buf; } protected void SetupMatrices() { float yaw = Environment.TickCount / 1000.0F; float pitch = Environment.TickCount / 612.0F; device.Transform.World = Matrix.RotationYawPitchRoll(yaw, pitch, 0); device.Transform.View = Matrix.LookAtLH(new Vector3(0, 0, -6), new Vector3(0, 0, 0), new Vector3(0, 1, 0)); device.Transform.Projection = Matrix.PerspectiveFovLH( (float)Math.PI/4.0F, 1.0F, 1.0F, 10.0F); } protected void SetupLights() { device.RenderState.Lighting = true; device.Lights[0].Diffuse = Color.White; device.Lights[0].Specular = Color.White; device.Lights[0].Type = LightType.Directional; device.Lights[0].Direction = new Vector3(-1, -1, 3); device.Lights[0].Update(); device.Lights[0].Enabled = true; device.RenderState.Ambient = Color.FromArgb(0x40, 0x40, 0x40); } protected void SetupMaterials() { Material mat = new Material(); // Set the properties of the material // The object itself will be blue mat.Diffuse = Color.Blue; // We want it to look slightly dull, so maybe a grey // wide highlight mat.Specular = Color.LightGray; mat.SpecularSharpness = 15.0F; device.Material = mat; // Very important - without this there is no specularity device.RenderState.SpecularEnable = true; } protected void Render() { device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.Bisque, 1.0F, 0); device.BeginScene(); SetupMatrices(); SetupLights(); SetupMaterials(); device.VertexFormat = CustomVertex.PositionNormal.Format; device.SetStreamSource(0, vertices, 0); device.DrawPrimitives(PrimitiveType.TriangleStrip, 0, 98); device.EndScene(); device.Present(); } protected void DisposeGraphics() { vertices.Dispose(); device.Dispose(); } } }