1/**************************************************************************/
2/* resource_importer_texture_atlas.cpp */
3/**************************************************************************/
4/* This file is part of: */
5/* GODOT ENGINE */
6/* https://godotengine.org */
7/**************************************************************************/
8/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10/* */
11/* Permission is hereby granted, free of charge, to any person obtaining */
12/* a copy of this software and associated documentation files (the */
13/* "Software"), to deal in the Software without restriction, including */
14/* without limitation the rights to use, copy, modify, merge, publish, */
15/* distribute, sublicense, and/or sell copies of the Software, and to */
16/* permit persons to whom the Software is furnished to do so, subject to */
17/* the following conditions: */
18/* */
19/* The above copyright notice and this permission notice shall be */
20/* included in all copies or substantial portions of the Software. */
21/* */
22/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29/**************************************************************************/
30
31#include "resource_importer_texture_atlas.h"
32
33#include "atlas_import_failed.xpm"
34#include "core/io/file_access.h"
35#include "core/io/image_loader.h"
36#include "core/io/resource_saver.h"
37#include "core/math/geometry_2d.h"
38#include "editor/editor_atlas_packer.h"
39#include "scene/resources/atlas_texture.h"
40#include "scene/resources/image_texture.h"
41#include "scene/resources/mesh.h"
42#include "scene/resources/mesh_texture.h"
43
44String ResourceImporterTextureAtlas::get_importer_name() const {
45 return "texture_atlas";
46}
47
48String ResourceImporterTextureAtlas::get_visible_name() const {
49 return "TextureAtlas";
50}
51
52void ResourceImporterTextureAtlas::get_recognized_extensions(List<String> *p_extensions) const {
53 ImageLoader::get_recognized_extensions(p_extensions);
54}
55
56String ResourceImporterTextureAtlas::get_save_extension() const {
57 return "res";
58}
59
60String ResourceImporterTextureAtlas::get_resource_type() const {
61 return "Texture2D";
62}
63
64bool ResourceImporterTextureAtlas::get_option_visibility(const String &p_path, const String &p_option, const HashMap<StringName, Variant> &p_options) const {
65 if (p_option == "crop_to_region" && int(p_options["import_mode"]) != IMPORT_MODE_REGION) {
66 return false;
67 } else if (p_option == "trim_alpha_border_from_region" && int(p_options["import_mode"]) != IMPORT_MODE_REGION) {
68 return false;
69 }
70
71 return true;
72}
73
74int ResourceImporterTextureAtlas::get_preset_count() const {
75 return 0;
76}
77
78String ResourceImporterTextureAtlas::get_preset_name(int p_idx) const {
79 return String();
80}
81
82void ResourceImporterTextureAtlas::get_import_options(const String &p_path, List<ImportOption> *r_options, int p_preset) const {
83 r_options->push_back(ImportOption(PropertyInfo(Variant::STRING, "atlas_file", PROPERTY_HINT_SAVE_FILE, "*.png"), ""));
84 r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "import_mode", PROPERTY_HINT_ENUM, "Region,Mesh2D", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 0));
85 r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "crop_to_region"), false));
86 r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "trim_alpha_border_from_region"), true));
87}
88
89String ResourceImporterTextureAtlas::get_option_group_file() const {
90 return "atlas_file";
91}
92
93Error ResourceImporterTextureAtlas::import(const String &p_source_file, const String &p_save_path, const HashMap<StringName, Variant> &p_options, List<String> *r_platform_variants, List<String> *r_gen_files, Variant *r_metadata) {
94 /* If this happens, it's because the atlas_file field was not filled, so just import a broken texture */
95
96 //use an xpm because it's size independent, the editor images are vector and size dependent
97 //it's a simple hack
98 Ref<Image> broken = memnew(Image((const char **)atlas_import_failed_xpm));
99 ResourceSaver::save(ImageTexture::create_from_image(broken), p_save_path + ".tex");
100
101 return OK;
102}
103
104// FIXME: Rasterization has issues, see https://github.com/godotengine/godot/issues/68350#issuecomment-1305610290
105static void _plot_triangle(Vector2i *p_vertices, const Vector2i &p_offset, bool p_transposed, Ref<Image> p_image, const Ref<Image> &p_src_image) {
106 int width = p_image->get_width();
107 int height = p_image->get_height();
108 int src_width = p_src_image->get_width();
109 int src_height = p_src_image->get_height();
110
111 int x[3];
112 int y[3];
113
114 for (int j = 0; j < 3; j++) {
115 x[j] = p_vertices[j].x;
116 y[j] = p_vertices[j].y;
117 }
118
119 // sort the points vertically
120 if (y[1] > y[2]) {
121 SWAP(x[1], x[2]);
122 SWAP(y[1], y[2]);
123 }
124 if (y[0] > y[1]) {
125 SWAP(x[0], x[1]);
126 SWAP(y[0], y[1]);
127 }
128 if (y[1] > y[2]) {
129 SWAP(x[1], x[2]);
130 SWAP(y[1], y[2]);
131 }
132
133 double dx_far = double(x[2] - x[0]) / (y[2] - y[0] + 1);
134 double dx_upper = double(x[1] - x[0]) / (y[1] - y[0] + 1);
135 double dx_low = double(x[2] - x[1]) / (y[2] - y[1] + 1);
136 double xf = x[0];
137 double xt = x[0] + dx_upper; // if y[0] == y[1], special case
138 int max_y = MIN(y[2], p_transposed ? (width - p_offset.x - 1) : (height - p_offset.y - 1));
139 for (int yi = y[0]; yi < max_y; yi++) {
140 if (yi >= 0) {
141 for (int xi = (xf > 0 ? int(xf) : 0); xi < (xt <= src_width ? xt : src_width); xi++) {
142 int px = xi, py = yi;
143 int sx = px, sy = py;
144 sx = CLAMP(sx, 0, src_width - 1);
145 sy = CLAMP(sy, 0, src_height - 1);
146 Color color = p_src_image->get_pixel(sx, sy);
147 if (p_transposed) {
148 SWAP(px, py);
149 }
150 px += p_offset.x;
151 py += p_offset.y;
152
153 //may have been cropped, so don't blit what is not visible?
154 if (px < 0 || px >= width) {
155 continue;
156 }
157 if (py < 0 || py >= height) {
158 continue;
159 }
160 p_image->set_pixel(px, py, color);
161 }
162
163 for (int xi = (xf < src_width ? int(xf) : src_width - 1); xi >= (xt > 0 ? xt : 0); xi--) {
164 int px = xi, py = yi;
165 int sx = px, sy = py;
166 sx = CLAMP(sx, 0, src_width - 1);
167 sy = CLAMP(sy, 0, src_height - 1);
168 Color color = p_src_image->get_pixel(sx, sy);
169 if (p_transposed) {
170 SWAP(px, py);
171 }
172 px += p_offset.x;
173 py += p_offset.y;
174
175 //may have been cropped, so don't blit what is not visible?
176 if (px < 0 || px >= width) {
177 continue;
178 }
179 if (py < 0 || py >= height) {
180 continue;
181 }
182 p_image->set_pixel(px, py, color);
183 }
184 }
185 xf += dx_far;
186 if (yi < y[1]) {
187 xt += dx_upper;
188 } else {
189 xt += dx_low;
190 }
191 }
192}
193
194Error ResourceImporterTextureAtlas::import_group_file(const String &p_group_file, const HashMap<String, HashMap<StringName, Variant>> &p_source_file_options, const HashMap<String, String> &p_base_paths) {
195 ERR_FAIL_COND_V(p_source_file_options.size() == 0, ERR_BUG); //should never happen
196
197 Vector<EditorAtlasPacker::Chart> charts;
198 Vector<PackData> pack_data_files;
199
200 pack_data_files.resize(p_source_file_options.size());
201
202 int idx = 0;
203 for (const KeyValue<String, HashMap<StringName, Variant>> &E : p_source_file_options) {
204 PackData &pack_data = pack_data_files.write[idx];
205 const String &source = E.key;
206 const HashMap<StringName, Variant> &options = E.value;
207
208 Ref<Image> image;
209 image.instantiate();
210 Error err = ImageLoader::load_image(source, image);
211 ERR_CONTINUE(err != OK);
212
213 pack_data.image = image;
214
215 int mode = options["import_mode"];
216
217 if (mode == IMPORT_MODE_REGION) {
218 pack_data.is_mesh = false;
219 pack_data.is_cropped = options["crop_to_region"];
220
221 EditorAtlasPacker::Chart chart;
222
223 Rect2i used_rect = Rect2i(Vector2i(), image->get_size());
224 if (options["trim_alpha_border_from_region"]) {
225 // Clip a region from the image.
226 used_rect = image->get_used_rect();
227 }
228 pack_data.region = used_rect;
229
230 chart.vertices.push_back(used_rect.position);
231 chart.vertices.push_back(used_rect.position + Vector2i(used_rect.size.x, 0));
232 chart.vertices.push_back(used_rect.position + Vector2i(used_rect.size.x, used_rect.size.y));
233 chart.vertices.push_back(used_rect.position + Vector2i(0, used_rect.size.y));
234 EditorAtlasPacker::Chart::Face f;
235 f.vertex[0] = 0;
236 f.vertex[1] = 1;
237 f.vertex[2] = 2;
238 chart.faces.push_back(f);
239 f.vertex[0] = 0;
240 f.vertex[1] = 2;
241 f.vertex[2] = 3;
242 chart.faces.push_back(f);
243 chart.can_transpose = false;
244 pack_data.chart_vertices.push_back(chart.vertices);
245 pack_data.chart_pieces.push_back(charts.size());
246 charts.push_back(chart);
247
248 } else {
249 pack_data.is_mesh = true;
250
251 Ref<BitMap> bit_map;
252 bit_map.instantiate();
253 bit_map->create_from_image_alpha(image);
254 Vector<Vector<Vector2>> polygons = bit_map->clip_opaque_to_polygons(Rect2(Vector2(), image->get_size()));
255
256 for (int j = 0; j < polygons.size(); j++) {
257 EditorAtlasPacker::Chart chart;
258 chart.vertices = polygons[j];
259 chart.can_transpose = true;
260
261 Vector<int> poly = Geometry2D::triangulate_polygon(polygons[j]);
262 for (int i = 0; i < poly.size(); i += 3) {
263 EditorAtlasPacker::Chart::Face f;
264 f.vertex[0] = poly[i + 0];
265 f.vertex[1] = poly[i + 1];
266 f.vertex[2] = poly[i + 2];
267 chart.faces.push_back(f);
268 }
269
270 pack_data.chart_pieces.push_back(charts.size());
271 charts.push_back(chart);
272
273 pack_data.chart_vertices.push_back(polygons[j]);
274 }
275 }
276 idx++;
277 }
278
279 //pack the charts
280 int atlas_width, atlas_height;
281 EditorAtlasPacker::chart_pack(charts, atlas_width, atlas_height);
282
283 //blit the atlas
284 Ref<Image> new_atlas = Image::create_empty(atlas_width, atlas_height, false, Image::FORMAT_RGBA8);
285
286 for (int i = 0; i < pack_data_files.size(); i++) {
287 PackData &pack_data = pack_data_files.write[i];
288
289 for (int j = 0; j < pack_data.chart_pieces.size(); j++) {
290 const EditorAtlasPacker::Chart &chart = charts[pack_data.chart_pieces[j]];
291 for (int k = 0; k < chart.faces.size(); k++) {
292 Vector2i positions[3];
293 for (int l = 0; l < 3; l++) {
294 int vertex_idx = chart.faces[k].vertex[l];
295 positions[l] = Vector2i(chart.vertices[vertex_idx]);
296 }
297
298 _plot_triangle(positions, Vector2i(chart.final_offset), chart.transposed, new_atlas, pack_data.image);
299 }
300 }
301 }
302
303 //save the atlas
304
305 new_atlas->save_png(p_group_file);
306
307 //update cache if existing, else create
308 Ref<Texture2D> cache;
309 cache = ResourceCache::get_ref(p_group_file);
310 if (!cache.is_valid()) {
311 Ref<ImageTexture> res_cache = ImageTexture::create_from_image(new_atlas);
312 res_cache->set_path(p_group_file);
313 cache = res_cache;
314 }
315
316 //save the images
317 idx = 0;
318 for (const KeyValue<String, HashMap<StringName, Variant>> &E : p_source_file_options) {
319 PackData &pack_data = pack_data_files.write[idx];
320
321 Ref<Texture2D> texture;
322
323 if (!pack_data.is_mesh) {
324 Vector2 offset = charts[pack_data.chart_pieces[0]].vertices[0] + charts[pack_data.chart_pieces[0]].final_offset;
325
326 //region
327 Ref<AtlasTexture> atlas_texture;
328 atlas_texture.instantiate();
329 atlas_texture->set_atlas(cache);
330 atlas_texture->set_region(Rect2(offset, pack_data.region.size));
331
332 if (!pack_data.is_cropped) {
333 atlas_texture->set_margin(Rect2(pack_data.region.position, pack_data.image->get_size() - pack_data.region.size));
334 }
335
336 texture = atlas_texture;
337 } else {
338 Ref<ArrayMesh> mesh;
339 mesh.instantiate();
340
341 for (int i = 0; i < pack_data.chart_pieces.size(); i++) {
342 const EditorAtlasPacker::Chart &chart = charts[pack_data.chart_pieces[i]];
343 Vector<Vector2> vertices;
344 Vector<int> indices;
345 Vector<Vector2> uvs;
346 int vc = chart.vertices.size();
347 int fc = chart.faces.size();
348 vertices.resize(vc);
349 uvs.resize(vc);
350 indices.resize(fc * 3);
351
352 {
353 Vector2 *vw = vertices.ptrw();
354 int *iw = indices.ptrw();
355 Vector2 *uvw = uvs.ptrw();
356
357 for (int j = 0; j < vc; j++) {
358 vw[j] = chart.vertices[j];
359 Vector2 uv = chart.vertices[j];
360 if (chart.transposed) {
361 SWAP(uv.x, uv.y);
362 }
363 uv += chart.final_offset;
364 uv /= new_atlas->get_size(); //normalize uv to 0-1 range
365 uvw[j] = uv;
366 }
367
368 for (int j = 0; j < fc; j++) {
369 iw[j * 3 + 0] = chart.faces[j].vertex[0];
370 iw[j * 3 + 1] = chart.faces[j].vertex[1];
371 iw[j * 3 + 2] = chart.faces[j].vertex[2];
372 }
373 }
374
375 Array arrays;
376 arrays.resize(Mesh::ARRAY_MAX);
377 arrays[Mesh::ARRAY_VERTEX] = vertices;
378 arrays[Mesh::ARRAY_TEX_UV] = uvs;
379 arrays[Mesh::ARRAY_INDEX] = indices;
380
381 mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arrays);
382 }
383
384 Ref<MeshTexture> mesh_texture;
385 mesh_texture.instantiate();
386 mesh_texture->set_base_texture(cache);
387 mesh_texture->set_image_size(pack_data.image->get_size());
388 mesh_texture->set_mesh(mesh);
389
390 texture = mesh_texture;
391 }
392
393 String save_path = p_base_paths[E.key] + ".res";
394 ResourceSaver::save(texture, save_path);
395 idx++;
396 }
397
398 return OK;
399}
400
401ResourceImporterTextureAtlas::ResourceImporterTextureAtlas() {
402}
403