One of the concepts which once dominated CGI was, that textures assigned to 3D models needed to include a “Normal-Map”, so that even early in the days of 3D gaming, textured surfaces would seem to have ‘bumps’, and these normal-maps were more significant, than displacement-maps – i.e., height- or depth-maps – because shaders were actually able to compute lighting subtleties more easily, using the normal-maps. But additionally, it was always quite common that ordinary 8x8x8 (R,G,B) texel-formats needed to store the normal-maps, just because images could more-easily be prepared and loaded with that pixel-format. (:1)
The old-fashioned way to code that was, that the 8-bit integer (128) was taken to symbolize (0.0), that (255) was taken to symbolize a maximally positive value, and that the integer (0) was decoded to (-1.0). The reason for this, AFAIK, was the use by the old graphics cards, of the 8-bit integer, as a binary fraction.
In the spirit of recreating that, and, because it’s sometimes still necessary to store an approximation of a normal-vector, using only 32 bits, the code has been offered as follows:
Out.Pos_Normal.w = dot(floor(normal * 127.5 + 127.5), float3(1 / 256.0, 1.0, 256.0)); float3 normal = frac(Pos_Normal.w * float3(1.0, 1 / 256.0, 1 / 65536.0)) * 2.0 - 1.0;
There’s an obvious problem with this backwards-emulation: It can’t seem to reproduce the value (0.0) for any of the elements of the normal-vector. And then, what some people do is, to throw their arms in the air, and to say: ‘This problem just can’t be solved!’ Well, what about:
// Assumed: normal = normalize(normal); Out.Pos_Normal.w = dot(floor(normal * 127.0 + 128.5), float3(1 / 256.0, 1.0, 256.0));
A side effect of this will definitely be, that no uncompressed value belonging to the interval [-1.0 .. +1.0] will lead to a compressed series of 8 zeros.
Mind you, because of the way the resulting value was now decoded again, the question of whether zero can actually result, is not as easy to address. And one reason is the fact that, for all the elements except the first, additional bits after the first 8 fractional bits, have not been removed. But that’s just a problem owing to the one-line decoding that was suggested. That could be changed to:
float3 normal = floor(Pos_Normal.w * float3(256.0, 1.0, 1 / 256.0)); normal = frac(normal * (1 / 256.0)) * (256.0 / 127.0) - (128.0 / 127.0);
Suddenly, the impossible has become possible.
N.B. I would not use the customized decoder, unless I was also sure, that the input floating-point value, came from my customized encoder. It can easily happen that the shader needs to work with texture images prepared by an external program, and then, because of the way their channel-values get normalized today, I might use this as the decoder:
float3 normal = texel.rgb * (255.0 / 128.0) - 1.0;
However, if I did, a texel-value of (128) would still be required, to result in a floating-point value of (0.0)…
(Updated 5/10/2020, 19h00… )
(As of 5/09/2020: )
Personally, I don’t know why a range of results from (-1.0 .. +1.0] might be preferable to [-1.0 .. +1.0). But if the reader finds that it is, then the following decoder will achieve that:
float3 normal = texel.rgb * (255.0 / 128.0) - (127.0 / 128.0);
In this case, an integer of (127) will achieve a floating-point value of (0.0).
(Update 5/10/2020, 8h15: )
I just checked, how ‘GIMP’ generates normal-maps. The way to do that, if all the required plug-ins are installed, is to go into the command menu and then click on ‘Filters -> Map -> Normalmap’.
Here, I was able to perform the basic operation on a single rectangle, which was uniformly grey. What I discovered was, that GIMP encodes the vector-component (0.0) to the pixel-value (127):
What I can infer from that is that in the modern era, the code which actually gets used to perform this encoding, is the code that was suggested to begin with, according to the start of my posting.
Yet, if the standard code was used to decode, then the (floating-point) value that will result is ((((127.0 / 255.0) * 2) – 1) == (-1.0 / 255.0)). If the first decoder was used, that I cited at the beginning of this posting, the floating-point value that will result is ((((127.0 / 256.0) * 2) – 1) == (-1.0 / 128.0)).
Therefore, the last decoder which I suggested in this posting, would seem to be the most appropriate to use, with externally prepared normal-maps.
(Update 5/10/2020, 19h00: )
I suppose that I could insert a little Segway, into the evolution of game-design…
The 3-component normal-maps from ‘the old days’, have now largely been replaced with 4-component texels, the 4th component of which contains either a ‘height-map’, or an ‘inverted height-map’ – aka, a ‘depth-map’ – belonging to the same (U,V) texture coordinates that the normal-map belongs to. Thus, typically, if the texel has an (RGBA) format, which would usually be a (Red, Green, Blue, Alpha) pixel format, then its components are all normalized to floating-point numbers in the interval [0.0 .. +1.0], when the GPU fetches it. And then, the following two notations, in shader script, reference a GPU register as meaning the same thing:
r0.rgba == r0.xyzw
Texels are fetched one GPU-register at a time. And for the time being, I’ve ignored the fact that the real order of the 8-bit channels in various texture images, is not ‘R, G, B, A’. The components are “Swizzled”.
That 4th texel-component is important, because in modern game-design, parallax-mapping has become important. And this concept is based on the height-map, which is converted into the accompanying normal-map, at the time the texture image is generated – with Human involvement – not, at the time the GPU makes use of it. Thus, according to the above notation, the components of GPU register ‘
r0‘ that would be dereferenced as ‘
r0.xyz‘ contain the normal-map, while ‘
r0.w‘ contains this height-map.
Using the 2D Graphical Editor named ‘GIMP’ (the “GNU Image Manipulation Program”), the image to be worked on is assumed to start out as a height-map, and must already be an (RGBA)-format image, so that when the user goes into the menu entry ‘Filters -> Map -> Normalmap’, the fields will then not be greyed out, from which the user can select what to store in the Alpha-channel.
Selecting that menu command will display a message-box, through which a user eventually transforms a height-map, into an ‘RGB’ Normal-Map, or an ‘RGBA’ (Normal+Height)-Map.