Table of Contents
The height2bump.py ProgramThis page explains my cross platform open source program height2bump.py that converts heightmaps into normalmaps. Here is how to use it as a command line program (import as a module is also possible). Here is the source again:
What is a heightmap ?A heightmap is a greyscale image describing the local geometry of an object by giving its height (coded as brightness, 0=black=“low”, 255=white=“high”) for every texture pixel. It is applied to a much coarser polygon model to give the impression of surface details that are not really there in the model. Depending on your shader program, you can use the heightmap to
What is a Normalmap ? What is a Bumpmap ?As you can see, to show “Bumps” on a polygon surface (that in reality consists of flat facets) you have more than one way. If you want to modify the lighting of the surface according to your texture map, you usually need the surface normal. The surface normal of the flat facet is constant over the facet (A). The first approximation would be a “smooth” look by interpolating the normals between the vertex normals (B). A normal map gives you the normal (relative to the interpolated normal) for every pixel in the texture (C). To code the normal (Vx, Vy, Vz with length = 1, direction in x, y, z) in an RGB-Image, we define: R = (Vx + 1.0) * 127 G = (Vy + 1.0) * 127 B = (Vz + 1.0) * 127 Vx, Vy, Vz all vary between -1 and +1. By adding ”+1” we get the range 0..+2 and after the multiply we have 0..255 to fit it nicely into an 8-bit-Color-Channel.
Because most of the time the height does not change too much, the normalmaps in general have a light blue tint - see an example here. The filter kernelTo calculate the normalmap, we need to
FilterThis means to take some kind of weighted sum of some pixels - called a “Gaussian Blur”. x-Change and y-ChangeThis means calculating the difference in height between a pixel more “east” and a pixel more “west” for x, and “north” to “south” for y. This is called “partial differential”. You can do this in one step - this is called an “edge detection filter”. I use two kinds:
See Sobel Operator or the source file for details. The “Scharr”-Filter looks like the Sobel-Filter but has better properties for edges with arbitrary angles. The nice thing is that PIL (the Python Image Library) has a high speed implementation for applying a filter to an image. You just have to define the filter kernel (the factors for the weighted sum) and off you go: r = heightBand.filter(ImageFilter.Kernel((5,5), kernel[0], scale=scale, offset=128.0)) g = heightBand.filter(ImageFilter.Kernel((5,5), kernel[1], scale=scale, offset=128.0)) Calculate zBecause the normal's length should be 1, good old Pythagoras is used such that x^2 + y^2 + z^2 = 1. I did not succeed to get PIL do this calculation for me (although it should be possible). So I have to loop over the whole image one pixel at the time - this is a slow operation in Python: for y in range( r.size[1] ): for x in range( r.size[0] ): op = 1.0 - (rr[x,y]*2.0/255.0 - 1.0)**2 - (gg[x,y]*2.0/255.0 - 1.0)**2 bb[x,y] = 128.0 + 128.0 * sqrt(op) (Handling of “negative square root” removed for clarity). I worked around the performance problem by
infile_stamp = os.path.getmtime(infn) outfile_stamp = os.path.getmtime(outfn) if infile_stamp < outfile_stamp: .... Program structureAll the weight-lifting is done in the function def height2bump( heightBand, filter="Scharr" ): # normal[0..2] band-array Input is a PIL-“band” you can get from any PIL-Image by using Image.split(). The output are 3 Bands you can merge into a Multiband-Image for texturing or saving as an image file. If you create a 4-Band-Image (RGBA) you must use ”.tga” as image format. Depending on the “filter” Option you get different filter kernels. Because of some symmetry, you only have to define 6 Numbers to define a 5×5-Filter. To have fast access to the pixel data, the function r.load() creates the access methods to use the image just as any array (see the PIL Documentation for details). I really wanted to use the ImageMath module of PIL to calculate the z Component of the Normalmap - but I did not succeed. So I loop over the array. If the filter calculates x- and y-values that are too large ( x*x + y*y > 1.0 ) an error in the sqrt() function would follow - so I have a special case in the nested loop (that is already slow). The filter scaling factor is calculated dynamically - if you want to add your own custom filter, this is rather easy: Another “if” for the filtername and 6 float values for the filter kernel. The function readHeight2Bump( infn, outfn ) is the “high level”-Interface to the conversion routine: it looks at the file timestamps to decide if the calculation can be skipped entirly. With the option “v” you get a lot of information what it thinks it is doing The part after if __name__ == "__main__": does the command line option processing. If anything goes wrong, it shows a usage info and exits. This is skipped if the code is used as a module. height2normal.py as a moduleSimply import it and use the readHeight2Bump() function: from height2bump import readHeight2Bump result = readHeight2Bump( heightfilename, bumpfilename, options="tqa" ) result is 4-band PIL-Image containing x,y,z,h. The filenames define the (monochrome) Heightmap and the Normalmap. If the Normalmap exists and is newer than the Heightmap, it is read and returned - this is faster than doing the calculations. If the Heightmap is newer, the Normalmap is calculated and written to the Normalmap directory. In this case write access to the Normalmap-File/Directory is needed! If the Normalmap exists, the Heightmap does not need to exist. |