├── .gitignore ├── LICENSE ├── NormalTextureProcessor.cpp ├── NormalTextureProcessor.sln ├── NormalTextureProcessor.vcxproj ├── README.md ├── image_iterator.cpp ├── lodepng.cpp ├── lodepng.h ├── readme_clearcoat_diff.png ├── readme_heatmap.png ├── readme_lamp_render_diff.png ├── readme_sample.png ├── readme_wicker_render_diff.png ├── readtga.cpp ├── readtga.h ├── rwpng.cpp ├── rwpng.h ├── stdafx.h ├── stdio.cpp ├── targetver.h ├── test_files ├── Heightfields │ ├── grayscale_wood_floor.png │ ├── ntp_heightfield.png │ ├── r_bump_map.png │ └── squiggles.png ├── LICENSE ├── NoneOfTheAbove │ ├── black.tga │ ├── cyan.png │ ├── fade.png │ ├── gray.png │ ├── white.tga │ ├── wood_floor.png │ └── yellow.png ├── README.md ├── Standard │ ├── lava_flow_n.png │ ├── ntp_heightfield_gimp.png │ ├── ntp_heightfield_gimp_5scale.png │ ├── ntp_heightfield_normalmap_online.png │ ├── ntp_resize_128.png │ ├── ntp_resize_512.png │ ├── r_normal_map.png │ ├── r_normal_map_reversed_y.png │ ├── squiggles_gimp.png │ ├── squiggles_normalmap_online_zneg.png │ ├── squiggles_ntp_quick.png │ ├── squiggles_ntp_roundtrip.png │ └── targa_r_normal_map.tga ├── XYtextures │ └── acacia_door_bottom_n.png ├── ZZero │ ├── r_normal_map_reversed_x_0_bias_z.png │ └── squiggles_normalmap_online_zzero.png └── run_test_suite.bat ├── tga.h └── tga_decoder.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | x64 2 | .vs 3 | NormalTextureProcessor.vcxproj.filters 4 | NormalTextureProcessor.vcxproj.user 5 | test_files/_output_* 6 | test_files/*.log 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Eric Haines 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NormalTextureProcessor.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34511.84 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NormalTextureProcessor", "NormalTextureProcessor.vcxproj", "{D8E8388B-6047-4D9D-BA41-6C6ECC5FC3E4}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {D8E8388B-6047-4D9D-BA41-6C6ECC5FC3E4}.Debug|x64.ActiveCfg = Debug|x64 17 | {D8E8388B-6047-4D9D-BA41-6C6ECC5FC3E4}.Debug|x64.Build.0 = Debug|x64 18 | {D8E8388B-6047-4D9D-BA41-6C6ECC5FC3E4}.Debug|x86.ActiveCfg = Debug|Win32 19 | {D8E8388B-6047-4D9D-BA41-6C6ECC5FC3E4}.Debug|x86.Build.0 = Debug|Win32 20 | {D8E8388B-6047-4D9D-BA41-6C6ECC5FC3E4}.Release|x64.ActiveCfg = Release|x64 21 | {D8E8388B-6047-4D9D-BA41-6C6ECC5FC3E4}.Release|x64.Build.0 = Release|x64 22 | {D8E8388B-6047-4D9D-BA41-6C6ECC5FC3E4}.Release|x86.ActiveCfg = Release|Win32 23 | {D8E8388B-6047-4D9D-BA41-6C6ECC5FC3E4}.Release|x86.Build.0 = Release|Win32 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {2CEEC209-F87E-4C0C-9AFF-60F3C436F6EF} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /NormalTextureProcessor.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 17.0 23 | Win32Proj 24 | {d8e8388b-6047-4d9d-ba41-6c6ecc5fc3e4} 25 | NormalTextureProcessor 26 | 10.0 27 | 28 | 29 | 30 | Application 31 | true 32 | v143 33 | Unicode 34 | 35 | 36 | Application 37 | false 38 | v143 39 | true 40 | Unicode 41 | 42 | 43 | Application 44 | true 45 | v143 46 | Unicode 47 | 48 | 49 | Application 50 | false 51 | v143 52 | true 53 | Unicode 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Level3 76 | true 77 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 78 | true 79 | 80 | 81 | Console 82 | true 83 | 84 | 85 | 86 | 87 | Level3 88 | true 89 | true 90 | true 91 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 92 | true 93 | 94 | 95 | Console 96 | true 97 | true 98 | true 99 | 100 | 101 | 102 | 103 | true 104 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions) 105 | true 106 | Level3 107 | 108 | 109 | Console 110 | true 111 | 112 | 113 | 114 | 115 | Level4 116 | true 117 | true 118 | true 119 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 120 | true 121 | 122 | 123 | Console 124 | true 125 | true 126 | true 127 | 128 | 129 | 130 | 131 | 132 | 133 | Level4 134 | true 135 | true 136 | 137 | 138 | 139 | Level4 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NormalTextureProcessor (aka NTP) 2 | 3 | ## Description 4 | 5 | This C++ project examines normals textures (i.e., textures for applying bumps to surfaces), checking them for validity, and cleaning them up or converting them as desired. 6 | 7 | ![Sample conversion](readme_sample.png "Sample conversion") 8 | 9 | A considerable percentage of normals textures out there have badly computed Z (and sometimes X and Y) values. In practice you could always derive Z from X and Y in your shaders. However, that can have a (usually small) performance hit. Also, files for glTF and USD files, among others, expect that the XYZ values are valid. Give the model to someone else and their viewer may not fix the Z values and then give a different result. So, if you want to store XYZ triplets with proper Z values, use this program to analyze and clean up your normals textures. 10 | 11 | ### How Normals Go Bad 12 | 13 | First and foremost, it's hard for us humans to see when a normals textures _is_ bad. There's a certain color to the textures, but unless they're wildly wrong, it's not easy to notice that the Z values are incorrect. Because they're hard to detect, normals can go bad and not be noticed. That's why I wrote this program, because I kept running into incorrect normals that the creators didn't realize were wrong. 14 | 15 | There are plenty of ways that the XYZ values for a normals texture can go bad. The tool producing the normals texture, typically converting from a grayscale heightfield, can be faulty. As much as I like [Normalmap Online](https://cpetry.github.io/NormalMap-Online/), [github](https://github.com/cpetry/NormalMap-Online), I know it produces imprecise normals textures. I hope to examine and evaluate other programs as I go, which is why I provide copious algorithm notes below - there's a right way to convert from XYZ to RGB. I have also seen evidence that along the fringes of cutout objects the normals will be off; I'm not sure why. 16 | 17 | Another source of error occurs when a normals texture is resized. If you blithely change the dimensions of a texture, the new normals texture will look reasonable and probably work fairly well. But, it will likely be wrong, a little bit or a lot. Blending two or more normalized normals together will rarely give a normalized result. After resizing, the Z values stored should be rederived. X and Y values may also stray a bit, creating vectors that are illegal. Whatever the case, this program can clean up such data. That said, a more accurate way to resize would be to work from the original heightfield image and convert to a normals texture after resizing. 18 | 19 | Finally, I've heard - see [tweet replies](https://twitter.com/pointinpolygon/status/1768730543234117924) - that artists sometimes modify the separate RGB channels for effect. 20 | 21 | ## Installation 22 | 23 | Develop on Windows 10 on Visual Studio 2022 (double-click NormalTextureProcessor.sln, build Release version). You might be able to compile and run it elsewhere, as the program is pretty Windows-free and is purely command-line driven, with no graphical user interface. 24 | 25 | The executable is available for download, [zip file here](https://www.realtimerendering.com/erich/download/NormalTextureProcessor.zip). Last updated March 20, 2024. 26 | 27 | ### Test Suite 28 | 29 | Optionally, after building the Release version, you should be able to go into the **test_files** directory and double-click on **run_test_suite.bat**. As the [README in that directory](https://github.com/erich666/NormalTextureProcessor/tree/main/test_files) notes, running this test file will create various separate output directories where the results are put. You can look at **run_test_suite.bat** to see various options in use and look at the resulting files. 30 | 31 | ## Usage 32 | 33 | **Quick start:** to analyze textures in a particular directory, run the exe like so: 34 | ```sh 35 | NormalTextureProcessor.exe -a -v -idir path_to_your_input_directory 36 | ``` 37 | This will give an analysis of the textures in that directory. At the end of the analysis will be a summary like this: 38 | File types found, by category: 39 | ```sh 40 | Standard normals textures: chessboard_normal.png Knight_normal.png 41 | Standard normals textures that are not perfectly normalized: Knight_normal.png 42 | XY-only normals textures: bishop_black_normal.png bishop_white_normal.png Castle_normal.png 43 | ``` 44 | "XY-only normals textures" are ones where the analyzer found that some of the Z values stored are wrong, they don't form normalized normals. This can sometimes be on purpose, but if you're expecting RGB to convert to XYZ, then those textures are poorly formed and should be cleaned up (see more about that below). The "Standard normals textures" are basically fine. Those list as not perfectly normalized are just that: the Z's are close, but could be better. Cleaning these, too, is a fine thing to do, though usually not as critical. 45 | 46 | Currently the system is limited to reading in 8-bit PNG and TGA (Targa) textures. 16-bit PNG images can be read in, but are currently converted to 8 bits. To avoid name collisions with PNGs, Targa files read in and manipulated are output with the suffix "_TGA" added to the input file name, output as PNGs. JPEG is not supported, and I warn against using it unless forced, as its lossy compression is almost guaranteed to generate RGB's that convert to XYZ's that are not normalized. 47 | 48 | A fussy note on terminology: I use the term "normals texture" for any (usually blue-ish) texture that stores normal directions as RGB (or just RG) values. "Normal texture" can be confusing - what's an "abnormal texture"? I avoid the word "map" and always use "texture," but you'll see "normal map" commonly used instead of "normal texture" elsewhere. Technically, the "map" is how the texture is applied to a surface, a separate function irrelevant here. Various analysis messages in the program talk about "one level" or "two levels", which refers to 8-bit values. For example, if a blue channel value was expected to be 228 but was 230, that is two levels of difference. 49 | 50 | ### Analysis 51 | 52 | To list out all the command-line options, simply don't put any: 53 | ```sh 54 | NormalTextureProcessor.exe 55 | ``` 56 | 57 | This list of options will be confusing, so here are some typical combinations. To analyze all PNG and TGA textures in the current directory: 58 | ```sh 59 | NormalTextureProcessor.exe -a -v 60 | ``` 61 | The '-v' is optional. It means "verbose", giving you additional information during processing, mostly about what files were cleaned. I recommend trying it on and off to see what you're missing. 62 | 63 | A report is generated when '-a' is selected. The lists at the end of the analysis can be used to copy and paste specific files into the program itself, e.g.: 64 | ```sh 65 | NormalTextureProcessor.exe -izneg -a bishop_black_normal.png bishop_white_normal.png Castle_normal.png 66 | ``` 67 | Adding '-izneg' says to assume the incoming textures are "standard" -1 to 1 Z-value textures and analyze them as such. This setting is recommended for checking compliance with the glTF and USD standards, for example. By putting specific files on the command line, the program processes only them. This example doesn't do much, just analyzes the (already analyzed) set of files again. The main use is that you can specify individual files for other operations, such as cleanup and conversion. 68 | 69 | You can also feed in an input directory located elsewhere: 70 | ```sh 71 | NormalTextureProcessor.exe -a -idir flora/materials/normal_textures > analysis.log 2>&1 72 | ``` 73 | The "> analysis.log 2>&1" is optional, it's a way to put all the analysis and any warnings or errors into a file. 74 | 75 | There are three main types of normals textures this program recognizes. They are: 76 | * RGB "standard" normals textures: each color channel is interpreted as representing numbers that go from -1.0 to 1.0, for all channels. 77 | * RGB normal, "Z-zero": as above, but the blue, Z, channel is instead interpreted as going from 0.0 to 1.0. 78 | * heightfield texture: an elevation texture, usually a grayscale image, typically with black representing the lowest elevation and white the highest. 79 | 80 | The first type, where Z goes from -1 to 1, is the type that is the most popular nowadays. Usually this texture is applied to a surface, so that the normals on the texture are relative to the interpolated (shading) normal of the surface itself. So, for example, a curved surface made of polygons is rendered. At a given surface location (typically) the shading normal is interpolated from vertex normals, defining the normal (Z) direction of the surface, along with X and Y basis vectors. This is known as ["tangent space"](https://80.lv/articles/tutorial-types-of-normal-maps-common-problems/). The normal is retrieved from the normals texture and modifies the surface normal. 81 | 82 | This first type has a few variants to it. One is that Y direction on the texture can be up or down. That is, the normals texture can be thought of as "X to the right, Y up" for how its normal is interpreted. A normal of, say, (0.2, 0.3, 0.933) can be thought of as pointing 0.2 to the right, 0.3 up. That is the OpenGL interpretation. The normal's Y direction could also be considered as pointing down, i.e., think of the upper left corner as the origin and Y pointing down. This is the DirectX interpretation. See [this page](https://github.com/usd-wg/assets/tree/main/test_assets/NormalsTextureBiasAndScale) for more information and examples, and search [this page](https://80.lv/articles/tutorial-types-of-normal-maps-common-problems/) for "invert" for a wider view. OpenGL and DirectX are two major different forms you'll run across, i.e., no one reverses the X direction normally. If you use the wrong type of texture in a renderer you will see artifacts such as objects looking like they're being lit from below, for example. The glTF file format requires the OpenGL style normals texture, USD assumes OpenGL but can use scale and bias settings to support DirectX. Currently the analysis program does not have code to tell OpenGL-style files from DirectX-style files. I have some ideas how to guess using statistics as to which format a normals texture is, but haven't examined the problem yet. 83 | 84 | For this first type, all (8-bit) values map from value 0 through 255 to the range -1.0 to 1.0. However, if you think about putting normals on a surface, a value where Z is less than 0.0 makes little sense. It would define a normal pointing into the surface, but the surface itself is at best thought of as a displaced heightfield (faked by using a normals texture instead), so there should not be any overhangs or other geometry where the Z value could go negative. One of the checks the analysis mode "-a" does is checks if the Z value is ever lower than 0.0. If it does, this is flagged as a problem. 85 | 86 | The second type of normals texture, what I call "Z-zero", maps the 0 to 255 8-bit blue channel value to the range 0.0 to 1.0, instead of -1.0 to 1.0. There then can be no negative values for Z, and the precision of the Z value can be one bit higher. This all sounds good, but this format is (in my experience) now rarely seen. If anyone understands this evolutionary step, let me know. The reality is that this format is not supported in glTF. It can be supported in USD by using bias and scale values. Again, see [this page](https://github.com/usd-wg/assets/tree/main/test_assets/NormalsTextureBiasAndScale) for USD examples and more information. It is difficult to simply look at a "Z-zero" texture and tell it apart from a normals texture of the (now-popular) first type, especially if the bumps are not sharp. By analyzing the Z values of the input texture and seeing whether they are negative and whether the (X,Y,Z) triplet formed has a vector length of nearly 1.0, we can usually tell these two types apart. That is, the program tries both conversions, standard and Z-zero, and sees which of the two normals formed is approximately 1.0 in length. This determination is the major reason this program exists, as sometimes incorrect textures slip through into test suites. 87 | 88 | Another problem sometimes seen with both types of normals texture is that just the X and Y values (ignoring Z) form a vector that is longer than 1.0. This can happen due to (poor) resizing or filtering of normals textures, slightly incorrect conversion formulas, or who knows what else. The analysis notes how many (X,Y) vectors are too long. 89 | 90 | The first two types, "standard" and "Z-zero", map red and green to X and Y in the same way, with the possible reversal of Y for the DirectX style texture. Given that Z cannot be a negative value (for most applications), some applications forego saving a Z value channel at all. The Z value is simply derived by sqrt(1.0 - (X\*X + Y\*Y)). For example, the [LabPBR Material](https://shaderlabs.org/wiki/LabPBR_Material_Standard) used for Minecraft modding specifies the texture holding normals as X and Y in red and green (in DirectX format), ambient occlusion is stored in the blue channel, and alpha holds the heightfield texture. The analyze mode attempts to identify these types of textures by looking at the Z values and see if the vector XYZ forms unit (length of 1.0) normals or if the Z's are independent values. If the latter, these normals textures are called "XY-only" textures in the analysis report. 91 | 92 | A few heuristics are used to classify textures that don't clearly fall into a single category. The analysis will give one or more reasons why a texture is categorized as a particular type. Two tolerances, -etol and -etolxy, can be adjusted to be less or more strict as to what falls in a category. The '-etol' option specifies a percentage of normal lengths that can be a fair bit far from 1.0 before the texture is categorized as not being a standard normals texture. The '-etolxy' option gives how much the average of all of the X or Y values can vary from 0.0, which is usually the average for most normals textures formed. If these averages are off by more than 0.1 (the default), the input image may not even be an XY texture. 93 | 94 | There are other variants of using XYZ coordinates, such as ["best fit normals" (BFN)](https://sebh-blog.blogspot.com/2010/08/cryteks-best-fit-normals.html). As we've seen, storing the Z value is superfluous, adding no real information, since it can be derived from X and Y. XYZ define a sphere (or hemisphere) of length 1, meaning a huge number of XYZ triplets are invalid, what this program fixes. It means that, of the 256-cubed (16 million) possible 8-bit RGB triplets storable in a texel, only 51,040 (0.3%) form valid XYZ triplets. BFN lets all RGB triplets be valid (assuming blue is mapped to Z 0 to 1, or B < 128 is not allowed), with the idea being that the corresponding XYZ formed will always then be normalized. This program currently ignores the possibility that the normals texture is a BFN. Detection is perhaps possible by trying to convert back to a heightfield and see if the result is valid by some metric. 95 | 96 | A different class of normals texture is the heightfield texture, often called a bump or height map. The format is usually a single channel (such as the red channel) or a grayscale image. Black is usually the lowest level, white the highest elevation. Analysis tries some basic testing to see if a texture may be a heightfield texture. Heightfield textures are often used to create normals textures, e.g., [GIMP has this functionality](https://docs.gimp.org/en/gimp-filter-normal-map.html). There is a scale (not stored in the image) that needs to be provided for the heightfield so that the conversion can be done. I have occasionally seen heightmap textures get intermingled with normals textures, so the program tries to identify these. 97 | 98 | You can also output a heatmap of where errors are in your input image file (except for heightfields). Specify '-oheatmap' says to make the output file show these. The red channel is set to 255 wherever the texel has X and Y values that give a vector length > 1.0 + epsilon (of 1/255), red of 128 if the length is just barely above 1.0. For input normals textures categorized as standard or Z-zero, the green channel shows a medium green when the texel's XYZ normal length, converted to RGB, is off by two color levels, and shows full green when 3 levels or more. The blue channel shows negative Z values for standard input normal textures, starting with 128 for slightly negative scaling up to 255 for Z stored as -1. Remember that "XY-only" normal textures can be forced to be interpreted as standard by '-izneg', which will then highlight the XYZ length differences found. 99 | 100 | Here's an example run to generate a heatmap of the test file "ntp_resize_512.png", which has normalization errors due to resizing, and the result: 101 | ```sh 102 | .\x64\Release\NormalTextureProcessor.exe -idir test_files\Standard -odir test_files\output_Heatmap -oall -a -v -oheatmap ntp_resize_512.png 103 | ``` 104 | ![Heatmap of ntp_resize_512.png](readme_heatmap.png "Heatmap of ntp_resize_512.png") 105 | 106 | You can also dump out the bad pixels found in a file, or all pixel values and their conversions. Add the '-csve' argument to dump to the output directory a comma-separated-value file of texels that are not "perfect" (roundtrippable - see "Algorithms"), or '-csv' to dump all texel values. You also _must_ specify the input file type (-izneg|-izzero|-ixy) as the CSV file is generated as the file is read in, before any analysis. Here's an example: 107 | ```sh 108 | .\x64\Release\NormalTextureProcessor.exe-idir test_files\Standard -odir test_files\output_Standard -oall -a -v squiggles_normalmap_online_zneg.png -izneg -csve 109 | ``` 110 | which will generate the file test_files/output_Standard/squiggles_normalmap_online_zneg.csv. The rightmost columns will show what sort of bad thing is happening with each texel: whether it's near (1 or 2 levels) or far (X) away from being an XY length below 1.0, whether Z is as expected or not, and whether the stored Z was negative. 111 | 112 | It's important to look at the normal length vs. variation in the separate XYZ values. An an example, say a texel's RGB is (127,0,139). XYZ=(-0.00392, -1.0, 0.09019). The vector length is 1.004066. If you cleaned this pixel, holding X and Y constant, the blue value would change, to (127,0,128). The difference in blue is large: 139 vs. 128, 0.09 vs. 0.00. The problem is that an extreme Y value, where G=0 gives Y=-1.0, forces Z to be as small as possible, B=128. 113 | 114 | This program considers (127,0,139) to be correct. It tests by varying the RGB values of the triplet by +/-1, converts, and looks at the normal lengths formed for all nine triplet combinations. If at least one triplet gives a normal length of less than or equal to 1.0 and at least one has normal length of greater than or equal to 1.0, then the triplet is considered valid. If you want something more exact, change the code! Only if a triplet gives a normal outside this range is the texture flagged as in need of cleaning. 115 | 116 | ### Cleanup and Conversion 117 | 118 | In addition to analysis, this program cleans up and convert to different formats. Specify an output directory with '-odir _out_directory_name_'. The output file name will match the input file name, though always uses PNG for output. Be careful: if the input and output directories are the same or both are not specified (and so match), texture files will be written over in place. 119 | 120 | Typical operations include: 121 | * Clean up the normals in a texture. Use option '-oclean' to output only those files that need cleanup, '-oall' to output all files. Given a particular type of RGB texture, the program makes sure the normals are all properly normalized and that no Z values are negative. 122 | * If you want to maintain the (possibly negative) sign of the Z value, use the '-allownegz' option. 123 | * If you set the input file type (-izneg|-izzero|-ixy), '-oclean' will clean all files found to not be of that type. 124 | * Convert between formats. 125 | * OpenGL to DirectX, standard to Z-zero, or vice versa. OpenGl-style is the default, use '-idx' to note that the input files are DirectX-style (does not affect heightfields), '-odx' to set that you want to output in DirectX-style. 126 | * To export to the Z-Zero format, where Z goes from 0 to 1 instead of -1 to 1, use '-ozzero'. 127 | * Heightfields can be converted to RGB textures of any type, using the options above. 128 | * To force all input files to be treated as heightfields, use '-ihf'. 129 | * To specify how the input heightfield data is scaled, use '-hfs #', where you give a scale factor. The value is 5.0 by default. 130 | * By default heightfields are considered to repeat, meaning that along the edges heights "off the texture" are taken from the opposite edge. TBD: do the borders wrap or are they "doubled" or something else, such as maintaining slope. '-hborder' 131 | 132 | This cleanup process comes with a caveat: garbage in, garbage out. If your normals textures have XY values that are trustworthy and you know the Z values are wonky for some reason, this tool can properly fix the Z values, deriving from XY. If your normals are just plain unnormalized in general, where X, Y, and Z are the right direction but the wrong length (see "BFN"), this tool will not properly fix your texture, since it assumes X and Y are their proper lengths and Z was badly derived. The program itself cannot determine which type of error is occurring, so it assumes that only the Z value is invalid. Also, if an image is cleaned, currently _all_ normals will be examined cleaned and made "roundtrippable" (see below). Basically, if bad normals are found, it's assumed all Z values should be rederived. 133 | 134 | Tools such as [BeyondCompare](https://www.scootersoftware.com/) are worthwhile for comparing the original image with its modified version. You can hover over individual texels and see how the values have changed. Here's an example diff, with the tolerance set to 2 texel levels difference in any channel; red is more, blue is equal or less, gray is no difference. 135 | 136 | ![Comparison of clearcoat_normal.png, original vs. cleaned](readme_clearcoat_diff.png "Comparison of clearcoat_normal.png, original vs. cleaned") 137 | 138 | In this example, from [glTF-sample-assets](https://github.com/KhronosGroup/glTF-Sample-Assets/blob/main/Models/ClearcoatWicker/glTF/clearcoat_normal.png), you can see that the more curved areas are far off. For example, texel X:155, Y:48 has an RGB value of (100,145,240). This gives an XYZ normal of (-0.216,0.137,0.882), which has a normal length of 0.918, well short of 1.0. Assuming X and Y are valid and the blue channel was formed incorrectly, we recompute Z as 0.967 to make a normal of length 1.0, which translates to a blue value of 251. 139 | 140 | Does it matter? Using the original vs. corrected clearcoat_normal.png with the other files in the [ClearcoatWicker](https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main/Models/ClearcoatWicker/glTF) test directory, using Don McCurdy's (excellent) [glTF Viewer](https://gltf-viewer.donmccurdy.com/), we get these two renderings, diff'ed: 141 | 142 | ![ClearcoatWicker, original vs. normalization corrected](readme_wicker_render_diff.png "ClearcoatWicker, original vs. normalization corrected") 143 | 144 | Visually the changes are subtle. In the middle along the vertical seam the white highlight shapes are different. In the indentations along the seam at the very bottom, the original image has darker ones. 145 | 146 | In other tests, such as the [StainedGlassLamp](https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main/Models/StainedGlassLamp), where the goal is to accurately capture the real-world model, fixing the four normals textures for this model also results in slightly different renderings: 147 | 148 | ![StainedGlassLamp, original vs. normalization corrected](readme_lamp_render_diff.png "StainedGlassLamp, original vs. normalization corrected") 149 | 150 | Long and short, storing the normals incorrectly is a source of error. This program can, at the minimum, tip you off when your normals are not or poorly normalized, and provide properly normalized textures by correcting the Z values. 151 | 152 | ## Algorithms 153 | 154 | The key thing is to get the conversion right. Here's how to go from an 8-bit color channel value "rgb" to the floating-point range -1.0 to 1.0 for "xyz": 155 | ```sh 156 | x = ((float)r / 255.0f) * 2.0f - 1.0f; 157 | y = ((float)g / 255.0f) * 2.0f - 1.0f; 158 | z = ((float)b / 255.0f) * 2.0f - 1.0f; 159 | ``` 160 | In [UsdPreviewSurface](https://openusd.org/release/spec_usdpreviewsurface.html#texture-reader) terms, the 2.0f is the scale, the -1.0f is the bias. Note that the RGB triplet is assumed to map directly to linear values -1.0 through 1.0, there's no sRGB version or similar. In other words, the data is considered to be "just numbers" and not some gamma-corrected color. 161 | 162 | If you want to go to the range 0.0 to 1.0, it's simpler still. Here's Z: 163 | ```sh 164 | z = (float)(b) / 255.0f 165 | ``` 166 | 167 | If you want to compute Z from X and Y, it's: 168 | ```sh 169 | z = sqrt(1.0f - (x * x + y * y)); 170 | ``` 171 | In practice I recommend always deriving Z from X and Y in your shaders, even if Z is available, as a considerable percentage of normals textures examined often have bad Z (blue) values stored. 172 | 173 | Getting the rounding right for the return trip is important. Here's that: 174 | ```sh 175 | r = (unsigned char)(((x + 1.0f)/2.0f)*255.0f + 0.5f); 176 | g = (unsigned char)(((y + 1.0f)/2.0f)*255.0f + 0.5f); 177 | b = (unsigned char)(((z + 1.0f)/2.0f)*255.0f + 0.5f); 178 | ``` 179 | The final 0.5f rounding value on each line is important for consistency of conversion, to make roundtripping work properly. I believe that not adding in the 0.5f and rounding is the source of error in various normals textures creators. See the analysis below of GIMP's (incorrect) formula for more explanation. 180 | 181 | The code above shows where the bias of 1.0 and scale of 2.0 are applied. The equations can be rearranged to: 182 | ```sh 183 | r = (unsigned char)((0.5f*x + 0.5f)*255.0f + 0.5f); 184 | g = (unsigned char)((0.5f*x + 0.5f)*255.0f + 0.5f); 185 | b = (unsigned char)((0.5f*x + 0.5f)*255.0f + 0.5f); 186 | ``` 187 | 188 | For Z-zero to the blue channel, the conversion is simply: 189 | ```sh 190 | b = (unsigned char)(z*255.0f + 0.5f); 191 | ``` 192 | 193 | See the code in the SCRATCHPAD section of NormalTextureProcessor.cpp for a roundtrip test and other procedures. 194 | 195 | This program aims to make it so that rgb -> xy -> derive z from xy -> rgb will give the same rgb's. This is not a given. For example, say you have computed an XYZ triplet: 196 | ```sh 197 | x = -0.423667967 198 | y = 0.271147490 199 | z = 0.864282646 200 | ``` 201 | This vector has a length of 0.99999999988 - very close to 1.0. The Z value is precise. 202 | 203 | If you convert each channel directly to 8-bit rgb, you get: 204 | ```sh 205 | r = 73 // actually 73.9823342075, drop the fraction 206 | g = 162 // 162.571304975, drop the fraction 207 | b = 238 // 238.196037365, drop the fraction 208 | ``` 209 | Convert rgb back to XYZ, you get similar values to those found before, but not quite the same: 210 | ```sh 211 | x = -0.4274509 // a little higher magnitude than -0.423667967 212 | y = 0.2705882 // a tiny bit lower than 0.271147490 213 | z = 0.8666666 // a bit higher than 0.864282646 214 | ``` 215 | The vector formed is also less precise, 1.0035154. This is to be expected with 8-bit numbers representing XYZ values. Say we immediately convert this xyz back to rgb: 216 | ```sh 217 | r = 73 // in fact, 73.50001025, drop the fraction, right in the middle of the unsigned char 218 | g = 162 // 162.4999955, drop the fraction 219 | b = 238 // 238.4999915, drop the fraction. Came from converting z=0.8666666 220 | ``` 221 | No surprise, each floating-point value is right in the middle of the "range" for each RGB value, with each fraction being near 0.5. This is a good reality check that our functions for rgb -> xyz and xyz -> rgb are solid in their own right. 222 | 223 | The problem happens if we now derive Z from x and y, instead of using the converted z value of 0.8666666: 224 | ```sh 225 | z = sqrt(1.0 - (-0.4274509*-0.4274509 + 0.2705882*0.2705882)) 226 | z = 0.8625936 // different than 0.8666666 saved in the b channel 227 | ``` 228 | 229 | Converting this xyz, with the new z value computed from the new vector length, back to rgb, we get: 230 | ```sh 231 | r = 73 // again 73.50001025 232 | g = 162 // again 162.4999955 233 | b = 237 // 237.980684, dropping the fraction gives 237 234 | ``` 235 | This b value is one level less than the original 238 we computed for it. This difference happens because we lose precision converting from xyz to rgb the first time, so that when we convert from rgb the second time and then compute Z from X and Y, the X and Y being different causes Z to be enough different that it changes a level. 236 | 237 | What is interesting at this point is that "rgb -> xy -> derive z from xy -> rgb" is now stable. The values "rg" never change, so X and Y don't change, so the Z derived from them is also the same, giving the same "b" value when converted back. With b=237, Z is 0.8588235. The normal length of this new XYZ is 0.9967497, which is in this case ever so slightly better than 1.0035154 (it's 0.0032503 away from 1.0, vs. 0.0035154). 238 | 239 | What roundtripping implies is that the length of the normal being close to 1.0 is the metric that controls things. If you convert r and g to X and Y, then use these to compute Z and convert to "b", you have the same "b", a "b" that gives a normal as close as possible to 1.0, given r and g. The alternative is to treat X, Y, and Z as entirely separate channels having nothing to do with each other once we have the values in floating point. However, doing so means round-tripping doesn't work. This in turn means (admittedly usually tiny) differences with shaders: one shader uses Z computed from the stored b, another uses Z computed from r and g, and these Z's are sometimes off by a texel level. 240 | 241 | This program aims to provide stable rgb triplets, ones where if the Z value is derived from X and Y, the "b" value is the same as the original value. Internally, this means that we always derive the Z value from the 8-bit r and g values, as shown, so that the result "b" value is the same as the derived "b". When reading in normals rgb textures, the program always derives the Z value as explained, so gives consistent results. That is, if you clean up a normals texture, the program's clean image will not change if you clean it again. Where this principle affects the code a bit more is when using a heightfield to compute an XYZ normal. As shown above, to make a roundtrippable rgb triplet, we have to convert XY to rg, then rg back to let's call it XY', the XY values that come from the 8-bit precision r and g values. Then Z is computed as usual and the b value created from it. 242 | 243 | ## Creator Testing 244 | 245 | Ideally, it would be great if all tools creating normals textures did things perfectly. I examined some tools' code to see if they were correct. 246 | 247 | ### GIMP 248 | 249 | [GIMP](https://www.gimp.org/) has a tool to make [normals textures from heightfields](https://docs.gimp.org/en/gimp-filter-normal-map.html). It's easy to use, with lots of options for different mappings. The code is [here](https://code.google.com/archive/p/gimp-normalmap/source/default/source) and [here](https://github.com/RobertBeckebans/gimp-plugin-normalmap), with normalmap.c appearing to be the key piece. 250 | 251 | Looking at the code, I noticed a problem with conversion back to RGB. [Lines 1056-1058 of normalmap.c](https://github.com/RobertBeckebans/gimp-plugin-normalmap/blob/master/normalmap.c#L1056) are: 252 | ```sh 253 | *d++ = (unsigned char)((n[0] + 1.0f) * 127.5f); 254 | *d++ = (unsigned char)((n[1] + 1.0f) * 127.5f); 255 | *d++ = (unsigned char)((n[2] + 1.0f) * 127.5f); 256 | ``` 257 | This is not quite right, as there's no rounding value added in. The code should be: 258 | ```sh 259 | *d++ = (unsigned char)((n[0] + 1.0f) * 127.5f + 0.5f); 260 | *d++ = (unsigned char)((n[1] + 1.0f) * 127.5f + 0.5f); 261 | *d++ = (unsigned char)((n[2] + 1.0f) * 127.5f + 0.5f); 262 | ``` 263 | Proof by examination: say I convert X=-0.999 and X=0.999. By the original code I would get R=0 and R=254. I would expect symmetric results, R rounding to the same limits. With the corrected formula I get R=0 and R=255, maintaining symmetry. Another proof is that X=0 converts to 127.5 in the original code, 128.0 in the new. The value 127.5 has a logic to it, being exactly between 0.0 and 255.0, but the fraction 0.5 is then dropped instead of rounded. By adding 0.5 we get 128.0, which in floating point terms is right on the border between 127 and 128 (that is, level 127 goes from 127.0 to 127.99999...). So, adding 0.5 and rounding is correct, as 127 and 128 is the dividing line between negative and positive X (or Y or Z) values. 264 | 265 | What makes this all a bit confusing is that X=0.0 doesn't have an exact 8-bit RGB equivalent, since 127/128 is the dividing line. Whenever I see a B value of 127 stored in a normals texture, I suspect the round-off was done incorrectly, because 127 converts to a slightly negative value, -0.003921. 266 | 267 | Note that my corrected code here would affect all channels, R, G, and B, so that the XY values stored in RG are also slightly off. With the original, incorrect code, they're a bit more negative than expected because of the missing roundoff value. 268 | 269 | ### Normals Online 270 | 271 | [Normalmap Online](https://cpetry.github.io/NormalMap-Online/), [github](https://github.com/cpetry/NormalMap-Online), has interactive conversion of heightfields to normals textures. The results are good, with normals appearing to be properly formed. The code [here](https://github.com/cpetry/NormalMap-Online/blob/gh-pages/javascripts/shader/NormalMapFromPicturesShader.js#L53) and [here](https://github.com/cpetry/NormalMap-Online/blob/gh-pages/javascripts/shader/NormalMapShader.js#L83) looks fine, it normalizes the data before assignment. I assume gl_FragColor properly rounds XYZ values fed to it. 272 | 273 | ## Resources 274 | 275 | Some resources I've found useful: 276 | * [Types of Normal Maps & Common Problems](https://80.lv/articles/tutorial-types-of-normal-maps-common-problems/) - a useful page, talking about what uses OpenGL vs. DirectX format by default, XY format, and other aspects, such as not gamma-correcting your normals textures (something I currently don't test for). 277 | * [Normalmap Online](https://cpetry.github.io/NormalMap-Online/), [github](https://github.com/cpetry/NormalMap-Online) - a web-based normal map creation tool, it takes heightfields and converts to normals textures. By default, Z is mapped from 0 to 1 (old style), but there is a "Z Range" option (that I helped add) that lets you choose -1 to 1. A bit confusing, but what you do is click on the "wavy rings" heightmap image itself. A file dialog comes up and you load your own image. You can then see the RGB texture formed, and the texture applied to a cube in the right. Mouse-drag to change the view of the cube. Lots of options, including true displacement, so you can see the difference between that and just normals textures. Note: the precision of the conversion is not perfect, with some computed normal lengths varying up to four levels from the correct answer. 278 | * [GIMP conversion](https://docs.gimp.org/en/gimp-filter-normal-map.html) - how to make normals textures from heightfields in GIMP. 279 | * [Blender's documentation](https://docs.blender.org/manual/en/2.79/render/blender_render/textures/properties/influence/bump_normal.html) - pointers to the format used and baking polygon meshes to normals textures. 280 | * Other tools to test: [ShaderMap](https://shadermap.com/), [DeepBump](https://hugotini.github.io/deepbump.html), [PixelGraph](https://github.com/null511/PixelGraph-Release); more recommendations appreciated! Fixing the tools themselves is the best long-term solution. 281 | * [glTF 2.0 specification](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html) - has much about normals textures. I [filed a pull request](https://github.com/KhronosGroup/glTF-Sample-Assets/pull/114) submitting a large number of fixed normals textures for the glTF Sample Assets repository. 282 | * [USD Normals Texture Bias And Scale](https://github.com/usd-wg/assets/tree/main/test_assets/NormalsTextureBiasAndScale) - a set of test normals textures of different types in a USD file, along with copious documentation of how each cube is formed. I made this test. For USD, I [filed a bug](https://github.com/PixarAnimationStudios/OpenUSD/issues/3013) reporting that one (of the two) normalMap.png images in the OpenUSD repository is off. 283 | * [UsdPreviewSurface](https://openusd.org/release/spec_usdpreviewsurface.html#texture-reader) - the USD specification's basic material. Search on "normal3f" to see more about how to specify it. 284 | * [_Survey of Efficient Representations for Independent Unit Vectors_](https://jcgt.org/published/0003/02/01/) - Storing normals as XYZ triplets is wasteful, since Z could just be computed from X and Y in most usages. Even storing just X and Y gives pairs of numbers that are unusable, forming vectors longer than 1.0, and the precision is poorly distributed for the rest. This paper and code gives some better alternate representations. 285 | 286 | ## License 287 | 288 | MIT license. See the [LICENSE](https://github.com/erich666/NormalTextureProcessor/blob/main/LICENSE) file in this directory. 289 | 290 | ## Acknowledgements 291 | 292 | Thanks to Nick Porcino for the quick check on my conversion equations. Thanks to Koen Vroeijenstijn for discussions about 16-bit PNG formats, and John Stone for even higher precision representations. 293 | 294 | ## Roadmap 295 | 296 | Potential tasks (no promises! and, suggestions welcome): 297 | 298 | - [ ] Figure out way (if any) to tell if a texture is OpenGL or DirectX oriented. I suspect there's some curvature analysis, perhaps converting into a heightfield, that could be done. For example, take differences between texels horizontally and vertically and see the correspondence - bumps are more likely than hyperboloids. Or maybe it's not possible (well, it certainly isn't if there's no Y variance). 299 | - [ ] Convert back from normals textures to heightfields. Not sure this is useful, but might be worth adding, for visualization and analysis. Sources of information: [ShaderToy](https://www.shadertoy.com/view/XcjXDc), [Stannum](https://stannum.io/blog/0IwyJ-) and [code](https://stannum.io/blog/data/debump.c), [tweet](https://mastodon.gamedev.place/@castano/110396678556411922), [solver presentation](https://www.activision.com/cdn/research/MaterialAdvancesInWWII.pdf), [paper](https://webdocs.cs.ualberta.ca/~vis/courses/CompVis/readings/photometric/FrankotIntegrpami88.pdf) _(thanks to Ignacio Castaño for these links)_ 300 | - [ ] Ignore texels that are black or white (probably unused in texture, except for heightfields). Adjust statistics, CSV output, etc. 301 | - [ ] Support 16-bit PNG files fully, input and output. Also properly convert channel values that have less than 8 bits. 302 | - [ ] Ignore texels with alpha values of 0. 303 | - [ ] Recursively examine directories and clean up normals texture files, searching by file type. If the output directory is specified, cleaned or heatmap files would be put there, without the corresponding subdirectory paths (possibly overwriting results, if name collisions). 304 | -------------------------------------------------------------------------------- /image_iterator.cpp: -------------------------------------------------------------------------------- 1 | // Aseprite TGA Library 2 | // Copyright (C) 2020 Igara Studio S.A. 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "tga.h" 8 | 9 | namespace tga { 10 | namespace details { 11 | 12 | ImageIterator::ImageIterator() 13 | : m_image(nullptr) 14 | { 15 | } 16 | 17 | ImageIterator::ImageIterator(const Header& header, Image& image) 18 | : m_image(&image) 19 | , m_x(header.leftToRight() ? 0: header.width-1) 20 | , m_y(header.topToBottom() ? 0: header.height-1) 21 | , m_w(header.width) 22 | , m_h(header.height) 23 | , m_dx(header.leftToRight() ? +1: -1) 24 | , m_dy(header.topToBottom() ? +1: -1) 25 | { 26 | calcPtr(); 27 | } 28 | 29 | bool ImageIterator::advance() 30 | { 31 | m_x += m_dx; 32 | m_ptr += m_dx*m_image->bytesPerPixel; 33 | 34 | if ((m_dx < 0 && m_x < 0) || 35 | (m_dx > 0 && m_x == m_w)) { 36 | m_x = (m_dx > 0 ? 0: m_w-1); 37 | m_y += m_dy; 38 | if ((m_dy < 0 && m_y < 0) || 39 | (m_dy > 0 && m_y == m_h)) { 40 | return true; 41 | } 42 | calcPtr(); 43 | } 44 | return false; 45 | } 46 | 47 | void ImageIterator::calcPtr() 48 | { 49 | m_ptr = 50 | m_image->pixels 51 | + m_image->rowstride*m_y 52 | + m_image->bytesPerPixel*m_x; 53 | } 54 | 55 | } // namespace details 56 | } // namespace tga 57 | -------------------------------------------------------------------------------- /readme_clearcoat_diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/readme_clearcoat_diff.png -------------------------------------------------------------------------------- /readme_heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/readme_heatmap.png -------------------------------------------------------------------------------- /readme_lamp_render_diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/readme_lamp_render_diff.png -------------------------------------------------------------------------------- /readme_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/readme_sample.png -------------------------------------------------------------------------------- /readme_wicker_render_diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/readme_wicker_render_diff.png -------------------------------------------------------------------------------- /readtga.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2021, Eric Haines 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 19 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 25 | THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | #include "stdafx.h" 29 | #include "readtga.h" 30 | 31 | #include 32 | 33 | #include 34 | 35 | // calls https://github.com/aseprite/tga 36 | 37 | //Decode from disk to raw pixels with a single function call 38 | // return 0 on success 39 | int readtga(progimage_info *im, wchar_t *filename, LodePNGColorType colortype) 40 | { 41 | im->width = im->height = 0; 42 | 43 | FILE* f; 44 | _wfopen_s(&f, filename, L"rb"); 45 | tga::StdioFileInterface file(f); 46 | tga::Decoder decoder(&file); 47 | tga::Header header; 48 | if (!decoder.readHeader(header)) 49 | return 102; 50 | 51 | bool match = false; 52 | int channels_in = header.bytesPerPixel(); 53 | switch (channels_in) { 54 | case 1: 55 | if ( colortype == LCT_GREY ) { 56 | match = true; 57 | } 58 | break; 59 | case 3: 60 | if (colortype == LCT_RGB) { 61 | match = true; 62 | } 63 | break; 64 | case 4: 65 | if (colortype == LCT_RGBA) { 66 | match = true; 67 | } 68 | break; 69 | default: 70 | // unsupported file type, we assume 71 | return 104; 72 | } 73 | 74 | im->width = (int)header.width; 75 | im->height = (int)header.height; 76 | 77 | tga::Image image; 78 | image.bytesPerPixel = header.bytesPerPixel(); 79 | image.rowstride = header.width * header.bytesPerPixel(); 80 | 81 | std::vector buffer(image.rowstride * header.height); 82 | image.pixels = &buffer[0]; 83 | 84 | if (!decoder.readImage(header, image, nullptr)) 85 | return 103; 86 | 87 | // Optional post-process to fix the alpha channel in 88 | // some TGA files where alpha=0 for all pixels when 89 | // it shouldn't. 90 | decoder.postProcessImage(header, image); 91 | 92 | if (match) { 93 | im->image_data = buffer; 94 | } 95 | else { 96 | int channels_out = 0; 97 | 98 | // color types don't match - convert from one to the other 99 | switch (channels_in) { 100 | case 1: 101 | // gray to RGB or RGBA 102 | channels_out = (colortype == LCT_RGB) ? 3 : 4; 103 | break; 104 | case 3: 105 | // RGB to gray or RGBA 106 | channels_out = (colortype == LCT_RGBA) ? 1 : 4; 107 | break; 108 | case 4: 109 | // RGBA to gray or RGB (ignore alpha, I guess...) 110 | channels_out = (colortype == LCT_RGB) ? 3 : 1; 111 | break; 112 | } 113 | int num_pixels = header.width * header.height; 114 | im->image_data.resize(channels_out * num_pixels); 115 | int i; 116 | unsigned char* src_data = &buffer[0]; 117 | unsigned char* dst_data = &im->image_data[0]; 118 | switch (channels_in) { 119 | case 1: 120 | // gray to RGB or RGBA 121 | for (i = 0; i < num_pixels; i++) { 122 | if (channels_out == 4) { 123 | *dst_data++ = *src_data; 124 | } 125 | *dst_data++ = *src_data; 126 | *dst_data++ = *src_data; 127 | *dst_data++ = *src_data++; 128 | } 129 | break; 130 | case 3: 131 | // RGB to gray or RGBA 132 | if (channels_out == 4) { 133 | for (i = 0; i < num_pixels; i++) { 134 | *dst_data++ = *src_data++; 135 | *dst_data++ = *src_data++; 136 | *dst_data++ = *src_data++; 137 | *dst_data++ = 255; 138 | } 139 | } 140 | else { 141 | // RGB to gray? Why? Is the incoming RGB image actually gray? 142 | assert(0); 143 | for (i = 0; i < num_pixels; i++) { 144 | *dst_data++ = *src_data++; 145 | // skip green and blue 146 | src_data++; 147 | src_data++; 148 | } 149 | } 150 | break; 151 | case 4: 152 | // RGBA to gray or RGB (ignore alpha, I guess...) 153 | if (channels_out == 3) { 154 | for (i = 0; i < num_pixels; i++) { 155 | *dst_data++ = *src_data++; 156 | *dst_data++ = *src_data++; 157 | *dst_data++ = *src_data++; 158 | src_data++; // skip alpha 159 | } 160 | } 161 | else { 162 | // RGBA to gray? Why? Is the incoming RGB image actually gray? 163 | assert(0); 164 | for (i = 0; i < num_pixels; i++) { 165 | *dst_data++ = *src_data++; 166 | // skip green and blue and alpha 167 | src_data++; 168 | src_data++; 169 | src_data++; 170 | } 171 | } 172 | break; 173 | } 174 | } 175 | 176 | return 0; 177 | } 178 | 179 | // same as PNG's 180 | //void readtga_cleanup(int mode, progimage_info *im) 181 | //{ 182 | // // mode was important back when libpng was in use 183 | // if ( mode == 1 ) 184 | // { 185 | // im->image_data.clear(); 186 | // } 187 | //} 188 | 189 | int readtgaheader(progimage_info* im, wchar_t* filename, LodePNGColorType& colortype) 190 | { 191 | im->width = im->height = 0; 192 | 193 | FILE* f; 194 | _wfopen_s(&f, filename, L"rb"); 195 | tga::StdioFileInterface file(f); 196 | tga::Decoder decoder(&file); 197 | tga::Header header; 198 | if (!decoder.readHeader(header)) 199 | return 102; 200 | 201 | im->width = (int)header.width; 202 | im->height = (int)header.height; 203 | 204 | switch (header.bytesPerPixel()) { 205 | case 1: 206 | colortype = LCT_GREY; 207 | break; 208 | default: 209 | assert(0); 210 | case 3: 211 | colortype = LCT_RGB; 212 | break; 213 | case 4: 214 | colortype = LCT_RGBA; 215 | break; 216 | } 217 | 218 | fclose(f); 219 | 220 | return 0; 221 | } 222 | 223 | //============================ 224 | // Yes, these should be in yet another separate file, but alas 225 | 226 | int readImage(progimage_info* im, wchar_t* filename, LodePNGColorType colortype, int imageFileType) 227 | { 228 | if (imageFileType == 1) { 229 | return readpng(im, filename, colortype); 230 | } else if (imageFileType == 2) { 231 | return readtga(im, filename, colortype); 232 | } 233 | assert(0); 234 | // unknown image type 235 | return 1; 236 | } 237 | 238 | void readImage_cleanup(int mode, progimage_info* im) 239 | { 240 | // same for TGA 241 | readpng_cleanup(mode, im); 242 | } 243 | 244 | int readImageHeader(progimage_info* im, wchar_t* filename, LodePNGColorType& colortype, int imageFileType) 245 | { 246 | if (imageFileType == 1) { 247 | return readpngheader(im, filename, colortype); 248 | } 249 | else if (imageFileType == 2) { 250 | return readtgaheader(im, filename, colortype); 251 | } 252 | assert(0); 253 | // unknown image type 254 | return 999; 255 | } 256 | 257 | -------------------------------------------------------------------------------- /readtga.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2011, Eric Haines 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 19 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 25 | THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | #pragma once 29 | 30 | #include "lodepng.h" 31 | #include "rwpng.h" 32 | #include "tga.h" 33 | 34 | int readtga(progimage_info *mainprog_ptr, wchar_t *filename, LodePNGColorType colortype /*= LCT_RGBA*/); 35 | //void readtga_cleanup(int free_image_data, progimage_info *mainprog_ptr); 36 | int readtgaheader(progimage_info* im, wchar_t* filename, LodePNGColorType& colortype); 37 | 38 | int readImage(progimage_info* im, wchar_t* filename, LodePNGColorType colortype, int imageFileType); 39 | void readImage_cleanup(int mode, progimage_info* im); 40 | int readImageHeader(progimage_info* im, wchar_t* filename, LodePNGColorType& colortype, int imageFileType); 41 | -------------------------------------------------------------------------------- /rwpng.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2011, Eric Haines 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 19 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 25 | THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | #include "stdafx.h" 29 | #include "rwpng.h" 30 | 31 | #include 32 | 33 | #include 34 | 35 | // from http://lodev.org/lodepng/example_decode.cpp 36 | 37 | //Decode from disk to raw pixels with a single function call 38 | // return 0 on success 39 | int readpng(progimage_info *im, wchar_t *filename, LodePNGColorType colortype) 40 | { 41 | //char filename[MAX_PATH]; 42 | //dumb_wcharToChar(wfilename,filename); 43 | 44 | //decode 45 | unsigned int width, height; 46 | unsigned int error = lodepng::decode(im->image_data, width, height, filename, colortype); 47 | 48 | //if there's an error, display it 49 | if (error) 50 | { 51 | im->width = 0; 52 | im->height = 0; 53 | //std::cout << "decoder error " << error << ": " << lodepng_error_text(error) << std::endl; 54 | return (int)error; 55 | } 56 | 57 | im->width = (int)width; 58 | im->height = (int)height; 59 | 60 | //the pixels are now in the vector "image", for color+alpha these are 4 bytes per pixel, ordered RGBARGBA..., use it as texture, draw it, ... 61 | 62 | return 0; 63 | } 64 | 65 | void readpng_cleanup(int mode, progimage_info *im) 66 | { 67 | // mode was important back when libpng was in use 68 | if ( mode == 1 ) 69 | { 70 | im->image_data.clear(); 71 | } 72 | } 73 | 74 | // return 0 if no error 75 | int readpngheader(progimage_info* im, wchar_t* filename, LodePNGColorType& colortype) 76 | { 77 | unsigned int width, height; 78 | std::vector buffer; 79 | lodepng::load_file(buffer, filename); 80 | 81 | colortype = LCT_RGBA; 82 | unsigned bitdepth = 8; 83 | 84 | LodePNGState state; 85 | lodepng_state_init(&state); 86 | state.info_raw.colortype = colortype; 87 | state.info_raw.bitdepth = bitdepth; 88 | // reads header and resets other parameters in state->info_png 89 | state.error = lodepng_inspect(&width, &height, &state, buffer.empty() ? 0 : &buffer[0], (unsigned)buffer.size()); 90 | unsigned int error = state.error; 91 | colortype = state.info_png.color.colortype; 92 | 93 | lodepng_state_cleanup(&state); 94 | 95 | //if there's an error, display it 96 | if (error) 97 | { 98 | im->width = 0; 99 | im->height = 0; 100 | //std::cout << "decoder error " << error << ": " << lodepng_error_text(error) << std::endl; 101 | return (int)error; 102 | } 103 | 104 | im->width = (int)width; 105 | im->height = (int)height; 106 | 107 | return 0; 108 | } 109 | 110 | // from http://lodev.org/lodepng/example_encode.cpp 111 | 112 | //Encode from raw pixels to disk with a single function call 113 | //The image argument has width * height RGBA pixels or width * height * channels 114 | // return 0 on success 115 | int writepng(progimage_info *im, int channels, wchar_t *filename) 116 | { 117 | //char filename[MAX_PATH]; 118 | //dumb_wcharToChar(wfilename,filename); 119 | 120 | //Encode the image, depending on type 121 | unsigned int error = 1; // 1 means didn't reach lodepng 122 | if ( channels == 4 ) 123 | { 124 | // 32 bit RGBA, the default 125 | error = lodepng::encode(filename, im->image_data, (unsigned int)im->width, (unsigned int)im->height, LCT_RGBA ); 126 | } 127 | else if ( channels == 3 ) 128 | { 129 | // 24 bit RGB 130 | error = lodepng::encode(filename, im->image_data, (unsigned int)im->width, (unsigned int)im->height, LCT_RGB ); 131 | } 132 | else if (channels == 1) 133 | { 134 | // 8 bit grayscale 135 | error = lodepng::encode(filename, im->image_data, (unsigned int)im->width, (unsigned int)im->height, LCT_GREY); 136 | } 137 | else 138 | { 139 | assert(0); 140 | } 141 | 142 | //if there's an error, display it 143 | if (error) 144 | { 145 | //std::cout << "encoder error " << error << ": "<< lodepng_error_text(error) << std::endl; 146 | return (int)error; 147 | } 148 | 149 | return 0; 150 | } 151 | 152 | 153 | void writepng_cleanup(progimage_info *im) 154 | { 155 | im->image_data.clear(); 156 | } 157 | 158 | progimage_info* allocateGrayscaleImage(progimage_info* source_ptr) 159 | { 160 | // allocate output image and fill it up 161 | progimage_info* destination_ptr = new progimage_info(); 162 | 163 | destination_ptr->width = source_ptr->width; 164 | destination_ptr->height = source_ptr->height; 165 | destination_ptr->image_data.resize(destination_ptr->width * destination_ptr->height * 1 * sizeof(unsigned char), 0x0); 166 | 167 | return destination_ptr; 168 | } 169 | 170 | progimage_info* allocateRGBImage(progimage_info* source_ptr) 171 | { 172 | // allocate output image and fill it up 173 | progimage_info* destination_ptr = new progimage_info(); 174 | 175 | destination_ptr->width = source_ptr->width; 176 | destination_ptr->height = source_ptr->height; 177 | destination_ptr->image_data.resize(destination_ptr->width * destination_ptr->height * 3 * sizeof(unsigned char), 0x0); 178 | 179 | return destination_ptr; 180 | } 181 | 182 | void copyOneChannel(progimage_info* dst, int channel, progimage_info* src, LodePNGColorType colortype) 183 | { 184 | int row, col; 185 | dst->width = src->width; 186 | dst->height = src->height; 187 | unsigned char* dst_data = &dst->image_data[0]; 188 | unsigned char* src_data = &src->image_data[0] + channel; 189 | int channelIncrement = (colortype == LCT_RGB) ? 3 : 4; 190 | for (row = 0; row < src->height; row++) 191 | { 192 | for (col = 0; col < src->width; col++) 193 | { 194 | *dst_data++ = *src_data; 195 | src_data += channelIncrement; 196 | } 197 | } 198 | } 199 | 200 | void convertToGrayscale(progimage_info* dst, progimage_info* src, LodePNGColorType colortype) 201 | { 202 | int row, col; 203 | dst->width = src->width; 204 | dst->height = src->height; 205 | unsigned char* dst_data = &dst->image_data[0]; 206 | unsigned char* src_data = &src->image_data[0]; 207 | int channelIncrement = (colortype == LCT_RGB) ? 3 : 4; 208 | for (row = 0; row < src->height; row++) 209 | { 210 | for (col = 0; col < src->width; col++) 211 | { 212 | // https://en.wikipedia.org/wiki/Grayscale#Luma_coding_in_video_systems 213 | *dst_data++ = (unsigned char)((0.2126f * ((float)src_data[0] / 255.0f) + 0.7152f * ((float)src_data[0] / 255.0f) + 0.0722f * ((float)src_data[0] / 255.0f)) * 255.0f + 0.5f); 214 | src_data += channelIncrement; 215 | } 216 | } 217 | } 218 | 219 | 220 | // to avoid defining boolean, etc., make this one return 1 if true, 0 if false 221 | int channelEqualsValue(progimage_info* src, int channel, int numChannels, unsigned char value, int ignoreGrayscale) 222 | { 223 | // look at all data in given channel - all equal to the given value? 224 | assert(numChannels > 0); 225 | assert(channel < numChannels); 226 | assert(ignoreGrayscale ? numChannels > 1 : 1); 227 | int row, col; 228 | unsigned char* src_data = &src->image_data[0] + channel; 229 | for (row = 0; row < src->height; row++) 230 | { 231 | for (col = 0; col < src->width; col++) 232 | { 233 | if (*src_data != value) 234 | { 235 | // do grayscale test? 236 | if (ignoreGrayscale) { 237 | if ((src_data[-channel] == src_data[1 - channel]) && (src_data[1 - channel] == src_data[2 - channel])) { 238 | // it's gray, so ignore it (could be a cutout background pixel) 239 | src_data += numChannels; 240 | continue; 241 | } 242 | } 243 | return 0; 244 | } 245 | src_data += numChannels; 246 | } 247 | } 248 | return 1; 249 | } 250 | 251 | void changeValueToValue(progimage_info* src, int channel, int numChannels, unsigned char value, unsigned char newValue) 252 | { 253 | // if value in channel is equal to input value, change it to the new value 254 | assert(numChannels > 0); 255 | assert(channel < numChannels); 256 | int row, col; 257 | unsigned char* src_data = &src->image_data[0] + channel; 258 | for (row = 0; row < src->height; row++) 259 | { 260 | for (col = 0; col < src->width; col++) 261 | { 262 | if (*src_data == value) 263 | { 264 | *src_data = newValue; 265 | } 266 | src_data += numChannels; 267 | } 268 | } 269 | } 270 | 271 | 272 | 273 | -------------------------------------------------------------------------------- /rwpng.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2011, Eric Haines 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 19 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 24 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 25 | THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | #pragma once 29 | 30 | #include "lodepng.h" 31 | 32 | typedef struct _progimage_info { 33 | std::vector image_data; //the raw pixels 34 | int width = 0; 35 | int height = 0; 36 | } progimage_info; 37 | 38 | int readpng(progimage_info *mainprog_ptr, wchar_t *filename, LodePNGColorType colortype /*= LCT_RGBA*/); 39 | void readpng_cleanup(int free_image_data, progimage_info *mainprog_ptr); 40 | int readpngheader(progimage_info* im, wchar_t* filename, LodePNGColorType& colortype); 41 | 42 | int writepng(progimage_info *mainprog_ptr, int channels, wchar_t *filename); 43 | void writepng_cleanup(progimage_info *mainprog_ptr); 44 | 45 | progimage_info* allocateGrayscaleImage(progimage_info* source_ptr); 46 | progimage_info* allocateRGBImage(progimage_info* source_ptr); 47 | void copyOneChannel(progimage_info* dst, int channel, progimage_info* src, LodePNGColorType colortype); 48 | void convertToGrayscale(progimage_info* dst, progimage_info* src, LodePNGColorType colortype); 49 | 50 | int channelEqualsValue(progimage_info* src, int channel, int numChannels, unsigned char value, int ignoreGrayscale); 51 | void changeValueToValue(progimage_info* src, int channel, int numChannels, unsigned char value, unsigned char newValue); -------------------------------------------------------------------------------- /stdafx.h: -------------------------------------------------------------------------------- 1 | // stdafx.h : include file for standard system include files, 2 | // or project specific include files that are used frequently, but 3 | // are changed infrequently 4 | // 5 | 6 | #pragma once 7 | 8 | #include "targetver.h" 9 | 10 | #include 11 | #include 12 | 13 | 14 | 15 | // TODO: reference additional headers your program requires here 16 | -------------------------------------------------------------------------------- /stdio.cpp: -------------------------------------------------------------------------------- 1 | // Aseprite TGA Library 2 | // Copyright (C) 2020 Igara Studio S.A. 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "tga.h" 8 | 9 | namespace tga { 10 | 11 | StdioFileInterface::StdioFileInterface(FILE* file) 12 | : m_file(file) 13 | , m_ok(true) 14 | { 15 | } 16 | 17 | bool StdioFileInterface::ok() const 18 | { 19 | return m_ok; 20 | } 21 | 22 | size_t StdioFileInterface::tell() 23 | { 24 | return ftell(m_file); 25 | } 26 | 27 | void StdioFileInterface::seek(size_t absPos) 28 | { 29 | fseek(m_file, (long)absPos, SEEK_SET); 30 | } 31 | 32 | uint8_t StdioFileInterface::read8() 33 | { 34 | int value = fgetc(m_file); 35 | if (value != EOF) 36 | return (uint8_t)value; 37 | 38 | m_ok = false; 39 | return 0; 40 | } 41 | 42 | void StdioFileInterface::write8(uint8_t value) 43 | { 44 | fputc(value, m_file); 45 | } 46 | 47 | } // namespace tga 48 | -------------------------------------------------------------------------------- /targetver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Including SDKDDKVer.h defines the highest available Windows platform. 4 | 5 | // If you wish to build your application for a previous Windows platform, include WinSDKVer.h and 6 | // set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. 7 | 8 | #include 9 | -------------------------------------------------------------------------------- /test_files/Heightfields/grayscale_wood_floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Heightfields/grayscale_wood_floor.png -------------------------------------------------------------------------------- /test_files/Heightfields/ntp_heightfield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Heightfields/ntp_heightfield.png -------------------------------------------------------------------------------- /test_files/Heightfields/r_bump_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Heightfields/r_bump_map.png -------------------------------------------------------------------------------- /test_files/Heightfields/squiggles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Heightfields/squiggles.png -------------------------------------------------------------------------------- /test_files/LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-ShareAlike 4.0 International Public 58 | License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-ShareAlike 4.0 International Public License ("Public 63 | License"). To the extent this Public License may be interpreted as a 64 | contract, You are granted the Licensed Rights in consideration of Your 65 | acceptance of these terms and conditions, and the Licensor grants You 66 | such rights in consideration of benefits the Licensor receives from 67 | making the Licensed Material available under these terms and 68 | conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. BY-SA Compatible License means a license listed at 88 | creativecommons.org/compatiblelicenses, approved by Creative 89 | Commons as essentially the equivalent of this Public License. 90 | 91 | d. Copyright and Similar Rights means copyright and/or similar rights 92 | closely related to copyright including, without limitation, 93 | performance, broadcast, sound recording, and Sui Generis Database 94 | Rights, without regard to how the rights are labeled or 95 | categorized. For purposes of this Public License, the rights 96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 97 | Rights. 98 | 99 | e. Effective Technological Measures means those measures that, in the 100 | absence of proper authority, may not be circumvented under laws 101 | fulfilling obligations under Article 11 of the WIPO Copyright 102 | Treaty adopted on December 20, 1996, and/or similar international 103 | agreements. 104 | 105 | f. Exceptions and Limitations means fair use, fair dealing, and/or 106 | any other exception or limitation to Copyright and Similar Rights 107 | that applies to Your use of the Licensed Material. 108 | 109 | g. License Elements means the license attributes listed in the name 110 | of a Creative Commons Public License. The License Elements of this 111 | Public License are Attribution and ShareAlike. 112 | 113 | h. Licensed Material means the artistic or literary work, database, 114 | or other material to which the Licensor applied this Public 115 | License. 116 | 117 | i. Licensed Rights means the rights granted to You subject to the 118 | terms and conditions of this Public License, which are limited to 119 | all Copyright and Similar Rights that apply to Your use of the 120 | Licensed Material and that the Licensor has authority to license. 121 | 122 | j. Licensor means the individual(s) or entity(ies) granting rights 123 | under this Public License. 124 | 125 | k. Share means to provide material to the public by any means or 126 | process that requires permission under the Licensed Rights, such 127 | as reproduction, public display, public performance, distribution, 128 | dissemination, communication, or importation, and to make material 129 | available to the public including in ways that members of the 130 | public may access the material from a place and at a time 131 | individually chosen by them. 132 | 133 | l. Sui Generis Database Rights means rights other than copyright 134 | resulting from Directive 96/9/EC of the European Parliament and of 135 | the Council of 11 March 1996 on the legal protection of databases, 136 | as amended and/or succeeded, as well as other essentially 137 | equivalent rights anywhere in the world. 138 | 139 | m. You means the individual or entity exercising the Licensed Rights 140 | under this Public License. Your has a corresponding meaning. 141 | 142 | 143 | Section 2 -- Scope. 144 | 145 | a. License grant. 146 | 147 | 1. Subject to the terms and conditions of this Public License, 148 | the Licensor hereby grants You a worldwide, royalty-free, 149 | non-sublicensable, non-exclusive, irrevocable license to 150 | exercise the Licensed Rights in the Licensed Material to: 151 | 152 | a. reproduce and Share the Licensed Material, in whole or 153 | in part; and 154 | 155 | b. produce, reproduce, and Share Adapted Material. 156 | 157 | 2. Exceptions and Limitations. For the avoidance of doubt, where 158 | Exceptions and Limitations apply to Your use, this Public 159 | License does not apply, and You do not need to comply with 160 | its terms and conditions. 161 | 162 | 3. Term. The term of this Public License is specified in Section 163 | 6(a). 164 | 165 | 4. Media and formats; technical modifications allowed. The 166 | Licensor authorizes You to exercise the Licensed Rights in 167 | all media and formats whether now known or hereafter created, 168 | and to make technical modifications necessary to do so. The 169 | Licensor waives and/or agrees not to assert any right or 170 | authority to forbid You from making technical modifications 171 | necessary to exercise the Licensed Rights, including 172 | technical modifications necessary to circumvent Effective 173 | Technological Measures. For purposes of this Public License, 174 | simply making modifications authorized by this Section 2(a) 175 | (4) never produces Adapted Material. 176 | 177 | 5. Downstream recipients. 178 | 179 | a. Offer from the Licensor -- Licensed Material. Every 180 | recipient of the Licensed Material automatically 181 | receives an offer from the Licensor to exercise the 182 | Licensed Rights under the terms and conditions of this 183 | Public License. 184 | 185 | b. Additional offer from the Licensor -- Adapted Material. 186 | Every recipient of Adapted Material from You 187 | automatically receives an offer from the Licensor to 188 | exercise the Licensed Rights in the Adapted Material 189 | under the conditions of the Adapter's License You apply. 190 | 191 | c. No downstream restrictions. You may not offer or impose 192 | any additional or different terms or conditions on, or 193 | apply any Effective Technological Measures to, the 194 | Licensed Material if doing so restricts exercise of the 195 | Licensed Rights by any recipient of the Licensed 196 | Material. 197 | 198 | 6. No endorsement. Nothing in this Public License constitutes or 199 | may be construed as permission to assert or imply that You 200 | are, or that Your use of the Licensed Material is, connected 201 | with, or sponsored, endorsed, or granted official status by, 202 | the Licensor or others designated to receive attribution as 203 | provided in Section 3(a)(1)(A)(i). 204 | 205 | b. Other rights. 206 | 207 | 1. Moral rights, such as the right of integrity, are not 208 | licensed under this Public License, nor are publicity, 209 | privacy, and/or other similar personality rights; however, to 210 | the extent possible, the Licensor waives and/or agrees not to 211 | assert any such rights held by the Licensor to the limited 212 | extent necessary to allow You to exercise the Licensed 213 | Rights, but not otherwise. 214 | 215 | 2. Patent and trademark rights are not licensed under this 216 | Public License. 217 | 218 | 3. To the extent possible, the Licensor waives any right to 219 | collect royalties from You for the exercise of the Licensed 220 | Rights, whether directly or through a collecting society 221 | under any voluntary or waivable statutory or compulsory 222 | licensing scheme. In all other cases the Licensor expressly 223 | reserves any right to collect such royalties. 224 | 225 | 226 | Section 3 -- License Conditions. 227 | 228 | Your exercise of the Licensed Rights is expressly made subject to the 229 | following conditions. 230 | 231 | a. Attribution. 232 | 233 | 1. If You Share the Licensed Material (including in modified 234 | form), You must: 235 | 236 | a. retain the following if it is supplied by the Licensor 237 | with the Licensed Material: 238 | 239 | i. identification of the creator(s) of the Licensed 240 | Material and any others designated to receive 241 | attribution, in any reasonable manner requested by 242 | the Licensor (including by pseudonym if 243 | designated); 244 | 245 | ii. a copyright notice; 246 | 247 | iii. a notice that refers to this Public License; 248 | 249 | iv. a notice that refers to the disclaimer of 250 | warranties; 251 | 252 | v. a URI or hyperlink to the Licensed Material to the 253 | extent reasonably practicable; 254 | 255 | b. indicate if You modified the Licensed Material and 256 | retain an indication of any previous modifications; and 257 | 258 | c. indicate the Licensed Material is licensed under this 259 | Public License, and include the text of, or the URI or 260 | hyperlink to, this Public License. 261 | 262 | 2. You may satisfy the conditions in Section 3(a)(1) in any 263 | reasonable manner based on the medium, means, and context in 264 | which You Share the Licensed Material. For example, it may be 265 | reasonable to satisfy the conditions by providing a URI or 266 | hyperlink to a resource that includes the required 267 | information. 268 | 269 | 3. If requested by the Licensor, You must remove any of the 270 | information required by Section 3(a)(1)(A) to the extent 271 | reasonably practicable. 272 | 273 | b. ShareAlike. 274 | 275 | In addition to the conditions in Section 3(a), if You Share 276 | Adapted Material You produce, the following conditions also apply. 277 | 278 | 1. The Adapter's License You apply must be a Creative Commons 279 | license with the same License Elements, this version or 280 | later, or a BY-SA Compatible License. 281 | 282 | 2. You must include the text of, or the URI or hyperlink to, the 283 | Adapter's License You apply. You may satisfy this condition 284 | in any reasonable manner based on the medium, means, and 285 | context in which You Share Adapted Material. 286 | 287 | 3. You may not offer or impose any additional or different terms 288 | or conditions on, or apply any Effective Technological 289 | Measures to, Adapted Material that restrict exercise of the 290 | rights granted under the Adapter's License You apply. 291 | 292 | 293 | Section 4 -- Sui Generis Database Rights. 294 | 295 | Where the Licensed Rights include Sui Generis Database Rights that 296 | apply to Your use of the Licensed Material: 297 | 298 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 299 | to extract, reuse, reproduce, and Share all or a substantial 300 | portion of the contents of the database; 301 | 302 | b. if You include all or a substantial portion of the database 303 | contents in a database in which You have Sui Generis Database 304 | Rights, then the database in which You have Sui Generis Database 305 | Rights (but not its individual contents) is Adapted Material, 306 | 307 | including for purposes of Section 3(b); and 308 | c. You must comply with the conditions in Section 3(a) if You Share 309 | all or a substantial portion of the contents of the database. 310 | 311 | For the avoidance of doubt, this Section 4 supplements and does not 312 | replace Your obligations under this Public License where the Licensed 313 | Rights include other Copyright and Similar Rights. 314 | 315 | 316 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 317 | 318 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 319 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 320 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 321 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 322 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 323 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 324 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 325 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 326 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 327 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 328 | 329 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 330 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 331 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 332 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 333 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 334 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 335 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 336 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 337 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 338 | 339 | c. The disclaimer of warranties and limitation of liability provided 340 | above shall be interpreted in a manner that, to the extent 341 | possible, most closely approximates an absolute disclaimer and 342 | waiver of all liability. 343 | 344 | 345 | Section 6 -- Term and Termination. 346 | 347 | a. This Public License applies for the term of the Copyright and 348 | Similar Rights licensed here. However, if You fail to comply with 349 | this Public License, then Your rights under this Public License 350 | terminate automatically. 351 | 352 | b. Where Your right to use the Licensed Material has terminated under 353 | Section 6(a), it reinstates: 354 | 355 | 1. automatically as of the date the violation is cured, provided 356 | it is cured within 30 days of Your discovery of the 357 | violation; or 358 | 359 | 2. upon express reinstatement by the Licensor. 360 | 361 | For the avoidance of doubt, this Section 6(b) does not affect any 362 | right the Licensor may have to seek remedies for Your violations 363 | of this Public License. 364 | 365 | c. For the avoidance of doubt, the Licensor may also offer the 366 | Licensed Material under separate terms or conditions or stop 367 | distributing the Licensed Material at any time; however, doing so 368 | will not terminate this Public License. 369 | 370 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 371 | License. 372 | 373 | 374 | Section 7 -- Other Terms and Conditions. 375 | 376 | a. The Licensor shall not be bound by any additional or different 377 | terms or conditions communicated by You unless expressly agreed. 378 | 379 | b. Any arrangements, understandings, or agreements regarding the 380 | Licensed Material not stated herein are separate from and 381 | independent of the terms and conditions of this Public License. 382 | 383 | 384 | Section 8 -- Interpretation. 385 | 386 | a. For the avoidance of doubt, this Public License does not, and 387 | shall not be interpreted to, reduce, limit, restrict, or impose 388 | conditions on any use of the Licensed Material that could lawfully 389 | be made without permission under this Public License. 390 | 391 | b. To the extent possible, if any provision of this Public License is 392 | deemed unenforceable, it shall be automatically reformed to the 393 | minimum extent necessary to make it enforceable. If the provision 394 | cannot be reformed, it shall be severed from this Public License 395 | without affecting the enforceability of the remaining terms and 396 | conditions. 397 | 398 | c. No term or condition of this Public License will be waived and no 399 | failure to comply consented to unless expressly agreed to by the 400 | Licensor. 401 | 402 | d. Nothing in this Public License constitutes or may be interpreted 403 | as a limitation upon, or waiver of, any privileges and immunities 404 | that apply to the Licensor or You, including from the legal 405 | processes of any jurisdiction or authority. 406 | 407 | 408 | ======================================================================= 409 | 410 | Creative Commons is not a party to its public licenses. 411 | Notwithstanding, Creative Commons may elect to apply one of its public 412 | licenses to material it publishes and in those instances will be 413 | considered the “Licensor.” The text of the Creative Commons public 414 | licenses is dedicated to the public domain under the CC0 Public Domain 415 | Dedication. Except for the limited purpose of indicating that material 416 | is shared under a Creative Commons public license or as otherwise 417 | permitted by the Creative Commons policies published at 418 | creativecommons.org/policies, Creative Commons does not authorize the 419 | use of the trademark "Creative Commons" or any other trademark or logo 420 | of Creative Commons without its prior written consent including, 421 | without limitation, in connection with any unauthorized modifications 422 | to any of its public licenses or any other arrangements, 423 | understandings, or agreements concerning use of licensed material. For 424 | the avoidance of doubt, this paragraph does not form part of the public 425 | licenses. 426 | 427 | Creative Commons may be contacted at creativecommons.org. -------------------------------------------------------------------------------- /test_files/NoneOfTheAbove/black.tga: -------------------------------------------------------------------------------- 1 | d2 -------------------------------------------------------------------------------- /test_files/NoneOfTheAbove/cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/NoneOfTheAbove/cyan.png -------------------------------------------------------------------------------- /test_files/NoneOfTheAbove/fade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/NoneOfTheAbove/fade.png -------------------------------------------------------------------------------- /test_files/NoneOfTheAbove/gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/NoneOfTheAbove/gray.png -------------------------------------------------------------------------------- /test_files/NoneOfTheAbove/white.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/NoneOfTheAbove/white.tga -------------------------------------------------------------------------------- /test_files/NoneOfTheAbove/wood_floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/NoneOfTheAbove/wood_floor.png -------------------------------------------------------------------------------- /test_files/NoneOfTheAbove/yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/NoneOfTheAbove/yellow.png -------------------------------------------------------------------------------- /test_files/README.md: -------------------------------------------------------------------------------- 1 | # Normal Map test files 2 | 3 | Execute **run_test_suite.bat** on Windows to analyze and convert/clean-up the texture test files in this directory and subdirectories. Running this test file will create various separate output directories where the results are put. 4 | 5 | ## Directories 6 | * Standard - standard RGB -> XYZ, where Z is -1 to 1 range 7 | * XYtextures - RG -> XY, and Z is something else (like ambient occlusion, e.g., LabPBR) 8 | * ZZero - RGB -> XYZ, but Z goes from 0 to 1 instead of -1 to 1 9 | * Heightfields - heightfield test images 10 | * NoneOfTheAbove - other test images of various sorts 11 | 12 | Each directory's contents: 13 | 14 | ### Standard 15 | 16 | **lava_flow_n.png** - a file with almost no bumps, all nearly the same value. File is from the [jg-rtx](https://github.com/jasonjgardner/jg-rtx) resource pack for Minecraft, license [CC-NC-BY-SA](https://github.com/jasonjgardner/jg-rtx/blob/main/LICENSE). 17 | 18 | ![lava_flow_n.png](./Standard/lava_flow_n.png "lava_flow_n.png") 19 | 20 | **ntp_heightfield_gimp.png** - conversion of the **ntp_heightfield.png** heightfield to a normals texture with [GIMP](https://www.gimp.org/) using [these instructions](https://docs.gimp.org/en/gimp-filter-normal-map.html). 21 | 22 | ![ntp_heightfield_gimp.png](./Standard/ntp_heightfield_gimp.png "ntp_heightfield_gimp.png") 23 | 24 | **ntp_heightfield_gimp_5scale.png** - conversion of the **ntp_heightfield.png** heightfield to a normals texture with [GIMP](https://www.gimp.org/), changing the scale to 5, using [these instructions](https://docs.gimp.org/en/gimp-filter-normal-map.html). 25 | 26 | ![ntp_heightfield_gimp_5scale.png](./Standard/ntp_heightfield_gimp_5scale.png "ntp_heightfield_gimp_5scale.png") 27 | 28 | **ntp_heightfield_normalmap_online.png** - conversion of the **ntp_heightfield.png** heightfield to a normals texture with [Normalmap Online](https://cpetry.github.io/NormalMap-Online/), setting "Z-Range -1 to +1". 29 | 30 | ![ntp_heightfield_normalmap_online.png](./Standard/ntp_heightfield_normalmap_online.png "ntp_heightfield_normalmap_online.png") 31 | 32 | **ntp_resize_128.png** - the result file **ntp_heightfield_normalmap_online.png** resized to 128x128 using IrfanView. Doing a naive resize causes normals to no longer be normalized, see results in directory \_output_Heatmap. 33 | 34 | ![ntp_resize_128.png](./Standard/ntp_resize_128.png "ntp_resize_128.png") 35 | 36 | **ntp_resize_512.png** - the result file **ntp_heightfield_normalmap_online.png** resized to 512x512 using IrfanView. Doing a naive resize causes normals to no longer be normalized, see results in directory \_output_Heatmap. 37 | 38 | ![ntp_resize_512.png](./Standard/ntp_resize_512.png "ntp_resize_512.png") 39 | 40 | **r_normal_map.png** - letter R squiggles (see Heightfields directory) in standard form. Not quite correct, making it a good test. From the [USD Assets Working Group](https://github.com/usd-wg/assets/tree/main/test_assets/NormalsTextureBiasAndScale) Normals Texture Bias And Scale test, license CC-NC-BY-SA. 41 | 42 | ![r_normal_map.png](./Standard/r_normal_map.png "r_normal_map.png") 43 | 44 | **r_normal_map_reversed_y.png** - letter R in standard form, DirectX orientation. Not quite correct, making it a good test. From the [USD Assets Working Group](https://github.com/usd-wg/assets/tree/main/test_assets/NormalsTextureBiasAndScale) Normals Texture Bias And Scale test, license CC-NC-BY-SA. 45 | 46 | ![r_normal_map_reversed_y.png](./Standard/r_normal_map_reversed_y.png "r_normal_map_reversed_y.png") 47 | 48 | **squiggles_gimp.png** - squiggles (see Heightfields directory) converted with [GIMP](https://www.gimp.org/) using [these instructions](https://docs.gimp.org/en/gimp-filter-normal-map.html). GIMP evidently uses the DirectX style for output by default, though has a "Flip Y" and other options to adjust the style. 49 | 50 | ![squiggles_gimp.png](./Standard/squiggles_gimp.png "squiggles_gimp.png") 51 | 52 | **squiggles_normalmap_online_zneg.png** - converted using [NormalMap Online](https://cpetry.github.io/NormalMap-Online/), with the "Z Range -1 to +1" box checked. 53 | 54 | ![squiggles_normalmap_online_zneg.png](./Standard/squiggles_normalmap_online_zneg.png "squiggles_normalmap_online_zneg.png") 55 | 56 | **squiggles_ntp_quick.png** - converted by an earlier version of the NormalTextureProcessor.exe's heightfield converter, but without adjusting for roundtripping. Some pixels have one-level differences with **squiggles_ntp_roundtrip.png**. Use a "diff" program such as Beyond Compare to see the differences between the two. Note if you read this file in and convert it, the result should be the same as **squiggles_ntp_roundtrip.png**. 57 | 58 | ![squiggles_ntp_quick.png](./Standard/squiggles_ntp_quick.png "squiggles_ntp_quick.png") 59 | 60 | **squiggles_ntp_roundtrip.png** - converted by NormalTextureProcessor.exe's heightfield converter using roundtripping. If you try to clean this file, the output file should be identical, since all the texels are perfectly roundtrippable (see the **Algorithms** section on the main page). 61 | 62 | ![squiggles_ntp_roundtrip.png](./Standard/squiggles_ntp_roundtrip.png "squiggles_ntp_roundtrip.png") 63 | 64 | **targa_r_normal_map.tga** - (no image shown) same as r_normal_map.png**, the letter R in standard form. Test that Targa input works. Converted from the [USD Assets Working Group](https://github.com/usd-wg/assets/tree/main/test_assets/NormalsTextureBiasAndScale) Normals Texture Bias And Scale test, license CC-NC-BY-SA. 65 | 66 | ### XYtextures 67 | 68 | **acacia_door_bottom_n.png** - the blue channel is ambient occlusion, [LabPBR format](https://shaderlabs.org/wiki/LabPBR_Material_Standard). File is from the [jg-rtx](https://github.com/jasonjgardner/jg-rtx) resource pack for Minecraft, license [CC-NC-BY-SA](https://github.com/jasonjgardner/jg-rtx/blob/main/LICENSE). 69 | 70 | ![acacia_door_bottom_n.png](./XYtextures/acacia_door_bottom_n.png "acacia_door_bottom_n.png") 71 | 72 | ### ZZero 73 | 74 | **r_normal_map_reversed_x_0_bias_z.png** - letter R in Z-Zero form, and the X axis is reversed (non-standard, just meant as a test). Not quite correct, making it a good test. From the [USD Assets Working Group](https://github.com/usd-wg/assets/tree/main/test_assets/NormalsTextureBiasAndScale) Normals Texture Bias And Scale test, license CC-NC-BY-SA. 75 | 76 | ![r_normal_map_reversed_x_0_bias_z.png](./ZZero/r_normal_map_reversed_x_0_bias_z.png "r_normal_map_reversed_x_0_bias_z.png") 77 | 78 | **squiggles_normalmap_online_zzero.png** - converted using [NormalMap Online](https://cpetry.github.io/NormalMap-Online/). 79 | 80 | ![squiggles_normalmap_online_zzero.png](./ZZero/squiggles_normalmap_online_zzero.png "squiggles_normalmap_online_zzero.png") 81 | 82 | ### Heightfields 83 | 84 | **grayscale_wood_floor.png** - grayscale wood floor sample, non-tiling. 85 | 86 | ![grayscale_wood_floor.png](./Heightfields/grayscale_wood_floor.png "grayscale_wood_floor.png") 87 | 88 | **ntp_heightfield.png** - text and brushes heightfield, giving a wide variety of slopes 89 | 90 | ![ntp_heightfield.png](./Heightfields/ntp_heightfield.png "ntp_heightfield.png") 91 | 92 | **r_bump_map.png** - grayscale letter R. From the [USD Assets Working Group](https://github.com/usd-wg/assets/tree/main/test_assets/NormalsTextureBiasAndScale) Normals Texture Bias And Scale test, license CC-NC-BY-SA. 93 | 94 | ![r_bump_map.png](./Heightfields/r_bump_map.png "r_bump_map.png") 95 | 96 | **squiggles.png** - brushstrokes of similar slopes. Note there are strokes that touch the top edge, causing conversion glitches due to wrap-around; using '-hborder' would be a more sensible conversion, which is done as a separate test. 97 | 98 | ![squiggles.png](./Heightfields/squiggles.png "squiggles.png") 99 | 100 | ### NoneOfTheAbove 101 | 102 | **black.tga** - (no image shown) all black, as a test. Non-power-of-two non-square size. 103 | 104 | **cyan.png** - solid cyan, as a test. 105 | 106 | ![cyan.png](./NoneOfTheAbove/cyan.png "cyan.png") 107 | 108 | **fade.png** - fade from blue to green to red, as a test. 109 | 110 | ![fade.png](./NoneOfTheAbove/fade.png "cyan.png") 111 | 112 | **gray.png** - a neutral gray, as a test. Non-power-of-two non-square size. 113 | 114 | ![gray.png](./NoneOfTheAbove/gray.png "gray.png") 115 | 116 | **white.tga** - (no image shown) all white, as a test. 117 | 118 | **wood_floor.png** - colored wood floor sample, non-tiling. Currently this file gets misinterpreted as being an XY-only texture, because the Z's are clearly not used properly and the X and Y values, due to the coloration, mostly form vectors that have a length less than 1.0. 119 | 120 | ![wood_floor.png](./NoneOfTheAbove/wood_floor.png "wood_floor.png") 121 | 122 | **yellow.png** - solid yellow, as a test. 123 | 124 | ![yellow.png](./NoneOfTheAbove/yellow.png "yellow.png") 125 | 126 | ## License 127 | 128 | For images, the license is [CC-NC-BY-SA](https://github.com/erich666/NormalTextureProcessor/test_files/LICENSE) and created by Eric Haines, unless noted otherwise above. -------------------------------------------------------------------------------- /test_files/Standard/lava_flow_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Standard/lava_flow_n.png -------------------------------------------------------------------------------- /test_files/Standard/ntp_heightfield_gimp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Standard/ntp_heightfield_gimp.png -------------------------------------------------------------------------------- /test_files/Standard/ntp_heightfield_gimp_5scale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Standard/ntp_heightfield_gimp_5scale.png -------------------------------------------------------------------------------- /test_files/Standard/ntp_heightfield_normalmap_online.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Standard/ntp_heightfield_normalmap_online.png -------------------------------------------------------------------------------- /test_files/Standard/ntp_resize_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Standard/ntp_resize_128.png -------------------------------------------------------------------------------- /test_files/Standard/ntp_resize_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Standard/ntp_resize_512.png -------------------------------------------------------------------------------- /test_files/Standard/r_normal_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Standard/r_normal_map.png -------------------------------------------------------------------------------- /test_files/Standard/r_normal_map_reversed_y.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Standard/r_normal_map_reversed_y.png -------------------------------------------------------------------------------- /test_files/Standard/squiggles_gimp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Standard/squiggles_gimp.png -------------------------------------------------------------------------------- /test_files/Standard/squiggles_normalmap_online_zneg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Standard/squiggles_normalmap_online_zneg.png -------------------------------------------------------------------------------- /test_files/Standard/squiggles_ntp_quick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Standard/squiggles_ntp_quick.png -------------------------------------------------------------------------------- /test_files/Standard/squiggles_ntp_roundtrip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Standard/squiggles_ntp_roundtrip.png -------------------------------------------------------------------------------- /test_files/Standard/targa_r_normal_map.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/Standard/targa_r_normal_map.tga -------------------------------------------------------------------------------- /test_files/XYtextures/acacia_door_bottom_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/XYtextures/acacia_door_bottom_n.png -------------------------------------------------------------------------------- /test_files/ZZero/r_normal_map_reversed_x_0_bias_z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/ZZero/r_normal_map_reversed_x_0_bias_z.png -------------------------------------------------------------------------------- /test_files/ZZero/squiggles_normalmap_online_zzero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erich666/NormalTextureProcessor/6c4ec4c2315393b9e73f74ee7404369528538a39/test_files/ZZero/squiggles_normalmap_online_zzero.png -------------------------------------------------------------------------------- /test_files/run_test_suite.bat: -------------------------------------------------------------------------------- 1 | @rem run_test_suite.bat - run to test the NormalTextureProcessor. Assumes the release version of the program has been compiled and is in the normal place. 2 | set NTP_EXE="..\x64\Release\NormalTextureProcessor.exe" 3 | 4 | @rem run everything, as a start 5 | @rem we could force everything to be considered a heightfield for this next test by using -ihf, but it turns out these all pass anyway 6 | %NTP_EXE% -idir Standard -odir _output_Standard -oall -a -v 7 | %NTP_EXE% -idir ZZero -odir _output_ZZero -oall -a -v 8 | %NTP_EXE% -idir XYtextures -odir _output_XYtextures -oall -a -v 9 | %NTP_EXE% -idir Heightfields -odir _output_Heightfields -oall -a -v 10 | @rem how are various non-normal, non-bump files interpreted? 11 | %NTP_EXE% -idir NoneOfTheAbove -odir _output_NoneOfTheAbove -oall -a -v 12 | 13 | @rem Other tests follow, mostly unit tests to exercise and demonstrate the various options 14 | 15 | @rem the 0 to 1 file produced by NormalMap Online is so funky we have to force it to be read in as Z-zero, using -izzero. Also output as -ozzero, making this a cleanup sort of pass 16 | %NTP_EXE% -idir ZZero -odir _output_ZZero_stay_zzero -oclean -a -v -izzero -ozzero squiggles_normalmap_online_zzero.png 17 | 18 | @rem another way is to set the tolerance percentage higher, '-etol 8', which increases the tolerance for bad normals (and other bad data), so letting this one be classified as Z-zero 19 | %NTP_EXE% -idir ZZero -odir _output_ZZero_find_zzero -oclean -a -v -etol 8 squiggles_normalmap_online_zzero.png 20 | 21 | @rem make the wood bumpmap have more extreme bumps and have borders instead of being tiling (repeating) 22 | %NTP_EXE% -idir Heightfields -odir _output_Heightfields_scale -oall -a -v -hfs 15 grayscale_wood_floor.png -hborder 23 | 24 | @rem convert the squiggles heightfield to have a border, which makes more sense since it's not meant to be tiling 25 | %NTP_EXE% -idir Heightfields -odir _output_Heightfields_border -oall -a -v squiggles.png -hborder 26 | 27 | @rem force colored wood floor to be considered a heightfield 28 | %NTP_EXE% -idir NoneOfTheAbove -odir _output_NoneOfTheAbove_force -oall -a -v -ihf wood_floor.png 29 | 30 | @rem GIMP gives DirectX-style by default. Convert to OpenGL-style, and force to be standard Z, just to test the -izneg flag 31 | %NTP_EXE% -idir Standard -odir _output_Standard_OpenGL -oall -a -v -izneg -idx squiggles_gimp.png 32 | 33 | @rem convert the r_normal_map.png OpenGL-style file to DirectX style. Note there is no '-iogl' input option needed, since OpenGL format is the default. 34 | %NTP_EXE% -idir Standard -odir _output_Standard_DirectX -oall -a -v -izneg -odx r_normal_map.png 35 | 36 | @rem export to CSV, showing only errors 37 | %NTP_EXE% -idir Standard -odir _output_CSV -a -v squiggles_normalmap_online_zneg.png -csve -izneg 38 | %NTP_EXE% -idir Standard -odir _output_CSV -a -v ntp_heightfield_normalmap_online.png -csve -izneg 39 | 40 | @rem export to CSV, dumping all pixels 41 | %NTP_EXE% -idir NoneOfTheAbove -odir _output_CSV -a -v fade.png -csv -ixy 42 | 43 | @rem make heatmaps, tell program that files are of a given format 44 | %NTP_EXE% -idir Standard -odir _output_Heatmap -oall -izneg -a -v -oheatmap 45 | %NTP_EXE% -idir XYtextures -odir _output_Heatmap -oall -ixy -a -v -oheatmap 46 | %NTP_EXE% -idir ZZero -odir _output_Heatmap -oall -izzero -a -v -oheatmap 47 | 48 | @rem TODO: Flag currently not tested here, due to a lack of bad test data: -allownegz -etolxy 49 | -------------------------------------------------------------------------------- /tga.h: -------------------------------------------------------------------------------- 1 | // Aseprite TGA Library 2 | // Copyright (C) 2020 Igara Studio S.A. 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #ifndef TGA_TGA_H_INCLUDED 8 | #define TGA_TGA_H_INCLUDED 9 | #pragma once 10 | 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | namespace tga { 18 | 19 | enum ImageType { 20 | NoImage = 0, 21 | UncompressedIndexed = 1, 22 | UncompressedRgb = 2, 23 | UncompressedGray = 3, 24 | RleIndexed = 9, 25 | RleRgb = 10, 26 | RleGray = 11, 27 | }; 28 | 29 | typedef uint32_t color_t; 30 | 31 | const color_t color_r_shift = 0; 32 | const color_t color_g_shift = 8; 33 | const color_t color_b_shift = 16; 34 | const color_t color_a_shift = 24; 35 | const color_t color_r_mask = 0x000000ff; 36 | const color_t color_g_mask = 0x0000ff00; 37 | const color_t color_b_mask = 0x00ff0000; 38 | const color_t color_rgb_mask = 0x00ffffff; 39 | const color_t color_a_mask = 0xff000000; 40 | 41 | inline uint8_t getr(color_t c) { return (c >> color_r_shift) & 0xff; } 42 | inline uint8_t getg(color_t c) { return (c >> color_g_shift) & 0xff; } 43 | inline uint8_t getb(color_t c) { return (c >> color_b_shift) & 0xff; } 44 | inline uint8_t geta(color_t c) { return (c >> color_a_shift) & 0xff; } 45 | inline color_t rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255) { 46 | return ((r << color_r_shift) | 47 | (g << color_g_shift) | 48 | (b << color_b_shift) | 49 | (a << color_a_shift)); 50 | } 51 | 52 | class Colormap { 53 | public: 54 | Colormap() { } 55 | Colormap(const int n) : m_color(n) { } 56 | 57 | int size() const { return int(m_color.size()); } 58 | 59 | const color_t& operator[](int i) const { 60 | return m_color[i]; 61 | } 62 | 63 | color_t& operator[](int i) { 64 | return m_color[i]; 65 | } 66 | 67 | bool operator==(const Colormap& o) const { 68 | for (int i=0; i m_color; 81 | }; 82 | 83 | struct Image { 84 | uint8_t* pixels; 85 | uint32_t bytesPerPixel; 86 | uint32_t rowstride; 87 | }; 88 | 89 | struct Header { 90 | uint8_t idLength; 91 | uint8_t colormapType; 92 | uint8_t imageType; 93 | uint16_t colormapOrigin; 94 | uint16_t colormapLength; 95 | uint8_t colormapDepth; 96 | uint16_t xOrigin; 97 | uint16_t yOrigin; 98 | uint16_t width; 99 | uint16_t height; 100 | uint8_t bitsPerPixel; 101 | uint8_t imageDescriptor; 102 | std::string imageId; 103 | Colormap colormap; 104 | 105 | bool leftToRight() const { return !(imageDescriptor & 0x10); } 106 | bool topToBottom() const { return (imageDescriptor & 0x20); } 107 | 108 | bool hasColormap() const { 109 | return (colormapLength > 0); 110 | } 111 | 112 | bool isRgb() const { 113 | return (imageType == UncompressedRgb || 114 | imageType == RleRgb); 115 | } 116 | 117 | bool isIndexed() const { 118 | return (imageType == UncompressedIndexed || 119 | imageType == RleIndexed); 120 | } 121 | 122 | bool isGray() const { 123 | return (imageType == UncompressedGray || 124 | imageType == RleGray); 125 | } 126 | 127 | bool isUncompressed() const { 128 | return (imageType == UncompressedIndexed || 129 | imageType == UncompressedRgb || 130 | imageType == UncompressedGray); 131 | } 132 | 133 | bool isRle() const { 134 | return (imageType == RleIndexed || 135 | imageType == RleRgb || 136 | imageType == RleGray); 137 | } 138 | 139 | bool validColormapType() const { 140 | return 141 | // Indexed with palette 142 | (isIndexed() && bitsPerPixel == 8 && colormapType == 1) || 143 | // Grayscale without palette 144 | (isGray() && bitsPerPixel == 8 && colormapType == 0) || 145 | // Non-indexed without palette 146 | (bitsPerPixel > 8 && colormapType == 0); 147 | } 148 | 149 | bool valid() const { 150 | switch (imageType) { 151 | case UncompressedIndexed: 152 | case RleIndexed: 153 | return (bitsPerPixel == 8); 154 | case UncompressedRgb: 155 | case RleRgb: 156 | return (bitsPerPixel == 15 || 157 | bitsPerPixel == 16 || 158 | bitsPerPixel == 24 || 159 | bitsPerPixel == 32); 160 | case UncompressedGray: 161 | case RleGray: 162 | return (bitsPerPixel == 8); 163 | } 164 | return false; 165 | } 166 | 167 | // Returns the number of bytes per pixel needed in an image 168 | // created with this Header information. 169 | int bytesPerPixel() const { 170 | if (isRgb()) 171 | return 4; 172 | else 173 | return 1; 174 | } 175 | 176 | }; 177 | 178 | namespace details { 179 | 180 | class ImageIterator { 181 | public: 182 | ImageIterator(); 183 | ImageIterator(const Header& header, Image& image); 184 | 185 | // Put a pixel value into the image and advance the iterator. 186 | template 187 | bool putPixel(const T value) { 188 | *((T*)m_ptr) = value; 189 | return advance(); 190 | } 191 | 192 | // Get one pixel from the image and advance the iterator. 193 | template 194 | T getPixel() { 195 | T value = *((T*)m_ptr); 196 | advance(); 197 | return value; 198 | } 199 | 200 | public: 201 | bool advance(); 202 | void calcPtr(); 203 | 204 | Image* m_image; 205 | int m_x, m_y; 206 | int m_w, m_h; 207 | int m_dx, m_dy; 208 | uint8_t* m_ptr; 209 | }; 210 | 211 | } // namespace details 212 | 213 | class FileInterface { 214 | public: 215 | virtual ~FileInterface() { } 216 | 217 | // Returns true if we can read/write bytes from/into the file 218 | virtual bool ok() const = 0; 219 | 220 | // Current position in the file 221 | virtual size_t tell() = 0; 222 | 223 | // Jump to the given position in the file 224 | virtual void seek(size_t absPos) = 0; 225 | 226 | // Returns the next byte in the file or 0 if ok() = false 227 | virtual uint8_t read8() = 0; 228 | 229 | // Writes one byte in the file (or do nothing if ok() = false) 230 | virtual void write8(uint8_t value) = 0; 231 | }; 232 | 233 | class StdioFileInterface : public tga::FileInterface { 234 | public: 235 | StdioFileInterface(FILE* file); 236 | bool ok() const override; 237 | size_t tell() override; 238 | void seek(size_t absPos) override; 239 | uint8_t read8() override; 240 | void write8(uint8_t value) override; 241 | 242 | private: 243 | FILE* m_file; 244 | bool m_ok; 245 | }; 246 | 247 | class Delegate { 248 | public: 249 | virtual ~Delegate() {} 250 | // Must true if we should continue the decoding process. 251 | virtual bool notifyProgress(double progress) = 0; 252 | }; 253 | 254 | class Decoder { 255 | public: 256 | Decoder(FileInterface* file); 257 | 258 | bool hasAlpha() const { return m_hasAlpha; } 259 | 260 | // Reads the header + colormap (if the file has a 261 | // colormap). Returns true if the header is valid. 262 | bool readHeader(Header& header); 263 | 264 | // Reads the image. 265 | bool readImage(const Header& header, 266 | Image& image, 267 | Delegate* delegate = nullptr); 268 | 269 | // Fixes alpha channel for images with invalid alpha values (this 270 | // is optional, in case you want to preserve the original data 271 | // from the file, don't use it). 272 | void postProcessImage(const Header& header, 273 | Image& image); 274 | 275 | private: 276 | void readColormap(Header& header); 277 | 278 | template 279 | bool readUncompressedData(const int w, uint32_t (Decoder::*readPixel)()); 280 | 281 | template 282 | bool readRleData(const int w, uint32_t (Decoder::*readPixel)()); 283 | 284 | uint32_t read8(); 285 | uint32_t read16(); 286 | uint32_t read32(); 287 | uint32_t read32AsRgb(); 288 | uint32_t read24AsRgb(); 289 | uint32_t read16AsRgb(); 290 | 291 | FileInterface* m_file; 292 | bool m_hasAlpha = false; 293 | details::ImageIterator m_iterator; 294 | }; 295 | 296 | class Encoder { 297 | public: 298 | Encoder(FileInterface* file); 299 | 300 | // Writes the header + colormap 301 | void writeHeader(const Header& header); 302 | void writeImage(const Header& header, 303 | const Image& image, 304 | Delegate* delegate = nullptr); 305 | void writeFooter(); 306 | 307 | private: 308 | template 309 | void writeRleScanline(const int w, const Image& image, 310 | int y, void (Encoder::*writePixel)(color_t)); 311 | 312 | template 313 | void countRepeatedPixels(const int w, const Image& image, 314 | int x0, int y, int& offset, int& count); 315 | 316 | void write8(uint32_t value); 317 | void write16(uint32_t value); 318 | void write32(uint32_t value); 319 | void write16Rgb(uint32_t c); 320 | void write24Rgb(uint32_t c); 321 | void write32Rgb(uint32_t c); 322 | 323 | FileInterface* m_file; 324 | bool m_hasAlpha = false; 325 | details::ImageIterator m_iterator; 326 | }; 327 | 328 | } // namespace tga 329 | 330 | #endif 331 | -------------------------------------------------------------------------------- /tga_decoder.cpp: -------------------------------------------------------------------------------- 1 | // Aseprite TGA Library 2 | // Copyright (C) 2020 Igara Studio S.A. 3 | // 4 | // This file is released under the terms of the MIT license. 5 | // Read LICENSE.txt for more information. 6 | 7 | #include "tga.h" 8 | 9 | #include 10 | 11 | namespace tga { 12 | 13 | static inline int scale_5bits_to_8bits(int v) { 14 | assert(v >= 0 && v < 32); 15 | return (v << 3) | (v >> 2); 16 | } 17 | 18 | Decoder::Decoder(FileInterface* file) 19 | : m_file(file) 20 | { 21 | } 22 | 23 | bool Decoder::readHeader(Header& header) 24 | { 25 | header.idLength = read8(); 26 | header.colormapType = read8(); 27 | header.imageType = read8(); 28 | header.colormapOrigin = read16(); 29 | header.colormapLength = read16(); 30 | header.colormapDepth = read8(); 31 | header.xOrigin = read16(); 32 | header.yOrigin = read16(); 33 | header.width = read16(); 34 | header.height = read16(); 35 | header.bitsPerPixel = read8(); 36 | header.imageDescriptor = read8(); 37 | 38 | // Invalid image size 39 | if (header.width == 0 || 40 | header.height == 0) 41 | return false; 42 | 43 | // Skip ID string (idLength bytes) 44 | if (header.idLength > 0) { 45 | uint8_t i = header.idLength; 46 | while (i--) { 47 | uint8_t chr = m_file->read8(); 48 | header.imageId.push_back(chr); 49 | } 50 | } 51 | 52 | #if 0 53 | // In the best case the "alphaBits" should be valid, but there are 54 | // invalid TGA files out there which don't indicate the 55 | // "alphaBits" correctly, so they could be 0 and use the alpha 56 | // channel anyway on each pixel. 57 | int alphaBits = (header.imageDescriptor & 15); 58 | m_hasAlpha = 59 | (header.bitsPerPixel == 32 && alphaBits == 8) || 60 | (header.bitsPerPixel == 16 && alphaBits == 1); 61 | #else 62 | // So to detect if a 32bpp or 16bpp TGA image has alpha, we'll use 63 | // the "alpha histogram" in postProcessImage() to check if there are 64 | // different alpha values. If there is only one alpha value (all 0 65 | // or all 255), we create an opaque image anyway. The only exception 66 | // to this rule is when all pixels are black and transparent 67 | // (RGBA=0), that is the only case when an image is fully 68 | // transparent. 69 | // 70 | // Note: This same heuristic is used in apps like macOS Preview: 71 | // https://twitter.com/davidcapello/status/1242803110868893697 72 | m_hasAlpha = 73 | (header.bitsPerPixel == 32) || 74 | (header.bitsPerPixel == 16); 75 | #endif 76 | 77 | // Read colormap 78 | if (header.colormapType == 1) 79 | readColormap(header); 80 | 81 | return (header.validColormapType() && 82 | header.valid()); 83 | } 84 | 85 | void Decoder::readColormap(Header& header) 86 | { 87 | header.colormap = Colormap(header.colormapLength); 88 | 89 | for (int i=0; i> 10) & 0x1F), 97 | scale_5bits_to_8bits((c >> 5) & 0x1F), 98 | scale_5bits_to_8bits(c & 0x1F)); 99 | break; 100 | } 101 | 102 | case 24: 103 | case 32: { 104 | const int b = read8(); 105 | const int g = read8(); 106 | const int r = read8(); 107 | int a; 108 | if (header.colormapDepth == 32) 109 | a = read8(); 110 | else 111 | a = 255; 112 | header.colormap[i] = rgba(r, g, b, a); 113 | break; 114 | } 115 | } 116 | } 117 | } 118 | 119 | bool Decoder::readImage(const Header& header, 120 | Image& image, 121 | Delegate* delegate) 122 | { 123 | // Bit 4 means right-to-left, else left-to-right 124 | // Bit 5 means top-to-bottom, else bottom-to-top 125 | m_iterator = details::ImageIterator(header, image); 126 | 127 | for (int y=0; y(header.width, &Decoder::read8)) 133 | return true; 134 | break; 135 | 136 | case UncompressedRgb: 137 | switch (header.bitsPerPixel) { 138 | case 15: 139 | case 16: 140 | if (readUncompressedData(header.width, &Decoder::read16AsRgb)) 141 | return true; 142 | break; 143 | case 24: 144 | if (readUncompressedData(header.width, &Decoder::read24AsRgb)) 145 | return true; 146 | break; 147 | case 32: 148 | if (readUncompressedData(header.width, &Decoder::read32AsRgb)) 149 | return true; 150 | break; 151 | default: 152 | assert(false); 153 | break; 154 | } 155 | break; 156 | 157 | case UncompressedGray: 158 | assert(header.bitsPerPixel == 8); 159 | if (readUncompressedData(header.width, &Decoder::read8)) 160 | return true; 161 | break; 162 | 163 | case RleIndexed: 164 | assert(header.bitsPerPixel == 8); 165 | if (readRleData(header.width, &Decoder::read8)) 166 | return true; 167 | break; 168 | 169 | case RleRgb: 170 | switch (header.bitsPerPixel) { 171 | case 15: 172 | case 16: 173 | if (readRleData(header.width, &Decoder::read16AsRgb)) 174 | return true; 175 | break; 176 | case 24: 177 | if (readRleData(header.width, &Decoder::read24AsRgb)) 178 | return true; 179 | break; 180 | case 32: 181 | if (readRleData(header.width, &Decoder::read32AsRgb)) 182 | return true; 183 | break; 184 | default: 185 | assert(false); 186 | break; 187 | } 188 | break; 189 | 190 | case RleGray: 191 | assert(header.bitsPerPixel == 8); 192 | if (readRleData(header.width, &Decoder::read8)) 193 | return true; 194 | break; 195 | } 196 | 197 | if (delegate && 198 | !delegate->notifyProgress(float(y) / float(header.height))) { 199 | break; 200 | } 201 | } 202 | 203 | return true; 204 | } 205 | 206 | void Decoder::postProcessImage(const Header& header, 207 | Image& image) 208 | { 209 | // The post-processing is only for RGB images with possible invalid 210 | // alpha information. 211 | if (!header.isRgb() || !m_hasAlpha) 212 | return; 213 | 214 | bool transparentImage = true; 215 | bool blackImage = true; 216 | 217 | for (int y=0; y 251 | bool Decoder::readUncompressedData(const int w, color_t (Decoder::*readPixel)()) 252 | { 253 | for (int x=0; x((this->*readPixel)())) 255 | return true; 256 | } 257 | return false; 258 | } 259 | 260 | // In the best case (TGA 2.0 spec) this should read just one 261 | // scanline, but in old TGA versions (1.0) it was possible to save 262 | // several scanlines with the same RLE data. 263 | // 264 | // Returns true when are are done. 265 | template 266 | bool Decoder::readRleData(const int w, color_t (Decoder::*readPixel)()) 267 | { 268 | for (int x=0; xok(); ) { 269 | int c = read8(); 270 | if (c & 0x80) { 271 | c = (c & 0x7f) + 1; 272 | x += c; 273 | const T pixel = (this->*readPixel)(); 274 | while (c-- > 0) 275 | if (m_iterator.putPixel(pixel)) 276 | return true; 277 | } 278 | else { 279 | ++c; 280 | x += c; 281 | while (c-- > 0) { 282 | if (m_iterator.putPixel((this->*readPixel)())) 283 | return true; 284 | } 285 | } 286 | } 287 | return false; 288 | } 289 | 290 | uint32_t Decoder::read8() 291 | { 292 | return m_file->read8(); 293 | } 294 | 295 | // Reads a WORD (16 bits) using in little-endian byte ordering. 296 | uint32_t Decoder::read16() 297 | { 298 | uint8_t b1 = m_file->read8(); 299 | uint8_t b2 = m_file->read8(); 300 | 301 | if (m_file->ok()) { 302 | return ((b2 << 8) | b1); // Little endian 303 | } 304 | else 305 | return 0; 306 | } 307 | 308 | // Reads a DWORD (32 bits) using in little-endian byte ordering. 309 | uint32_t Decoder::read32() 310 | { 311 | const uint8_t b1 = m_file->read8(); 312 | const uint8_t b2 = m_file->read8(); 313 | const uint8_t b3 = m_file->read8(); 314 | const uint8_t b4 = m_file->read8(); 315 | 316 | if (m_file->ok()) { 317 | // Little endian 318 | return ((b4 << 24) | (b3 << 16) | (b2 << 8) | b1); 319 | } 320 | else 321 | return 0; 322 | } 323 | 324 | uint32_t Decoder::read32AsRgb() 325 | { 326 | const uint8_t b = read8(); 327 | const uint8_t g = read8(); 328 | const uint8_t r = read8(); 329 | uint8_t a = read8(); 330 | if (!m_hasAlpha) 331 | a = 255; 332 | return rgba(r, g, b, a); 333 | } 334 | 335 | uint32_t Decoder::read24AsRgb() 336 | { 337 | const uint8_t b = read8(); 338 | const uint8_t g = read8(); 339 | const uint8_t r = read8(); 340 | return rgba(r, g, b, 255); 341 | } 342 | 343 | uint32_t Decoder::read16AsRgb() 344 | { 345 | const uint16_t v = read16(); 346 | uint8_t a = 255; 347 | if (m_hasAlpha) { 348 | if ((v & 0x8000) == 0) // Transparent bit 349 | a = 0; 350 | } 351 | return rgba(scale_5bits_to_8bits((v >> 10) & 0x1F), 352 | scale_5bits_to_8bits((v >> 5) & 0x1F), 353 | scale_5bits_to_8bits(v & 0x1F), 354 | a); 355 | } 356 | 357 | } // namespace tga 358 | --------------------------------------------------------------------------------