Skip to content

Commit 03b7934

Browse files
committed
GLTF export: Allow using a PNG or JPEG fallback image
1 parent a210fe6 commit 03b7934

File tree

4 files changed

+173
-78
lines changed

4 files changed

+173
-78
lines changed

modules/gltf/doc_classes/GLTFDocument.xml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,16 @@
115115
</method>
116116
</methods>
117117
<members>
118+
<member name="fallback_image_format" type="String" setter="set_fallback_image_format" getter="get_fallback_image_format" default="&quot;None&quot;">
119+
The user-friendly name of the fallback image format. This is used when exporting the glTF file, including writing to a file and writing to a byte array.
120+
This property may only be one of "None", "PNG", or "JPEG", and is only used when the [member image_format] is not one of "None", "PNG", or "JPEG". If having multiple extension image formats is desired, that can be done using a [GLTFDocumentExtension] class - this property only covers the use case of providing a base glTF fallback image when using a custom image format.
121+
</member>
122+
<member name="fallback_image_quality" type="float" setter="set_fallback_image_quality" getter="get_fallback_image_quality" default="0.25">
123+
The quality of the fallback image, if any. For PNG files, this downscales the image on both dimensions by this factor. For JPEG files, this is the lossy quality of the image. A low value is recommended, since including multiple high quality images in a glTF file defeats the file size gains of using a more efficient image format.
124+
</member>
118125
<member name="image_format" type="String" setter="set_image_format" getter="get_image_format" default="&quot;PNG&quot;">
119126
The user-friendly name of the export image format. This is used when exporting the glTF file, including writing to a file and writing to a byte array.
120-
By default, Godot allows the following options: "None", "PNG", "JPEG", "Lossless WebP", and "Lossy WebP". Support for more image formats can be added in [GLTFDocumentExtension] classes.
127+
By default, Godot allows the following options: "None", "PNG", "JPEG", "Lossless WebP", and "Lossy WebP". Support for more image formats can be added in [GLTFDocumentExtension] classes. A single extension class can provide multiple options for the specific format to use, or even an option that uses multiple formats at once.
121128
</member>
122129
<member name="lossy_quality" type="float" setter="set_lossy_quality" getter="get_lossy_quality" default="0.75">
123130
If [member image_format] is a lossy image format, this determines the lossy quality of the image. On a range of [code]0.0[/code] to [code]1.0[/code], where [code]0.0[/code] is the lowest quality and [code]1.0[/code] is the highest quality. A lossy quality of [code]1.0[/code] is not the same as lossless.

modules/gltf/editor/editor_scene_exporter_gltf_settings.cpp

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ bool EditorSceneExporterGLTFSettings::_set(const StringName &p_name, const Varia
4646
_document->set_lossy_quality(p_value);
4747
return true;
4848
}
49+
if (p_name == StringName("fallback_image_format")) {
50+
_document->set_fallback_image_format(p_value);
51+
emit_signal(CoreStringName(property_list_changed));
52+
return true;
53+
}
54+
if (p_name == StringName("fallback_image_quality")) {
55+
_document->set_fallback_image_quality(p_value);
56+
return true;
57+
}
4958
if (p_name == StringName("root_node_mode")) {
5059
_document->set_root_node_mode((GLTFDocument::RootNodeMode)(int64_t)p_value);
5160
return true;
@@ -66,6 +75,14 @@ bool EditorSceneExporterGLTFSettings::_get(const StringName &p_name, Variant &r_
6675
r_ret = _document->get_lossy_quality();
6776
return true;
6877
}
78+
if (p_name == StringName("fallback_image_format")) {
79+
r_ret = _document->get_fallback_image_format();
80+
return true;
81+
}
82+
if (p_name == StringName("fallback_image_quality")) {
83+
r_ret = _document->get_fallback_image_quality();
84+
return true;
85+
}
6986
if (p_name == StringName("root_node_mode")) {
7087
r_ret = _document->get_root_node_mode();
7188
return true;
@@ -76,10 +93,21 @@ bool EditorSceneExporterGLTFSettings::_get(const StringName &p_name, Variant &r_
7693
void EditorSceneExporterGLTFSettings::_get_property_list(List<PropertyInfo> *p_list) const {
7794
for (PropertyInfo prop : _property_list) {
7895
if (prop.name == "lossy_quality") {
79-
String image_format = get("image_format");
80-
bool is_image_format_lossy = image_format == "JPEG" || image_format.containsn("Lossy");
96+
const String image_format = get("image_format");
97+
const bool is_image_format_lossy = image_format == "JPEG" || image_format.containsn("Lossy");
8198
prop.usage = is_image_format_lossy ? PROPERTY_USAGE_DEFAULT : PROPERTY_USAGE_STORAGE;
8299
}
100+
if (prop.name == "fallback_image_format") {
101+
const String image_format = get("image_format");
102+
const bool is_image_format_extension = image_format != "None" && image_format != "PNG" && image_format != "JPEG";
103+
prop.usage = is_image_format_extension ? PROPERTY_USAGE_DEFAULT : PROPERTY_USAGE_STORAGE;
104+
}
105+
if (prop.name == "fallback_image_quality") {
106+
const String image_format = get("image_format");
107+
const bool is_image_format_extension = image_format != "None" && image_format != "PNG" && image_format != "JPEG";
108+
const String fallback_format = get("fallback_image_format");
109+
prop.usage = (is_image_format_extension && fallback_format != "None") ? PROPERTY_USAGE_DEFAULT : PROPERTY_USAGE_STORAGE;
110+
}
83111
p_list->push_back(prop);
84112
}
85113
}
@@ -117,7 +145,7 @@ String get_friendly_config_prefix(Ref<GLTFDocumentExtension> p_extension) {
117145
return config_prefix;
118146
}
119147
const String class_name = p_extension->get_class_name();
120-
config_prefix = class_name.trim_prefix("GLTFDocumentExtension").capitalize();
148+
config_prefix = class_name.trim_prefix("GLTFDocumentExtension").trim_suffix("GLTFDocumentExtension").capitalize();
121149
if (!config_prefix.is_empty()) {
122150
return config_prefix;
123151
}
@@ -166,6 +194,10 @@ void EditorSceneExporterGLTFSettings::generate_property_list(Ref<GLTFDocument> p
166194
_property_list.push_back(image_format_prop);
167195
PropertyInfo lossy_quality_prop = PropertyInfo(Variant::FLOAT, "lossy_quality", PROPERTY_HINT_RANGE, "0,1,0.01");
168196
_property_list.push_back(lossy_quality_prop);
197+
PropertyInfo fallback_image_format_prop = PropertyInfo(Variant::STRING, "fallback_image_format", PROPERTY_HINT_ENUM, "None,PNG,JPEG");
198+
_property_list.push_back(fallback_image_format_prop);
199+
PropertyInfo fallback_image_quality_prop = PropertyInfo(Variant::FLOAT, "fallback_image_quality", PROPERTY_HINT_RANGE, "0,1,0.01");
200+
_property_list.push_back(fallback_image_quality_prop);
169201
PropertyInfo root_node_mode_prop = PropertyInfo(Variant::INT, "root_node_mode", PROPERTY_HINT_ENUM, "Single Root,Keep Root,Multi Root");
170202
_property_list.push_back(root_node_mode_prop);
171203
}

modules/gltf/gltf_document.cpp

Lines changed: 123 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -3804,6 +3804,22 @@ float GLTFDocument::get_lossy_quality() const {
38043804
return _lossy_quality;
38053805
}
38063806

3807+
void GLTFDocument::set_fallback_image_format(const String &p_fallback_image_format) {
3808+
_fallback_image_format = p_fallback_image_format;
3809+
}
3810+
3811+
String GLTFDocument::get_fallback_image_format() const {
3812+
return _fallback_image_format;
3813+
}
3814+
3815+
void GLTFDocument::set_fallback_image_quality(float p_fallback_image_quality) {
3816+
_fallback_image_quality = p_fallback_image_quality;
3817+
}
3818+
3819+
float GLTFDocument::get_fallback_image_quality() const {
3820+
return _fallback_image_quality;
3821+
}
3822+
38073823
Error GLTFDocument::_serialize_images(Ref<GLTFState> p_state) {
38083824
Array images;
38093825
// Check if any extension wants to be the image saver.
@@ -3819,83 +3835,21 @@ Error GLTFDocument::_serialize_images(Ref<GLTFState> p_state) {
38193835
// Serialize every image in the state's images array.
38203836
for (int i = 0; i < p_state->images.size(); i++) {
38213837
Dictionary image_dict;
3822-
3823-
ERR_CONTINUE(p_state->images[i].is_null());
3824-
3825-
Ref<Image> image = p_state->images[i]->get_image();
3826-
ERR_CONTINUE(image.is_null());
3827-
if (image->is_compressed()) {
3828-
image->decompress();
3829-
ERR_FAIL_COND_V_MSG(image->is_compressed(), ERR_INVALID_DATA, "glTF: Image was compressed, but could not be decompressed.");
3830-
}
3831-
3832-
if (p_state->filename.to_lower().ends_with("gltf")) {
3833-
String img_name = p_state->images[i]->get_name();
3834-
if (img_name.is_empty()) {
3835-
img_name = itos(i).pad_zeros(3);
3836-
}
3837-
img_name = _gen_unique_name(p_state, img_name);
3838-
String relative_texture_dir = "textures";
3839-
String full_texture_dir = p_state->base_path.path_join(relative_texture_dir);
3840-
Ref<DirAccess> da = DirAccess::open(p_state->base_path);
3841-
ERR_FAIL_COND_V(da.is_null(), FAILED);
3842-
3843-
if (!da->dir_exists(full_texture_dir)) {
3844-
da->make_dir(full_texture_dir);
3845-
}
3846-
if (_image_save_extension.is_valid()) {
3847-
img_name = img_name + _image_save_extension->get_image_file_extension();
3848-
Error err = _image_save_extension->save_image_at_path(p_state, image, full_texture_dir.path_join(img_name), _image_format, _lossy_quality);
3849-
ERR_FAIL_COND_V_MSG(err != OK, err, "glTF: Failed to save image in '" + _image_format + "' format as a separate file.");
3850-
} else if (_image_format == "PNG") {
3851-
img_name = img_name + ".png";
3852-
image->save_png(full_texture_dir.path_join(img_name));
3853-
} else if (_image_format == "JPEG") {
3854-
img_name = img_name + ".jpg";
3855-
image->save_jpg(full_texture_dir.path_join(img_name), _lossy_quality);
3856-
} else {
3857-
ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "glTF: Unknown image format '" + _image_format + "'.");
3858-
}
3859-
image_dict["uri"] = relative_texture_dir.path_join(img_name).uri_encode();
3838+
if (p_state->images[i].is_null()) {
3839+
ERR_PRINT("glTF export: Image Texture2D is null.");
38603840
} else {
3861-
GLTFBufferViewIndex bvi;
3862-
3863-
Ref<GLTFBufferView> bv;
3864-
bv.instantiate();
3865-
3866-
const GLTFBufferIndex bi = 0;
3867-
bv->buffer = bi;
3868-
bv->byte_offset = p_state->buffers[bi].size();
3869-
ERR_FAIL_INDEX_V(bi, p_state->buffers.size(), ERR_PARAMETER_RANGE_ERROR);
3870-
3871-
Vector<uint8_t> buffer;
3872-
Ref<ImageTexture> img_tex = image;
3873-
if (img_tex.is_valid()) {
3874-
image = img_tex->get_image();
3875-
}
3876-
// Save in various image formats. Note that if the format is "None",
3877-
// the state's images will be empty, so this code will not be reached.
3878-
if (_image_save_extension.is_valid()) {
3879-
buffer = _image_save_extension->serialize_image_to_bytes(p_state, image, image_dict, _image_format, _lossy_quality);
3880-
} else if (_image_format == "PNG") {
3881-
buffer = image->save_png_to_buffer();
3882-
image_dict["mimeType"] = "image/png";
3883-
} else if (_image_format == "JPEG") {
3884-
buffer = image->save_jpg_to_buffer(_lossy_quality);
3885-
image_dict["mimeType"] = "image/jpeg";
3841+
Ref<Image> image = p_state->images[i]->get_image();
3842+
if (image.is_null()) {
3843+
ERR_PRINT("glTF export: Image's image is null.");
38863844
} else {
3887-
ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "glTF: Unknown image format '" + _image_format + "'.");
3845+
String image_name = p_state->images[i]->get_name();
3846+
if (image_name.is_empty()) {
3847+
image_name = itos(i).pad_zeros(3);
3848+
}
3849+
image_name = _gen_unique_name(p_state, image_name);
3850+
image->set_name(image_name);
3851+
image_dict = _serialize_image(p_state, image, _image_format, _lossy_quality, _image_save_extension);
38883852
}
3889-
ERR_FAIL_COND_V_MSG(buffer.is_empty(), ERR_INVALID_DATA, "glTF: Failed to save image in '" + _image_format + "' format.");
3890-
3891-
bv->byte_length = buffer.size();
3892-
p_state->buffers.write[bi].resize(p_state->buffers[bi].size() + bv->byte_length);
3893-
memcpy(&p_state->buffers.write[bi].write[bv->byte_offset], buffer.ptr(), buffer.size());
3894-
ERR_FAIL_COND_V(bv->byte_offset + bv->byte_length > p_state->buffers[bi].size(), ERR_FILE_CORRUPT);
3895-
3896-
p_state->buffer_views.push_back(bv);
3897-
bvi = p_state->buffer_views.size() - 1;
3898-
image_dict["bufferView"] = bvi;
38993853
}
39003854
images.push_back(image_dict);
39013855
}
@@ -3910,6 +3864,80 @@ Error GLTFDocument::_serialize_images(Ref<GLTFState> p_state) {
39103864
return OK;
39113865
}
39123866

3867+
Dictionary GLTFDocument::_serialize_image(Ref<GLTFState> p_state, Ref<Image> p_image, const String &p_image_format, float p_lossy_quality, Ref<GLTFDocumentExtension> p_image_save_extension) {
3868+
Dictionary image_dict;
3869+
if (p_image->is_compressed()) {
3870+
p_image->decompress();
3871+
ERR_FAIL_COND_V_MSG(p_image->is_compressed(), image_dict, "glTF: Image was compressed, but could not be decompressed.");
3872+
}
3873+
3874+
if (p_state->filename.to_lower().ends_with("gltf")) {
3875+
String relative_texture_dir = "textures";
3876+
String full_texture_dir = p_state->base_path.path_join(relative_texture_dir);
3877+
Ref<DirAccess> da = DirAccess::open(p_state->base_path);
3878+
ERR_FAIL_COND_V(da.is_null(), image_dict);
3879+
3880+
if (!da->dir_exists(full_texture_dir)) {
3881+
da->make_dir(full_texture_dir);
3882+
}
3883+
String image_file_name = p_image->get_name();
3884+
if (p_image_save_extension.is_valid()) {
3885+
image_file_name = image_file_name + p_image_save_extension->get_image_file_extension();
3886+
Error err = p_image_save_extension->save_image_at_path(p_state, p_image, full_texture_dir.path_join(image_file_name), p_image_format, p_lossy_quality);
3887+
ERR_FAIL_COND_V_MSG(err != OK, image_dict, "glTF: Failed to save image in '" + p_image_format + "' format as a separate file, error " + itos(err) + ".");
3888+
} else if (p_image_format == "PNG") {
3889+
image_file_name = image_file_name + ".png";
3890+
p_image->save_png(full_texture_dir.path_join(image_file_name));
3891+
} else if (p_image_format == "JPEG") {
3892+
image_file_name = image_file_name + ".jpg";
3893+
p_image->save_jpg(full_texture_dir.path_join(image_file_name), p_lossy_quality);
3894+
} else {
3895+
ERR_FAIL_V_MSG(image_dict, "glTF: Unknown image format '" + p_image_format + "'.");
3896+
}
3897+
image_dict["uri"] = relative_texture_dir.path_join(image_file_name).uri_encode();
3898+
} else {
3899+
GLTFBufferViewIndex bvi;
3900+
3901+
Ref<GLTFBufferView> bv;
3902+
bv.instantiate();
3903+
3904+
const GLTFBufferIndex bi = 0;
3905+
bv->buffer = bi;
3906+
ERR_FAIL_INDEX_V(bi, p_state->buffers.size(), image_dict);
3907+
bv->byte_offset = p_state->buffers[bi].size();
3908+
3909+
Vector<uint8_t> buffer;
3910+
Ref<ImageTexture> img_tex = p_image;
3911+
if (img_tex.is_valid()) {
3912+
p_image = img_tex->get_image();
3913+
}
3914+
// Save in various image formats. Note that if the format is "None",
3915+
// the state's images will be empty, so this code will not be reached.
3916+
if (_image_save_extension.is_valid()) {
3917+
buffer = _image_save_extension->serialize_image_to_bytes(p_state, p_image, image_dict, p_image_format, p_lossy_quality);
3918+
} else if (p_image_format == "PNG") {
3919+
buffer = p_image->save_png_to_buffer();
3920+
image_dict["mimeType"] = "image/png";
3921+
} else if (p_image_format == "JPEG") {
3922+
buffer = p_image->save_jpg_to_buffer(p_lossy_quality);
3923+
image_dict["mimeType"] = "image/jpeg";
3924+
} else {
3925+
ERR_FAIL_V_MSG(image_dict, "glTF: Unknown image format '" + p_image_format + "'.");
3926+
}
3927+
ERR_FAIL_COND_V_MSG(buffer.is_empty(), image_dict, "glTF: Failed to save image in '" + p_image_format + "' format.");
3928+
3929+
bv->byte_length = buffer.size();
3930+
p_state->buffers.write[bi].resize(p_state->buffers[bi].size() + bv->byte_length);
3931+
memcpy(&p_state->buffers.write[bi].write[bv->byte_offset], buffer.ptr(), buffer.size());
3932+
ERR_FAIL_COND_V(bv->byte_offset + bv->byte_length > p_state->buffers[bi].size(), image_dict);
3933+
3934+
p_state->buffer_views.push_back(bv);
3935+
bvi = p_state->buffer_views.size() - 1;
3936+
image_dict["bufferView"] = bvi;
3937+
}
3938+
return image_dict;
3939+
}
3940+
39133941
Ref<Image> GLTFDocument::_parse_image_bytes_into_image(Ref<GLTFState> p_state, const Vector<uint8_t> &p_bytes, const String &p_mime_type, int p_index, String &r_file_extension) {
39143942
Ref<Image> r_image;
39153943
r_image.instantiate();
@@ -4199,6 +4227,21 @@ Error GLTFDocument::_serialize_textures(Ref<GLTFState> p_state) {
41994227
if (_image_save_extension.is_valid()) {
42004228
Error err = _image_save_extension->serialize_texture_json(p_state, texture_dict, gltf_texture, _image_format);
42014229
ERR_FAIL_COND_V(err != OK, err);
4230+
// If a fallback image format was specified, serialize another image for it.
4231+
// Note: This must only be done after serializing other images to keep the indices of those consistent.
4232+
if (_fallback_image_format != "None" && p_state->json.has("images")) {
4233+
Array json_images = p_state->json["images"];
4234+
texture_dict["source"] = json_images.size();
4235+
Ref<Image> image = p_state->source_images[gltf_texture->get_src_image()];
4236+
String fallback_name = _gen_unique_name(p_state, image->get_name() + "_fallback");
4237+
image = image->duplicate();
4238+
image->set_name(fallback_name);
4239+
ERR_CONTINUE(image.is_null());
4240+
if (_fallback_image_format == "PNG") {
4241+
image->resize(image->get_width() * _fallback_image_quality, image->get_height() * _fallback_image_quality);
4242+
}
4243+
json_images.push_back(_serialize_image(p_state, image, _fallback_image_format, _fallback_image_quality, nullptr));
4244+
}
42024245
} else {
42034246
ERR_CONTINUE(gltf_texture->get_src_image() == -1);
42044247
texture_dict["source"] = gltf_texture->get_src_image();
@@ -8159,6 +8202,10 @@ void GLTFDocument::_bind_methods() {
81598202
ClassDB::bind_method(D_METHOD("get_image_format"), &GLTFDocument::get_image_format);
81608203
ClassDB::bind_method(D_METHOD("set_lossy_quality", "lossy_quality"), &GLTFDocument::set_lossy_quality);
81618204
ClassDB::bind_method(D_METHOD("get_lossy_quality"), &GLTFDocument::get_lossy_quality);
8205+
ClassDB::bind_method(D_METHOD("set_fallback_image_format", "fallback_image_format"), &GLTFDocument::set_fallback_image_format);
8206+
ClassDB::bind_method(D_METHOD("get_fallback_image_format"), &GLTFDocument::get_fallback_image_format);
8207+
ClassDB::bind_method(D_METHOD("set_fallback_image_quality", "fallback_image_quality"), &GLTFDocument::set_fallback_image_quality);
8208+
ClassDB::bind_method(D_METHOD("get_fallback_image_quality"), &GLTFDocument::get_fallback_image_quality);
81628209
ClassDB::bind_method(D_METHOD("set_root_node_mode", "root_node_mode"), &GLTFDocument::set_root_node_mode);
81638210
ClassDB::bind_method(D_METHOD("get_root_node_mode"), &GLTFDocument::get_root_node_mode);
81648211
ClassDB::bind_method(D_METHOD("append_from_file", "path", "state", "flags", "base_path"),
@@ -8176,6 +8223,8 @@ void GLTFDocument::_bind_methods() {
81768223

81778224
ADD_PROPERTY(PropertyInfo(Variant::STRING, "image_format"), "set_image_format", "get_image_format");
81788225
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lossy_quality"), "set_lossy_quality", "get_lossy_quality");
8226+
ADD_PROPERTY(PropertyInfo(Variant::STRING, "fallback_image_format"), "set_fallback_image_format", "get_fallback_image_format");
8227+
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "fallback_image_quality"), "set_fallback_image_quality", "get_fallback_image_quality");
81798228
ADD_PROPERTY(PropertyInfo(Variant::INT, "root_node_mode"), "set_root_node_mode", "get_root_node_mode");
81808229

81818230
ClassDB::bind_static_method("GLTFDocument", D_METHOD("import_object_model_property", "state", "json_pointer"), &GLTFDocument::import_object_model_property);

modules/gltf/gltf_document.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ class GLTFDocument : public Resource {
6565
int _naming_version = 1;
6666
String _image_format = "PNG";
6767
float _lossy_quality = 0.75f;
68+
String _fallback_image_format = "None";
69+
float _fallback_image_quality = 0.25f;
6870
Ref<GLTFDocumentExtension> _image_save_extension;
6971
RootNodeMode _root_node_mode = RootNodeMode::ROOT_NODE_MODE_SINGLE_ROOT;
7072

@@ -92,6 +94,10 @@ class GLTFDocument : public Resource {
9294
String get_image_format() const;
9395
void set_lossy_quality(float p_lossy_quality);
9496
float get_lossy_quality() const;
97+
void set_fallback_image_format(const String &p_fallback_image_format);
98+
String get_fallback_image_format() const;
99+
void set_fallback_image_quality(float p_fallback_image_quality);
100+
float get_fallback_image_quality() const;
95101
void set_root_node_mode(RootNodeMode p_root_node_mode);
96102
RootNodeMode get_root_node_mode() const;
97103
static String _gen_unique_name_static(HashSet<String> &r_unique_names, const String &p_name);
@@ -182,6 +188,7 @@ class GLTFDocument : public Resource {
182188
Error _serialize_textures(Ref<GLTFState> p_state);
183189
Error _serialize_texture_samplers(Ref<GLTFState> p_state);
184190
Error _serialize_images(Ref<GLTFState> p_state);
191+
Dictionary _serialize_image(Ref<GLTFState> p_state, Ref<Image> p_image, const String &p_image_format, float p_lossy_quality, Ref<GLTFDocumentExtension> p_image_save_extension);
185192
Error _serialize_lights(Ref<GLTFState> p_state);
186193
Ref<Image> _parse_image_bytes_into_image(Ref<GLTFState> p_state, const Vector<uint8_t> &p_bytes, const String &p_mime_type, int p_index, String &r_file_extension);
187194
void _parse_image_save_image(Ref<GLTFState> p_state, const Vector<uint8_t> &p_bytes, const String &p_resource_uri, const String &p_file_extension, int p_index, Ref<Image> p_image);

0 commit comments

Comments
 (0)