diff --git a/modules/gltf/doc_classes/GLTFDocument.xml b/modules/gltf/doc_classes/GLTFDocument.xml
index 9b760a997a9..9b84397c7ed 100644
--- a/modules/gltf/doc_classes/GLTFDocument.xml
+++ b/modules/gltf/doc_classes/GLTFDocument.xml
@@ -5,7 +5,7 @@
GLTFDocument supports reading data from a glTF file, buffer, or Godot scene. This data can then be written to the filesystem, buffer, or used to create a Godot scene.
- All of the data in a GLTF scene is stored in the [GLTFState] class. GLTFDocument processes state objects, but does not contain any scene data itself.
+ All of the data in a GLTF scene is stored in the [GLTFState] class. GLTFDocument processes state objects, but does not contain any scene data itself. GLTFDocument has member variables to store export configuration settings such as the image format, but is otherwise stateless. Multiple scenes can be processed with the same settings using the same GLTFDocument object and different [GLTFState] objects.
GLTFDocument can be extended with arbitrary functionality by extending the [GLTFDocumentExtension] class and registering it with GLTFDocument via [method register_gltf_document_extension]. This allows for custom data to be imported and exported.
@@ -87,4 +87,13 @@
+
+
+ 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.
+ 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.
+
+
+ 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.
+
+
diff --git a/modules/gltf/doc_classes/GLTFDocumentExtension.xml b/modules/gltf/doc_classes/GLTFDocumentExtension.xml
index bae980fb562..b54e0f8df04 100644
--- a/modules/gltf/doc_classes/GLTFDocumentExtension.xml
+++ b/modules/gltf/doc_classes/GLTFDocumentExtension.xml
@@ -28,7 +28,7 @@
- Part of the export process. This method is run after [method _export_preserialize] and before [method _export_post].
+ Part of the export process. This method is run after [method _get_saveable_image_formats] and before [method _export_post]. If this [GLTFDocumentExtension] is used for exporting images, this runs after [method _serialize_texture_json].
This method can be used to modify the final JSON of each node.
@@ -53,7 +53,7 @@
- Part of the export process. This method is run after [method _convert_scene_node] and before [method _export_node].
+ Part of the export process. This method is run after [method _convert_scene_node] and before [method _get_saveable_image_formats].
This method can be used to alter the state before performing serialization. It runs every time when generating a buffer with [method GLTFDocument.generate_buffer] or writing to the file system with [method GLTFDocument.write_to_filesystem].
@@ -73,6 +73,13 @@
Returns the file extension to use for saving image data into, for example, [code]".png"[/code]. If defined, when this extension is used to handle images, and the images are saved to a separate file, the image bytes will be copied to a file with this extension. If this is set, there should be a [ResourceImporter] class able to import the file. If not defined or empty, Godot will save the image into a PNG file.
+
+
+
+ Part of the export process. This method is run after [method _convert_scene_node] and before [method _export_node].
+ Returns an array of the image formats that can be saved/exported by this extension. This extension will only be selected as the image exporter if the [GLTFDocument]'s [member GLTFDocument.image_format] is in this array. If this [GLTFDocumentExtension] is selected as the image exporter, one of the [method _save_image_at_path] or [method _serialize_image_to_bytes] methods will run next, otherwise [method _export_node] will run next. If the format name contains [code]"Lossy"[/code], the lossy quality slider will be displayed.
+
+
@@ -148,5 +155,41 @@
Runs when parsing the texture JSON from the GLTF textures array. This can be used to set the source image index to use as the texture.
+
+
+
+
+
+
+
+
+ Part of the export process. This method is run after [method _get_saveable_image_formats] and before [method _serialize_texture_json].
+ This method is run when saving images separately from the GLTF file. When images are embedded, [method _serialize_image_to_bytes] runs instead. Note that these methods only run when this [GLTFDocumentExtension] is selected as the image exporter.
+
+
+
+
+
+
+
+
+
+
+ Part of the export process. This method is run after [method _get_saveable_image_formats] and before [method _serialize_texture_json].
+ This method is run when embedding images in the GLTF file. When images are saved separately, [method _save_image_at_path] runs instead. Note that these methods only run when this [GLTFDocumentExtension] is selected as the image exporter.
+ This method must set the image MIME type in the [param image_dict] with the [code]"mimeType"[/code] key. For example, for a PNG image, it would be set to [code]"image/png"[/code]. The return value must be a [PackedByteArray] containing the image data.
+
+
+
+
+
+
+
+
+
+ Part of the export process. This method is run after [method _save_image_at_path] or [method _serialize_image_to_bytes], and before [method _export_node]. Note that this method only runs when this [GLTFDocumentExtension] is selected as the image exporter.
+ This method can be used to set up the extensions for the texture JSON by editing [param texture_json]. The extension must also be added as used extension with [method GLTFState.add_used_extension], be sure to set [code]required[/code] to [code]true[/code] if you are not providing a fallback.
+
+
diff --git a/modules/gltf/extensions/gltf_document_extension.cpp b/modules/gltf/extensions/gltf_document_extension.cpp
index 11718ba78a0..e16e3666920 100644
--- a/modules/gltf/extensions/gltf_document_extension.cpp
+++ b/modules/gltf/extensions/gltf_document_extension.cpp
@@ -46,6 +46,10 @@ void GLTFDocumentExtension::_bind_methods() {
GDVIRTUAL_BIND(_export_preflight, "state", "root");
GDVIRTUAL_BIND(_convert_scene_node, "state", "gltf_node", "scene_node");
GDVIRTUAL_BIND(_export_preserialize, "state");
+ GDVIRTUAL_BIND(_get_saveable_image_formats);
+ GDVIRTUAL_BIND(_serialize_image_to_bytes, "state", "image", "image_dict", "image_format", "lossy_quality");
+ GDVIRTUAL_BIND(_save_image_at_path, "state", "image", "file_path", "image_format", "lossy_quality");
+ GDVIRTUAL_BIND(_serialize_texture_json, "state", "texture_json", "gltf_texture", "image_format");
GDVIRTUAL_BIND(_export_node, "state", "gltf_node", "json", "node");
GDVIRTUAL_BIND(_export_post, "state");
}
@@ -149,6 +153,36 @@ Error GLTFDocumentExtension::export_preserialize(Ref p_state) {
return err;
}
+Vector GLTFDocumentExtension::get_saveable_image_formats() {
+ Vector ret;
+ GDVIRTUAL_CALL(_get_saveable_image_formats, ret);
+ return ret;
+}
+
+PackedByteArray GLTFDocumentExtension::serialize_image_to_bytes(Ref p_state, Ref p_image, Dictionary p_image_dict, const String &p_image_format, float p_lossy_quality) {
+ PackedByteArray ret;
+ ERR_FAIL_NULL_V(p_state, ret);
+ ERR_FAIL_NULL_V(p_image, ret);
+ GDVIRTUAL_CALL(_serialize_image_to_bytes, p_state, p_image, p_image_dict, p_image_format, p_lossy_quality, ret);
+ return ret;
+}
+
+Error GLTFDocumentExtension::save_image_at_path(Ref p_state, Ref p_image, const String &p_file_path, const String &p_image_format, float p_lossy_quality) {
+ ERR_FAIL_NULL_V(p_state, ERR_INVALID_PARAMETER);
+ ERR_FAIL_NULL_V(p_image, ERR_INVALID_PARAMETER);
+ Error ret = OK;
+ GDVIRTUAL_CALL(_save_image_at_path, p_state, p_image, p_file_path, p_image_format, p_lossy_quality, ret);
+ return ret;
+}
+
+Error GLTFDocumentExtension::serialize_texture_json(Ref p_state, Dictionary p_texture_json, Ref p_gltf_texture, const String &p_image_format) {
+ ERR_FAIL_NULL_V(p_state, ERR_INVALID_PARAMETER);
+ ERR_FAIL_NULL_V(p_gltf_texture, ERR_INVALID_PARAMETER);
+ Error err = OK;
+ GDVIRTUAL_CALL(_serialize_texture_json, p_state, p_texture_json, p_gltf_texture, p_image_format, err);
+ return err;
+}
+
Error GLTFDocumentExtension::export_node(Ref p_state, Ref p_gltf_node, Dictionary &r_dict, Node *p_node) {
ERR_FAIL_NULL_V(p_state, ERR_INVALID_PARAMETER);
ERR_FAIL_NULL_V(p_gltf_node, ERR_INVALID_PARAMETER);
diff --git a/modules/gltf/extensions/gltf_document_extension.h b/modules/gltf/extensions/gltf_document_extension.h
index 0a631bb6c54..cd1e7695565 100644
--- a/modules/gltf/extensions/gltf_document_extension.h
+++ b/modules/gltf/extensions/gltf_document_extension.h
@@ -55,6 +55,10 @@ public:
virtual Error export_preflight(Ref p_state, Node *p_root);
virtual void convert_scene_node(Ref p_state, Ref p_gltf_node, Node *p_scene_node);
virtual Error export_preserialize(Ref p_state);
+ virtual Vector get_saveable_image_formats();
+ virtual PackedByteArray serialize_image_to_bytes(Ref p_state, Ref p_image, Dictionary p_image_dict, const String &p_image_format, float p_lossy_quality);
+ virtual Error save_image_at_path(Ref p_state, Ref p_image, const String &p_file_path, const String &p_image_format, float p_lossy_quality);
+ virtual Error serialize_texture_json(Ref p_state, Dictionary p_texture_json, Ref p_gltf_texture, const String &p_image_format);
virtual Error export_node(Ref p_state, Ref p_gltf_node, Dictionary &r_json, Node *p_node);
virtual Error export_post(Ref p_state);
@@ -73,6 +77,10 @@ public:
GDVIRTUAL2R(Error, _export_preflight, Ref, Node *);
GDVIRTUAL3(_convert_scene_node, Ref, Ref, Node *);
GDVIRTUAL1R(Error, _export_preserialize, Ref);
+ GDVIRTUAL0R(Vector, _get_saveable_image_formats);
+ GDVIRTUAL5R(PackedByteArray, _serialize_image_to_bytes, Ref, Ref, Dictionary, String, float);
+ GDVIRTUAL5R(Error, _save_image_at_path, Ref, Ref, String, String, float);
+ GDVIRTUAL4R(Error, _serialize_texture_json, Ref, Dictionary, Ref, String);
GDVIRTUAL4R(Error, _export_node, Ref, Ref, Dictionary, Node *);
GDVIRTUAL1R(Error, _export_post, Ref);
};
diff --git a/modules/gltf/extensions/gltf_document_extension_texture_webp.cpp b/modules/gltf/extensions/gltf_document_extension_texture_webp.cpp
index 73c869be3b5..f8bd6d57cf2 100644
--- a/modules/gltf/extensions/gltf_document_extension_texture_webp.cpp
+++ b/modules/gltf/extensions/gltf_document_extension_texture_webp.cpp
@@ -68,3 +68,42 @@ Error GLTFDocumentExtensionTextureWebP::parse_texture_json(Ref p_stat
r_gltf_texture->set_src_image(texture_webp["source"]);
return OK;
}
+
+Vector GLTFDocumentExtensionTextureWebP::get_saveable_image_formats() {
+ Vector ret;
+ ret.push_back("Lossless WebP");
+ ret.push_back("Lossy WebP");
+ return ret;
+}
+
+PackedByteArray GLTFDocumentExtensionTextureWebP::serialize_image_to_bytes(Ref p_state, Ref p_image, Dictionary p_image_dict, const String &p_image_format, float p_lossy_quality) {
+ if (p_image_format == "Lossless WebP") {
+ p_image_dict["mimeType"] = "image/webp";
+ return p_image->save_webp_to_buffer(false);
+ } else if (p_image_format == "Lossy WebP") {
+ p_image_dict["mimeType"] = "image/webp";
+ return p_image->save_webp_to_buffer(true, p_lossy_quality);
+ }
+ ERR_FAIL_V(PackedByteArray());
+}
+
+Error GLTFDocumentExtensionTextureWebP::save_image_at_path(Ref p_state, Ref p_image, const String &p_file_path, const String &p_image_format, float p_lossy_quality) {
+ if (p_image_format == "Lossless WebP") {
+ p_image->save_webp(p_file_path, false);
+ return OK;
+ } else if (p_image_format == "Lossy WebP") {
+ p_image->save_webp(p_file_path, true, p_lossy_quality);
+ return OK;
+ }
+ return ERR_INVALID_PARAMETER;
+}
+
+Error GLTFDocumentExtensionTextureWebP::serialize_texture_json(Ref p_state, Dictionary p_texture_json, Ref p_gltf_texture, const String &p_image_format) {
+ Dictionary ext_texture_webp;
+ ext_texture_webp["source"] = p_gltf_texture->get_src_image();
+ Dictionary texture_extensions;
+ texture_extensions["EXT_texture_webp"] = ext_texture_webp;
+ p_texture_json["extensions"] = texture_extensions;
+ p_state->add_used_extension("EXT_texture_webp", true);
+ return OK;
+}
diff --git a/modules/gltf/extensions/gltf_document_extension_texture_webp.h b/modules/gltf/extensions/gltf_document_extension_texture_webp.h
index d2654aae8cf..2113bd4768f 100644
--- a/modules/gltf/extensions/gltf_document_extension_texture_webp.h
+++ b/modules/gltf/extensions/gltf_document_extension_texture_webp.h
@@ -43,6 +43,11 @@ public:
Error parse_image_data(Ref p_state, const PackedByteArray &p_image_data, const String &p_mime_type, Ref r_image) override;
String get_image_file_extension() override;
Error parse_texture_json(Ref p_state, const Dictionary &p_texture_json, Ref r_gltf_texture) override;
+ // Export process.
+ Vector get_saveable_image_formats() override;
+ PackedByteArray serialize_image_to_bytes(Ref p_state, Ref p_image, Dictionary p_image_dict, const String &p_image_format, float p_lossy_quality) override;
+ Error save_image_at_path(Ref p_state, Ref p_image, const String &p_full_path, const String &p_image_format, float p_lossy_quality) override;
+ Error serialize_texture_json(Ref p_state, Dictionary p_texture_json, Ref p_gltf_texture, const String &p_image_format) override;
};
#endif // GLTF_DOCUMENT_EXTENSION_TEXTURE_WEBP_H
diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp
index 984052d2cad..3076eb55e69 100644
--- a/modules/gltf/gltf_document.cpp
+++ b/modules/gltf/gltf_document.cpp
@@ -42,7 +42,6 @@
#include "core/io/stream_peer.h"
#include "core/math/disjoint_set.h"
#include "core/version.h"
-#include "drivers/png/png_driver_common.h"
#include "scene/3d/bone_attachment_3d.h"
#include "scene/3d/camera_3d.h"
#include "scene/3d/importer_mesh_instance_3d.h"
@@ -3001,8 +3000,35 @@ Error GLTFDocument::_parse_meshes(Ref p_state) {
return OK;
}
+void GLTFDocument::set_image_format(const String &p_image_format) {
+ _image_format = p_image_format;
+}
+
+String GLTFDocument::get_image_format() const {
+ return _image_format;
+}
+
+void GLTFDocument::set_lossy_quality(float p_lossy_quality) {
+ _lossy_quality = p_lossy_quality;
+}
+
+float GLTFDocument::get_lossy_quality() const {
+ return _lossy_quality;
+}
+
Error GLTFDocument::_serialize_images(Ref p_state) {
Array images;
+ // Check if any extension wants to be the image saver.
+ _image_save_extension = Ref();
+ for (Ref ext : document_extensions) {
+ ERR_CONTINUE(ext.is_null());
+ Vector image_formats = ext->get_saveable_image_formats();
+ if (image_formats.has(_image_format)) {
+ _image_save_extension = ext;
+ break;
+ }
+ }
+ // Serialize every image in the state's images array.
for (int i = 0; i < p_state->images.size(); i++) {
Dictionary image_dict;
@@ -3010,6 +3036,10 @@ Error GLTFDocument::_serialize_images(Ref p_state) {
Ref image = p_state->images[i]->get_image();
ERR_CONTINUE(image.is_null());
+ if (image->is_compressed()) {
+ image->decompress();
+ ERR_FAIL_COND_V_MSG(image->is_compressed(), ERR_INVALID_DATA, "GLTF: Image was compressed, but could not be decompressed.");
+ }
if (p_state->filename.to_lower().ends_with("gltf")) {
String img_name = p_state->images[i]->get_name();
@@ -3017,14 +3047,26 @@ Error GLTFDocument::_serialize_images(Ref p_state) {
img_name = itos(i);
}
img_name = _gen_unique_name(p_state, img_name);
- img_name = img_name.pad_zeros(3) + ".png";
+ img_name = img_name.pad_zeros(3);
String relative_texture_dir = "textures";
String full_texture_dir = p_state->base_path.path_join(relative_texture_dir);
Ref da = DirAccess::open(p_state->base_path);
if (!da->dir_exists(full_texture_dir)) {
da->make_dir(full_texture_dir);
}
- image->save_png(full_texture_dir.path_join(img_name));
+ if (_image_save_extension.is_valid()) {
+ img_name = img_name + _image_save_extension->get_image_file_extension();
+ Error err = _image_save_extension->save_image_at_path(p_state, image, full_texture_dir.path_join(img_name), _image_format, _lossy_quality);
+ ERR_FAIL_COND_V_MSG(err != OK, err, "GLTF: Failed to save image in '" + _image_format + "' format as a separate file.");
+ } else if (_image_format == "PNG") {
+ img_name = img_name + ".png";
+ image->save_png(full_texture_dir.path_join(img_name));
+ } else if (_image_format == "JPEG") {
+ img_name = img_name + ".jpg";
+ image->save_jpg(full_texture_dir.path_join(img_name), _lossy_quality);
+ } else {
+ ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "GLTF: Unknown image format '" + _image_format + "'.");
+ }
image_dict["uri"] = relative_texture_dir.path_join(img_name).uri_encode();
} else {
GLTFBufferViewIndex bvi;
@@ -3042,8 +3084,20 @@ Error GLTFDocument::_serialize_images(Ref p_state) {
if (img_tex.is_valid()) {
image = img_tex->get_image();
}
- Error err = PNGDriverCommon::image_to_png(image, buffer);
- ERR_FAIL_COND_V_MSG(err, err, "Can't convert image to PNG.");
+ // Save in various image formats. Note that if the format is "None",
+ // the state's images will be empty, so this code will not be reached.
+ if (_image_save_extension.is_valid()) {
+ buffer = _image_save_extension->serialize_image_to_bytes(p_state, image, image_dict, _image_format, _lossy_quality);
+ } else if (_image_format == "PNG") {
+ buffer = image->save_png_to_buffer();
+ image_dict["mimeType"] = "image/png";
+ } else if (_image_format == "JPEG") {
+ buffer = image->save_jpg_to_buffer(_lossy_quality);
+ image_dict["mimeType"] = "image/jpeg";
+ } else {
+ ERR_FAIL_V_MSG(ERR_UNAVAILABLE, "GLTF: Unknown image format '" + _image_format + "'.");
+ }
+ ERR_FAIL_COND_V_MSG(buffer.is_empty(), ERR_INVALID_DATA, "GLTF: Failed to save image in '" + _image_format + "' format.");
bv->byte_length = buffer.size();
p_state->buffers.write[bi].resize(p_state->buffers[bi].size() + bv->byte_length);
@@ -3053,7 +3107,6 @@ Error GLTFDocument::_serialize_images(Ref p_state) {
p_state->buffer_views.push_back(bv);
bvi = p_state->buffer_views.size() - 1;
image_dict["bufferView"] = bvi;
- image_dict["mimeType"] = "image/png";
}
images.push_back(image_dict);
}
@@ -3332,9 +3385,13 @@ Error GLTFDocument::_serialize_textures(Ref p_state) {
for (int32_t i = 0; i < p_state->textures.size(); i++) {
Dictionary texture_dict;
Ref gltf_texture = p_state->textures[i];
- ERR_CONTINUE(gltf_texture->get_src_image() == -1);
- texture_dict["source"] = gltf_texture->get_src_image();
-
+ if (_image_save_extension.is_valid()) {
+ Error err = _image_save_extension->serialize_texture_json(p_state, texture_dict, gltf_texture, _image_format);
+ ERR_FAIL_COND_V(err != OK, err);
+ } else {
+ ERR_CONTINUE(gltf_texture->get_src_image() == -1);
+ texture_dict["source"] = gltf_texture->get_src_image();
+ }
GLTFTextureSamplerIndex sampler_index = gltf_texture->get_sampler();
if (sampler_index != -1) {
texture_dict["sampler"] = sampler_index;
@@ -3543,7 +3600,7 @@ Error GLTFDocument::_serialize_materials(Ref p_state) {
arr.push_back(c.a);
mr["baseColorFactor"] = arr;
}
- {
+ if (_image_format != "None") {
Dictionary bct;
Ref albedo_texture = base_material->get_texture(BaseMaterial3D::TEXTURE_ALBEDO);
GLTFTextureIndex gltf_texture_index = -1;
@@ -7158,6 +7215,14 @@ void GLTFDocument::_bind_methods() {
ClassDB::bind_method(D_METHOD("write_to_filesystem", "state", "path"),
&GLTFDocument::write_to_filesystem);
+ ClassDB::bind_method(D_METHOD("set_image_format", "image_format"), &GLTFDocument::set_image_format);
+ ClassDB::bind_method(D_METHOD("get_image_format"), &GLTFDocument::get_image_format);
+ ClassDB::bind_method(D_METHOD("set_lossy_quality", "lossy_quality"), &GLTFDocument::set_lossy_quality);
+ ClassDB::bind_method(D_METHOD("get_lossy_quality"), &GLTFDocument::get_lossy_quality);
+
+ ADD_PROPERTY(PropertyInfo(Variant::STRING, "image_format"), "set_image_format", "get_image_format");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lossy_quality"), "set_lossy_quality", "get_lossy_quality");
+
ClassDB::bind_static_method("GLTFDocument", D_METHOD("register_gltf_document_extension", "extension", "first_priority"),
&GLTFDocument::register_gltf_document_extension, DEFVAL(false));
ClassDB::bind_static_method("GLTFDocument", D_METHOD("unregister_gltf_document_extension", "extension"),
diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h
index f2e36a04576..79d8e56e572 100644
--- a/modules/gltf/gltf_document.h
+++ b/modules/gltf/gltf_document.h
@@ -42,6 +42,9 @@ class GLTFDocument : public Resource {
private:
const float BAKE_FPS = 30.0f;
+ String _image_format = "PNG";
+ float _lossy_quality = 0.75f;
+ Ref _image_save_extension;
public:
const int32_t JOINT_GROUP_SIZE = 4;
@@ -77,6 +80,11 @@ public:
static void unregister_gltf_document_extension(Ref p_extension);
static void unregister_all_gltf_document_extensions();
+ void set_image_format(const String &p_image_format);
+ String get_image_format() const;
+ void set_lossy_quality(float p_lossy_quality);
+ float get_lossy_quality() const;
+
private:
void _build_parent_hierachy(Ref p_state);
double _filter_number(double p_float);