Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/editor/import/resource_importer_texture.cpp
9903 views
1
/**************************************************************************/
2
/* resource_importer_texture.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.h"
32
33
#include "core/config/project_settings.h"
34
#include "core/io/config_file.h"
35
#include "core/io/image_loader.h"
36
#include "core/version.h"
37
#include "editor/file_system/editor_file_system.h"
38
#include "editor/gui/editor_toaster.h"
39
#include "editor/import/resource_importer_texture_settings.h"
40
#include "editor/settings/editor_settings.h"
41
#include "editor/themes/editor_scale.h"
42
#include "editor/themes/editor_theme_manager.h"
43
#include "scene/resources/compressed_texture.h"
44
45
void ResourceImporterTexture::_texture_reimport_roughness(const Ref<CompressedTexture2D> &p_tex, const String &p_normal_path, RS::TextureDetectRoughnessChannel p_channel) {
46
ERR_FAIL_COND(p_tex.is_null());
47
48
MutexLock lock(singleton->mutex);
49
StringName path = p_tex->get_path();
50
51
if (!singleton->make_flags.has(path)) {
52
singleton->make_flags[path] = MakeInfo();
53
}
54
55
singleton->make_flags[path].flags |= MAKE_ROUGHNESS_FLAG;
56
singleton->make_flags[path].channel_for_roughness = p_channel;
57
singleton->make_flags[path].normal_path_for_roughness = p_normal_path;
58
}
59
60
void ResourceImporterTexture::_texture_reimport_3d(const Ref<CompressedTexture2D> &p_tex) {
61
ERR_FAIL_COND(p_tex.is_null());
62
63
MutexLock lock(singleton->mutex);
64
StringName path = p_tex->get_path();
65
66
if (!singleton->make_flags.has(path)) {
67
singleton->make_flags[path] = MakeInfo();
68
}
69
70
singleton->make_flags[path].flags |= MAKE_3D_FLAG;
71
}
72
73
void ResourceImporterTexture::_texture_reimport_normal(const Ref<CompressedTexture2D> &p_tex) {
74
ERR_FAIL_COND(p_tex.is_null());
75
76
MutexLock lock(singleton->mutex);
77
StringName path = p_tex->get_path();
78
79
if (!singleton->make_flags.has(path)) {
80
singleton->make_flags[path] = MakeInfo();
81
}
82
83
singleton->make_flags[path].flags |= MAKE_NORMAL_FLAG;
84
}
85
86
void ResourceImporterTexture::update_imports() {
87
if (EditorFileSystem::get_singleton()->is_scanning() || EditorFileSystem::get_singleton()->is_importing()) {
88
return; // Don't update when EditorFileSystem is doing something else.
89
}
90
91
MutexLock lock(mutex);
92
Vector<String> to_reimport;
93
94
if (make_flags.is_empty()) {
95
return;
96
}
97
98
for (const KeyValue<StringName, MakeInfo> &E : make_flags) {
99
Ref<ConfigFile> cf;
100
cf.instantiate();
101
String src_path = String(E.key) + ".import";
102
103
Error err = cf->load(src_path);
104
ERR_CONTINUE(err != OK);
105
106
bool changed = false;
107
108
if (E.value.flags & MAKE_NORMAL_FLAG && int(cf->get_value("params", "compress/normal_map")) == 0) {
109
print_line(
110
vformat(TTR("%s: Texture detected as used as a normal map in 3D. Enabling red-green texture compression to reduce memory usage (blue channel is discarded)."),
111
String(E.key)));
112
113
cf->set_value("params", "compress/normal_map", 1);
114
changed = true;
115
}
116
117
if (E.value.flags & MAKE_ROUGHNESS_FLAG && int(cf->get_value("params", "roughness/mode")) == 0) {
118
print_line(
119
vformat(TTR("%s: Texture detected as used as a roughness map in 3D. Enabling roughness limiter based on the detected associated normal map at %s."),
120
String(E.key), E.value.normal_path_for_roughness));
121
122
cf->set_value("params", "roughness/mode", E.value.channel_for_roughness + 2);
123
cf->set_value("params", "roughness/src_normal", E.value.normal_path_for_roughness);
124
changed = true;
125
}
126
127
if (E.value.flags & MAKE_3D_FLAG && bool(cf->get_value("params", "detect_3d/compress_to"))) {
128
const int compress_to = cf->get_value("params", "detect_3d/compress_to");
129
130
// 3D detected, disable the callback.
131
cf->set_value("params", "detect_3d/compress_to", 0);
132
133
String compress_string;
134
if (compress_to == 1) {
135
cf->set_value("params", "compress/mode", COMPRESS_VRAM_COMPRESSED);
136
compress_string = "VRAM Compressed (S3TC/ETC/BPTC)";
137
138
} else if (compress_to == 2) {
139
cf->set_value("params", "compress/mode", COMPRESS_BASIS_UNIVERSAL);
140
compress_string = "Basis Universal";
141
}
142
143
print_line(
144
vformat(TTR("%s: Texture detected as used in 3D. Enabling mipmap generation and setting the texture compression mode to %s."),
145
String(E.key), compress_string));
146
147
cf->set_value("params", "mipmaps/generate", true);
148
changed = true;
149
}
150
151
if (changed) {
152
cf->save(src_path);
153
to_reimport.push_back(E.key);
154
}
155
}
156
157
make_flags.clear();
158
159
if (!to_reimport.is_empty()) {
160
EditorFileSystem::get_singleton()->reimport_files(to_reimport);
161
}
162
}
163
164
String ResourceImporterTexture::get_importer_name() const {
165
return "texture";
166
}
167
168
String ResourceImporterTexture::get_visible_name() const {
169
return "Texture2D";
170
}
171
172
void ResourceImporterTexture::get_recognized_extensions(List<String> *p_extensions) const {
173
ImageLoader::get_recognized_extensions(p_extensions);
174
}
175
176
String ResourceImporterTexture::get_save_extension() const {
177
return "ctex";
178
}
179
180
String ResourceImporterTexture::get_resource_type() const {
181
return "CompressedTexture2D";
182
}
183
184
bool ResourceImporterTexture::get_option_visibility(const String &p_path, const String &p_option, const HashMap<StringName, Variant> &p_options) const {
185
if (p_option == "compress/high_quality" || p_option == "compress/hdr_compression") {
186
int compress_mode = int(p_options["compress/mode"]);
187
if (compress_mode != COMPRESS_VRAM_COMPRESSED) {
188
return false;
189
}
190
191
} else if (p_option == "compress/lossy_quality") {
192
int compress_mode = int(p_options["compress/mode"]);
193
if (compress_mode != COMPRESS_LOSSY) {
194
return false;
195
}
196
197
} else if (p_option == "compress/hdr_mode") {
198
int compress_mode = int(p_options["compress/mode"]);
199
if (compress_mode < COMPRESS_VRAM_COMPRESSED) {
200
return false;
201
}
202
203
} else if (p_option == "compress/normal_map") {
204
int compress_mode = int(p_options["compress/mode"]);
205
if (compress_mode == COMPRESS_LOSSLESS) {
206
return false;
207
}
208
209
} else if (p_option == "mipmaps/limit") {
210
return p_options["mipmaps/generate"];
211
212
} else if (p_option == "compress/uastc_level" || p_option == "compress/rdo_quality_loss") {
213
return int(p_options["compress/mode"]) == COMPRESS_BASIS_UNIVERSAL;
214
}
215
216
return true;
217
}
218
219
int ResourceImporterTexture::get_preset_count() const {
220
return 3;
221
}
222
223
String ResourceImporterTexture::get_preset_name(int p_idx) const {
224
static const char *preset_names[] = {
225
TTRC("2D/3D (Auto-Detect)"),
226
TTRC("2D"),
227
TTRC("3D"),
228
};
229
230
return TTRGET(preset_names[p_idx]);
231
}
232
233
void ResourceImporterTexture::get_import_options(const String &p_path, List<ImportOption> *r_options, int p_preset) const {
234
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/mode", PROPERTY_HINT_ENUM, "Lossless,Lossy,VRAM Compressed,VRAM Uncompressed,Basis Universal", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), p_preset == PRESET_3D ? 2 : 0));
235
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "compress/high_quality"), false));
236
r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "compress/lossy_quality", PROPERTY_HINT_RANGE, "0,1,0.01"), 0.7));
237
238
Image::BasisUniversalPackerParams basisu_params;
239
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/uastc_level", PROPERTY_HINT_ENUM, "Fastest,Faster,Medium,Slower,Slowest"), basisu_params.uastc_level));
240
r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "compress/rdo_quality_loss", PROPERTY_HINT_RANGE, "0,10,0.001,or_greater"), basisu_params.rdo_quality_loss));
241
242
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/hdr_compression", PROPERTY_HINT_ENUM, "Disabled,Opaque Only,Always"), 1));
243
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/normal_map", PROPERTY_HINT_ENUM, "Detect,Enable,Disabled"), 0));
244
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "compress/channel_pack", PROPERTY_HINT_ENUM, "sRGB Friendly,Optimized"), 0));
245
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "mipmaps/generate", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), (p_preset == PRESET_3D ? true : false)));
246
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "mipmaps/limit", PROPERTY_HINT_RANGE, "-1,256"), -1));
247
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "roughness/mode", PROPERTY_HINT_ENUM, "Detect,Disabled,Red,Green,Blue,Alpha,Gray"), 0));
248
r_options->push_back(ImportOption(PropertyInfo(Variant::STRING, "roughness/src_normal", PROPERTY_HINT_FILE, "*.bmp,*.dds,*.exr,*.jpeg,*.jpg,*.hdr,*.png,*.svg,*.tga,*.webp"), ""));
249
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "process/channel_remap/red", PROPERTY_HINT_ENUM, "Red,Green,Blue,Alpha,Inverted Red,Inverted Green,Inverted Blue,Inverted Alpha,Unused,Zero,One"), 0));
250
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "process/channel_remap/green", PROPERTY_HINT_ENUM, "Red,Green,Blue,Alpha,Inverted Red,Inverted Green,Inverted Blue,Inverted Alpha,Unused,Zero,One"), 1));
251
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "process/channel_remap/blue", PROPERTY_HINT_ENUM, "Red,Green,Blue,Alpha,Inverted Red,Inverted Green,Inverted Blue,Inverted Alpha,Unused,Zero,One"), 2));
252
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "process/channel_remap/alpha", PROPERTY_HINT_ENUM, "Red,Green,Blue,Alpha,Inverted Red,Inverted Green,Inverted Blue,Inverted Alpha,Unused,Zero,One"), 3));
253
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "process/fix_alpha_border"), p_preset != PRESET_3D));
254
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "process/premult_alpha"), false));
255
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "process/normal_map_invert_y"), false));
256
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "process/hdr_as_srgb"), false));
257
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "process/hdr_clamp_exposure"), false));
258
259
// Maximum bound is the highest allowed value for lossy compression (the lowest common denominator).
260
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "process/size_limit", PROPERTY_HINT_RANGE, "0,16383,1"), 0));
261
262
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "detect_3d/compress_to", PROPERTY_HINT_ENUM, "Disabled,VRAM Compressed,Basis Universal"), (p_preset == PRESET_DETECT) ? 1 : 0));
263
264
// Do path based customization only if a path was passed.
265
if (p_path.is_empty() || p_path.get_extension() == "svg") {
266
r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "svg/scale", PROPERTY_HINT_RANGE, "0.001,100,0.001"), 1.0));
267
268
// Editor use only, applies to SVG.
269
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "editor/scale_with_editor_scale"), false));
270
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "editor/convert_colors_with_editor_theme"), false));
271
}
272
}
273
274
void ResourceImporterTexture::save_to_ctex_format(Ref<FileAccess> f, const Ref<Image> &p_image, CompressMode p_compress_mode, Image::UsedChannels p_channels, Image::CompressMode p_compress_format, float p_lossy_quality, const Image::BasisUniversalPackerParams &p_basisu_params) {
275
switch (p_compress_mode) {
276
case COMPRESS_LOSSLESS: {
277
bool lossless_force_png = GLOBAL_GET("rendering/textures/lossless_compression/force_png") || !Image::_webp_mem_loader_func; // WebP module disabled or png is forced.
278
bool use_webp = !lossless_force_png && p_image->get_width() <= 16383 && p_image->get_height() <= 16383; // WebP has a size limit.
279
280
f->store_32(use_webp ? CompressedTexture2D::DATA_FORMAT_WEBP : CompressedTexture2D::DATA_FORMAT_PNG);
281
f->store_16(p_image->get_width());
282
f->store_16(p_image->get_height());
283
f->store_32(p_image->get_mipmap_count());
284
f->store_32(p_image->get_format());
285
286
for (int i = 0; i < p_image->get_mipmap_count() + 1; i++) {
287
Vector<uint8_t> data;
288
if (use_webp) {
289
data = Image::webp_lossless_packer(i ? p_image->get_image_from_mipmap(i) : p_image);
290
} else {
291
data = Image::png_packer(i ? p_image->get_image_from_mipmap(i) : p_image);
292
}
293
294
const uint64_t data_size = data.size();
295
296
f->store_32(data_size);
297
f->store_buffer(data.ptr(), data_size);
298
}
299
300
} break;
301
case COMPRESS_LOSSY: {
302
f->store_32(CompressedTexture2D::DATA_FORMAT_WEBP);
303
f->store_16(p_image->get_width());
304
f->store_16(p_image->get_height());
305
f->store_32(p_image->get_mipmap_count());
306
f->store_32(p_image->get_format());
307
308
for (int i = 0; i < p_image->get_mipmap_count() + 1; i++) {
309
Vector<uint8_t> data = Image::webp_lossy_packer(i ? p_image->get_image_from_mipmap(i) : p_image, p_lossy_quality);
310
const uint64_t data_size = data.size();
311
312
f->store_32(data_size);
313
f->store_buffer(data.ptr(), data_size);
314
}
315
316
} break;
317
case COMPRESS_VRAM_COMPRESSED: {
318
Ref<Image> image = p_image->duplicate();
319
image->compress_from_channels(p_compress_format, p_channels);
320
321
f->store_32(CompressedTexture2D::DATA_FORMAT_IMAGE);
322
f->store_16(image->get_width());
323
f->store_16(image->get_height());
324
f->store_32(image->get_mipmap_count());
325
f->store_32(image->get_format());
326
f->store_buffer(image->get_data());
327
328
} break;
329
case COMPRESS_VRAM_UNCOMPRESSED: {
330
f->store_32(CompressedTexture2D::DATA_FORMAT_IMAGE);
331
f->store_16(p_image->get_width());
332
f->store_16(p_image->get_height());
333
f->store_32(p_image->get_mipmap_count());
334
f->store_32(p_image->get_format());
335
f->store_buffer(p_image->get_data());
336
337
} break;
338
case COMPRESS_BASIS_UNIVERSAL: {
339
f->store_32(CompressedTexture2D::DATA_FORMAT_BASIS_UNIVERSAL);
340
f->store_16(p_image->get_width());
341
f->store_16(p_image->get_height());
342
f->store_32(p_image->get_mipmap_count());
343
f->store_32(p_image->get_format());
344
345
Vector<uint8_t> data = Image::basis_universal_packer(p_image, p_channels, p_basisu_params);
346
const uint64_t data_size = data.size();
347
348
f->store_32(data_size);
349
f->store_buffer(data.ptr(), data_size);
350
} break;
351
}
352
}
353
354
void ResourceImporterTexture::_save_ctex(const Ref<Image> &p_image, const String &p_to_path, CompressMode p_compress_mode, float p_lossy_quality, const Image::BasisUniversalPackerParams &p_basisu_params, Image::CompressMode p_vram_compression, bool p_mipmaps, bool p_streamable, bool p_detect_3d, bool p_detect_roughness, bool p_detect_normal, bool p_force_normal, bool p_srgb_friendly, bool p_force_po2_for_compressed, uint32_t p_limit_mipmap, const Ref<Image> &p_normal, Image::RoughnessChannel p_roughness_channel) {
355
Ref<FileAccess> f = FileAccess::open(p_to_path, FileAccess::WRITE);
356
ERR_FAIL_COND(f.is_null());
357
358
// Godot Streamable Texture 2D.
359
f->store_8('G');
360
f->store_8('S');
361
f->store_8('T');
362
f->store_8('2');
363
364
// Current format version.
365
f->store_32(CompressedTexture2D::FORMAT_VERSION);
366
367
// Texture may be resized later, so original size must be saved first.
368
f->store_32(p_image->get_width());
369
f->store_32(p_image->get_height());
370
371
uint32_t flags = 0;
372
if (p_streamable) {
373
flags |= CompressedTexture2D::FORMAT_BIT_STREAM;
374
}
375
if (p_mipmaps) {
376
flags |= CompressedTexture2D::FORMAT_BIT_HAS_MIPMAPS;
377
}
378
if (p_detect_3d) {
379
flags |= CompressedTexture2D::FORMAT_BIT_DETECT_3D;
380
}
381
if (p_detect_roughness) {
382
flags |= CompressedTexture2D::FORMAT_BIT_DETECT_ROUGNESS;
383
}
384
if (p_detect_normal) {
385
flags |= CompressedTexture2D::FORMAT_BIT_DETECT_NORMAL;
386
}
387
388
f->store_32(flags);
389
f->store_32(p_limit_mipmap);
390
391
// Reserved.
392
f->store_32(0);
393
f->store_32(0);
394
f->store_32(0);
395
396
if ((p_compress_mode == COMPRESS_LOSSLESS || p_compress_mode == COMPRESS_LOSSY) && p_image->get_format() >= Image::FORMAT_RF) {
397
p_compress_mode = COMPRESS_VRAM_UNCOMPRESSED; //these can't go as lossy
398
}
399
400
Ref<Image> image = p_image->duplicate();
401
402
if (p_mipmaps) {
403
if (p_force_po2_for_compressed && (p_compress_mode == COMPRESS_BASIS_UNIVERSAL || p_compress_mode == COMPRESS_VRAM_COMPRESSED)) {
404
image->resize_to_po2();
405
}
406
407
if (!image->has_mipmaps() || p_force_normal) {
408
image->generate_mipmaps(p_force_normal);
409
}
410
411
} else {
412
image->clear_mipmaps();
413
}
414
415
// Generate roughness mipmaps from normal texture.
416
if (image->has_mipmaps() && p_normal.is_valid()) {
417
image->generate_mipmap_roughness(p_roughness_channel, p_normal);
418
}
419
420
// Optimization: Only check for color channels when compressing as BasisU or VRAM.
421
Image::UsedChannels used_channels = Image::USED_CHANNELS_RGBA;
422
423
if (p_compress_mode == COMPRESS_BASIS_UNIVERSAL || p_compress_mode == COMPRESS_VRAM_COMPRESSED) {
424
Image::CompressSource comp_source = Image::COMPRESS_SOURCE_GENERIC;
425
if (p_force_normal) {
426
comp_source = Image::COMPRESS_SOURCE_NORMAL;
427
} else if (p_srgb_friendly) {
428
comp_source = Image::COMPRESS_SOURCE_SRGB;
429
}
430
431
used_channels = image->detect_used_channels(comp_source);
432
}
433
434
save_to_ctex_format(f, image, p_compress_mode, used_channels, p_vram_compression, p_lossy_quality, p_basisu_params);
435
}
436
437
void ResourceImporterTexture::_save_editor_meta(const Dictionary &p_metadata, const String &p_to_path) {
438
Ref<FileAccess> f = FileAccess::open(p_to_path, FileAccess::WRITE);
439
ERR_FAIL_COND(f.is_null());
440
441
f->store_var(p_metadata);
442
}
443
444
Dictionary ResourceImporterTexture::_load_editor_meta(const String &p_path) const {
445
Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);
446
ERR_FAIL_COND_V_MSG(f.is_null(), Dictionary(), vformat("Missing required editor-specific import metadata for a texture (please reimport it using the 'Import' tab): '%s'", p_path));
447
448
return f->get_var();
449
}
450
451
void ResourceImporterTexture::_remap_channels(Ref<Image> &r_image, ChannelRemap p_options[4]) {
452
ERR_FAIL_COND(r_image->is_compressed());
453
454
// Currently HDR inverted remapping is not allowed.
455
bool attempted_hdr_inverted = false;
456
if (r_image->get_format() >= Image::FORMAT_RF && r_image->get_format() <= Image::FORMAT_RGBE9995) {
457
// Formats which can hold HDR data cannot be inverted the same way as unsigned normalized ones (1.0 - channel).
458
for (int i = 0; i < 4; i++) {
459
switch (p_options[i]) {
460
case REMAP_INV_R:
461
attempted_hdr_inverted = true;
462
p_options[i] = REMAP_R;
463
break;
464
case REMAP_INV_G:
465
attempted_hdr_inverted = true;
466
p_options[i] = REMAP_G;
467
break;
468
case REMAP_INV_B:
469
attempted_hdr_inverted = true;
470
p_options[i] = REMAP_B;
471
break;
472
case REMAP_INV_A:
473
attempted_hdr_inverted = true;
474
p_options[i] = REMAP_A;
475
break;
476
default:
477
break;
478
}
479
}
480
}
481
482
if (attempted_hdr_inverted) {
483
WARN_PRINT("Attempted to use an inverted channel remap on an HDR image. The remap has been changed to its uninverted equivalent.");
484
}
485
486
// Optimization: Set the remap from 'unused' to either 0 or 1 to avoid repeated checks in the conversion loop.
487
for (int i = 0; i < 4; i++) {
488
if (p_options[i] == REMAP_UNUSED) {
489
p_options[i] = i == 3 ? REMAP_1 : REMAP_0;
490
}
491
}
492
493
// Expand the image's channel count in the event that the current set of channels doesn't allow for the desired remap.
494
const Image::Format original_format = r_image->get_format();
495
const uint32_t channel_mask = Image::get_format_component_mask(original_format);
496
497
// Whether a channel is supported by the format itself.
498
const bool has_channel_r = channel_mask & 0x1;
499
const bool has_channel_g = channel_mask & 0x2;
500
const bool has_channel_b = channel_mask & 0x4;
501
const bool has_channel_a = channel_mask & 0x8;
502
503
// Whether a certain channel needs to be remapped.
504
const bool remap_r = p_options[0] != REMAP_R ? !(!has_channel_r && p_options[0] == REMAP_0) : false;
505
const bool remap_g = p_options[1] != REMAP_G ? !(!has_channel_g && p_options[1] == REMAP_0) : false;
506
const bool remap_b = p_options[2] != REMAP_B ? !(!has_channel_b && p_options[2] == REMAP_0) : false;
507
const bool remap_a = p_options[3] != REMAP_A ? !(!has_channel_a && p_options[3] == REMAP_1) : false;
508
509
if (!(remap_r || remap_g || remap_b || remap_a)) {
510
// Default color map, do nothing.
511
return;
512
}
513
514
// Whether a certain channel set is needed, either from the source or the remap.
515
const bool needs_rg = remap_g || has_channel_g;
516
const bool needs_rgb = remap_b || has_channel_b;
517
const bool needs_rgba = remap_a || has_channel_a;
518
519
bool could_not_expand = false;
520
switch (original_format) {
521
case Image::FORMAT_R8:
522
case Image::FORMAT_RG8:
523
case Image::FORMAT_RGB8: {
524
// Convert to either RGBA8, RGB8 or RG8.
525
if (needs_rgba) {
526
r_image->convert(Image::FORMAT_RGBA8);
527
} else if (needs_rgb) {
528
r_image->convert(Image::FORMAT_RGB8);
529
} else if (needs_rg) {
530
r_image->convert(Image::FORMAT_RG8);
531
}
532
} break;
533
case Image::FORMAT_RH:
534
case Image::FORMAT_RGH:
535
case Image::FORMAT_RGBH: {
536
// Convert to either RGBAH, RGBH or RGH.
537
if (needs_rgba) {
538
r_image->convert(Image::FORMAT_RGBAH);
539
} else if (needs_rgb) {
540
r_image->convert(Image::FORMAT_RGBH);
541
} else if (needs_rg) {
542
r_image->convert(Image::FORMAT_RGH);
543
}
544
} break;
545
case Image::FORMAT_RF:
546
case Image::FORMAT_RGF:
547
case Image::FORMAT_RGBF: {
548
// Convert to either RGBAF, RGBF or RGF.
549
if (needs_rgba) {
550
r_image->convert(Image::FORMAT_RGBAF);
551
} else if (needs_rgb) {
552
r_image->convert(Image::FORMAT_RGBF);
553
} else if (needs_rg) {
554
r_image->convert(Image::FORMAT_RGF);
555
}
556
} break;
557
case Image::FORMAT_L8: {
558
const bool uniform_rgb = (p_options[0] == p_options[1] && p_options[1] == p_options[2]) || !(remap_r || remap_g || remap_b);
559
if (uniform_rgb) {
560
// Uniform RGB.
561
if (needs_rgba) {
562
r_image->convert(Image::FORMAT_LA8);
563
}
564
} else {
565
// Non-uniform RGB.
566
if (needs_rgba) {
567
r_image->convert(Image::FORMAT_RGBA8);
568
} else {
569
r_image->convert(Image::FORMAT_RGB8);
570
}
571
could_not_expand = true;
572
}
573
} break;
574
case Image::FORMAT_LA8: {
575
const bool uniform_rgb = (p_options[0] == p_options[1] && p_options[1] == p_options[2]) || !(remap_r || remap_g || remap_b);
576
if (!uniform_rgb) {
577
// Non-uniform RGB.
578
r_image->convert(Image::FORMAT_RGBA8);
579
could_not_expand = true;
580
}
581
} break;
582
case Image::FORMAT_RGB565: {
583
if (needs_rgba) {
584
// RGB565 doesn't have an alpha expansion, convert to RGBA8.
585
r_image->convert(Image::FORMAT_RGBA8);
586
could_not_expand = true;
587
}
588
} break;
589
case Image::FORMAT_RGBE9995: {
590
if (needs_rgba) {
591
// RGB9995 doesn't have an alpha expansion, convert to RGBAH.
592
r_image->convert(Image::FORMAT_RGBAH);
593
could_not_expand = true;
594
}
595
} break;
596
597
default: {
598
} break;
599
}
600
601
if (could_not_expand) {
602
WARN_PRINT(vformat("Unable to expand image format %s's channels (the target format does not exist), converting to %s as a fallback.",
603
Image::get_format_name(original_format), Image::get_format_name(r_image->get_format())));
604
}
605
606
// Remap the channels.
607
for (int x = 0; x < r_image->get_width(); x++) {
608
for (int y = 0; y < r_image->get_height(); y++) {
609
Color src = r_image->get_pixel(x, y);
610
Color dst;
611
612
for (int i = 0; i < 4; i++) {
613
switch (p_options[i]) {
614
case REMAP_R:
615
dst[i] = src.r;
616
break;
617
case REMAP_G:
618
dst[i] = src.g;
619
break;
620
case REMAP_B:
621
dst[i] = src.b;
622
break;
623
case REMAP_A:
624
dst[i] = src.a;
625
break;
626
627
case REMAP_INV_R:
628
dst[i] = 1.0f - src.r;
629
break;
630
case REMAP_INV_G:
631
dst[i] = 1.0f - src.g;
632
break;
633
case REMAP_INV_B:
634
dst[i] = 1.0f - src.b;
635
break;
636
case REMAP_INV_A:
637
dst[i] = 1.0f - src.a;
638
break;
639
640
case REMAP_0:
641
dst[i] = 0.0f;
642
break;
643
case REMAP_1:
644
dst[i] = 1.0f;
645
break;
646
647
default:
648
break;
649
}
650
}
651
652
r_image->set_pixel(x, y, dst);
653
}
654
}
655
}
656
657
void ResourceImporterTexture::_invert_y_channel(Ref<Image> &r_image) {
658
// Inverting the green channel can be used to flip a normal map's direction.
659
// There's no standard when it comes to normal map Y direction, so this is
660
// sometimes needed when using a normal map exported from another program.
661
// See <http://wiki.polycount.com/wiki/Normal_Map_Technical_Details#Common_Swizzle_Coordinates>.
662
const int height = r_image->get_height();
663
const int width = r_image->get_width();
664
665
for (int i = 0; i < width; i++) {
666
for (int j = 0; j < height; j++) {
667
const Color color = r_image->get_pixel(i, j);
668
r_image->set_pixel(i, j, Color(color.r, 1 - color.g, color.b, color.a));
669
}
670
}
671
}
672
673
void ResourceImporterTexture::_clamp_hdr_exposure(Ref<Image> &r_image) {
674
// Clamp HDR exposure following Filament's tonemapping formula.
675
// This can be used to reduce fireflies in environment maps or reduce the influence
676
// of the sun from an HDRI panorama on environment lighting (when a DirectionalLight3D is used instead).
677
const int height = r_image->get_height();
678
const int width = r_image->get_width();
679
680
// These values are chosen arbitrarily and seem to produce good results with 4,096 samples.
681
const float linear = 4096.0;
682
const float compressed = 16384.0;
683
684
for (int i = 0; i < width; i++) {
685
for (int j = 0; j < height; j++) {
686
const Color color = r_image->get_pixel(i, j);
687
const float luma = color.get_luminance();
688
689
Color clamped_color;
690
if (luma <= linear) {
691
clamped_color = color;
692
} else {
693
clamped_color = (color / luma) * ((linear * linear - compressed * luma) / (2 * linear - compressed - luma));
694
}
695
696
r_image->set_pixel(i, j, clamped_color);
697
}
698
}
699
}
700
701
Error ResourceImporterTexture::import(ResourceUID::ID p_source_id, 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) {
702
// Parse import options.
703
int32_t loader_flags = ImageFormatLoader::FLAG_NONE;
704
705
// Compression.
706
CompressMode compress_mode = CompressMode(int(p_options["compress/mode"]));
707
const float lossy = p_options["compress/lossy_quality"];
708
const int pack_channels = p_options["compress/channel_pack"];
709
const int normal = p_options["compress/normal_map"];
710
const int hdr_compression = p_options["compress/hdr_compression"];
711
const int high_quality = p_options["compress/high_quality"];
712
713
// Mipmaps.
714
const bool mipmaps = p_options["mipmaps/generate"];
715
const uint32_t mipmap_limit = mipmaps ? uint32_t(p_options["mipmaps/limit"]) : uint32_t(-1);
716
717
// Roughness.
718
const int roughness = p_options["roughness/mode"];
719
const String normal_map = p_options["roughness/src_normal"];
720
721
// Processing.
722
const int remap_r = p_options["process/channel_remap/red"];
723
const int remap_g = p_options["process/channel_remap/green"];
724
const int remap_b = p_options["process/channel_remap/blue"];
725
const int remap_a = p_options["process/channel_remap/alpha"];
726
const bool fix_alpha_border = p_options["process/fix_alpha_border"];
727
const bool premult_alpha = p_options["process/premult_alpha"];
728
const bool normal_map_invert_y = p_options["process/normal_map_invert_y"];
729
730
const bool hdr_as_srgb = p_options["process/hdr_as_srgb"];
731
const bool hdr_clamp_exposure = p_options["process/hdr_clamp_exposure"];
732
int size_limit = p_options["process/size_limit"];
733
734
const Image::BasisUniversalPackerParams basisu_params = {
735
p_options["compress/uastc_level"],
736
p_options["compress/rdo_quality_loss"],
737
};
738
739
bool using_fallback_size_limit = false;
740
if (size_limit == 0) {
741
using_fallback_size_limit = true;
742
// If no size limit is defined, use a fallback size limit to prevent textures from looking incorrect or failing to import.
743
switch (compress_mode) {
744
case COMPRESS_LOSSY:
745
// Maximum WebP size on either axis.
746
size_limit = 16383;
747
break;
748
case COMPRESS_BASIS_UNIVERSAL:
749
// Maximum Basis Universal size on either axis.
750
size_limit = 16384;
751
break;
752
default:
753
// As of June 2024, no GPU can correctly display a texture larger than 32768 pixels on either axis.
754
size_limit = 32768;
755
break;
756
}
757
}
758
759
// Support for texture streaming is not implemented yet.
760
const bool stream = false;
761
762
// SVG-specific options.
763
float scale = p_options.has("svg/scale") ? float(p_options["svg/scale"]) : 1.0f;
764
765
// Editor-specific options.
766
bool use_editor_scale = p_options.has("editor/scale_with_editor_scale") && p_options["editor/scale_with_editor_scale"];
767
bool convert_editor_colors = p_options.has("editor/convert_colors_with_editor_theme") && p_options["editor/convert_colors_with_editor_theme"];
768
769
if (hdr_as_srgb) {
770
loader_flags |= ImageFormatLoader::FLAG_FORCE_LINEAR;
771
}
772
773
// Start importing images.
774
LocalVector<Ref<Image>> images_imported;
775
776
// Load the normal image.
777
Ref<Image> normal_image;
778
Image::RoughnessChannel roughness_channel = Image::ROUGHNESS_CHANNEL_R;
779
780
if (mipmaps && roughness > 1 && FileAccess::exists(normal_map)) {
781
normal_image.instantiate();
782
if (ImageLoader::load_image(normal_map, normal_image) == OK) {
783
roughness_channel = Image::RoughnessChannel(roughness - 2);
784
}
785
}
786
787
// Load the main image.
788
Ref<Image> image;
789
image.instantiate();
790
Error err = ImageLoader::load_image(p_source_file, image, nullptr, loader_flags, scale);
791
if (err != OK) {
792
return err;
793
}
794
images_imported.push_back(image);
795
796
// Load the editor-only image.
797
Ref<Image> editor_image;
798
799
if (use_editor_scale || convert_editor_colors) {
800
float editor_scale = use_editor_scale ? scale * EDSCALE : scale;
801
802
int32_t editor_loader_flags = loader_flags;
803
if (convert_editor_colors) {
804
editor_loader_flags |= ImageFormatLoader::FLAG_CONVERT_COLORS;
805
}
806
807
editor_image.instantiate();
808
err = ImageLoader::load_image(p_source_file, editor_image, nullptr, editor_loader_flags, editor_scale);
809
810
if (err != OK) {
811
WARN_PRINT(vformat("Failed to import an image resource for editor use from '%s'.", p_source_file));
812
} else {
813
if (convert_editor_colors) {
814
float image_saturation = EDITOR_GET("interface/theme/icon_saturation");
815
editor_image->adjust_bcs(1.0, 1.0, image_saturation);
816
}
817
818
images_imported.push_back(editor_image);
819
}
820
}
821
822
for (Ref<Image> &target_image : images_imported) {
823
// Apply the size limit.
824
if (size_limit > 0 && (target_image->get_width() > size_limit || target_image->get_height() > size_limit)) {
825
if (target_image->get_width() >= target_image->get_height()) {
826
int new_width = size_limit;
827
int new_height = target_image->get_height() * new_width / target_image->get_width();
828
829
if (using_fallback_size_limit) {
830
// Only warn if downsizing occurred when the user did not explicitly request it.
831
WARN_PRINT(vformat("%s: Texture was downsized on import as its width (%d pixels) exceeded the importable size limit (%d pixels).", p_source_file, target_image->get_width(), size_limit));
832
}
833
target_image->resize(new_width, new_height, Image::INTERPOLATE_CUBIC);
834
} else {
835
int new_height = size_limit;
836
int new_width = target_image->get_width() * new_height / target_image->get_height();
837
838
if (using_fallback_size_limit) {
839
// Only warn if downsizing occurred when the user did not explicitly request it.
840
WARN_PRINT(vformat("%s: Texture was downsized on import as its height (%d pixels) exceeded the importable size limit (%d pixels).", p_source_file, target_image->get_height(), size_limit));
841
}
842
target_image->resize(new_width, new_height, Image::INTERPOLATE_CUBIC);
843
}
844
845
if (normal == 1) {
846
target_image->normalize();
847
}
848
}
849
850
{
851
ChannelRemap remaps[4] = {
852
(ChannelRemap)remap_r,
853
(ChannelRemap)remap_g,
854
(ChannelRemap)remap_b,
855
(ChannelRemap)remap_a,
856
};
857
858
_remap_channels(target_image, remaps);
859
}
860
861
// Fix alpha border.
862
if (fix_alpha_border) {
863
target_image->fix_alpha_edges();
864
}
865
866
// Premultiply the alpha.
867
if (premult_alpha) {
868
target_image->premultiply_alpha();
869
}
870
871
// Invert the green channel of the image to flip the normal map it contains.
872
if (normal_map_invert_y) {
873
_invert_y_channel(target_image);
874
}
875
876
// Clamp HDR exposure.
877
if (hdr_clamp_exposure) {
878
_clamp_hdr_exposure(target_image);
879
}
880
}
881
882
bool detect_3d = int(p_options["detect_3d/compress_to"]) > 0;
883
bool detect_roughness = roughness == 0;
884
bool detect_normal = normal == 0;
885
bool force_normal = normal == 1;
886
bool srgb_friendly_pack = pack_channels == 0;
887
888
Array formats_imported;
889
890
if (compress_mode == COMPRESS_VRAM_COMPRESSED) {
891
// Must import in desktop and mobile formats in order of priority, so platform chooses the best supported one (e.g. s3tc over etc2 on desktop).
892
const bool is_hdr = (image->get_format() >= Image::FORMAT_RF && image->get_format() <= Image::FORMAT_RGBE9995);
893
const bool can_s3tc_bptc = ResourceImporterTextureSettings::should_import_s3tc_bptc();
894
const bool can_etc2_astc = ResourceImporterTextureSettings::should_import_etc2_astc();
895
896
// Add list of formats imported.
897
if (can_s3tc_bptc) {
898
formats_imported.push_back("s3tc_bptc");
899
}
900
if (can_etc2_astc) {
901
formats_imported.push_back("etc2_astc");
902
}
903
904
bool can_compress_hdr = hdr_compression > 0;
905
bool has_alpha = image->detect_alpha() != Image::ALPHA_NONE;
906
bool force_uncompressed = false;
907
908
if (is_hdr) {
909
if (has_alpha) {
910
// Can compress HDR, but HDR with alpha is not compressible.
911
if (hdr_compression == 2) {
912
// But user selected to compress HDR anyway, so force an alpha-less format.
913
if (image->get_format() == Image::FORMAT_RGBAF) {
914
image->convert(Image::FORMAT_RGBF);
915
} else if (image->get_format() == Image::FORMAT_RGBAH) {
916
image->convert(Image::FORMAT_RGBH);
917
}
918
} else {
919
can_compress_hdr = false;
920
}
921
}
922
923
// Fall back to RGBE99995.
924
if (!can_compress_hdr && image->get_format() != Image::FORMAT_RGBE9995) {
925
image->convert(Image::FORMAT_RGBE9995);
926
force_uncompressed = true;
927
}
928
}
929
930
if (force_uncompressed) {
931
_save_ctex(image, p_save_path + ".ctex", COMPRESS_VRAM_UNCOMPRESSED, lossy, basisu_params, Image::COMPRESS_S3TC /* This is ignored. */,
932
mipmaps, stream, detect_3d, detect_roughness, detect_normal, force_normal, srgb_friendly_pack, false, mipmap_limit, normal_image, roughness_channel);
933
} else {
934
if (can_s3tc_bptc) {
935
Image::CompressMode image_compress_mode;
936
String image_compress_format;
937
if (high_quality || is_hdr) {
938
image_compress_mode = Image::COMPRESS_BPTC;
939
image_compress_format = "bptc";
940
} else {
941
image_compress_mode = Image::COMPRESS_S3TC;
942
image_compress_format = "s3tc";
943
}
944
945
_save_ctex(image, p_save_path + "." + image_compress_format + ".ctex", compress_mode, lossy, basisu_params, image_compress_mode, mipmaps,
946
stream, detect_3d, detect_roughness, detect_normal, force_normal, srgb_friendly_pack, false, mipmap_limit, normal_image, roughness_channel);
947
r_platform_variants->push_back(image_compress_format);
948
}
949
950
if (can_etc2_astc) {
951
Image::CompressMode image_compress_mode;
952
String image_compress_format;
953
if (high_quality || is_hdr) {
954
image_compress_mode = Image::COMPRESS_ASTC;
955
image_compress_format = "astc";
956
} else {
957
image_compress_mode = Image::COMPRESS_ETC2;
958
image_compress_format = "etc2";
959
}
960
961
_save_ctex(image, p_save_path + "." + image_compress_format + ".ctex", compress_mode, lossy, basisu_params, image_compress_mode, mipmaps, stream, detect_3d,
962
detect_roughness, detect_normal, force_normal, srgb_friendly_pack, false, mipmap_limit, normal_image, roughness_channel);
963
r_platform_variants->push_back(image_compress_format);
964
}
965
}
966
} else {
967
// Import normally.
968
_save_ctex(image, p_save_path + ".ctex", compress_mode, lossy, basisu_params, Image::COMPRESS_S3TC /* This is ignored. */,
969
mipmaps, stream, detect_3d, detect_roughness, detect_normal, force_normal, srgb_friendly_pack, false, mipmap_limit, normal_image, roughness_channel);
970
}
971
972
if (editor_image.is_valid()) {
973
_save_ctex(editor_image, p_save_path + ".editor.ctex", compress_mode, lossy, basisu_params, Image::COMPRESS_S3TC /* This is ignored. */,
974
mipmaps, stream, detect_3d, detect_roughness, detect_normal, force_normal, srgb_friendly_pack, false, mipmap_limit, normal_image, roughness_channel);
975
976
// Generate and save editor-specific metadata, which we cannot save to the .import file.
977
Dictionary editor_meta;
978
979
if (use_editor_scale) {
980
editor_meta["editor_scale"] = EDSCALE;
981
}
982
983
if (convert_editor_colors) {
984
editor_meta["editor_dark_theme"] = EditorThemeManager::is_dark_theme();
985
}
986
987
_save_editor_meta(editor_meta, p_save_path + ".editor.meta");
988
}
989
990
if (r_metadata) {
991
Dictionary meta;
992
meta["vram_texture"] = compress_mode == COMPRESS_VRAM_COMPRESSED;
993
994
if (formats_imported.size()) {
995
meta["imported_formats"] = formats_imported;
996
}
997
998
if (editor_image.is_valid()) {
999
meta["has_editor_variant"] = true;
1000
}
1001
1002
*r_metadata = meta;
1003
}
1004
1005
return OK;
1006
}
1007
1008
const char *ResourceImporterTexture::compression_formats[] = {
1009
"s3tc_bptc",
1010
"etc2_astc",
1011
nullptr
1012
};
1013
1014
String ResourceImporterTexture::get_import_settings_string() const {
1015
String s;
1016
1017
int index = 0;
1018
while (compression_formats[index]) {
1019
const String setting_path = "rendering/textures/vram_compression/import_" + String(compression_formats[index]);
1020
if (bool(GLOBAL_GET(setting_path))) {
1021
s += String(compression_formats[index]);
1022
}
1023
1024
index++;
1025
}
1026
1027
return s;
1028
}
1029
1030
bool ResourceImporterTexture::are_import_settings_valid(const String &p_path, const Dictionary &p_meta) const {
1031
if (p_meta.has("has_editor_variant")) {
1032
String imported_path = ResourceFormatImporter::get_singleton()->get_internal_resource_path(p_path);
1033
if (!FileAccess::exists(imported_path)) {
1034
return false;
1035
}
1036
1037
String editor_meta_path = imported_path.replace(".editor.ctex", ".editor.meta");
1038
Dictionary editor_meta = _load_editor_meta(editor_meta_path);
1039
1040
if (editor_meta.has("editor_scale") && (float)editor_meta["editor_scale"] != EDSCALE) {
1041
return false;
1042
}
1043
1044
if (editor_meta.has("editor_dark_theme") && (bool)editor_meta["editor_dark_theme"] != EditorThemeManager::is_dark_theme()) {
1045
return false;
1046
}
1047
}
1048
1049
if (!p_meta.has("vram_texture")) {
1050
return false;
1051
}
1052
1053
if (!bool(p_meta["vram_texture"])) {
1054
return true; // Do not care about non-VRAM.
1055
}
1056
1057
// Will become invalid if formats are missing to import.
1058
Vector<String> formats_imported;
1059
if (p_meta.has("imported_formats")) {
1060
formats_imported = p_meta["imported_formats"];
1061
}
1062
1063
int index = 0;
1064
bool valid = true;
1065
while (compression_formats[index]) {
1066
const String setting_path = "rendering/textures/vram_compression/import_" + String(compression_formats[index]);
1067
if (ProjectSettings::get_singleton()->has_setting(setting_path)) {
1068
if (bool(GLOBAL_GET(setting_path)) && !formats_imported.has(compression_formats[index])) {
1069
valid = false;
1070
break;
1071
}
1072
} else {
1073
WARN_PRINT("Setting for imported format not found: " + setting_path);
1074
}
1075
1076
index++;
1077
}
1078
1079
return valid;
1080
}
1081
1082
ResourceImporterTexture *ResourceImporterTexture::singleton = nullptr;
1083
1084
ResourceImporterTexture::ResourceImporterTexture(bool p_singleton) {
1085
// This should only be set through the EditorNode.
1086
if (p_singleton) {
1087
singleton = this;
1088
}
1089
1090
CompressedTexture2D::request_3d_callback = _texture_reimport_3d;
1091
CompressedTexture2D::request_roughness_callback = _texture_reimport_roughness;
1092
CompressedTexture2D::request_normal_callback = _texture_reimport_normal;
1093
}
1094
1095
ResourceImporterTexture::~ResourceImporterTexture() {
1096
if (singleton == this) {
1097
singleton = nullptr;
1098
}
1099
}
1100
1101