Generating a Normal Map from a Height Map

Generating a normal map from a height map?

Example GLSL code from my water surface rendering shader:

#version 130
uniform sampler2D unit_wave
noperspective in vec2 tex_coord;
const vec2 size = vec2(2.0,0.0);
const ivec3 off = ivec3(-1,0,1);

vec4 wave = texture(unit_wave, tex_coord);
float s11 = wave.x;
float s01 = textureOffset(unit_wave, tex_coord, off.xy).x;
float s21 = textureOffset(unit_wave, tex_coord, off.zy).x;
float s10 = textureOffset(unit_wave, tex_coord, off.yx).x;
float s12 = textureOffset(unit_wave, tex_coord, off.yz).x;
vec3 va = normalize(vec3(size.xy,s21-s01));
vec3 vb = normalize(vec3(size.yx,s12-s10));
vec4 bump = vec4( cross(va,vb), s11 );

The result is a bump vector: xyz=normal, a=height

Is it possible generate normal map from heightmap texture from math point of view?

For me my own initial question is invalid and it has a bug!!!

I – original parametric surface with domain as cartesian product of [0,1] and range of it as euclidean space

II - normal to original surface

III - modified original surface with heightmap

IV - normal map which we want to receive with even ignoring geometric modification of the surface by “III”

Sample Image

Final step IV includes a lot of stuff to differentiate: H(s,t) and original definition of the function...I don't perform futher analytic of that equations...But as for me you can't generate normalmap only from (heightmap)...

P.S. To perform futher analytics if you want to do it retrive *.docx file from that place https://yadi.sk/d/Qqx-jO1Ugo3uL As I know it impossible to convert formulas in ms word to latex, but in any case please use it asa draft.

Unity shader - How to generate normal map based on texture map

It looks like your "TargetTexture" is giving you back a height map. Here is a post I found about how to turn a height map into a normal map. I've mashed the code you had originally together with the core of that forum post and output the normals as color so you can test and see how this works:

Shader "Unlit/HeightToNormal"
{
Properties
{
_MainTex("Albedo (RGB)", 2D) = "white" {}
_Color("Color", Color) = (1,1,1,1)
_NormalIntensity("NormalIntensity",Float) = 1
_HeightMapSizeX("HeightMapSizeX",Float) = 1024
_HeightMapSizeY("HeightMapSizeY",Float) = 1024
}
SubShader
{

Tags { "RenderType" = "Opaque" }
LOD 100

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"

struct VertexInput {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 normal : NORMAL;
float3 tangent : TANGENT;

};

struct VertexOutput {
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float2 uv1 : TEXCOORD1;
float4 normals : NORMAL;

//float3 tangentSpaceLight: TANGENT;
};

sampler2D _MainTex;
float4 _MainTex_ST;
half4 _Color;
float _NormalIntensity;
float _HeightMapSizeX;
float _HeightMapSizeY;

VertexOutput vert(VertexInput v) {

VertexOutput o;
o.uv = TRANSFORM_TEX( v.uv, _MainTex ); // used for texture
o.uv1 = v.uv;
o.normals = v.normal;
o.vertex = UnityObjectToClipPos(v.vertex);

return o;
}

float4 frag(VertexOutput i) : COLOR
{
float me = tex2D(_MainTex,i.uv1).x;
float n = tex2D(_MainTex,float2(i.uv1.x, i.uv1.y + 1.0 / _HeightMapSizeY)).x;
float s = tex2D(_MainTex,float2(i.uv1.x, i.uv1.y - 1.0 / _HeightMapSizeY)).x;
float e = tex2D(_MainTex,float2(i.uv1.x + 1.0 / _HeightMapSizeX,i.uv1.y)).x;
float w = tex2D(_MainTex,float2(i.uv1.x - 1.0 / _HeightMapSizeX,i.uv1.y)).x;

// defining starting normal as color has some interesting effects, generally makes this more flexible
float3 norm = _Color;
float3 temp = norm; //a temporary vector that is not parallel to norm
if (norm.x == 1)
temp.y += 0.5;
else
temp.x += 0.5;

//form a basis with norm being one of the axes:
float3 perp1 = normalize(cross(i.normals,temp));
float3 perp2 = normalize(cross(i.normals,perp1));

//use the basis to move the normal i its own space by the offset
float3 normalOffset = -_NormalIntensity * (((n - me) - (s - me)) * perp1 + ((e - me) - (w - me)) * perp2);
norm += normalOffset;
norm = normalize(norm);

// it's also interesting to output temp, perp1, and perp1, or combinations of the float samples.
return float4(norm, 1);
}
ENDCG
}
}
}

To generate a normal map from a height map, you're trying to use the oriented rate of change in your height map to come up with a normal vector which can be represented using 3 float values (or color channels, if it's an image). You can sample the point of image you are on, and then small steps away from that point in four cardinal directions. Using the cross product to guarantee orthogonality you can define a basis. Using your oriented steps on your image, you can scale the two basis vectors and add them together to find a "normal offset", which is the 3D representation approximating the oriented change in value on your heightmap. Basically it's your normal.

You can see the effects of me playing with normal intensity here, and the "normal color" here. When this looks right for your use case, you can try using normals as normals instead of colored output.

Some tweaking of values will probably still be required. Good luck!

Normal map from height map

One possibility is that your normals are pointing in the wrong direction. This could happen because OpenGL uses texture coordinates with the origin at the bottom left of the image where the Y axis points up and DirectX uses texture coordinates with the origin at the top left where the Y axis points down.

If you map out where the code is taking height samples for DirectX, you'll see that s01 is to the left, s21 is to the right, s10 is to the top, and s12 is to the bottom. In OpenGL s10 and s12 will be reversed.

When you create the vector vb by subtracting s10 from s12 it will point one way for DirectX and the opposite way for OpenGL. Now when you take a cross product between two vectors, it forms a perpendicular vector that can point in either of two directions depending on the order of the cross product using the right hand rule: http://en.wikipedia.org/wiki/Cross_product. This will cause the cross products for DirectX and OpenGL to point in opposite directions.

So, if this is an issue, the fix could be to swap the order of the vectors in the cross product or to swap the locations of the s10 and s12 samples.

There is also an issue with your texture coordinates. uv should be in the range of [0, 1]. When you add off to it, it puts it out of range and the result depends on whatever texture addressing mode is active. In each of your tex2D samples, you'll need to divide the first component of off by the texture width and the second by the texture height.

Finally, the result of the cross product with have values in the range [-1, 1]. If you want to store this as a color, you need to convert these to [0, 1]. So something like cross(vb, va) * 0.5 + 0.5 will work. When normal maps are sampled in a shader, the colors are then converted back into normal range of [-1, 1].

Efficient way to generate normal map (from height) for all 360 degrees of a rotated image

It's pretty simple really: just rotate the normals in the map with the rotation of the map after generating them normally. You don't even need to regenerate them strictly speaking; just adjust your shader.

Calculate Normals from Heightmap

From vector calculus, the normal of a surface is given by the gradient operator:

Sample Image

A height map h(x, y) is a special form of the function f:

Sample Image

For a discretized height map, assuming that the grid size is 1, the first-order approximations to the two derivative terms above are given by:

Sample Image

Since the x step from L to R is 2, and same for y. The above is exactly the formula you had, divided through by 4. When this vector is normalized, the factor of 4 is canceled.

(No linear algebra was harmed in the writing of this answer)

Can normal maps be generated from a texture?

Yes. Well, sort of. Normal maps can be accurately made from height-maps. Generally, you can also put a regular texture through and get decent results as well. Keep in mind there are other methods of making a normal map, such as taking a high-resolution model, making it low resolution, then doing ray casting to see what the normal should be for the low-resolution model to simulate the higher one.

For height-map to normal-map, you can use the Sobel Operator. This operator can be run in the x-direction, telling you the x-component of the normal, and then the y-direction, telling you the y-component. You can calculate z with 1.0 / strength where strength is the emphasis or "deepness" of the normal map. Then, take that x, y, and z, throw them into a vector, normalize it, and you have your normal at that point. Encode it into the pixel and you're done.

Here's some older incomplete-code that demonstrates this:

// pretend types, something like this
struct pixel
{
uint8_t red;
uint8_t green;
uint8_t blue;
};

struct vector3d; // a 3-vector with doubles
struct texture; // a 2d array of pixels

// determine intensity of pixel, from 0 - 1
const double intensity(const pixel& pPixel)
{
const double r = static_cast<double>(pPixel.red);
const double g = static_cast<double>(pPixel.green);
const double b = static_cast<double>(pPixel.blue);

const double average = (r + g + b) / 3.0;

return average / 255.0;
}

const int clamp(int pX, int pMax)
{
if (pX > pMax)
{
return pMax;
}
else if (pX < 0)
{
return 0;
}
else
{
return pX;
}
}

// transform -1 - 1 to 0 - 255
const uint8_t map_component(double pX)
{
return (pX + 1.0) * (255.0 / 2.0);
}

texture normal_from_height(const texture& pTexture, double pStrength = 2.0)
{
// assume square texture, not necessarily true in real code
texture result(pTexture.size(), pTexture.size());

const int textureSize = static_cast<int>(pTexture.size());
for (size_t row = 0; row < textureSize; ++row)
{
for (size_t column = 0; column < textureSize; ++column)
{
// surrounding pixels
const pixel topLeft = pTexture(clamp(row - 1, textureSize), clamp(column - 1, textureSize));
const pixel top = pTexture(clamp(row - 1, textureSize), clamp(column, textureSize));
const pixel topRight = pTexture(clamp(row - 1, textureSize), clamp(column + 1, textureSize));
const pixel right = pTexture(clamp(row, textureSize), clamp(column + 1, textureSize));
const pixel bottomRight = pTexture(clamp(row + 1, textureSize), clamp(column + 1, textureSize));
const pixel bottom = pTexture(clamp(row + 1, textureSize), clamp(column, textureSize));
const pixel bottomLeft = pTexture(clamp(row + 1, textureSize), clamp(column - 1, textureSize));
const pixel left = pTexture(clamp(row, textureSize), clamp(column - 1, textureSize));

// their intensities
const double tl = intensity(topLeft);
const double t = intensity(top);
const double tr = intensity(topRight);
const double r = intensity(right);
const double br = intensity(bottomRight);
const double b = intensity(bottom);
const double bl = intensity(bottomLeft);
const double l = intensity(left);

// sobel filter
const double dX = (tr + 2.0 * r + br) - (tl + 2.0 * l + bl);
const double dY = (bl + 2.0 * b + br) - (tl + 2.0 * t + tr);
const double dZ = 1.0 / pStrength;

math::vector3d v(dX, dY, dZ);
v.normalize();

// convert to rgb
result(row, column) = pixel(map_component(v.x), map_component(v.y), map_component(v.z));
}
}

return result;
}


Related Topics



Leave a reply



Submit