Last active
May 7, 2024 17:55
-
-
Save Huud/63bacf5b8fe9b7b205ee42a786f922f0 to your computer and use it in GitHub Desktop.
Generate Normal Map From Height Map
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# A fast - numpy based - CPU functions that take a height map and generate a normal map from it | |
import numpy as np | |
import matplotlib.image as mpimg | |
# a function that takes a vector - three numbers - and normalize it, i.e make it's length = 1 | |
def normalizeRGB(vec): | |
length = np.sqrt(vec[:,:,0]**2 + vec[:,:,1]**2 + vec[:,:,2]**2) | |
vec[:,:,0] = vec[:,:,0] / length | |
vec[:,:,1] = vec[:,:,1] / length | |
vec[:,:,2] = vec[:,:,2] / length | |
return vec | |
# height_image_path is a string to your height map file, e.g "C:\mymap.png": | |
def heightMapToNormalMap(height_image_path): | |
# read the file | |
image = mpimg.imread(height_image_path) | |
# only use one channel, it can be any sice B&W image channels are equal | |
image = image[:,:,0] | |
# initialize the normal map, and the two tangents: | |
normalMap = np.zeros((image.shape[0],image.shape[1],3)) | |
tan = np.zeros((image.shape[0],image.shape[1],3)) | |
bitan = np.zeros((image.shape[0],image.shape[1],3)) | |
# we get the normal of a pixel by the 4 pixels around it, sodefine the top, buttom, left and right pixels arrays, | |
# which are just the input image shifted one pixel to the corrosponding direction. We do this by padding the image | |
# and then 'cropping' the unneeded sides | |
B = np.pad(image,1,mode='edge')[2:,1:-1] | |
T = np.pad(image,1,mode='edge')[:-2,1:-1] | |
L = np.pad(image,1,mode='edge')[1:-1,0:-2] | |
R = np.pad(image,1,mode='edge')[1:-1,2:] | |
# to get a good scale/intensity multiplier, i.e a number that let's the R and G channels occupy most of their available | |
# space between 0-1 without clipping, we will start with an overly strong multiplier - the smaller the the multiplier is, the | |
# stronger it is -, to practically guarantee clipping then incrementally increase it until no clipping is happening | |
scale = .05 | |
while True: | |
#get the tangents of the surface, the normal is thier cross product | |
tan[:,:,0],tan[:,:,1],tan[:,:,2] = np.asarray([scale, 0 , R-L]) | |
bitan[:,:,0],bitan[:,:,1],bitan[:,:,2] = np.asarray([0, scale , B-T]) | |
normalMap = np.cross(tan,bitan) | |
# normalize the normals to get their length to 1, they are called normals after all | |
normalMap = normalizeRGB(normalMap) | |
# increment the multiplier until the desired range of R and G is reached | |
if scale > 2: break | |
if np.max(normalMap[:,:,0]) > 0.95 or np.max(normalMap[:,:,1]) > 0.95 \ | |
or np.min(normalMap[:,:,0]) < -0.95 or np.min(normalMap[:,:,1]) < -0.95: | |
scale += .05 | |
continue | |
else: | |
break | |
# calculations were done for the channels to be in range -1 to 1 for the channels, however the image saving function | |
# expects the range 0-1, so just divide these channels by 2 and add 0.5 to be in that range, we also flip the | |
# G channel to comply with the OpenGL normal map convention | |
normalMap[:,:,0] = (normalMap[:,:,0]/2)+.5 | |
normalMap[:,:,1] = (0.5-(normalMap[:,:,1]/2)) | |
normalMap[:,:,2] = (normalMap[:,:,2]/2)+.5 | |
# normalizing does most of the job, but clip the remainder just in case | |
normalMap = np.clip(normalMap,0.0,1.0) | |
return normalMap |
hi, thanks for your helpful reply.
The height map I offered above, is the rendering result of a ball with that height map applied, in Blender. I should tell it more clearly. (I updated my post above to make this more clearly.)
I got this done with a weird way a few months ago. And your reply helps me to understand the right way. Thanks.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@butaixianran i've not jumped into blender add-ons except superficially, so I'll preface by suggesting I'd be quite shocked if you weren't able to access blender's bump and normal routines through bpy somehow.
that said, and despite the OPs' comments about it being openGL formatted, the second image has the classic look of DirectX in and openGL environment (i.e. Blender), or vice versa. basically, first thing I would try is to invert the G (up) channel, which is 1 - gchanvalue. Its late as hell here / my mind is shot but I believe the math in the OP's #63 is incorrect (forgive me if i'm wrong). so try:
using the consistent formula for normalization for the R/B channels. again, it's very late, but substracting half the g channel value from 0.5 is going to put all the values sub 0.5, rather than ranging from 1.0 - 0.0
@thygrrr the first image is, if im not mistaken, the heightmap loaded into a displacement modifier with a very low scale so the ridges don't break the silhouette