Advanced Lighting in 3D Graphics with XNA Game Studio 4.0

0
144
9 min read

 

3D Graphics with XNA Game Studio 4.0

3D Graphics with XNA Game Studio 4.0

A step-by-step guide to adding the 3D graphics effects used by professionals to your XNA games.

  • Improve the appearance of your games by implementing the same techniques used by professionals in the game industry
  • Learn the fundamentals of 3D graphics, including common 3D math and the graphics pipeline
  • Create an extensible system to draw 3D models and other effects, and learn the skills to create your own effects and animate them

 

        Read more about this book      

(For more resources on this subject, see here.)

Implementing a point light with HLSL

A point light is just a light that shines equally in all directions around itself (like a light bulb) and falls off over a given distance:

In this case, a point light is simply modeled as a directional light that will slowly fade to darkness over a given distance. To achieve a linear attenuation, we would simply divide the distance between the light and the object by the attenuation distance, invert the result (subtract from 1), and then multiply the lambertian lighting with the result. This would cause an object directly next to the light source to be fully lit, and an object at the maximum attenuation distance to be completely unlit.

However, in practice, we will raise the result of the division to a given power before inverting it to achieve a more exponential falloff:

Katt = 1 – (d / a) f

In the previous equation, Katt is the brightness scalar that we will multiply the lighting amount by, d is the distance between the vertex and light source, a is the distance at which the light should stop affecting objects, and f is the falloff exponent that determines the shape of the curve. We can implement this easily with HLSL and a new Material class. The new Material class is similar to the material for a directional light, but specifies a light position rather than a light direction. For the sake of simplicity, the effect we will use will not calculate specular highlights, so the material does not include a “specularity” value. It also includes new values, LightAttenuation and LightFalloff, which specify the distance at which the light is no longer visible and what power to raise the division to.

public class PointLightMaterial : Material
{
public Vector3 AmbientLightColor { get; set; }
public Vector3 LightPosition { get; set; }
public Vector3 LightColor { get; set; }
public float LightAttenuation { get; set; }
public float LightFalloff { get; set; }

public PointLightMaterial()
{
AmbientLightColor = new Vector3(.15f, .15f, .15f);
LightPosition = new Vector3(0, 0, 0);
LightColor = new Vector3(.85f, .85f, .85f);
LightAttenuation = 5000;
LightFalloff = 2;
}

public override void SetEffectParameters(Effect effect)
{
if (effect.Parameters["AmbientLightColor"] != null)
effect.Parameters["AmbientLightColor"].SetValue(
AmbientLightColor);

if (effect.Parameters["LightPosition"] != null)
effect.Parameters["LightPosition"].SetValue(LightPosition);

if (effect.Parameters["LightColor"] != null)
effect.Parameters["LightColor"].SetValue(LightColor);

if (effect.Parameters["LightAttenuation"] != null)
effect.Parameters["LightAttenuation"].SetValue(
LightAttenuation);

if (effect.Parameters["LightFalloff"] != null)
effect.Parameters["LightFalloff"].SetValue(LightFalloff);
}
}

The new effect has parameters to reflect those values:

float4x4 World;
float4x4 View;
float4x4 Projection;

float3 AmbientLightColor = float3(.15, .15, .15);
float3 DiffuseColor = float3(.85, .85, .85);
float3 LightPosition = float3(0, 0, 0);
float3 LightColor = float3(1, 1, 1);
float LightAttenuation = 5000;
float LightFalloff = 2;

texture BasicTexture;

sampler BasicTextureSampler = sampler_state {
texture = <BasicTexture>;
};

bool TextureEnabled = true;

The vertex shader output struct now includes a copy of the vertex’s world position that will be used to calculate the light falloff (attenuation) and light direction.

struct VertexShaderInput
{
float4 Position : POSITION0;
float2 UV : TEXCOORD0;
float3 Normal : NORMAL0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
float2 UV : TEXCOORD0;
float3 Normal : TEXCOORD1;
float4 WorldPosition : TEXCOORD2;
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;

float4 worldPosition = mul(input.Position, World);
float4 viewPosition = mul(worldPosition, View);
output.Position = mul(viewPosition, Projection);

output.WorldPosition = worldPosition;
output.UV = input.UV;
output.Normal = mul(input.Normal, World);

return output;
}

Finally, the pixel shader calculates the light much the same way that the directional light did, but uses a per-vertex light direction rather than a global light direction. It also determines how far along the attenuation value the vertex’s position is and darkens it accordingly. The texture, ambient light, and diffuse color are calculated as usual:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float3 diffuseColor = DiffuseColor;

if (TextureEnabled)
diffuseColor *= tex2D(BasicTextureSampler, input.UV).rgb;
float3 totalLight = float3(0, 0, 0);

totalLight += AmbientLightColor;

float3 lightDir = normalize(LightPosition - input.WorldPosition);
float diffuse = saturate(dot(normalize(input.Normal), lightDir));
float d = distance(LightPosition, input.WorldPosition);
float att = 1 - pow(clamp(d / LightAttenuation, 0, 1),
LightFalloff);

totalLight += diffuse * att * LightColor;

return float4(diffuseColor * totalLight, 1);
}

We can now achieve the above image using the following scene setup from the Game1 class:

models.Add(new CModel(Content.Load<Model>("teapot"),
new Vector3(0, 60, 0), Vector3.Zero, new Vector3(60),
GraphicsDevice));

models.Add(new CModel(Content.Load<Model>("ground"),
Vector3.Zero, Vector3.Zero, Vector3.One, GraphicsDevice));

Effect simpleEffect = Content.Load<Effect>("PointLightEffect");

models[0].SetModelEffect(simpleEffect, true);
models[1].SetModelEffect(simpleEffect, true);

PointLightMaterial mat = new PointLightMaterial();
mat.LightPosition = new Vector3(0, 1500, 1500);
mat.LightAttenuation = 3000;

models[0].Material = mat;
models[1].Material = mat;

camera = new FreeCamera(new Vector3(0, 300, 1600),
MathHelper.ToRadians(0), // Turned around 153 degrees
MathHelper.ToRadians(5), // Pitched up 13 degrees
GraphicsDevice);

Implementing a spot light with HLSL

A spot light is similar in theory to a point light—in that it fades out after a given distance. However, the fading is not done around the light source, but is based on the angle between the direction of an object and the light source, and the light’s actual direction. If the angle is larger than the light’s “cone angle”, we will not light the vertex.

Katt = (dot(p – lp, ld) / cos(a)) f

In the previous equation, Katt is still the scalar that we will multiply our diffuse lighting with, p is the position of the vertex, lp is the position of the light, ld is the direction of the light, a is the cone angle, and f is the falloff exponent. Our new spot light material reflects these values:

public class SpotLightMaterial : Material
{
public Vector3 AmbientLightColor { get; set; }
public Vector3 LightPosition { get; set; }
public Vector3 LightColor { get; set; }
public Vector3 LightDirection { get; set; }
public float ConeAngle { get; set; }
public float LightFalloff { get; set; }

public SpotLightMaterial()
{
AmbientLightColor = new Vector3(.15f, .15f, .15f);
LightPosition = new Vector3(0, 3000, 0);
LightColor = new Vector3(.85f, .85f, .85f);
ConeAngle = 30;
LightDirection = new Vector3(0, -1, 0);
LightFalloff = 20;
}

public override void SetEffectParameters(Effect effect)
{
if (effect.Parameters["AmbientLightColor"] != null)
effect.Parameters["AmbientLightColor"].SetValue(
AmbientLightColor);

if (effect.Parameters["LightPosition"] != null)
effect.Parameters["LightPosition"].SetValue(LightPosition);

if (effect.Parameters["LightColor"] != null)
effect.Parameters["LightColor"].SetValue(LightColor);

if (effect.Parameters["LightDirection"] != null)
effect.Parameters["LightDirection"].SetValue(LightDirection);

if (effect.Parameters["ConeAngle"] != null)
effect.Parameters["ConeAngle"].SetValue(
MathHelper.ToRadians(ConeAngle / 2));

if (effect.Parameters["LightFalloff"] != null)
effect.Parameters["LightFalloff"].SetValue(LightFalloff);
}
}

Now we can create a new effect that will render a spot light. We will start by copying the point light’s effect and making the following changes to the second block of effect parameters:

float3 AmbientLightColor = float3(.15, .15, .15);
float3 DiffuseColor = float3(.85, .85, .85);
float3 LightPosition = float3(0, 5000, 0);
float3 LightDirection = float3(0, -1, 0);
float ConeAngle = 90;
float3 LightColor = float3(1, 1, 1);
float LightFalloff = 20;

Finally, we can update the pixel shader to perform the lighting calculations:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
float3 diffuseColor = DiffuseColor;

if (TextureEnabled)
diffuseColor *= tex2D(BasicTextureSampler, input.UV).rgb;

float3 totalLight = float3(0, 0, 0);
totalLight += AmbientLightColor;
float3 lightDir = normalize(LightPosition - input.WorldPosition);
float diffuse = saturate(dot(normalize(input.Normal), lightDir));

// (dot(p - lp, ld) / cos(a))^f
float d = dot(-lightDir, normalize(LightDirection));
float a = cos(ConeAngle);

float att = 0;

if (a < d)
att = 1 - pow(clamp(a / d, 0, 1), LightFalloff);

totalLight += diffuse * att * LightColor;

return float4(diffuseColor * totalLight, 1);
}

If we were to then set up the material as follows and use our new effect, we would see the following result:

SpotLightMaterial mat = new SpotLightMaterial();
mat.LightDirection = new Vector3(0, -1, -1);
mat.LightPosition = new Vector3(0, 3000, 2700);
mat.LightFalloff = 200;

Drawing multiple lights

Now that we can draw one light, the natural question to ask is how to draw more than one light. Well this, unfortunately, is not simple. There are a number of approaches—the easiest of which is to simply loop through a certain number of lights in the pixel shader and sum a total lighting value. Let’s create a new shader based on the directional light effect that we created in the last chapter to do just that. We’ll start by copying that effect, then modifying some of the effect parameters as follows. Notice that instead of a single light direction and color, we instead have an array of three of each, allowing us to draw up to three lights:

#define NUMLIGHTS 3

float3 DiffuseColor = float3(1, 1, 1);
float3 AmbientColor = float3(0.1, 0.1, 0.1);
float3 LightDirection[NUMLIGHTS];
float3 LightColor[NUMLIGHTS];
float SpecularPower = 32;
float3 SpecularColor = float3(1, 1, 1);

Second, we need to update the pixel shader to do the lighting calculations one time per light:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
// Start with diffuse color
float3 color = DiffuseColor;

// Texture if necessary
if (TextureEnabled)
color *= tex2D(BasicTextureSampler, input.UV);

// Start with ambient lighting
float3 lighting = AmbientColor;

float3 normal = normalize(input.Normal);
float3 view = normalize(input.ViewDirection);

// Perform lighting calculations per light
for (int i = 0; i < NUMLIGHTS; i++)
{
float3 lightDir = normalize(LightDirection[i]);

// Add lambertian lighting
lighting += saturate(dot(lightDir, normal)) * LightColor[i];

float3 refl = reflect(lightDir, normal);

// Add specular highlights
lighting += pow(saturate(dot(refl, view)), SpecularPower)
* SpecularColor;
}

// Calculate final color
float3 output = saturate(lighting) * color;

return float4(output, 1);
}

We now need a new Material class to work with this shader:

public class MultiLightingMaterial : Material
{
public Vector3 AmbientColor { get; set; }
public Vector3[] LightDirection { get; set; }
public Vector3[] LightColor { get; set; }
public Vector3 SpecularColor { get; set; }

public MultiLightingMaterial()
{
AmbientColor = new Vector3(.1f, .1f, .1f);
LightDirection = new Vector3[3];
LightColor = new Vector3[] { Vector3.One, Vector3.One,
Vector3.One };
SpecularColor = new Vector3(1, 1, 1);
}

public override void SetEffectParameters(Effect effect)
{
if (effect.Parameters["AmbientColor"] != null)
effect.Parameters["AmbientColor"].SetValue(AmbientColor);

if (effect.Parameters["LightDirection"] != null)
effect.Parameters["LightDirection"].SetValue(LightDirection);

if (effect.Parameters["LightColor"] != null)
effect.Parameters["LightColor"].SetValue(LightColor);

if (effect.Parameters["SpecularColor"] != null)
effect.Parameters["SpecularColor"].SetValue(SpecularColor);
}
}

If we wanted to implement the three directional light systems found in the BasicEffect class, we would now just need to copy the light direction values over to our shader:

Effect simpleEffect = Content.Load<Effect>("MultiLightingEffect");

models[0].SetModelEffect(simpleEffect, true);
models[1].SetModelEffect(simpleEffect, true);

MultiLightingMaterial mat = new MultiLightingMaterial();

BasicEffect effect = new BasicEffect(GraphicsDevice);
effect.EnableDefaultLighting();

mat.LightDirection[0] = -effect.DirectionalLight0.Direction;
mat.LightDirection[1] = -effect.DirectionalLight1.Direction;
mat.LightDirection[2] = -effect.DirectionalLight2.Direction;

mat.LightColor = new Vector3[] {
new Vector3(0.5f, 0.5f, 0.5f),
new Vector3(0.5f, 0.5f, 0.5f),
new Vector3(0.5f, 0.5f, 0.5f) };

models[0].Material = mat;
models[1].Material = mat;

LEAVE A REPLY

Please enter your comment!
Please enter your name here