1 // NeL - MMORPG Framework <http://dev.ryzom.com/projects/nel/>
2 // Copyright (C) 2010-2019 Winch Gate Property Limited
4 // This source file has been modified by the following contributors:
5 // Copyright (C) 2020 Jan BOON (Kaetemi) <jan.boon@kaetemi.be>
7 // This program is free software: you can redistribute it and/or modify
8 // it under the terms of the GNU Affero General Public License as
9 // published by the Free Software Foundation, either version 3 of the
10 // License, or (at your option) any later version.
12 // This program is distributed in the hope that it will be useful,
13 // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 // GNU Affero General Public License for more details.
17 // You should have received a copy of the GNU Affero General Public License
18 // along with this program. If not, see <http://www.gnu.org/licenses/>.
22 // the goal is to regroup lightmap of a level into lightmap with a higher level
24 #include "nel/misc/common.h"
25 #include "nel/misc/file.h"
26 #include "nel/misc/bitmap.h"
27 #include "nel/misc/log.h"
28 #include "nel/misc/path.h"
29 #include "nel/misc/uv.h"
30 #include "nel/misc/cmd_args.h"
35 // ---------------------------------------------------------------------------
38 using namespace NLMISC
;
40 // ***************************************************************************
41 void outString(const string
&sText
)
43 printf("%s\n", sText
.c_str());
46 // ***************************************************************************
47 // test every 4 pixels for 2 reason: DXTC and speed
48 const uint32 posStep
= 4;
50 // ***************************************************************************
51 // Try all position to put pSrc in pDst
52 bool tryAllPos(NLMISC::CBitmap
*pSrc
, NLMISC::CBitmap
*pDst
, sint32
&x
, sint32
&y
)
55 CObjectVector
<uint8
> &rSrcPix
= pSrc
->getPixels();
56 CObjectVector
<uint8
> &rDstPix
= pDst
->getPixels();
58 // Recalculate real size of the source (without padding to power of 2)
59 uint32 nSrcWidth
= pSrc
->getWidth(), nSrcHeight
= pSrc
->getHeight();
61 if (nSrcWidth
> pDst
->getWidth() ) return false;
62 if (nSrcHeight
> pDst
->getHeight() ) return false;
64 // For all position test if the Src plane can be put in
65 for (j
= 0; j
<= (pDst
->getHeight() - nSrcHeight
); j
+= posStep
)
66 for (i
= 0; i
<= (pDst
->getWidth() - nSrcWidth
); i
+= posStep
)
72 for (b
= 0; b
< nSrcHeight
; ++b
)
74 for (a
= 0; a
< nSrcWidth
; ++a
)
76 if (rDstPix
[4*((x
+a
)+(y
+b
)*pDst
->getWidth())+3] != 0)
91 // ***************************************************************************
92 void putPixel(uint8
*dst
, uint8
*src
, bool alphaTransfert
)
103 // ***************************************************************************
104 bool putIn(NLMISC::CBitmap
*pSrc
, NLMISC::CBitmap
*pDst
, sint32 x
, sint32 y
, bool alphaTransfert
=true)
106 uint8
*rSrcPix
= &pSrc
->getPixels()[0];
107 uint8
*rDstPix
= &pDst
->getPixels()[0];
109 uint wSrc
= pSrc
->getWidth();
110 uint hSrc
= pSrc
->getHeight();
111 for (uint b
= 0; b
< hSrc
; ++b
)
112 for (uint a
= 0; a
< wSrc
; ++a
)
114 if (rDstPix
[4*((x
+a
)+(y
+b
)*pDst
->getWidth())+3] != 0)
118 putPixel(rDstPix
+ 4*((x
+a
)+(y
+b
)*pDst
->getWidth()), rSrcPix
+ 4*(a
+b
*pSrc
->getWidth()), alphaTransfert
);
121 // DXTC compression optim: fill last column block and last row block of 4 pixels with block color (don't let black or undefined)
122 uint wSrc4
= 4*((wSrc
+3)/4);
123 uint hSrc4
= 4*((hSrc
+3)/4);
127 for(uint a
=wSrc
;a
<wSrc4
;a
++)
129 for(uint b
=0;b
<hSrc4
;b
++)
131 putPixel(rDstPix
+ 4*((x
+a
)+(y
+b
)*pDst
->getWidth()), rDstPix
+ 4*((x
+wSrc
-1)+(y
+b
)*pDst
->getWidth()), alphaTransfert
);
138 for(uint b
=hSrc
;b
<hSrc4
;b
++)
140 for(uint a
=0;a
<wSrc4
;a
++)
142 putPixel(rDstPix
+ 4*((x
+a
)+(y
+b
)*pDst
->getWidth()), rDstPix
+ 4*((x
+a
)+(y
+hSrc
-1)*pDst
->getWidth()), alphaTransfert
);
150 // ***************************************************************************
151 string
getBaseName(const string
&fullname
)
154 string::size_type pos
= fullname
.rfind('_');
155 if (pos
!= string::npos
) basename
= fullname
.substr(0, pos
+1);
159 // ***************************************************************************
160 // resize the bitmap to the next power of 2 and preserve content
161 void enlargeCanvas(NLMISC::CBitmap
&b
)
163 sint32 nNewWidth
= b
.getWidth(), nNewHeight
= b
.getHeight();
164 if (nNewWidth
> nNewHeight
)
170 b2
.resize (nNewWidth
, nNewHeight
, NLMISC::CBitmap::RGBA
);
172 CObjectVector
<uint8
> &rPixelBitmap
= b2
.getPixels(0);
173 for (sint32 i
= 0; i
< nNewWidth
*nNewHeight
*4; ++i
)
176 putIn (&b
, &b2
, 0, 0);
180 bool writeFileDependingOnFilename(const std::string
&filename
, CBitmap
&bitmap
)
184 if (out
.open(filename
))
186 if (toLowerAscii(filename
).find(".png") != string::npos
)
188 bitmap
.writePNG(out
, 32);
192 bitmap
.writeTGA(out
, 32);
204 // ***************************************************************************
206 // ***************************************************************************
207 int main(int argc
, char **argv
)
209 CApplicationContext applicationContext
;
211 // Parse Command Line.
212 NLMISC::CCmdArgs args
;
214 args
.setDescription("Build a huge interface texture from several small elements to optimize video memory usage.");
215 args
.addArg("f", "format", "format", "Output format (png or tga)");
216 args
.addArg("s", "subset", "existing_uv_txt_name", "Build a subset of an existing interface definition while preserving the existing texture ids, to support freeing up VRAM by switching to the subset without rebuilding the entire interface.");
217 args
.addArg("x", "extract", "", "Extract all interface elements from <output_filename> to <input_path>.");
218 args
.addAdditionalArg("output_filename", "PNG or TGA file to generate", true);
219 args
.addAdditionalArg("input_path", "Path that containts interfaces elements", false);
220 args
.addArg("", "no-border", "", "Disable border duplication. Enabled by default");
222 if (!args
.parse(argc
, argv
)) return 1;
224 // build as a subset of existing interface
225 bool buildSubset
= false;
226 string existingUVfilename
;
228 if (args
.haveArg("s"))
231 existingUVfilename
= args
.getArg("s").front();
236 if (args
.haveLongArg("no-border"))
241 // extract all interface elements
242 bool extractElements
= args
.haveArg("x");
245 std::string outputFormat
;
247 if (args
.haveArg("f"))
249 outputFormat
= args
.getArg("f").front();
251 if (outputFormat
!= "png" && outputFormat
!= "tga")
253 outString(toString("ERROR: Format %s not supported, only png and tga formats are", outputFormat
.c_str()));
258 std::vector
<std::string
> inputDirs
= args
.getAdditionalArg("input_path");
260 string fmtName
= args
.getAdditionalArg("output_filename").front();
262 // append PNG extension if no one provided
263 if (fmtName
.rfind('.') == string::npos
) fmtName
+= "." + (outputFormat
.empty() ? "png":outputFormat
);
267 if (inputDirs
.empty())
269 outString(toString("ERROR: No input directories specified"));
274 existingUVfilename
= fmtName
.substr(0, fmtName
.rfind('.'));
275 existingUVfilename
+= ".txt";
277 // Load existing UV file
279 string filename
= CPath::lookup(existingUVfilename
, false);
281 if (filename
.empty() || !iFile
.open(filename
))
283 outString(toString("ERROR: Unable to open %s", existingUVfilename
.c_str()));
287 // Load existing bitmap file
290 if (!bitmapFile
.open(fmtName
))
292 outString(toString("ERROR: Unable to open %s", fmtName
.c_str()));
297 CBitmap textureBitmap
;
298 uint8 colors
= textureBitmap
.load(bitmapFile
);
300 // file already loaded in memory, close it
305 outString(toString("ERROR: %s is not a RGBA bitmap", existingUVfilename
.c_str()));
309 // make sure transparent pixels are black
310 textureBitmap
.makeTransparentPixelsBlack();
312 float textureWidth
= (float)textureBitmap
.getWidth();
313 float textureHeight
= (float)textureBitmap
.getHeight();
315 char bufTmp
[256], tgaName
[256];
317 float uvMinU
, uvMinV
, uvMaxU
, uvMaxV
;
320 iFile
.getline(bufTmp
, 256);
322 if (sscanf(bufTmp
, "%s %f %f %f %f", tgaName
, &uvMinU
, &uvMinV
, &uvMaxU
, &uvMaxV
) != 5)
324 nlwarning("Can't parse %s", bufTmp
);
328 float xf
= uvMinU
* textureWidth
;
329 float yf
= uvMinV
* textureHeight
;
330 float widthf
= (uvMaxU
- uvMinU
) * textureWidth
;
331 float heightf
= (uvMaxV
- uvMinV
) * textureHeight
;
335 uint width
= (uint
)widthf
;
336 uint height
= (uint
)heightf
;
338 if ((float)x
!= xf
|| (float)y
!= yf
|| (float)width
!= widthf
|| (float)height
!= heightf
)
340 nlwarning("Wrong round");
347 bitmap
.resize(width
, height
);
348 bitmap
.blit(textureBitmap
, x
, y
, width
, height
, 0, 0);
350 sTGAname
= inputDirs
.front() + "/" + tgaName
;
352 // force specific format instead of using original one
353 if (!outputFormat
.empty())
355 sTGAname
= sTGAname
.substr(0, sTGAname
.rfind('.'));
356 sTGAname
+= "." + outputFormat
;
360 if (writeFileDependingOnFilename(sTGAname
, bitmap
))
362 outString(toString("Writing file %s", sTGAname
.c_str()));
366 outString(toString("Unable to writing file %s", sTGAname
.c_str()));
371 outString(toString("Bitmap with wrong size"));
378 vector
<string
> AllMapNames
;
379 vector
<string
>::iterator it
= inputDirs
.begin(), itEnd
= inputDirs
.end();
385 if( !CFile::isDirectory(sDir
) )
387 outString(toString("ERROR: directory %s does not exist", sDir
.c_str()));
391 CPath::getPathContent(sDir
, false, false, true, AllMapNames
);
394 vector
<NLMISC::CBitmap
*> AllMaps
;
398 sint32 mapSize
= (sint32
)AllMapNames
.size();
399 AllMaps
.resize( mapSize
);
400 for(sint i
= 0; i
< mapSize
; ++i
)
402 NLMISC::CBitmap
*pBtmp
= NULL
;
406 pBtmp
= new NLMISC::CBitmap
;
407 NLMISC::CIFile inFile
;
409 if (!inFile
.open(AllMapNames
[i
])) throw NLMISC::Exception(toString("Unable to open %s", AllMapNames
[i
].c_str()));
411 uint8 colors
= pBtmp
->load(inFile
);
413 if (!colors
) throw NLMISC::Exception(toString("%s is not a bitmap", AllMapNames
[i
].c_str()));
415 if (pBtmp
->getPixelFormat() != CBitmap::RGBA
)
417 outString(toString("Converting %s to RGBA (32 bits), originally using %u bits...", AllMapNames
[i
].c_str(), (uint
)colors
));
418 pBtmp
->convertToType(CBitmap::RGBA
);
421 // duplicate icon border
424 NLMISC::CBitmap
*tmp
= new NLMISC::CBitmap
;
425 tmp
->resize(pBtmp
->getWidth(), pBtmp
->getHeight());
426 tmp
->blit(pBtmp
, 0, 0);
428 tmp
->resample(tmp
->getWidth() + borderSize
* 2, tmp
->getHeight() + borderSize
* 2);
430 tmp
->blit(pBtmp
, borderSize
, 0);
431 tmp
->blit(pBtmp
, borderSize
, borderSize
*2);
433 tmp
->blit(pBtmp
, 0, borderSize
);
434 tmp
->blit(pBtmp
, borderSize
*2, borderSize
);
436 tmp
->blit(pBtmp
, borderSize
, borderSize
);
444 catch (const NLMISC::Exception
&e
)
446 if (pBtmp
) delete pBtmp
;
448 outString(toString("ERROR : %s", e
.what()));
453 // Sort all maps by decreasing size
454 for (sint i
= 0; i
< mapSize
-1; ++i
)
455 for (j
= i
+1; j
< mapSize
; ++j
)
457 NLMISC::CBitmap
*pBI
= AllMaps
[i
];
458 NLMISC::CBitmap
*pBJ
= AllMaps
[j
];
459 if ((pBI
->getWidth()*pBI
->getHeight()) < (pBJ
->getWidth()*pBJ
->getHeight()))
461 NLMISC::CBitmap
*pBTmp
= AllMaps
[i
];
462 AllMaps
[i
] = AllMaps
[j
];
465 string sTmp
= AllMapNames
[i
];
466 AllMapNames
[i
] = AllMapNames
[j
];
467 AllMapNames
[j
] = sTmp
;
471 // Place all maps into the global texture
472 NLMISC::CBitmap GlobalTexture
, GlobalMask
;
473 GlobalTexture
.resize (1, 1, NLMISC::CBitmap::RGBA
);
474 GlobalMask
.resize (1, 1, NLMISC::CBitmap::RGBA
);
475 CObjectVector
<uint8
> &rPixelBitmap
= GlobalTexture
.getPixels(0);
476 rPixelBitmap
[0] = rPixelBitmap
[1] = rPixelBitmap
[2] = rPixelBitmap
[3] = 0;
477 CObjectVector
<uint8
> &rPixelMask
= GlobalMask
.getPixels(0);
478 rPixelMask
[0] = rPixelMask
[1] = rPixelMask
[2] = rPixelMask
[3] = 0;
479 vector
<NLMISC::CUV
> UVMin
, UVMax
;
480 UVMin
.resize (mapSize
, NLMISC::CUV(0.0f
, 0.0f
));
481 UVMax
.resize (mapSize
, NLMISC::CUV(0.0f
, 0.0f
));
483 for (sint i
= 0; i
< mapSize
; ++i
)
486 while (!tryAllPos(AllMaps
[i
], &GlobalMask
, x
, y
))
488 // Enlarge global texture
489 enlargeCanvas (GlobalTexture
);
490 enlargeCanvas (GlobalMask
);
493 putIn (AllMaps
[i
], &GlobalTexture
, x
, y
);
494 putIn (AllMaps
[i
], &GlobalMask
, x
, y
, false);
496 UVMin
[i
].U
= (float)x
+ borderSize
;
497 UVMin
[i
].V
= (float)y
+ borderSize
;
498 UVMax
[i
].U
= (float)x
+ AllMaps
[i
]->getWidth() - borderSize
;
499 UVMax
[i
].V
= (float)y
+ AllMaps
[i
]->getHeight() - borderSize
;
502 // Do not remove this is useful for debugging
503 writeFileDependingOnFilename(fmtName
.substr(0, fmtName
.rfind('.')) + "_txt.png", GlobalTexture
);
504 writeFileDependingOnFilename(fmtName
.substr(0, fmtName
.rfind('.')) + "_msk.png", GlobalMask
);
508 // Convert UV from pixel to ratio
509 for (sint i
= 0; i
< mapSize
; ++i
)
511 UVMin
[i
].U
= UVMin
[i
].U
/ (float)GlobalTexture
.getWidth();
512 UVMin
[i
].V
= UVMin
[i
].V
/ (float)GlobalTexture
.getHeight();
513 UVMax
[i
].U
= UVMax
[i
].U
/ (float)GlobalTexture
.getWidth();
514 UVMax
[i
].V
= UVMax
[i
].V
/ (float)GlobalTexture
.getHeight();
517 // make sure transparent pixels are black
518 GlobalTexture
.makeTransparentPixelsBlack();
520 // Write global texture file
521 if (writeFileDependingOnFilename(fmtName
, GlobalTexture
))
523 outString(toString("Writing %s", fmtName
.c_str()));
527 outString(toString("ERROR: Unable to write %s", fmtName
.c_str()));
530 // Write UV text file
533 fmtName
= fmtName
.substr(0, fmtName
.rfind('.'));
535 FILE *f
= nlfopen(fmtName
, "wb");
538 for (sint i
= 0; i
< mapSize
; ++i
)
540 // get the string whitout path
541 string fileName
= CFile::getFilename(AllMapNames
[i
]);
542 fprintf (f
, "%s %.12f %.12f %.12f %.12f\n", fileName
.c_str(), UVMin
[i
].U
, UVMin
[i
].V
, UVMax
[i
].U
, UVMax
[i
].V
);
547 outString(toString("Writing UV file %s", fmtName
.c_str()));
551 outString(toString("ERROR: Cannot write UV file %s", fmtName
.c_str()));
554 else // build as a subset
556 // Load existing uv file
558 string filename
= CPath::lookup (existingUVfilename
, false);
560 if( filename
.empty() || !iFile
.open(filename
) )
562 outString(toString("ERROR: Unable to open %s", existingUVfilename
.c_str()));
566 // Write subset UV text file
567 fmtName
= fmtName
.substr(0, fmtName
.rfind('.'));
569 FILE *f
= nlfopen(fmtName
, "wb");
573 outString(toString("ERROR: Unable to write UV file %s", fmtName
.c_str()));
577 char bufTmp
[256], tgaName
[256];
579 float uvMinU
, uvMinV
, uvMaxU
, uvMaxV
;
582 iFile
.getline (bufTmp
, 256);
583 if (sscanf (bufTmp
, "%s %f %f %f %f", tgaName
, &uvMinU
, &uvMinV
, &uvMaxU
, &uvMaxV
) != 5)
585 nlwarning("Can't parse %s", bufTmp
);
589 sTGAname
= toLowerAscii(string(tgaName
));
591 // search position of extension
592 std::string tgaExt
= CFile::getExtension(sTGAname
);
595 sTGAname
= CFile::getFilenameWithoutExtension(sTGAname
);
600 for (i
= 0; i
< mapSize
; ++i
)
602 // get the string whitout path
603 findTGAName
= toLowerAscii(CFile::getFilenameWithoutExtension(AllMapNames
[i
]));
604 if( findTGAName
== sTGAname
)
609 sTGAname
+= "." + tgaExt
;
613 // not present in subset: offset existing uv's to (0,0), preserving size
614 fprintf (f
, "%s %.12f %.12f %.12f %.12f\n", sTGAname
.c_str(), 0.0f
, 0.0f
, uvMaxU
- uvMinU
, uvMaxV
- uvMinV
);
618 // present in subset: use new uv's
619 fprintf (f
, "%s %.12f %.12f %.12f %.12f\n", sTGAname
.c_str(), UVMin
[i
].U
, UVMin
[i
].V
, UVMax
[i
].U
, UVMax
[i
].V
);
623 outString(toString("Writing UV file: %s", fmtName
.c_str()));