Normal Map Creation Using C#

Normal maps on the cheap.
Feb 26 2009 by James Craig

Last time I showed how to create a generic bump map. The benefits of that style is it can be used as a height map that adds some depth to an item, allowing you to use smaller poly counts. The down side is the simple fact that there is no information regarding lighting. Generally when you use a basic bump map the lighting is going to look rather uniform no matter where the light source is coming from. To fix this, people came up with normal maps. Normal maps are RGB images that use the various R, G, and B portions of each pixel to determine the direction of the normal for that location with R being the X, G being the Y, and B being the Z axis (although R, G, and B can really be any of the axis).

In order to create the normal map, we actually use two of the earlier bump maps. One that finds edges going top to bottom and one going left to right. Each of these are used to determine one of the channels, combining them into a vector in which we just leave the Z axis at 1.0, after which we normalize it, and then convert the vector to a pixel (multiply each channel by 255 and put it in a pixel).It's fairly simple, but takes a bit of code. Luckily I have the code here for you:

 /\*
Copyright (c) 2010 <a href="http://www.gutgames.com">James Craig</a>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.\*/

#region Usings
using System.Drawing;
using System.Drawing.Imaging;
#endregion

namespace Utilities.Media.Image
{
/// <summary>
/// Class for creating a normal map
/// </summary>
public class NormalMap
{
#region Constructors

/// <summary>
/// Constructor
/// </summary>
public NormalMap()
{
InvertX = false;
InvertY = false;
}

#endregion

#region Properties

/// <summary>
/// Determines the direction of the normal map in the x direction
/// </summary>
public bool InvertX { get; set; }

/// <summary>
/// Determines the direction of the normal map in the y direction
/// </summary>
public bool InvertY { get; set; }

/// <summary>
/// X filter
/// </summary>
protected BumpMap FilterX { get; set; }

/// <summary>
/// Y filter
/// </summary>
protected BumpMap FilterY { get; set; }

#endregion

#region Protected Functions

/// <summary>
/// Sets up the edge detection filter
/// </summary>
protected void CreateFilter()
{
FilterX = new BumpMap();
FilterY = new BumpMap();
FilterX.Invert = InvertX;
FilterY.Invert = InvertY;
FilterX.Direction = Direction.LeftRight;
FilterY.Direction = Direction.TopBottom;
}

#endregion

#region Public Functions

/// <summary>
/// Creates the bump map
/// </summary>
public Bitmap Create(Bitmap ImageUsing)
{
CreateFilter();
using (Bitmap TempImageX = FilterX.Create(ImageUsing))
{
using (Bitmap TempImageY = FilterY.Create(ImageUsing))
{
Bitmap ReturnImage = new Bitmap(TempImageX.Width, TempImageX.Height);
Math.Vector3 TempVector = new Utilities.Math.Vector3(0.0, 0.0, 0.0);
BitmapData TempImageXData = Image.LockImage(TempImageX);
BitmapData TempImageYData = Image.LockImage(TempImageY);
BitmapData ReturnImageData = Image.LockImage(ReturnImage);
int TempImageXPixelSize = Image.GetPixelSize(TempImageXData);
int TempImageYPixelSize = Image.GetPixelSize(TempImageYData);
int ReturnImagePixelSize = Image.GetPixelSize(ReturnImageData);
for (int y = 0; y < TempImageX.Height; ++y)
{
for (int x = 0; x < TempImageX.Width; ++x)
{
Color TempPixelX = Image.GetPixel(TempImageXData, x, y, TempImageXPixelSize);
Color TempPixelY = Image.GetPixel(TempImageYData, x, y, TempImageYPixelSize);
TempVector.X = (double)(TempPixelX.R) / 255.0;
TempVector.Y = (double)(TempPixelY.R) / 255.0;
TempVector.Z = 1.0;
TempVector.Normalize();
TempVector.X = ((TempVector.X + 1.0) / 2.0) \* 255.0;
TempVector.Y = ((TempVector.Y + 1.0) / 2.0) \* 255.0;
TempVector.Z = ((TempVector.Z + 1.0) / 2.0) \* 255.0;
Image.SetPixel(ReturnImageData, x, y,
Color.FromArgb((int)TempVector.X,
(int)TempVector.Y,
(int)TempVector.Z),
ReturnImagePixelSize);
}
}
Image.UnlockImage(TempImageX, TempImageXData);
Image.UnlockImage(TempImageY, TempImageYData);
Image.UnlockImage(ReturnImage, ReturnImageData);
return ReturnImage;
}
}
}

#endregion
}
}

Now if you try the code out, you'll notice that it uses the BumpMap class that I created last post (which uses the Filter class) and another class called Vector3 which I haven't shown thus far. So you can actually compile, here's the code for that:

 /\*
Copyright (c) 2010 <a href="http://www.gutgames.com">James Craig</a>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.\*/
#region Usings
using System;
using System.Xml.Serialization;
#endregion

namespace Utilities.Math
{
/// <summary>
/// Vector class (holds three items)
/// </summary>
\[Serializable()\]
public class Vector3
{
#region Constructor

/// <summary>
/// Constructor
/// </summary>
/// <param name="X">X direction</param>
/// <param name="Y">Y direction</param>
/// <param name="Z">Z direction</param>
public Vector3(double X, double Y, double Z)
{
this.X = X;
this.Y = Y;
this.Z = Z;
}

#endregion

#region Public Functions

/// <summary>
/// Normalizes the vector
/// </summary>
public void Normalize()
{
double Normal = Magnitude;
if (Normal > 0)
{
Normal = 1 / Normal;
X \*= Normal;
Y \*= Normal;
Z \*= Normal;
}
}

#endregion

#region Public Overridden Functions

/// <summary>
/// To string function
/// </summary>
/// <returns>String representation of the vector</returns>
public override string ToString()
{
return "(" + X + "," + Y + "," + Z + ")";
}

/// <summary>
/// Gets the hash code
/// </summary>
/// <returns>The hash code</returns>
public override int GetHashCode()
{
return (int)(X + Y + Z) % Int32.MaxValue;
}

/// <summary>
/// Determines if the items are equal
/// </summary>
/// <param name="obj">Object to compare</param>
/// <returns>true if they are, false otherwise</returns>
public override bool Equals(object obj)
{
if (obj is Vector3)
{
return this == (Vector3)obj;
}
return false;
}

#endregion

#region Public Properties

/// <summary>
/// Used for converting this to an array and back
/// </summary>
public double\[\] Array
{
get { return new double\[\] { X, Y, Z }; }
set
{
if (value.Length == 3)
{
X = value\[0\];
Y = value\[1\];
Z = value\[2\];
}
}
}

/// <summary>
/// Returns the magnitude of the vector
/// </summary>
public double Magnitude
{
get { return System.Math.Sqrt((X \* X) + (Y \* Y) + (Z \* Z)); }
}

/// <summary>
/// X value
/// </summary>
\[XmlElement\]
public double X { get; set; }

/// <summary>
/// Y Value
/// </summary>
\[XmlElement\]
public double Y { get; set; }

/// <summary>
/// Z value
/// </summary>
\[XmlElement\]
public double Z { get; set; }

#endregion

#region Public Static Functions

public static Vector3 operator +(Vector3 V1, Vector3 V2)
{
return new Vector3(V1.X + V2.X, V1.Y + V2.Y, V1.Z + V2.Z);
}

public static Vector3 operator -(Vector3 V1, Vector3 V2)
{
return new Vector3(V1.X - V2.X, V1.Y - V2.Y, V1.Z - V2.Z);
}

public static Vector3 operator -(Vector3 V1)
{
return new Vector3(-V1.X, -V1.Y, -V1.Z);
}

public static bool operator <(Vector3 V1, Vector3 V2)
{
return V1.Magnitude < V2.Magnitude;
}

public static bool operator <=(Vector3 V1, Vector3 V2)
{
return V1.Magnitude <= V2.Magnitude;
}

public static bool operator >(Vector3 V1, Vector3 V2)
{
return V1.Magnitude > V2.Magnitude;
}

public static bool operator >=(Vector3 V1, Vector3 V2)
{
return V1.Magnitude >= V2.Magnitude;
}

public static bool operator ==(Vector3 V1, Vector3 V2)
{
return V1.X == V2.X && V1.Y == V2.Y && V1.Z == V2.Z;
}

public static bool operator !=(Vector3 V1, Vector3 V2)
{
return !(V1 == V2);
}

public static Vector3 operator /(Vector3 V1, double D)
{
return new Vector3(V1.X / D, V1.Y / D, V1.Z / D);
}

public static Vector3 operator \*(Vector3 V1, double D)
{
return new Vector3(V1.X \* D, V1.Y \* D, V1.Z \* D);
}

public static Vector3 operator \*(double D, Vector3 V1)
{
return new Vector3(V1.X \* D, V1.Y \* D, V1.Z \* D);
}
/// <summary>
/// Does a cross product
/// </summary>
public static Vector3 operator \*(Vector3 V1, Vector3 V2)
{
Vector3 TempVector = new Vector3(0.0, 0.0, 0.0);
TempVector.X = (V1.Y \* V2.Z) - (V1.Z \* V2.Y);
TempVector.Y = (V1.Z \* V2.X) - (V1.X \* V2.Z);
TempVector.Z = (V1.X \* V2.Y) - (V1.Y \* V2.X);
return TempVector;
}

/// <summary>
/// Does a dot product
/// </summary>
/// <param name="V1">Vector 1</param>
/// <param name="V2">Vector 2</param>
/// <returns>a dot product</returns>
public static double DotProduct(Vector3 V1, Vector3 V2)
{
return (V1.X \* V2.X) + (V1.Y \* V2.Y) + (V1.Z \* V2.Z);
}

/// <summary>
/// Interpolates between the vectors
/// </summary>
/// <param name="V1">Vector 1</param>
/// <param name="V2">Vector 2</param>
/// <param name="Control">Percent to move between 1 and 2</param>
/// <returns>The interpolated vector</returns>
public static Vector3 Interpolate(Vector3 V1, Vector3 V2, double Control)
{
Vector3 TempVector = new Vector3(0.0, 0.0, 0.0);
TempVector.X = (V1.X \* (1 - Control)) + (V2.X \* Control);
TempVector.Y = (V1.Y \* (1 - Control)) + (V2.Y \* Control);
TempVector.Z = (V1.Z \* (1 - Control)) - (V2.Z \* Control);
return TempVector;
}

/// <summary>
/// The distance between two vectors
/// </summary>
/// <param name="V1">Vector 1</param>
/// <param name="V2">Vector 2</param>
/// <returns>Distance between the vectors</returns>
public static double Distance(Vector3 V1, Vector3 V2)
{
return System.Math.Sqrt(((V1.X - V2.X) \* (V1.X - V2.X)) + ((V1.Y - V2.Y) \* (V1.Y - V2.Y)) + ((V1.Z - V2.Z) \* (V1.Z - V2.Z)));
}

/// <summary>
/// Determines the angle between the vectors
/// </summary>
/// <param name="V1">Vector 1</param>
/// <param name="V2">Vector 2</param>
/// <returns>Angle between the vectors</returns>
public static double Angle(Vector3 V1, Vector3 V2)
{
V1.Normalize();
V2.Normalize();
return System.Math.Acos(Vector3.DotProduct(V1, V2));
}

#endregion
}
}

That's another rather large bit of code... But really it's just a vector class that allows you to do various things. The portion we care about is the normalization, but it has the ability to do addition, subtraction, cross products, dot products, etc. Anyway the two classes should allow you to create a normal map. Well that and you'll notice that the normal map creation code has references to Image.Lock, Unlock, etc. These are functions from my utility library to help speed up things but can be removed/replaced with built in GetPixel/SetPixel functions (or simply download my library as it has all of this code plus a bunch more). So try it out, leave feedback, and happy coding.