Custom ambient lighting color in a Cg shader

I need to have a custom ambient color in a surface shader (written in Cg). Please note, this is not the same as adding or multiplying a color by the texture*.

* If you need proof of this, just try making a scene’s ambient light equal to black. If it was simple multiplication, everything would be black. Now try setting it to white; if it was addition, everything would be ≥ [1,1,1] and probably clamped to white.

As far as I know, it needs to be a surface shader because I just want to modify a few existing surface shaders to include custom ambient light support.

The reason I need this is because there’s a certain area in our game that needs to be unaffected by (or at least, affected less than normal by) ambient light, because it’s a very occluded interior.

I think there is a needed ‘ambientIntensity’ which multiplies the color before adding it.
It seems there was a good article on this, but it’s ‘down’ but still in Google Cache:
http://webcache.googleusercontent.com/search?q=cache:eT31l1adFwYJ:www.sgtconker.com/2010/09/article-shaders-ambient-lighting/+&cd=4&hl=en&ct=clnk≷=us

I’ll paste the text of it here just in case it goes away. Sgt. Conker, I hope you don’t mind!

This is Google’s cache of sgtconker.com. It is a snapshot of the page as it appeared on Jul 12, 2012 11:02:55 GMT. The current page could have changed in the meantime. Learn more

Text-only version

Sgt. Conker We are “Absolutely Fine”

Subscribe via RSS
Home
Articles
Open Source Projects
Affiliated Projects
Tiny Engine
BRAINS
IceCream
Archive

20Sep/100
Article: Shaders – Ambient Lighting

by Daniel Greenheck

Since these are shader tutorials and not XNA tutorials, I’m guessing you already have a basic understanding of these topic

  • How to draw a simple model or some vertices in XNA.
  • How to add files to the Content Pipeline.
  • The content in my Shader Introduction.

A few words to start…

Before someone makes the wise-crack that ambient lighting is already taken care of in the BasicEffect class in XNA, I’m going to tell you: ambient lighting is already taken care of in the BasicEffect class in XNA. The reason I’m giving this tutorial is because these basic shaders lay the foundation for more advanced and complex shaders. If you can understand how simple shaders work, it will help you out profoundly when dealing with complex topics like bump mapping and projective texturing later on. I highly recommend avoiding the BasicEffect class if you are completely new to shaders. Instead focusing on building your foundation of shader knowledge; it’s completely necessary if you ever plan on making anything beyond a game that looks like Mario 64.
If you don’t plan on using advanced shaders or you already know the basics of HLSL, here’s a tutorial on how to use the BasicEffect class (which is also on my links page).

Now, for those of you who are still with me! Ambient lighting is about as simple as it gets, so this will be a good place to start if you are completely unfamiliar with HLSL. A scene with only ambient lighting by itself looks like a polar bear in a snow storm, so if you want a little more detailed effect, I would skip to the diffuse lighting tutorial. However, in later tutorials, I’ll be focusing mainly on the actual code inside the vertex and pixel shaders; I’m going to assume you’ve already read this tutorial and have a basic understanding of shaders. So consider yourself warned!

What is Ambient Lighting?

Well I see I’ve blabbed on without even mentioning what we are programming! Ambient lighting can best be described by going outside on a cloudy day. If you look around, there is really no direct light source: you don’t have the sun shining overhead, creating shadows with every object it hits. In fact, if you look around, there probably aren’t many shadows to be found (unless there is some artificial light, then that’s just cheating). Ambient light is the result of the suns rays bouncing of so many different surfaces that they light parts of the world that aren’t even directly lit. Go outside on a sunny day sometime and see for yourself. Notice how areas that aren’t being lit by direct light aren’t completely dark. I also like to think of ambient lighting as global lighting: it manages to light up every part of the world, the same amount, no matter where it is. An object lit by only ambient light will look something like this:

Blah! It’s boring and there’s no definition to any corners or edges. Although almost useless by itself, ambient lighting is a necessary part of any scene to give it some static light. Now, on to the fun stuff!

The Ambient Lighting Shader

I’m just going to show you the entire Ambient Lighting shader right away so you can glance over it. As we go along, I’ll explain each part of it, piece-by-piece. So let’s get started!

float4x4 World;             // World matrix
float4x4 View;              // View matrix
float4x4 Projection;        // Projection matrix

float4 AmbientColor = float4( 1, 1, 1, 1 );     // Ambient lighting color
float AmbientIntensity = 0.8;                   // How bright the ambient light is ( 0.0 - 1.0, 0 = No light, 1 = Full Light )

struct VertexShaderInput    // Input data for vertex shader
{
    float4 Position : POSITION0;    // Pass in just the position of the vertex to the vertex shader
};

struct VertexShaderOutput   // Output data for vertex shader/input data for pixel shader
{
    float4 Position : POSITION0;    // We'll be passing our transformed position to the pixel shader
};

// Vertex Shader

VertexShaderOutput VertexShaderFunction( VertexShaderInput input )
{
    VertexShaderOutput output;

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

    return output;
}

// Pixel Shader

float4 PixelShaderFunction( VertexShaderOutput input ) : COLOR0
{
    return ( AmbientColor * AmbientIntensity );
}

technique Ambient
{
    pass Pass0
    {
        VertexShader = compile vs_1_1 VertexShaderFunction();
        PixelShader = compile ps_1_1 PixelShaderFunction();
    }
}

As you can tell right away, there isn’t a whole lot going on here. Here’s what we can see in this shader:

The global variabes are defined at the top.
The input and output structures for our vertex shader are defined.
The vertex shader function is right below that.
Followed by the pixel shader.
And our Ambient technique with a single pass finishes up the shader.
Variable Declaration

float4x4 World;                                // World matrix
float4x4 View;                                 // View matrix
float4x4 Projection;                           // Projection matrix

float4 AmbientColor = float4( 1, 1, 1, 1 );    // Ambient lighting color
float AmbientIntensity = 0.8;                  // How bright the ambient light is ( 0.0 - 1.0, 0 = No light, 1 = Full Light )

There are five different global variables needed to make an ambient light shader: Our World, View and Projection matrices along with our AmbientColor and AmbientIntensity. The first three will make their appearance in practically every shader you will create. Those three float4x4 variables- World, View and Projection- are placeholders for your matrices. I’m not going to completely dive into the topic of matrices here since there is a lot of advanced math behind them which is beyond the scope and focus of this tutorial. If you want to know the actual mathematics behind them, I would suggest just the always informative Wikipedia or this link: Introduction to Matrices. All you need to know for now is that the matrices help XNA transform your model from its 3D coordinates you define it in to 2D screen space for your viewing pleasure.

The next two variables are unique to the ambient shader. AmbientColor is pretty self-explanatory: this stores the color of your ambient light. We use float4, each component of the float representing a component of our ambient color: red, green, blue and alpha (a fancy term for transparency). If you look above, I set the ambient color like this:

float4 AmbientColor = float4( 1, 1, 1, 1 );     // Ambient lighting color

Unlike in XNA or C#, you don’t need to specify the new keyword when you initialize an array. You just have float followed by a comma-delimited list of values in parenthesis. Make sure the number of items matches the dimensions of the float, or you’ll get a compile error. You can have float, float2, float3, but no more values than that. Anything beyond a float4 doesn’t have a lot of use in a shader.

AmbientIntensity: This is just a floating point from 0 to 1 which is a proportion of how much of the AmbientColor you want to apply to the object.

The Vertex Shader Input and Output Structures

struct VertexShaderInput
{
float4 Position : POSITION0; // Pass in just the position of the vertex to the vertex shader
};

struct VertexShaderOutput
{
float4 Position : POSITION0; // We’ll be passing our transformed position to the pixel shader
};
Our VertexShaderInput and VertexShaderOutput are extremely simple, so this is a good time to introduce a lot of idiosyncrasies about shader structures. Let’s look at format of the VertexShaderInput structure first. You see it starts with the struct identifier, followed by the name of the structure. Then we have the infamous curly brackets followed by a list of parameters the structure holds. This one only requires a float4 for position. Simple enough.
Now what’s this crazy business hanging off the end of our parameter? : POSITION0? What the heck is this?! This, my friend, is called a semantic. They tell the graphics card what type of data we’re passing in our shaders. Here’s a reference list of all the semantics available in HLSL (As you can see from the link, there’s a list for vertex shaders and pixel shaders; they are two different sets. Keep that in mind.) For position, you would use the POSITION[n] semantic, for color, you would use the COLOR[n] semantic, and so on. You can also see there is a little number after the semantic. This allows you to pass in more than one type of the same variable. For instance, if I wanted to pass a set of texture coordinates and a set of coordinates for my bump map, I would use the semantics TEXCOORD0, TEXCOORD1. They may seem a little confusing now, but don’t worry; in future tutorials you’ll see them used a little more extensively.

A Note About Semantics

It doesn’t matter if you pass in vertices with the type VertexPositionColor, VertexPositionNormalTextured, or even a custom vertex format. The graphics card will always know where the position is stored in your vertex data. When you set the VertexDeclaration of the GraphicsDevice, the VertexElements member (of your vertex structure) has all the data saying which parts of the vertex structure are mapped to which semantics. Every piece of data has a little tag on it defining what it is to keep things nice and organized.

The vertex shader output is exactly the same as VertexShaderInput, so we’ll leave it at that. We’ll be modifying our position we pass in from VertexShaderInput and passing that on to VertexShaderOutput, so it isn’t completely useless. As I mentioned in my shader introduction, remember that you don’t need these structures for your shader to work. You could change your vertex and pixel shader to this:

float4 VertexShaderFunction( float4 position : POSITION0 ) : POSITION0
{
float4 worldPosition = mul( position, World );
float4 viewPosition = mul( worldPosition, View );
return mul( viewPosition, Projection );
}

float4 PixelShaderFunction( float4 position : POSITION0 ) : COLOR0
{
return ( AmbientColor * AmbientIntensity );
}
and it would work exactly the same as if you used the structures. In this case I would almost prefer the above format over the structures since it is more succinct, but I like to use structures all the time for consistency’s sake. It’s up to you how you want to handle it. Okay, on to the brains of our shader.

Vertex Shader

// Vertex Shader
VertexShaderOutput VertexShaderFunction( VertexShaderInput input )
{
    VertexShaderOutput output;

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

    return output;
}

Here we have our simple vertex shader program. Let’s look at it step-by-step. First, we declare an instance of VertexShaderOutput so the vertex shader can return some data. Remember this structure holds only position, so that’s what the entirety of this vertex shader is going to work with. Next, there are three lines that are all related to each other: The first, takes the position based in to our vertex shader which is defined in 3D space and transforms it into world space. Now we have our world position. Next, we transform our world position by the view matrix to get in camera space. Finally, we transform our camera space position into screen space with the projection matrix.
Let’s take a step back and look at what’s really going on with the first line. What is a world matrix used for? A world matrix takes the vertices and converts them from object space into world space. What does this mean? Let’s say you have a model of a box, where each side is 2 units long. So each corner will be defined as 1 unit away from the axes of the object. The front upper-right corner will be (1,1,1), the back upper-left corner will be (-1,1,-1) and so on. These coordinates are defined in object space, relative to the box. Let’s say our cube is actually positioned at (5,5,5) in our world. So really, the front upper-right corner will be at (6,6,6) in world space, the back lower-left corner will be (4,6,4). Here’s a 2D representation of the concept:

Since all we needed to do was transform our 3D position into screen space, we’re all ready for the pixel shader!

Pixel Shader

// Pixel Shader
float4 PixelShaderFunction( VertexShaderOutput input ) : COLOR0
{
    return ( AmbientColor * AmbientIntensity );
}

Take a good look at this pixel shader and admire its beauty. Why? Because this is about as easy as it can get. Pixel shaders can get pretty beefy when advanced vector math and texturing sampling comes into play, so enjoy this now. All this pixel shader does is take the AmbientColor and multiply it with the AmbientIntensity. So if you make your intensity 0.8 like I did, it will return 80% of each color component, resulting in a darker color. Experiment with different values and see how the brightness of the color changes.
You can also see that we attached a semantic at the top of the pixel shader for COLOR0. Remember, there’s different semantics for vertex shaders than pixel shaders! COLOR0 maps to the first output color for the pixel shader, so we’ll use that one. If we were refer to COLOR0 in the context of a vertex shader, it would be first storage location for a color we wanted to apply to our vertices in some way.
You’re probably wondering one last thing: “We never used our transformed position!? Why even pass in the transformed position then?” Yes indeed, we definitely did not. In the pixel shader at least. We still had to do the transformations in the vertex shader, otherwise our graphics card would have no idea where our vertices would go on the screen. All the pixel shader does is interpolate the colors between the vertices. Since we’re just using an ambient color, we have the same color for every pixel. So to answer the final question: no reason at all. In every other shader, you’re going to need the position of the vertex to calculate values like how much light the pixel is getting. But in this case, we can get away with leaving the arguments of the pixel shader completely blank. Once again, I just pass it in for consistency.

Okay! We finally finished our shader. Now we just need to get XNA to draw using it…

Preparing XNA for Crazy Shadin’

Before we get too excited about using shaders, we need to set up XNA so we can render with shaders. First, put the variable declaration for our ambient effect at the top of your game file:

Effect ambientEffect;
Now our application has a reference to the effect. Then, in the LoadContent method in your game file, add this:

// Load our ambient effect
ambientEffect = Content.Load( “Ambient” );
This loads the effect file which I’ve called “Ambient” from the Content Pipeline into our ambientEffect reference. Make sure your Ambient shader is added to the Content Pipeline or XNA won’t be able to find it. There! That’s all we needed to set up our application for shaders.

Setting Global Shader Variables from XNA

Now that you have your shader all implemented, it needs to be passed information from XNA. Once you set a value of a variable in a shader, it stays at that value until it’s set again. If the state of the data never changes- let’s say the color will always be red- you only have to set that global variable once when you initialize the shader. That will save you the cost of redundantly setting variables. I like to set all of the global variables per frame. Not setting them all the time can be problematic if you add another model in that uses the same shader but different values. It can get pretty hairy to debug that situation if your code is pretty complex. However, if you’re looking to squeeze the last few drops of performance out of your application and you know when a global variable will always remain the same, you can try optimizing that block of code. Here’s the XNA code for setting your desired technique and setting the values of the parameters:

// Set what technique we want to render with
ambientEffect.CurrentTechnique = ambientEffect.Techniques[ "Ambient" ];

// Set the parameters of the shader
ambientEffect.Parameters[ "World" ].SetValue( Matrix.CreateRotationY( (float)gameTime.TotalGameTime.TotalSeconds ) );
ambientEffect.Parameters[ "View" ].SetValue( viewMatrix );
ambientEffect.Parameters[ "Projection" ].SetValue( projectionMatrix );
ambientEffect.Parameters[ "AmbientColor" ].SetValue( new Vector4( 1, 1, 1, 1 ) );
ambientEffect.Parameters[ "AmbientIntensity" ].SetValue( .8f );

This code goes right after you clear the GraphicsDevice in your draw method. In the first line, we are setting the current technique to “Ambient”. Nothing special there. You can see in the next five lines of code, we’re accessing each global variable from the list of parameters and setting its value. For the World matrix, I just set up a simple spin around the Y axis to give a little excitement to it. I have the AmbientColor set to white. I want that at 80% brightness, so I set the AmbientIntensity to 0.8. Easy as cake!

Drawing Vertices With Our Ambient Effect

// Begin the effect
ambientEffect.Begin();
foreach ( EffectPass pass in ambientEffect.CurrentTechnique.Passes )
{
pass.Begin();

// Set the vertex declaration so our graphics device knows what to render
GraphicsDevice.VertexDeclaration = new VertexDeclaration( GraphicsDevice, VertexPositionColor.VertexElements );
// Draw the box
GraphicsDevice.DrawUserPrimitives<VertexPositionColor>( PrimitiveType.TriangleList, box.Vertices, 0, box.Vertices.Length / 3 );

pass.End();

}
ambientEffect.End();
Okay, hang in there. We’re almost to the end. This is the final bit of code we need to add to our application to draw with shaders. Put this block of code directly after the global-variable-setting code from above. Just an FYI, I’m going to skip the draw stuff in the middle since I’ve assumed all along you know how to draw things in XNA. Our first line when drawing with shaders is always calling Begin() on the effect. This tells it that you’re about to use it for rendering. Next, you create a For loop to iterate through each pass in the shader. Remember in the introduction how I mentioned shaders could have several passes? In this case though we only have one, but put the For loop there anyway just for the sake of it in case I want to add more passes to my shader later. Inside of that For loop, we call Begin() on the pass, draw our stuff, then call End() on the pass to tell it we finished drawing. It loops through the rest of the passes until it’s completely done. Finally, you call End() on the effect to tell it we’re rendering with it. There! Run your application and you should end up with something like the video below. I created a box in my example, so you should get a solid white (rotating) silhouette of whatever model you use.

The (Almost) Beautiful Result

Conclusion

Well I hope my little (psh, far from it!) article on ambient shaders helped you get a better grasp on shaders. We’ve definitely covered a lot of material, way more than to be expected for the most basic shader, but that’s mainly because I spent the majority of the time introducing how shaders work and how they are set up. I know I’ve said this a million times already, but I’m going to say it a million times more: The basics covered in this article are necessary to understand more advanced shaders in the future. Also, in the next tutorials I’m now going to assume you have this stuff down pat, so I will focus much more on the actual code inside the vertex and pixel shader, what it’s doing and why it’s there, not the shader “etiquette” and structure. Once again, if you have any questions at all, feel free to drop me a line at dan@digitseven.com. Also, I’ve probably made a few mistakes as I went along too, so if you come across something that doesn’t match up I’d like to know so I can fix it. And be sure to head over to the home page and sign up for the newsletter to stay updated when new tutorials and code are released.

Download the XNA Project: Ambient Lighting

Be Sociable, Share!

inShare
Tagged as: Absolutely Fine Tutorial Contest, Article, Daniel Greenheck, digitseven, Shaders, XNA Leave a comment
Comments (0) Trackbacks (3) ( subscribe to comments on this post )

Leave a comment

Name(required)

Email(required)

Website

Article: Shaders – Diffuse Lighting « Sgt. Conker
Article: Shaders – Specular Lighting « Sgt. Conker
Article: Shaders – Rim Lighting « Sgt. Conker
Article: Shaders – Diffuse Lighting » « Article: Shaders – Introduction
Recent Posts

Cutting away archways and roofs to keep the player character visible
Quick fixes for XNA 4.0 spriteFonts
XNA – The state of play
Under new management
Ignoring a Hidden Clingerman to Visit Germany
Recent Comments

admin on XNA – The state of play
Michael Hansen on XNA – The state of play
Michael Hansen on XNA – The state of play
Michael Hansen on XNA – The state of play
admin on XNA – The state of play
Archives

May 2012
April 2012
July 2011
June 2011
May 2011
April 2011
March 2011
February 2011
January 2011
December 2010
November 2010
October 2010
September 2010
August 2010
July 2010
June 2010
May 2010
April 2010
March 2010
February 2010
January 2010
December 2009
November 2009
October 2009
September 2009
August 2009
July 2009
June 2009
May 2009
Categories

“Absolutely fine”
2010 Contest Entries
2D
3D
A.I
Accelerometer
Animation
Articles
Audio
Captain’s Log
Collision
Contest
General
Graphics
Input
ITS COMMING!!!
MVP Watch
Naming Convention
Networking
News
Physics
Post Processing
Scrolls from the past
Shaders
Sound
Storage
Testing
The Week In Code
Threading
Touch
UI
Uncategorized
WP7
XNA
ZuneHD
Meta

Log in
Entries RSS
Comments RSS

Copyright © 2012 Sgt. Conker · Powered by WordPress
Lightword Theme by Andrei Luca Go to top ↑

I didn’t figure out how to write this as a custom surface shader, but I did reverse-engineer Unity’s ambient lighting color mixing algorithm.

Given an object that is subject to a lighting condition of color C and intensity J, in a scene with ambient light of color A, the following describes the color of a pixel of that object. T represents the color of the base texture of the object (i.e. the color that the pixel would be if the object was Self-Illuminated / Unlit).

T * (A + 2CJ)

or

(Base Texture Color) * [ (Ambient Lighting Color) + (Object Lighting Intensity * Object Lighting Color * 2) ]