1#include <base/logger.h>
2#include <base/system.h>
3#include <engine/gfx/image_loader.h>
4#include <engine/graphics.h>
5#include <engine/shared/datafile.h>
6#include <engine/storage.h>
7#include <game/mapitems.h>
8
9bool CreatePixelArt(const char[3][64], const int[2], const int[2], int[2], const bool[2]);
10void InsertCurrentQuads(CDataFileReader &, CMapItemLayerQuads *, CQuad *);
11int InsertPixelArtQuads(CQuad *, int &, const CImageInfo &, const int[2], const int[2], const bool[2]);
12
13bool LoadPng(CImageInfo *, const char *);
14bool OpenMaps(const char[2][64], CDataFileReader &, CDataFileWriter &);
15void SaveOutputMap(CDataFileReader &, CDataFileWriter &, CMapItemLayerQuads *, int, CQuad *, int);
16
17CMapItemLayerQuads *GetQuadLayer(CDataFileReader &, const int[2], int *);
18CQuad CreateNewQuad(float, float, int, int, const uint8_t[4], const int[2]);
19
20bool GetPixelClamped(const CImageInfo &, int, int, uint8_t[4]);
21bool ComparePixel(const uint8_t[4], const uint8_t[4]);
22bool IsPixelOptimizable(const CImageInfo &, int, int, const uint8_t[4], const bool[]);
23void SetVisitedPixels(const CImageInfo &, int, int, int, int, bool[]);
24
25int GetImagePixelSize(const CImageInfo &);
26int FindSuperPixelSize(const CImageInfo &, const uint8_t[4], int, int, int, bool[]);
27void GetOptimizedQuadSize(const CImageInfo &, int, const uint8_t[4], int, int, int &, int &, bool[]);
28
29int main(int argc, const char **argv)
30{
31 CCmdlineFix CmdlineFix(&argc, &argv);
32 log_set_global_logger_default();
33
34 if(argc < 9 || argc > 12)
35 {
36 dbg_msg(sys: "map_create_pixelart", fmt: "Invalid arguments");
37 dbg_msg(sys: "map_create_pixelart", fmt: "Usage: %s <image.png> <img_pixelsize> <input_map> <layergroup_id> <layer_id> <pos_x> <pos_y> <quad_pixelsize> <output_map> [optimize=0|1] [centralize=0|1]", argv[0]);
38 dbg_msg(sys: "map_create_pixelart", fmt: "Note: use destination layer tiles as a reference for positions and pixels for sizes.");
39 dbg_msg(sys: "map_create_pixelart", fmt: "Note: set img_pixelsize to 0 to consider the largest possible size.");
40 dbg_msg(sys: "map_create_pixelart", fmt: "Note: set quad_pixelsize to 0 to consider the same value of img_pixelsize.");
41 dbg_msg(sys: "map_create_pixelart", fmt: "Note: if image.png has not a perfect pixelart grid, result might be unexpected, manually fix img_pixelsize to get it better.");
42 dbg_msg(sys: "map_create_pixelart", fmt: "Options: optimize tries to reduce the total number of quads (default: 1).");
43 dbg_msg(sys: "map_create_pixelart", fmt: "Options: centralize places all pivots at the same position (default: 0).");
44
45 return -1;
46 }
47
48 char aFilenames[3][64];
49 str_copy(dst&: aFilenames[0], src: argv[3]); //input_map
50 str_copy(dst&: aFilenames[1], src: argv[9]); //output_map
51 str_copy(dst&: aFilenames[2], src: argv[1]); //image_file
52
53 int aLayerId[2] = {str_toint(str: argv[4]), str_toint(str: argv[5])}; //layergroup_id, layer_id
54 int aStartingPos[2] = {str_toint(str: argv[6]) * 32, str_toint(str: argv[7]) * 32}; //pos_x, pos_y
55 int aPixelSizes[2] = {str_toint(str: argv[2]), str_toint(str: argv[8])}; //quad_pixelsize, img_pixelsize
56
57 bool aArtOptions[3];
58 aArtOptions[0] = argc >= 10 ? str_toint(str: argv[10]) : true; //optimize
59 aArtOptions[1] = argc >= 11 ? str_toint(str: argv[11]) : false; //centralize
60
61 dbg_msg(sys: "map_create_pixelart", fmt: "image_file='%s'; image_pixelsize='%dpx'; input_map='%s'; layergroup_id='#%d'; layer_id='#%d'; pos_x='#%dpx'; pos_y='%dpx'; quad_pixelsize='%dpx'; output_map='%s'; optimize='%d'; centralize='%d'",
62 aFilenames[2], aPixelSizes[0], aFilenames[1], aLayerId[0], aLayerId[1], aStartingPos[0], aStartingPos[1], aPixelSizes[1], aFilenames[2], aArtOptions[0], aArtOptions[1]);
63
64 return !CreatePixelArt(aFilenames, aLayerId, aStartingPos, aPixelSizes, aArtOptions);
65}
66
67bool CreatePixelArt(const char aFilenames[3][64], const int aLayerId[2], const int aStartingPos[2], int aPixelSizes[2], const bool aArtOptions[2])
68{
69 CImageInfo Img;
70 if(!LoadPng(&Img, aFilenames[2]))
71 return false;
72
73 aPixelSizes[0] = aPixelSizes[0] ? aPixelSizes[0] : GetImagePixelSize(Img);
74 aPixelSizes[1] = aPixelSizes[1] ? aPixelSizes[1] : aPixelSizes[0];
75
76 CDataFileReader InputMap;
77 CDataFileWriter OutputMap;
78 if(!OpenMaps(aFilenames, InputMap, OutputMap))
79 return false;
80
81 int ItemNumber = 0;
82 CMapItemLayerQuads *pQuadLayer = GetQuadLayer(InputMap, aLayerId, &ItemNumber);
83 if(!pQuadLayer)
84 return false;
85
86 int MaxNewQuads = std::ceil(x: (Img.m_Width * Img.m_Height) / aPixelSizes[0]);
87 CQuad *pQuads = new CQuad[pQuadLayer->m_NumQuads + MaxNewQuads];
88
89 InsertCurrentQuads(InputMap, pQuadLayer, pQuads);
90 int QuadsCounter = InsertPixelArtQuads(pQuads, pQuadLayer->m_NumQuads, Img, aStartingPos, aPixelSizes, aArtOptions);
91 SaveOutputMap(InputMap, OutputMap, pQuadLayer, ItemNumber, pQuads, ((int)sizeof(CQuad)) * (pQuadLayer->m_NumQuads + 1));
92 delete[] pQuads;
93
94 dbg_msg(sys: "map_create_pixelart", fmt: "INFO: successfully added %d new pixelart quads.", QuadsCounter);
95 return true;
96}
97
98void InsertCurrentQuads(CDataFileReader &InputMap, CMapItemLayerQuads *pQuadLayer, CQuad *pNewQuads)
99{
100 CQuad *pCurrentQuads = (CQuad *)InputMap.GetDataSwapped(Index: pQuadLayer->m_Data);
101 for(int i = 0; i < pQuadLayer->m_NumQuads; i++)
102 pNewQuads[i] = pCurrentQuads[i];
103}
104
105int InsertPixelArtQuads(CQuad *pQuads, int &NumQuads, const CImageInfo &Img, const int aStartingPos[2], const int aPixelSizes[2], const bool aArtOptions[2])
106{
107 int ImgPixelSize = aPixelSizes[0], QuadPixelSize = aPixelSizes[1], OriginalNumQuads = NumQuads;
108 int aForcedPivot[2] = {std::numeric_limits<int>::max(), std::numeric_limits<int>::max()};
109 bool *aVisitedPixels = new bool[Img.m_Height * Img.m_Width];
110 mem_zero(block: aVisitedPixels, size: sizeof(bool) * Img.m_Height * Img.m_Width);
111
112 for(int y = 0; y < Img.m_Height; y += ImgPixelSize)
113 for(int x = 0; x < Img.m_Width; x += ImgPixelSize)
114 {
115 uint8_t aPixel[4];
116 if(aVisitedPixels[x + y * Img.m_Width] || !GetPixelClamped(Img, x, y, aPixel))
117 continue;
118
119 int Width = 1, Height = 1;
120 if(aArtOptions[0])
121 GetOptimizedQuadSize(Img, ImgPixelSize, aPixel, x, y, Width, Height, aVisitedPixels);
122
123 float Posx = aStartingPos[0] + ((x / (float)ImgPixelSize) + (Width / 2.f)) * QuadPixelSize;
124 float Posy = aStartingPos[1] + ((y / (float)ImgPixelSize) + (Height / 2.f)) * QuadPixelSize;
125 if(aArtOptions[1] && aForcedPivot[0] == std::numeric_limits<int>::max())
126 {
127 aForcedPivot[0] = Posx;
128 aForcedPivot[1] = Posy;
129 }
130
131 pQuads[NumQuads] = CreateNewQuad(Posx, Posy, QuadPixelSize * Width, QuadPixelSize * Height, aPixel, aArtOptions[1] ? aForcedPivot : 0x0);
132 NumQuads++;
133 }
134 delete[] aVisitedPixels;
135
136 return NumQuads - OriginalNumQuads;
137}
138
139void GetOptimizedQuadSize(const CImageInfo &Img, const int ImgPixelSize, const uint8_t aPixel[4], const int PosX, const int PosY, int &Width, int &Height, bool aVisitedPixels[])
140{
141 int w = 0, h = 0, OptimizedWidth = 0, OptimizedHeight = 0;
142
143 while(IsPixelOptimizable(Img, PosX + w, PosY + h, aPixel, aVisitedPixels))
144 {
145 while(IsPixelOptimizable(Img, PosX + w, PosY + h, aPixel, aVisitedPixels) && (!OptimizedHeight || h < OptimizedHeight))
146 h += ImgPixelSize;
147
148 if(!OptimizedHeight || h < OptimizedHeight)
149 OptimizedHeight = h;
150
151 h = 0;
152 w += ImgPixelSize;
153 OptimizedWidth = w;
154 }
155
156 SetVisitedPixels(Img, PosX, PosY, OptimizedWidth, OptimizedHeight, aVisitedPixels);
157 Width = OptimizedWidth / ImgPixelSize;
158 Height = OptimizedHeight / ImgPixelSize;
159}
160
161int GetImagePixelSize(const CImageInfo &Img)
162{
163 int ImgPixelSize = std::numeric_limits<int>::max();
164 bool *aVisitedPixels = new bool[Img.m_Height * Img.m_Width];
165 mem_zero(block: aVisitedPixels, size: sizeof(bool) * Img.m_Height * Img.m_Width);
166
167 for(int y = 0; y < Img.m_Height && ImgPixelSize > 1; y++)
168 for(int x = 0; x < Img.m_Width && ImgPixelSize > 1; x++)
169 {
170 uint8_t aPixel[4];
171 if(aVisitedPixels[x + y * Img.m_Width])
172 continue;
173
174 GetPixelClamped(Img, x, y, aPixel);
175 int SuperPixelSize = FindSuperPixelSize(Img, aPixel, x, y, 1, aVisitedPixels);
176 if(SuperPixelSize < ImgPixelSize)
177 ImgPixelSize = SuperPixelSize;
178 }
179 delete[] aVisitedPixels;
180
181 dbg_msg(sys: "map_create_pixelart", fmt: "INFO: automatically detected img_pixelsize of %dpx", ImgPixelSize);
182 return ImgPixelSize;
183}
184
185int FindSuperPixelSize(const CImageInfo &Img, const uint8_t aPixel[4], const int PosX, const int PosY, const int CurrentSize, bool aVisitedPixels[])
186{
187 if(PosX + CurrentSize >= Img.m_Width || PosY + CurrentSize >= Img.m_Height)
188 {
189 SetVisitedPixels(Img, PosX, PosY, CurrentSize, CurrentSize, aVisitedPixels);
190 return CurrentSize;
191 }
192
193 for(int i = 0; i < 2; i++)
194 {
195 for(int j = 0; j < CurrentSize + 1; j++)
196 {
197 uint8_t aCheckPixel[4];
198 int x = PosX, y = PosY;
199 x += i == 0 ? j : CurrentSize;
200 y += i == 0 ? CurrentSize : j;
201
202 GetPixelClamped(Img, x, y, aCheckPixel);
203 if(x >= Img.m_Width || y >= Img.m_Height || !ComparePixel(aPixel, aCheckPixel))
204 {
205 SetVisitedPixels(Img, PosX, PosY, CurrentSize, CurrentSize, aVisitedPixels);
206 return CurrentSize;
207 }
208 }
209 }
210
211 return FindSuperPixelSize(Img, aPixel, PosX, PosY, CurrentSize: CurrentSize + 1, aVisitedPixels);
212}
213
214bool GetPixelClamped(const CImageInfo &Img, int x, int y, uint8_t aPixel[4])
215{
216 x = clamp<int>(val: x, lo: 0, hi: (int)Img.m_Width - 1);
217 y = clamp<int>(val: y, lo: 0, hi: (int)Img.m_Height - 1);
218 aPixel[0] = 255;
219 aPixel[1] = 255;
220 aPixel[2] = 255;
221 aPixel[3] = 255;
222
223 const size_t PixelSize = Img.PixelSize();
224 for(size_t i = 0; i < PixelSize; i++)
225 aPixel[i] = Img.m_pData[x * PixelSize + (Img.m_Width * PixelSize * y) + i];
226
227 return aPixel[3] > 0;
228}
229
230bool ComparePixel(const uint8_t aPixel1[4], const uint8_t aPixel2[4])
231{
232 for(int i = 0; i < 4; i++)
233 if(aPixel1[i] != aPixel2[i])
234 return false;
235 return true;
236}
237
238bool IsPixelOptimizable(const CImageInfo &Img, const int PosX, const int PosY, const uint8_t aPixel[4], const bool aVisitedPixels[])
239{
240 uint8_t aCheckPixel[4];
241 return PosX < Img.m_Width && PosY < Img.m_Height && !aVisitedPixels[PosX + PosY * Img.m_Width] && GetPixelClamped(Img, x: PosX, y: PosY, aPixel: aCheckPixel) && ComparePixel(aPixel1: aPixel, aPixel2: aCheckPixel);
242}
243
244void SetVisitedPixels(const CImageInfo &Img, int PosX, int PosY, int Width, int Height, bool aVisitedPixels[])
245{
246 for(int y = PosY; y < PosY + Height; y++)
247 for(int x = PosX; x < PosX + Width; x++)
248 aVisitedPixels[x + y * Img.m_Width] = true;
249}
250
251CMapItemLayerQuads *GetQuadLayer(CDataFileReader &InputMap, const int aLayerId[2], int *pItemNumber)
252{
253 int Start, Num;
254 InputMap.GetType(Type: MAPITEMTYPE_GROUP, pStart: &Start, pNum: &Num);
255
256 CMapItemGroup *pGroupItem = aLayerId[0] >= Num ? 0x0 : (CMapItemGroup *)InputMap.GetItem(Index: Start + aLayerId[0]);
257
258 if(!pGroupItem)
259 {
260 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: unable to find layergroup '#%d'", aLayerId[0]);
261 return 0x0;
262 }
263
264 InputMap.GetType(Type: MAPITEMTYPE_LAYER, pStart: &Start, pNum: &Num);
265 *pItemNumber = Start + pGroupItem->m_StartLayer + aLayerId[1];
266
267 CMapItemLayer *pLayerItem = aLayerId[1] >= pGroupItem->m_NumLayers ? 0x0 : (CMapItemLayer *)InputMap.GetItem(Index: *pItemNumber);
268 if(!pLayerItem)
269 {
270 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: unable to find layer '#%d' in group '#%d'", aLayerId[1], aLayerId[0]);
271 return 0x0;
272 }
273
274 if(pLayerItem->m_Type != LAYERTYPE_QUADS)
275 {
276 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: layer '#%d' in group '#%d' is not a quad layer", aLayerId[1], aLayerId[0]);
277 return 0x0;
278 }
279
280 return (CMapItemLayerQuads *)pLayerItem;
281}
282
283CQuad CreateNewQuad(const float PosX, const float PosY, const int Width, const int Height, const uint8_t aColor[4], const int aForcedPivot[2] = 0x0)
284{
285 CQuad Quad;
286 Quad.m_PosEnv = Quad.m_ColorEnv = -1;
287 Quad.m_PosEnvOffset = Quad.m_ColorEnvOffset = 0;
288 float x = f2fx(v: PosX), y = f2fx(v: PosY), w = f2fx(v: Width / 2.f), h = f2fx(v: Height / 2.f);
289
290 for(int i = 0; i < 2; i++)
291 {
292 Quad.m_aPoints[i].y = y - h;
293 Quad.m_aPoints[i + 2].y = y + h;
294 Quad.m_aPoints[i * 2].x = x - w;
295 Quad.m_aPoints[i * 2 + 1].x = x + w;
296 }
297
298 for(auto &QuadColor : Quad.m_aColors)
299 {
300 QuadColor.r = aColor[0];
301 QuadColor.g = aColor[1];
302 QuadColor.b = aColor[2];
303 QuadColor.a = aColor[3];
304 }
305
306 Quad.m_aPoints[4].x = aForcedPivot ? f2fx(v: aForcedPivot[0]) : x;
307 Quad.m_aPoints[4].y = aForcedPivot ? f2fx(v: aForcedPivot[1]) : y;
308
309 return Quad;
310}
311
312bool LoadPng(CImageInfo *pImg, const char *pFilename)
313{
314 IOHANDLE File = io_open(filename: pFilename, flags: IOFLAG_READ);
315 if(!File)
316 {
317 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: Unable to open file %s", pFilename);
318 return false;
319 }
320
321 io_seek(io: File, offset: 0, origin: IOSEEK_END);
322 long int FileSize = io_tell(io: File);
323 if(FileSize <= 0)
324 {
325 io_close(io: File);
326 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: Failed to get file size (%ld). filename='%s'", FileSize, pFilename);
327 return false;
328 }
329 io_seek(io: File, offset: 0, origin: IOSEEK_START);
330 TImageByteBuffer ByteBuffer;
331 SImageByteBuffer ImageByteBuffer(&ByteBuffer);
332
333 ByteBuffer.resize(new_size: FileSize);
334 io_read(io: File, buffer: &ByteBuffer.front(), size: FileSize);
335 io_close(io: File);
336
337 uint8_t *pImgBuffer = NULL;
338 EImageFormat ImageFormat;
339 int PngliteIncompatible;
340
341 if(!LoadPng(ByteLoader&: ImageByteBuffer, pFileName: pFilename, PngliteIncompatible, Width&: pImg->m_Width, Height&: pImg->m_Height, pImageBuff&: pImgBuffer, ImageFormat))
342 {
343 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: Unable to load a valid PNG from file %s", pFilename);
344 return false;
345 }
346
347 if(ImageFormat != IMAGE_FORMAT_RGBA && ImageFormat != IMAGE_FORMAT_RGB)
348 {
349 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: only RGB and RGBA PNG images are supported");
350 free(ptr: pImgBuffer);
351 return false;
352 }
353
354 pImg->m_pData = pImgBuffer;
355 pImg->m_Format = ImageFormat == IMAGE_FORMAT_RGB ? CImageInfo::FORMAT_RGB : CImageInfo::FORMAT_RGBA;
356
357 return true;
358}
359
360bool OpenMaps(const char pMapNames[2][64], CDataFileReader &InputMap, CDataFileWriter &OutputMap)
361{
362 IStorage *pStorage = CreateLocalStorage();
363
364 if(!InputMap.Open(pStorage, pFilename: pMapNames[0], StorageType: IStorage::TYPE_ABSOLUTE))
365 {
366 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: unable to open map '%s'", pMapNames[0]);
367 return false;
368 }
369
370 if(!OutputMap.Open(pStorage, pFilename: pMapNames[1], StorageType: IStorage::TYPE_ABSOLUTE))
371 {
372 dbg_msg(sys: "map_create_pixelart", fmt: "ERROR: unable to open map '%s'", pMapNames[1]);
373 return false;
374 }
375
376 return true;
377}
378
379void SaveOutputMap(CDataFileReader &InputMap, CDataFileWriter &OutputMap, CMapItemLayerQuads *pNewItem, const int NewItemNumber, CQuad *pNewData, const int NewDataSize)
380{
381 for(int i = 0; i < InputMap.NumItems(); i++)
382 {
383 int Id, Type;
384 CUuid Uuid;
385 void *pItem = InputMap.GetItem(Index: i, pType: &Type, pId: &Id, pUuid: &Uuid);
386
387 // Filter ITEMTYPE_EX items, they will be automatically added again.
388 if(Type == ITEMTYPE_EX)
389 {
390 continue;
391 }
392
393 if(i == NewItemNumber)
394 pItem = pNewItem;
395
396 int Size = InputMap.GetItemSize(Index: i);
397 OutputMap.AddItem(Type, Id, Size, pData: pItem, pUuid: &Uuid);
398 }
399
400 for(int i = 0; i < InputMap.NumData(); i++)
401 {
402 if(i == pNewItem->m_Data)
403 OutputMap.AddData(Size: NewDataSize, pData: pNewData);
404 else
405 OutputMap.AddData(Size: InputMap.GetDataSize(Index: i), pData: InputMap.GetData(Index: i));
406 }
407
408 OutputMap.Finish();
409}
410