-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathNormalTextureProcessor.cpp
2235 lines (2080 loc) · 85.8 KB
/
NormalTextureProcessor.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// NormalTextureProcessor.cpp : This file contains the 'main' function. Program execution begins and ends there.
// Program to read in normals textures and analyze or clean up/convert them to other normals texture forms.
#include <iostream>
#include <fstream>
#include <string>
#include <assert.h>
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include "readtga.h"
#define VERSION_STRING L"1.01"
static int gErrorCount = 0;
static int gWarningCount = 0;
static wchar_t gErrorString[1000];
// 1000 errors of 100 characters each - sounds sufficient. Sloppy, I know.
#define CONCAT_ERROR_LENGTH (1000*100)
static wchar_t gConcatErrorString[CONCAT_ERROR_LENGTH];
#define UNKNOWN_FILE_EXTENSION 0
#define PNG_EXTENSION_FOUND 1
#define TGA_EXTENSION_FOUND 2
#define JPG_EXTENSION_FOUND 3
#define BMP_EXTENSION_FOUND 4
#define IMAGE_BUMP_TYPE 0x3
#define IMAGE_IS_NORMAL_MAP 1
#define IMAGE_IS_HEIGHT_MAP 2
#define IMAGE_IS_NOT_BUMP_MAP 3
#define IMAGE_BUMP_REAL 0x4
#define CHANNEL_RED 0
#define CHANNEL_GREEN 1
#define CHANNEL_BLUE 2
#define IMAGE_ALL_SAME 0x0001
#define IMAGE_R_CHANNEL_ALL_SAME 0x0002
#define IMAGE_G_CHANNEL_ALL_SAME 0x0004
#define IMAGE_B_CHANNEL_ALL_SAME 0x0008
#define IMAGE_GRAYSCALE 0x0010
#define IMAGE_Z_NORMALS_GOOD 0x0020
#define IMAGE_Z_NORMALS_CLOSE 0x0040
#define IMAGE_Z_NORMALS_FAR 0x0080
//#define IMAGE_VALID_NORMALS_FULL 0x0020
//#define IMAGE_ALMOST_VALID_NORMALS_FULL 0x0040
//#define IMAGE_ALMOST2_VALID_NORMALS_FULL 0x0080
#define IMAGE_VALID_ZVAL_NONNEG 0x0100
#define IMAGE_Z_NORMALS_GOOD_ZZERO 0x0200
#define IMAGE_Z_NORMALS_CLOSE_ZZERO 0x0400
#define IMAGE_Z_NORMALS_FAR_ZZERO 0x0800
//#define IMAGE_VALID_NORMALS_ZERO 0x0200
//#define IMAGE_ALMOST_VALID_NORMALS_ZERO 0x0400
//#define IMAGE_ALMOST2_VALID_NORMALS_ZERO 0x0800
#define IMAGE_VALID_NORMALS_XY 0x1000
#define IMAGE_ALMOST_VALID_NORMALS_XY 0x2000
#define IMAGE_ALMOST2_VALID_NORMALS_XY 0x4000
#define IMAGE_TYPE_NORMAL_FULL 1
#define IMAGE_TYPE_NORMAL_ZERO 2
#define IMAGE_TYPE_NORMAL_XY_ONLY 3
#define IMAGE_TYPE_HEIGHTFIELD 4
// for command line parsing
#define INC_AND_TEST_ARG_INDEX( loc ) \
argLoc++; \
if (argLoc == argc) { \
printHelp(); \
return 1; \
}
#ifdef SHOW_PIXEL_VALUE
#define PRINT_PIXEL(s,t,ch,c,r) { unsigned char *p = &(t).image_data[(ch)*((t).width*(r)+(c))]; printf ("%s %d %d %d\n",(s),(int)p[0],(int)p[1],(int)p[2]);}
#else
#define PRINT_PIXEL(s,t,ch,c,r)
#endif
#define MAX_PATH_AND_FILE (2*MAX_PATH)
// Normal 0-255 RGB values to XYZ set of floats
#define CONVERT_RGB_TO_Z_FULL(r,g,b, x,y,z) \
x = ((float)(r) / 255.0f) * 2.0f - 1.0f; \
y = ((float)(g) / 255.0f) * 2.0f - 1.0f; \
z = ((float)(b) / 255.0f) * 2.0f - 1.0f;
// convert Z to 0 to 1 range instead of -1 to 1, the norm nowadays
#define CONVERT_RGB_TO_Z_ZERO(r,g,b, x,y,z) \
x = ((float)(r) / 255.0f) * 2.0f - 1.0f; \
y = ((float)(g) / 255.0f) * 2.0f - 1.0f; \
z = (float)(b) / 255.0f;
#define CONVERT_CHANNEL_TO_FULL(chan, val) \
val = ((float)(chan) / 255.0f) * 2.0f - 1.0f;
#define CONVERT_CHANNEL_TO_ZERO(chan, val) \
val = (float)(chan) / 255.0f;
// for direct output or setting a value
#define CONVERT_CHANNEL_FULL_DIRECT(chan) \
(((float)(chan) / 255.0f) * 2.0f - 1.0f)
#define CONVERT_CHANNEL_ZERO_DIRECT(chan) \
((float)(chan) / 255.0f)
#define CONVERT_Z_FULL_TO_RGB(x,y,z, r,g,b) \
r = (unsigned char)((((x) + 1.0f)/2.0f)*255.0f + 0.5f); \
g = (unsigned char)((((y) + 1.0f)/2.0f)*255.0f + 0.5f); \
b = (unsigned char)((((z) + 1.0f)/2.0f)*255.0f + 0.5f);
#define CONVERT_Z_ZERO_TO_RGB(x,y,z, r,g,b) \
r = (unsigned char)((((x) + 1.0f)/2.0f)*255.0f + 0.5f); \
g = (unsigned char)((((y) + 1.0f)/2.0f)*255.0f + 0.5f); \
b = (unsigned char)((z)*255.0f + 0.5f);
#define CONVERT_FULL_TO_CHANNEL(val, chan) \
chan = (unsigned char)((((val) + 1.0f)/2.0f)*255.0f + 0.5f);
#define CONVERT_ZERO_TO_CHANNEL(val, chan) \
chan = (unsigned char)((val)*255.0f + 0.5f);
#define COMPUTE_Z_FROM_XY(x,y, z) \
z = sqrt(1.0f - ((x) * (x) + (y) * (y)));
#define COMPUTE_GRAY_FROM_RGB(r,g,b, gray) \
gray = (unsigned char)((0.2126f * ((float)r / 255.0f) + 0.7152f * ((float)g / 255.0f) + 0.0722f * ((float)b / 255.0f)) * 255.0f + 0.5f);
// maximum difference in length from 1.0 for normals when saving in [0,255] numbers,
// when Z value is computed from X and Y
#define MAX_NORMAL_LENGTH_DIFFERENCE 0.00387f
#define MAX_Z_DIFFERENCE 0.00393f
#define VECTOR_LENGTH(x,y,z) \
sqrt(x*x + y*y + z*z)
struct Options {
bool analyze;
std::wstring inputDirectory;
bool inputZzeroToOne;
bool inputZnegOneToOne;
bool inputXYonly;
bool inputDirectX;
bool inputHeightfield;
bool outputAll;
bool outputClean;
std::wstring outputDirectory;
bool outputZzeroToOne;
bool outputDirectX;
bool outputHeatmap;
bool allowNegativeZ;
float heightfieldScale;
bool borderHeightfield;
bool verbose;
bool csvAll;
bool csvErrors;
float errorTolerance;
float errorToleranceXY;
//bool roundtrip;
};
Options gOptions;
int gFilesFound = 0;
int gFilesOutput = 0;
std::wstring gListUnknown;
std::wstring gListStandard;
std::wstring gListStandardDirty;
std::wstring gListStandardSame;
std::wstring gListZZero;
std::wstring gListZZeroDirty;
std::wstring gListXYonly;
std::wstring gListHeightfield;
std::wstring gListAllSame;
//-------------------------------------------------------------------------
void printHelp();
int filterAndProcessImageFile(wchar_t* inputFile);
bool processImageFile(wchar_t* inputFile, int fileType);
bool XYanalysis(int image_field_bits, int image_size, int* normalizable_xy, int xy_outside_bounds);
bool Zanalysis(int image_type, int image_field_bits, int image_size, int normal_good, int normal_close, float minnormallength, float maxnormallength, int normal_zval_nonnegative, float min_z, int zmaxabsdiff);
void reportReadError(int rc, const wchar_t* filename);
void saveErrorForEnd();
//bool examineForDirectXStyle(progimage_info* src);
void convertHeightfieldToXYZ(progimage_info* dst, progimage_info* src, float heightfieldScale, bool input_grayscale, bool y_flip, bool clamp_border);
void cleanAndCopyNormalTexture(progimage_info* dst, progimage_info* src, int image_type, bool output_zzero, bool y_flip, bool heatmap, bool reason_xy);
bool removeFileType(wchar_t* name);
int isImageFile(wchar_t* name);
bool dirExists(const wchar_t* path);
bool createDir(const wchar_t* path);
int wmain(int argc, wchar_t* argv[])
{
// experiment: try all possible X and Y values in a quadrant, compute Z. How close is the triplet formed to 1.0 length?
// Result: Average diff from length of 1.0f: 0.00129662, maximum length difference detected is 0.00386751 and
// maximum z difference detected is 0.00391376 for the 51040 valid pixel values
// Fun fact: 51040 combinations out of 256*256 of red and green combinations are valid, else red and green's defined length is > 1.0.
// So 77.88% of all r,g combinations [0..255],[0..255] are valid.
//#define SCRATCHPAD
#ifdef SCRATCHPAD
// This is just a place to check on such things, such as valid r,g combinations and how much various values vary in floating point.
int row, col;
int r, g, b;
float x, y, z, znew, len;
float absdiffsum = 0.0f;
float maxdiff = -1.0f;
float zmaxdiff = -1.0f;
int validcount = 0;
for (row = 0; row < 256; row++) {
for (col = 0; col < 256; col++) {
// really just getting the x and y here:
CONVERT_RGB_TO_Z_FULL(col, row, 255, x, y, z);
// is X and Y valid, forming a vector <= 1.0 in length?
if ((x * x + y * y) <= 1.0f) {
// find the proper z value that normalizes this vector
COMPUTE_Z_FROM_XY(x, y, z);
validcount++;
// convert back to rgb
CONVERT_Z_FULL_TO_RGB(x, y, z, r, g, b);
// and then to xyz again (really just need to convert Z in both cases)
CONVERT_RGB_TO_Z_FULL(r, g, b, x, y, znew);
// now, how far off from 1.0 is the xyz vector length?
len = VECTOR_LENGTH(x, y, znew);
float absdiff = abs(1.0f - len);
absdiffsum += absdiff;
if (absdiff > maxdiff) {
maxdiff = absdiff;
}
float zabsdiff = abs(z - znew);
if (zabsdiff > zmaxdiff) {
zmaxdiff = zabsdiff;
}
// std::wcout << absdiff << "\n";
// last little reality check: does it round-trip?
unsigned char r1, g1, b1;
CONVERT_Z_FULL_TO_RGB(x, y, z, r1, g1, b1);
// this passes with flying colors
assert(r == r1 && g == g1 && b == b1);
if (r != r1 || g != g1 || b != b1) {
std::wcerr << "ERROR: something weird's going on, the round trip test failed.\n" << std::flush;
}
}
}
}
std::wcout << "Average diff from length of 1.0f: " << absdiffsum / validcount << ", maximum length difference detected is " << maxdiff << " and maximum z difference detected is " << zmaxdiff << " for the " << validcount << " valid pixel values\n";
#endif
gConcatErrorString[0] = 0;
int argLoc = 1;
gOptions.analyze = false;
gOptions.inputHeightfield = false;
gOptions.inputXYonly = false;
gOptions.inputDirectX = false;
gOptions.outputAll = false;
gOptions.outputClean = false;
gOptions.outputZzeroToOne = false;
gOptions.outputDirectX = false;
gOptions.outputHeatmap = false;
gOptions.allowNegativeZ = false;
gOptions.heightfieldScale = 5.0f;
gOptions.borderHeightfield = false;
gOptions.verbose = false;
gOptions.csvAll = false;
gOptions.csvErrors = false;
gOptions.errorTolerance = 0.02f;
gOptions.errorToleranceXY = 0.10f;
//gOptions.roundtrip = false;
std::vector<std::wstring> fileList;
while (argLoc < argc)
{
if (wcscmp(argv[argLoc], L"-a") == 0)
{
gOptions.analyze = true;
}
else if (wcscmp(argv[argLoc], L"-idir") == 0)
{
// input directory
INC_AND_TEST_ARG_INDEX(argLoc);
gOptions.inputDirectory = argv[argLoc];
}
else if (wcscmp(argv[argLoc], L"-izzero") == 0)
{
gOptions.inputZzeroToOne = true;
}
else if (wcscmp(argv[argLoc], L"-izneg") == 0)
{
gOptions.inputZnegOneToOne = true;
}
else if (wcscmp(argv[argLoc], L"-ixy") == 0)
{
gOptions.inputXYonly = true;
}
else if (wcscmp(argv[argLoc], L"-idx") == 0)
{
gOptions.inputDirectX = true;
}
else if (wcscmp(argv[argLoc], L"-ihf") == 0)
{
gOptions.inputHeightfield = true;
}
else if (wcscmp(argv[argLoc], L"-oall") == 0)
{
gOptions.outputAll = true;
}
else if (wcscmp(argv[argLoc], L"-oclean") == 0)
{
gOptions.outputClean = true;
}
else if (wcscmp(argv[argLoc], L"-odir") == 0)
{
// output directory
INC_AND_TEST_ARG_INDEX(argLoc);
gOptions.outputDirectory = argv[argLoc];
}
else if (wcscmp(argv[argLoc], L"-ozzero") == 0)
{
gOptions.outputZzeroToOne = true;
}
else if (wcscmp(argv[argLoc], L"-odx") == 0)
{
gOptions.outputDirectX = true;
}
else if (wcscmp(argv[argLoc], L"-oheatmap") == 0)
{
gOptions.outputHeatmap = true;
}
else if (wcscmp(argv[argLoc], L"-allownegz") == 0)
{
gOptions.allowNegativeZ = true;
}
else if (wcscmp(argv[argLoc], L"-hfs") == 0)
{
// heightfield scale float value
INC_AND_TEST_ARG_INDEX(argLoc);
swscanf_s(argv[argLoc], L"%f", &gOptions.heightfieldScale);
}
else if (wcscmp(argv[argLoc], L"-hborder") == 0)
{
// TODO: should there also be a "continue slope" option for the border, too?
// Basically, if on the border, take the difference and double it (since we'll be using the texel itself for one offset).
gOptions.borderHeightfield = true;
}
else if (wcscmp(argv[argLoc], L"-csv") == 0)
{
gOptions.csvAll = true;
}
else if (wcscmp(argv[argLoc], L"-csve") == 0)
{
gOptions.csvErrors = true;
}
else if (wcscmp(argv[argLoc], L"-etol") == 0)
{
// heightfield scale float value
INC_AND_TEST_ARG_INDEX(argLoc);
swscanf_s(argv[argLoc], L"%f", &gOptions.errorTolerance);
if (gOptions.errorTolerance < 0.0f || gOptions.errorTolerance > 100.0f) {
std::wcerr << "ERROR: error tolerance percentage must be between 0 and 100.\n";
printHelp();
return 1;
}
// turn percent tolerance into 0-1 range value
gOptions.errorTolerance *= 0.01f;
}
else if (wcscmp(argv[argLoc], L"-etolxy") == 0)
{
// heightfield scale float value
INC_AND_TEST_ARG_INDEX(argLoc);
swscanf_s(argv[argLoc], L"%f", &gOptions.errorToleranceXY);
if (gOptions.errorToleranceXY < 0.0f || gOptions.errorToleranceXY > 1.0f) {
std::wcerr << "ERROR: error tolerance XY magnitude must be between 0.0 and 1.0.\n";
printHelp();
return 1;
}
}
else if (wcscmp(argv[argLoc], L"-v") == 0)
{
gOptions.verbose = true;
}
else
{
// assume it's a file name, but let's check first if the user thinks it's an option
if (argv[argLoc][0] == '-') {
// unknown option
std::wcerr << "ERROR: unknown option '" << argv[argLoc] << "'.\n";
printHelp();
return 1;
}
fileList.push_back(argv[argLoc]);
}
argLoc++;
}
if (gOptions.verbose) {
std::wcout << "NormalTextureProcessor version " << VERSION_STRING << "\n\n" << std::flush;
}
// check validity of options only if we're actually outputing
if (!(gOptions.outputAll || gOptions.outputClean || gOptions.csvAll || gOptions.csvErrors)) {
// No output mode chosen, so these options won't do anything
if (gOptions.inputHeightfield || gOptions.inputDirectX || gOptions.borderHeightfield ||
gOptions.outputDirectory.size() > 0 ||
gOptions.outputDirectX || gOptions.outputZzeroToOne || gOptions.outputHeatmap) {
std::wcerr << "WARNING: options associated with output are set, but -oall or -oclean are not set. One of these two must be chosen.\n" << std::flush;
}
}
if (gOptions.inputHeightfield && gOptions.inputDirectX) {
std::wcerr << "WARNING: -ihf says to assume input is a heightfield, but -idx says it's DirectX style; latter option is ignored.\n" << std::flush;
gOptions.inputDirectX = false;
}
if ((gOptions.inputZzeroToOne ? 1 : 0) + (gOptions.inputZnegOneToOne ? 1 : 0) + (gOptions.inputXYonly ? 1 : 0) + (gOptions.inputHeightfield ? 1 : 0) > 1) {
std::wcerr << "ERROR: more than one input specifier of [-izneg | -izzero | -ixy | -ihf] is given; choose at most one.\n" << std::flush;
printHelp();
return 1;
}
if (gOptions.inputHeightfield && gOptions.inputXYonly) {
std::wcerr << "ERROR: input specifiers -ihf and -xy cannot both be set.\n" << std::flush;
printHelp();
return 1;
}
if ((gOptions.csvAll || gOptions.csvErrors) && ((gOptions.inputZzeroToOne ? 1 : 0) + (gOptions.inputZnegOneToOne ? 1 : 0) + (gOptions.inputXYonly ? 1 : 0) != 1)) {
std::wcerr << "ERROR: for CSV analysis output you must specify the input type [-izneg | -izzero | -ixy].\n" << std::flush;
printHelp();
return 1;
}
int pos;
// Does input directory exist?
if (gOptions.inputDirectory.size() > 0) {
// not the current directory, so check
if (!dirExists(gOptions.inputDirectory.c_str())) {
std::wcerr << "ERROR: input directory " << gOptions.inputDirectory.c_str() << " does not exist.\n" << std::flush;
printHelp();
return 1;
}
}
// Does output directory exist?
if (gOptions.outputDirectory.size() > 0) {
// not the current directory, so check
if (!dirExists(gOptions.outputDirectory.c_str())) {
// try to create output directory
if (!createDir(gOptions.outputDirectory.c_str())) {
std::wcerr << "ERROR: output directory " << gOptions.outputDirectory.c_str() << " does not exist and cannot be created.\n" << std::flush;
printHelp();
return 1;
}
}
// add a "/" to the output directory path if not there; do it now
pos = (int)gOptions.outputDirectory.size() - 1;
if (gOptions.outputDirectory[pos] != '/' && gOptions.outputDirectory[pos] != '\\') {
gOptions.outputDirectory.push_back('/');
}
}
// look through files and process each one.
HANDLE hFind;
WIN32_FIND_DATA ffd;
// add "/" to end of directory if not already there
if (gOptions.inputDirectory.size() > 0) {
// input directory exists, so make sure there's a "\" or "/" at the end of this path
pos = (int)gOptions.inputDirectory.size() - 1;
if (gOptions.inputDirectory[pos] != '/' && gOptions.inputDirectory[pos] != '\\') {
gOptions.inputDirectory.push_back('/');
}
}
wchar_t fileSearch[MAX_PATH];
wcscpy_s(fileSearch, MAX_PATH, gOptions.inputDirectory.c_str());
// files to search on. We'll later filter by .png and .tga
if (fileList.size() > 0) {
bool bad_file = false;
for (int i = 0; i < fileList.size(); i++) {
wcscpy_s(fileSearch, MAX_PATH, fileList[i].c_str());
if (filterAndProcessImageFile(fileSearch) == UNKNOWN_FILE_EXTENSION) {
// file not an image file - note error
std::wcerr << "Error: file '" << fileSearch << "' is not an image file.\n";
bad_file = true;
}
}
if (bad_file) {
printHelp();
}
}
else {
wcscat_s(fileSearch, MAX_PATH, L"*");
hFind = FindFirstFile(fileSearch, &ffd);
if (hFind != INVALID_HANDLE_VALUE)
{
// go through all the files in the blocks directory
do {
filterAndProcessImageFile(ffd.cFileName);
} while (FindNextFile(hFind, &ffd) != 0);
FindClose(hFind);
}
}
if (gErrorCount) {
std::wcerr << "\nERROR SUMMARY:\n" << gConcatErrorString << "\n";
std::wcerr << "Error count: " << gErrorCount << " error";
if (gErrorCount == 1) {
std::wcerr << " was";
}
else {
std::wcerr << "s were";
}
std::wcerr << " generated.\n" << std::flush;
}
if (gOptions.analyze) {
// summary lists of different file types
std::wcout << "==============================\nFile types found, by category:\n";
bool output_any = false;
if (gListStandardSame.size() > 0) {
output_any = true;
std::wcout << " Standard normals textures that have no bumps, all texels are the same value:" << gListStandardSame << "\n";
}
if (gListStandard.size() > 0) {
output_any = true;
std::wcout << " Standard normals textures:" << gListStandard << "\n";
if (gListStandardDirty.size() > 0) {
output_any = true;
std::wcout << " Standard normals textures that are not well normalized:" << gListStandardDirty << "\n";
}
}
if (gListZZero.size() > 0) {
output_any = true;
std::wcout << " Z-Zero normals textures:" << gListZZero << "\n";
if (gListZZeroDirty.size() > 0) {
output_any = true;
std::wcout << " Z-Zero normals textures that are not well normalized:" << gListZZeroDirty << "\n";
}
}
if (gListXYonly.size() > 0) {
output_any = true;
std::wcout << " XY-only normals textures:" << gListXYonly << "\n";
}
if (gListHeightfield.size() > 0) {
output_any = true;
std::wcout << " Heightfield textures:" << gListHeightfield << "\n";
}
if (gListUnknown.size() > 0) {
output_any = true;
std::wcout << " Unknown textures:" << gListUnknown << "\n";
}
if (gListAllSame.size() > 0) {
output_any = true;
std::wcout << " Textures with no bumps:" << gListAllSame << "\n";
}
if (!output_any) {
if (gOptions.inputDirectory.size() > 0) {
std::wcout << " No PNG or TGA files found to analyze in directory '" << gOptions.inputDirectory << "'\n";
}
else {
std::wcout << " No PNG or TGA files found to analyze in current directory\n";
}
}
}
std::wcout << "\nNormalTextureProcessor summary: " << gFilesFound << " PNG/TGA file" << (gFilesFound == 1 ? "" : "s") << " discovered and " << gFilesOutput << " output.\n" << std::flush;
return 0;
}
int filterAndProcessImageFile(wchar_t* inputFile)
{
int file_type = isImageFile(inputFile);
if (file_type != UNKNOWN_FILE_EXTENSION) {
// Seems to be an image file. Can we process it?
if (file_type == PNG_EXTENSION_FOUND || file_type == TGA_EXTENSION_FOUND) {
gFilesFound++;
gFilesOutput += (processImageFile(inputFile, file_type) ? 1 : 0);
return file_type;
}
else {
std::wcerr << "WARNING: file " << inputFile << " is not an image file type currently supported in this program. Convert to PNG or TGA to process it.\n" << std::flush;
}
}
return UNKNOWN_FILE_EXTENSION;
}
// return true if successfully read in and output
bool processImageFile(wchar_t* inputFile, int file_type)
{
bool rc_output = false;
wchar_t inputPathAndFile[MAX_PATH];
// note that inputDirectory already has a "/" at the end, due to manipulation at the start of the program
wcscpy_s(inputPathAndFile, MAX_PATH, gOptions.inputDirectory.c_str());
wcscat_s(inputPathAndFile, MAX_PATH, inputFile);
// read input file
progimage_info imageInfo;
int rc = readImage(&imageInfo, inputPathAndFile, LCT_RGB, file_type);
if (rc != 0)
{
// file not found
reportReadError(rc, inputPathAndFile);
return rc_output;
}
// see how many bits etc. the input PNG has (if it's a PNG);
int bitdepth = 8;
LodePNGColorType colortype = LCT_RGB;
if (file_type == PNG_EXTENSION_FOUND) {
std::vector<unsigned char> buffer;
if (lodepng::load_file(buffer, inputPathAndFile) == 0) {
unsigned w, h;
lodepng::State state;
lodepng_inspect(&w, &h, &state, &buffer[0], buffer.size());
bitdepth = state.info_png.color.bitdepth;
colortype = state.info_png.color.colortype;
}
else {
// what? We just loaded the file earlier
std::wcerr << "ERROR: file " << inputFile << "' could not be read to get its header information.\n" << std::flush;
return 0;
}
}
// All the things to test:
// Are all the pixels the same color? If so, no normals data, no matter what format.
// Is this same color the "blue" of 127/127/255? Note it's a "proper" normal map format, but no real data.
// Is each separate channel entirely the same? If only the red channel has data (or green, or blue), then
// that might be a bump map - assume so.
// Are the pixel values a 0 to 1 Z or -1 to 1 Z? How close to each (how normalized are the directions)?
// Are the r and g values combined form a vector > 1.0f in length without even including b's?
int row, col, channel;
int image_field_bits = 0x0;
float x, y, z, zzero, zcalc;
x = 0; y = 0; z = 0; zzero = 0; // initialized mostly to make the compiler happy; these do get used in some analysis messages.
unsigned char* src_data = &imageInfo.image_data[0];
// various counters
int pixels_match = 0;
int rgb_match[3];
int grayscale = 0;
int normalizable_xy[3];
normalizable_xy[0] = normalizable_xy[1] = normalizable_xy[2] = 0;
int xy_outside_bounds = 0;
//int z_outside_bounds = 0;
//int z_outside_zzero_bounds = 0;
//int normal_length_zneg[3];
//normal_length_zneg[0] = normal_length_zneg[1] = normal_length_zneg[2] = 0;
int normal_zval_nonnegative = 0;
int normal_length_zzero[3];
normal_length_zzero[0] = normal_length_zzero[1] = normal_length_zzero[2] = 0;
float min_z = 999.0f;
float zzero_min_z = 999.0f;
int zmaxabsdiff = 0;
int zmaxabsdiff_zero = 0;
float xymaxlength = 0.0f;
float maxnormallength = 0.0f;
float minnormallength = 2.0f;
float zzero_maxnormallength = 0.0f;
float zzero_minnormallength = 2.0f;
double xsum, ysum, zsum, zzerosum;
xsum = ysum = zsum = zzerosum = 0.0;
int chan_min[3];
int chan_max[3];
int gray_min = 256*256;
int gray_max = -1;
unsigned char first_pixel[3];
for (channel = 0; channel < 3; channel++) {
first_pixel[channel] = src_data[channel];
rgb_match[channel] = 0;
chan_min[channel] = 256 * 256;
chan_max[channel] = -1;
}
int image_size = imageInfo.width * imageInfo.height;
if (image_size <= 0) {
std::wcerr << "WARNING: file '" << inputFile << "' has no data so is not processed further.\n";
return true;
}
// if CSV desired, open file
std::wfstream csvOutput;
if (gOptions.csvAll || gOptions.csvErrors) {
wchar_t csvOutputPathAndFile[MAX_PATH];
// note that outputDirectory already has a "/" at the end, due to manipulation at the start of the program
wcscpy_s(csvOutputPathAndFile, MAX_PATH, gOptions.outputDirectory.c_str());
// We always use PNG as output, so change the .TGA suffix if necouted be.
wchar_t csvFileName[MAX_PATH];
wcscpy_s(csvFileName, MAX_PATH, inputFile);
// change suffix to CSV
size_t csv_suffix_pos = wcslen(csvFileName) - 4;
csvFileName[csv_suffix_pos] = 0;
wcscat_s(csvFileName, MAX_PATH, L".csv");
// add the file name to the output directory
wcscat_s(csvOutputPathAndFile, MAX_PATH, csvFileName);
csvOutput.open(csvOutputPathAndFile, std::fstream::out);
if (csvOutput.fail()) {
std::wcerr << "ERROR: could not open file '" << csvOutputPathAndFile << "' for writing, failbit " << csvOutput.fail() << ".\n";
return false;
}
if (gOptions.verbose) {
std::wcout << "New CSV file '" << csvOutputPathAndFile << "' created.\n\n" << std::flush;
}
csvOutput << "Column, Row, r, g, b, X, Y, Z, Z-zero, XY length, XYZ length, XYZ-zero length, XY > 1, Z < 0, " <<
(!gOptions.inputZzeroToOne ? "XYZ != 1\n" : "XYZ - zero != 1\n");
}
// note these are reset each iteration only when writing to a CSV file, as these are used only for CSV output
char err_outside_xy = ' ';
char err_z_neg_is_negative = ' ';
char err_outside_z = ' ';
char err_outside_z_zero = ' ';
int gray = 0; // initialized to make the compiler happy
//int ignore_normal_texel_count = 0;
int b_less_than_255 = 0;
int normal_good = 0;
int zzero_normal_good = 0;
int normal_close = 0;
int zzero_normal_close = 0;
int normal_is_straight_up = 0;
for (row = 0; row < imageInfo.height; row++)
{
for (col = 0; col < imageInfo.width; col++)
{
float normal_length_max = -1.0f;
float normal_length_min = 999.0f;
float zzero_normal_length_max = -1.0f;
float zzero_normal_length_min = 999.0f;
// TODO: should probably ignore texels that are all-black or all-white as far as
// analysis goes. We could assume such texels are in unused parts of the texture.
// If we go this route, the XYZ statistics get messier.
//bool black_or_white = false;
//if ((src_data[0] == 0 && src_data[1] == 0 && src_data[2] == 0) ||
// (src_data[0] == 255 && src_data[1] == 255 && src_data[2] == 255)) {
// // note how many black and white
// ignore_normal_texel_count++;
// black_or_white = true;
//}
// Does this pixel match the first pixel? (i.e. are all pixels the same?)
if (src_data[0] == first_pixel[0] && src_data[1] == first_pixel[1] && src_data[2] == first_pixel[2]) {
pixels_match++;
}
// channel by channel, does the channel match the first channel value?
for (channel = 0; channel < 3; channel++) {
if (src_data[channel] == first_pixel[channel]) {
rgb_match[channel]++;
}
// min max
if (chan_min[channel] > src_data[channel]) {
chan_min[channel] = src_data[channel];
}
if (chan_max[channel] < src_data[channel]) {
chan_max[channel] = src_data[channel];
}
}
if (src_data[2] != 255) {
b_less_than_255++;
}
// heightfield height range
COMPUTE_GRAY_FROM_RGB(src_data[0], src_data[1], src_data[2], gray);
if (gray_min > gray) {
gray_min = gray;
}
if (gray_max < gray) {
gray_max = gray;
}
// are all three channel values the same? (grayscale)
if (src_data[0] == src_data[1] && src_data[1] == src_data[2]) {
grayscale++;
}
//if (!black_or_white) {
// Try conversion to XYZ's. Which if any make sense?
CONVERT_RGB_TO_Z_FULL(src_data[0], src_data[1], src_data[2], x, y, z);
CONVERT_CHANNEL_TO_ZERO(src_data[2], zzero);
xsum += x;
ysum += y;
zsum += z;
zzerosum += zzero;
// compute normal's length from values and compare
float normal_length = VECTOR_LENGTH(x, y, z);
if (maxnormallength < normal_length) {
maxnormallength = normal_length;
}
if (minnormallength > normal_length) {
minnormallength = normal_length;
}
// Check if the range of z values is non-negative. It may just be the normals are not normalized?
if (z > -MAX_Z_DIFFERENCE) {
normal_zval_nonnegative++;
if (z < 0.0f && !gOptions.allowNegativeZ) {
err_z_neg_is_negative = '1';
}
}
else {
if (!gOptions.allowNegativeZ) {
err_z_neg_is_negative = 'X';
}
}
// lowest z value found (for -1 to 1 conversion)
if (min_z > z) {
min_z = z;
}
if (zzero_min_z > zzero) {
zzero_min_z = zzero;
}
float zzero_normal_length = VECTOR_LENGTH(x, y, zzero);
if (zzero_maxnormallength < zzero_normal_length) {
zzero_maxnormallength = zzero_normal_length;
}
if (zzero_minnormallength > zzero_normal_length) {
zzero_minnormallength = zzero_normal_length;
}
// find the proper z value
float xy_len = sqrt(x * x + y * y);
if (xymaxlength < xy_len) {
xymaxlength = xy_len;
}
// is x,y "short enough" to properly represent a normalized vector?
if (xy_len <= 1.0f + 2.0f * MAX_NORMAL_LENGTH_DIFFERENCE) {
// it's (mostly) short enough in X and Y.
// determine what z really should be for this x,y pair
if (xy_len <= 1.0f) {
// xy_len is definitely valid
normalizable_xy[0]++;
COMPUTE_Z_FROM_XY(x, y, zcalc);
}
else {
// x,y's length is a tiny bit above 1.0 in length, but still not unreasonable.
// Set z to 0.0
zcalc = 0.0f;
if (xy_len <= 1.0f + MAX_NORMAL_LENGTH_DIFFERENCE) {
// it's one texel level difference
normalizable_xy[1]++;
err_outside_xy = '1';
}
else {
// it's a two-texel level difference
normalizable_xy[2]++;
err_outside_xy = '2';
}
}
// It's extremely possible that an input RGB is off by 1 in X or Y which radically changes the Z value
// we derive. For example, (0,128) and (1,128) change by only 1 in red. But the Z/blue values derived differ a lot:
// for (0,128), Z is 0.0 and blue is then 128. For (1,128), Z is 0.1249, which gives 143! So checking the difference
// in Z values is misleading, especially for extreme normals. Better is to see if the RGB vector is reasonable in
// its length. We do this by a simple heuristic: vary the RGB by +/-1 and get the length of each. If the minimum
// length and maximum length includes 1.0 in it, then the RGB normal is "good enough". We could get fancier, varying
// say just the two shortest components of the normal. KISS.
int iv;
float xv[3], yv[3], zv[3], zzerov[3];
float x_max = -999.0f;
float x_min = 999.0f;
float y_max = -999.0f;
float y_min = 999.0f;
for (iv = 0; iv <= 2; iv++) {
CONVERT_CHANNEL_TO_FULL((int)src_data[0] - 1 + iv, xv[iv]);
if (x_max < xv[iv]) {
x_max = xv[iv];
}
if (x_min > xv[iv]) {
x_min = xv[iv];
}
xv[iv] *= xv[iv];
CONVERT_CHANNEL_TO_FULL((int)src_data[1] - 1 + iv, yv[iv]);
if (y_max < yv[iv]) {
y_max = yv[iv];
}
if (y_min > yv[iv]) {
y_min = yv[iv];
}
yv[iv] *= yv[iv];
CONVERT_CHANNEL_TO_FULL((int)src_data[2] - 1 + iv, zv[iv]);
zv[iv] *= zv[iv];
CONVERT_CHANNEL_TO_ZERO((int)src_data[2] - 1 + iv, zzerov[iv]);
zzerov[iv] *= zzerov[iv];
}
// note whether normal "spans" straight up
if (x_max >= 0.0f && x_min <= 0.0f && y_max >= 0.0f && y_min <= 0.0f) {
normal_is_straight_up++;
}
int ix, iy, iz;
for (ix = 0; ix <= 2; ix++) {
for (iy = 0; iy <= 2; iy++) {
for (iz = 0; iz <= 2; iz++) {
float nl = sqrt(xv[ix] + yv[iy] + zv[iz]);
if (normal_length_max < nl) {
normal_length_max = nl;
}
if (normal_length_min > nl) {
normal_length_min = nl;
}
float zzero_nl = sqrt(xv[ix] + yv[iy] + zzerov[iz]);
if (zzero_normal_length_max < zzero_nl) {
zzero_normal_length_max = zzero_nl;
}
if (zzero_normal_length_min > zzero_nl) {
zzero_normal_length_min = zzero_nl;
}
}
}
}
if (normal_length_max >= 1.0f && normal_length_min <= 1.0f) {
normal_good++;
}
// TODO check this
else if (normal_length_max >= 1.0f - MAX_NORMAL_LENGTH_DIFFERENCE && normal_length_min <= 1.0f + MAX_NORMAL_LENGTH_DIFFERENCE) {
normal_close++;
err_outside_z = '1';
}
else {
err_outside_z = 'X';
}
if (zzero_normal_length_max >= 1.0f && zzero_normal_length_min <= 1.0f) {
zzero_normal_good++;
}
else if (zzero_normal_length_max >= 1.0f - MAX_NORMAL_LENGTH_DIFFERENCE && zzero_normal_length_min <= 1.0f + MAX_NORMAL_LENGTH_DIFFERENCE) {
zzero_normal_close++;
err_outside_z_zero = '1';
}
else {
err_outside_z_zero = 'X';
}
// Is the correctly z converted pixel level off by only one, compared to the original blue channel value?
// Keep this around out of interest, but it's not a good test.
unsigned char bcalc;
CONVERT_FULL_TO_CHANNEL(zcalc, bcalc);
int zabsdiff = (int)(abs(src_data[2] - bcalc));
//if (zabsdiff == 0) {
// normal_length_zneg[0]++;
//}
//if (zabsdiff == 1) {
// normal_length_zneg[1]++;
// err_outside_z = '1';
//}
//if (zabsdiff == 2) {
// normal_length_zneg[2]++;
// err_outside_z = '2';
//}
//if (zabsdiff > 2) {
// // computed z is considerably different from input z
// z_outside_bounds++;
// err_outside_z = 'X';
//}
if (zmaxabsdiff < zabsdiff) {
zmaxabsdiff = zabsdiff;
}
// try same thing with Z 0-1 range
CONVERT_ZERO_TO_CHANNEL(zcalc, bcalc);
zabsdiff = (int)(abs(src_data[2] - bcalc));
//if (zabsdiff == 0) {
// normal_length_zzero[0]++;
//}
//if (zabsdiff == 1) {
// normal_length_zzero[1]++;
// err_outside_z_zero = '1';
//}
//if (zabsdiff == 2) {
// normal_length_zzero[2]++;
// err_outside_z_zero = '2';
//}
//if (zabsdiff > 2) {
// z_outside_zzero_bounds++;
// err_outside_z_zero = 'X';
//}
if (zmaxabsdiff_zero < zabsdiff) {