blog games developers documentation portfolio gallery

More in this category:

Create normal maps for Unity

Posted on December 10, 2014, by Richard Knol

In my project, users can upload their own textures and also their own normal maps. But chances are, they will not upload an actual normal map and provide a regular image instead. So I need to generate a normal map from that image.

Don't forget to have a look at my AssetStore packages:
SimpleLOD - simplify meshes, merge meshes and create LODs
SimpleOBJ - import .OBJ models at runtime
SimpleCollada - import Collada models at runtime
SimpleXML - import and export XML
SimpleJSON - import and export JSON
Texture2D - image manipulation (scale, rotate, crop, etc)
ListBox - Adds a listbox UI control

Detecting normal map vs. regular texture


But first I need to recognize it as either a normal map or a regular image. The method I use is not 100% perfect, but works pretty good.
Here are some images and 2 normal maps.

I take the average colors from each image and also the relative blue value (blue / ((red + green) / 2)) and this gives the following result:
blue image 1: average color: RGBA(0.360, 0.577, 0.770, 1.000) blue/rg 1.643365
blue image 2: average color: RGBA(0.234, 0.340, 0.561, 1.000) blue/rg 1.953209
blue image 3: average color: RGBA(0.371, 0.659, 0.869, 1.000) blue/rg 1.688144
blue image 4: average color: RGBA(0.529, 0.681, 0.967, 1.000) blue/rg 1.59855

normal map 1: average color: RGBA(0.407, 0.405, 0.796, 1.000) blue/rg 1.958531
normal map 2: average color: RGBA(0.498, 0.498, 0.995, 1.000) blue/rg 1.999087

You can see that the red and green values of the real normal maps lay close together.
And the relative blue value is much higher with the real normal maps
So I will use 2 criteria:
1: relative blue value > 1.8
2: difference between red and green < 0.1

And here's the function:

public bool IsNormalMap(Texture2D aTexture) {
Color avgColor = aTexture.GetAverageColor();
float rg = (avgColor.r + avgColor.g) / 2f;
if(rg <= 0f) rg = 1f;
float relativeBlue = avgColor.b / rg;
if(relativeBlue > 1.8f && Mathf.Abs(avgColor.r - avgColor.g) < 0.1f) return true;
return false;
}


Generate normal map from texture or bump map


Next we need to generate a normal map from a regular texture based on gray scales.
For each pixel in the image, we compare the gray scale values of the 2 pixels surrounding it in x direction and in y direction and create 2 vectors from that.
Since textures are usually repeated, I used the opposit x or y coordinate when a pixel is on the edge of the texture.
We then take the cross product of these vectors to get the normal
And finally convert it to a color to store as pixel in the new normal map

public Texture2D ColorTextureToNormalMap(Texture2D aTexture, float strength) {
Texture2D normalTexture = new Texture2D(aTexture.width, aTexture.height, TextureFormat.RGB24, aTexture.mipmapCount > 1);
Color[] pixels = aTexture.GetPixels(0);
Color[] nPixels = new Color[pixels.Length];
for (int y=0; y<aTexture.height; y++) {
for (int x=0; x<aTexture.width; x++) {
int x_1 = x-1;
if(x_1 < 0) x_1 = aTexture.width - 1; // repeat the texture so use the opposit side
int x1 = x+1;
if(x1 >= aTexture.width) x1 = 0; // repeat the texture so use the opposit side
int y_1 = y-1;
if(y_1 < 0) y_1 = aTexture.height - 1; // repeat the texture so use the opposit side
int y1 = y+1;
if(y1 >= aTexture.height) y1 = 0; // repeat the texture so use the opposit side
float grayX_1 = pixels[(y * aTexture.width) + x_1].GrayScale();
float grayX1 = pixels[(y * aTexture.width) + x1].GrayScale();
float grayY_1 = pixels[(y_1 * aTexture.width) + x].GrayScale();
float grayY1 = pixels[(y1 * aTexture.width) + x].GrayScale();
Vector3 vx = new Vector3(0, 1, (grayX_1 - grayX1) * strength);
Vector3 vy = new Vector3(1, 0, (grayY_1 - grayY1) * strength);
Vector3 n = Vector3.Cross(vy, vx).normalized;
nPixels[(y * aTexture.width) + x] = (Vector4)((n + Vector3.one) * 0.5f);
}
}
normalTexture.SetPixels(nPixels, 0);
normalTexture.Apply(true);
return normalTexture;
}


Converting normal map to Unity format


We now have a perfectly working normal map, but Unity can't deal with it. This is because Unity uses a different format internally where RGB is the y normal and A is the x normal. Don't thank me for this. All credits for this next function go to SophieH and her answer can be found here

public Texture2D NormalMapToUnityFormat(Texture2D aTexture) {
Texture2D normalTexture = new Texture2D(aTexture.width, aTexture.height, TextureFormat.ARGB32, aTexture.mipmapCount > 1);
Color[] pixels = aTexture.GetPixels(0);
Color[] nPixels = new Color[pixels.Length];
for (int y=0; y<aTexture.height; y++) {
for (int x=0; x<aTexture.width; x++) {
Color p = pixels[(y * aTexture.width) + x];
Color np = new Color(0,0,0,0);
np.r = p.g;
np.g = p.g; // waste of memory space if you ask me
np.b = p.g;
np.a = p.r;
nPixels[(y * aTexture.width) + x] = np;
}
}
normalTexture.SetPixels(nPixels, 0);
normalTexture.Apply(true);
return normalTexture;
}


Putting it together


Let's say the user has uploaded a texture and I want to set it as a normal map in an object's material at runtime.

public void ApplyTextureAsNormalMap(Texture2D aTexture, Material aMaterial) {
if(IsNormalMap(aTexture)) {
Debug.Log("this is a normal map");
aMaterial.SetTexture("_BumpMap", NormalMapToUnityFormat(aTexture));
} else {
Debug.Log("this is not a normal map");
Texture2D normalTexture = ColorTextureToNormalMap(aTexture, scale);
aMaterial.SetTexture("_BumpMap", NormalMapToUnityFormat(normalTexture));
Texture2D.Destroy(normalTexture); // clean up the mess
}
}









follow us