GLTF: Allow specifying export image format including from extensions

This commit is contained in:
Aaron Franke 2023-07-16 01:03:49 -05:00
parent 787259441a
commit 2723f781dd
No known key found for this signature in database
GPG Key ID: 40A1750B977E56BF
8 changed files with 224 additions and 13 deletions

View File

@ -5,7 +5,7 @@
</brief_description>
<description>
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.
</description>
<tutorials>
@ -87,4 +87,13 @@
</description>
</method>
</methods>
<members>
<member name="image_format" type="String" setter="set_image_format" getter="get_image_format" default="&quot;PNG&quot;">
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.
</member>
<member name="lossy_quality" type="float" setter="set_lossy_quality" getter="get_lossy_quality" default="0.75">
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.
</member>
</members>
</class>

View File

@ -28,7 +28,7 @@
<param index="2" name="json" type="Dictionary" />
<param index="3" name="node" type="Node" />
<description>
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.
</description>
</method>
@ -53,7 +53,7 @@
<return type="int" enum="Error" />
<param index="0" name="state" type="GLTFState" />
<description>
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].
</description>
</method>
@ -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.
</description>
</method>
<method name="_get_saveable_image_formats" qualifiers="virtual">
<return type="PackedStringArray" />
<description>
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.
</description>
</method>
<method name="_get_supported_extensions" qualifiers="virtual">
<return type="PackedStringArray" />
<description>
@ -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.
</description>
</method>
<method name="_save_image_at_path" qualifiers="virtual">
<return type="int" enum="Error" />
<param index="0" name="state" type="GLTFState" />
<param index="1" name="image" type="Image" />
<param index="2" name="file_path" type="String" />
<param index="3" name="image_format" type="String" />
<param index="4" name="lossy_quality" type="float" />
<description>
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.
</description>
</method>
<method name="_serialize_image_to_bytes" qualifiers="virtual">
<return type="PackedByteArray" />
<param index="0" name="state" type="GLTFState" />
<param index="1" name="image" type="Image" />
<param index="2" name="image_dict" type="Dictionary" />
<param index="3" name="image_format" type="String" />
<param index="4" name="lossy_quality" type="float" />
<description>
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.
</description>
</method>
<method name="_serialize_texture_json" qualifiers="virtual">
<return type="int" enum="Error" />
<param index="0" name="state" type="GLTFState" />
<param index="1" name="texture_json" type="Dictionary" />
<param index="2" name="gltf_texture" type="GLTFTexture" />
<param index="3" name="image_format" type="String" />
<description>
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.
</description>
</method>
</methods>
</class>

View File

@ -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<GLTFState> p_state) {
return err;
}
Vector<String> GLTFDocumentExtension::get_saveable_image_formats() {
Vector<String> ret;
GDVIRTUAL_CALL(_get_saveable_image_formats, ret);
return ret;
}
PackedByteArray GLTFDocumentExtension::serialize_image_to_bytes(Ref<GLTFState> p_state, Ref<Image> 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<GLTFState> p_state, Ref<Image> 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<GLTFState> p_state, Dictionary p_texture_json, Ref<GLTFTexture> 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<GLTFState> p_state, Ref<GLTFNode> 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);

View File

@ -55,6 +55,10 @@ public:
virtual Error export_preflight(Ref<GLTFState> p_state, Node *p_root);
virtual void convert_scene_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Node *p_scene_node);
virtual Error export_preserialize(Ref<GLTFState> p_state);
virtual Vector<String> get_saveable_image_formats();
virtual PackedByteArray serialize_image_to_bytes(Ref<GLTFState> p_state, Ref<Image> p_image, Dictionary p_image_dict, const String &p_image_format, float p_lossy_quality);
virtual Error save_image_at_path(Ref<GLTFState> p_state, Ref<Image> p_image, const String &p_file_path, const String &p_image_format, float p_lossy_quality);
virtual Error serialize_texture_json(Ref<GLTFState> p_state, Dictionary p_texture_json, Ref<GLTFTexture> p_gltf_texture, const String &p_image_format);
virtual Error export_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Dictionary &r_json, Node *p_node);
virtual Error export_post(Ref<GLTFState> p_state);
@ -73,6 +77,10 @@ public:
GDVIRTUAL2R(Error, _export_preflight, Ref<GLTFState>, Node *);
GDVIRTUAL3(_convert_scene_node, Ref<GLTFState>, Ref<GLTFNode>, Node *);
GDVIRTUAL1R(Error, _export_preserialize, Ref<GLTFState>);
GDVIRTUAL0R(Vector<String>, _get_saveable_image_formats);
GDVIRTUAL5R(PackedByteArray, _serialize_image_to_bytes, Ref<GLTFState>, Ref<Image>, Dictionary, String, float);
GDVIRTUAL5R(Error, _save_image_at_path, Ref<GLTFState>, Ref<Image>, String, String, float);
GDVIRTUAL4R(Error, _serialize_texture_json, Ref<GLTFState>, Dictionary, Ref<GLTFTexture>, String);
GDVIRTUAL4R(Error, _export_node, Ref<GLTFState>, Ref<GLTFNode>, Dictionary, Node *);
GDVIRTUAL1R(Error, _export_post, Ref<GLTFState>);
};

View File

@ -68,3 +68,42 @@ Error GLTFDocumentExtensionTextureWebP::parse_texture_json(Ref<GLTFState> p_stat
r_gltf_texture->set_src_image(texture_webp["source"]);
return OK;
}
Vector<String> GLTFDocumentExtensionTextureWebP::get_saveable_image_formats() {
Vector<String> ret;
ret.push_back("Lossless WebP");
ret.push_back("Lossy WebP");
return ret;
}
PackedByteArray GLTFDocumentExtensionTextureWebP::serialize_image_to_bytes(Ref<GLTFState> p_state, Ref<Image> 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<GLTFState> p_state, Ref<Image> 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<GLTFState> p_state, Dictionary p_texture_json, Ref<GLTFTexture> 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;
}

View File

@ -43,6 +43,11 @@ public:
Error parse_image_data(Ref<GLTFState> p_state, const PackedByteArray &p_image_data, const String &p_mime_type, Ref<Image> r_image) override;
String get_image_file_extension() override;
Error parse_texture_json(Ref<GLTFState> p_state, const Dictionary &p_texture_json, Ref<GLTFTexture> r_gltf_texture) override;
// Export process.
Vector<String> get_saveable_image_formats() override;
PackedByteArray serialize_image_to_bytes(Ref<GLTFState> p_state, Ref<Image> p_image, Dictionary p_image_dict, const String &p_image_format, float p_lossy_quality) override;
Error save_image_at_path(Ref<GLTFState> p_state, Ref<Image> p_image, const String &p_full_path, const String &p_image_format, float p_lossy_quality) override;
Error serialize_texture_json(Ref<GLTFState> p_state, Dictionary p_texture_json, Ref<GLTFTexture> p_gltf_texture, const String &p_image_format) override;
};
#endif // GLTF_DOCUMENT_EXTENSION_TEXTURE_WEBP_H

View File

@ -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<GLTFState> 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<GLTFState> p_state) {
Array images;
// Check if any extension wants to be the image saver.
_image_save_extension = Ref<GLTFDocumentExtension>();
for (Ref<GLTFDocumentExtension> ext : document_extensions) {
ERR_CONTINUE(ext.is_null());
Vector<String> 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<GLTFState> p_state) {
Ref<Image> 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<GLTFState> 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<DirAccess> 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<GLTFState> 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<GLTFState> 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<GLTFState> p_state) {
for (int32_t i = 0; i < p_state->textures.size(); i++) {
Dictionary texture_dict;
Ref<GLTFTexture> 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<GLTFState> p_state) {
arr.push_back(c.a);
mr["baseColorFactor"] = arr;
}
{
if (_image_format != "None") {
Dictionary bct;
Ref<Texture2D> 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"),

View File

@ -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<GLTFDocumentExtension> _image_save_extension;
public:
const int32_t JOINT_GROUP_SIZE = 4;
@ -77,6 +80,11 @@ public:
static void unregister_gltf_document_extension(Ref<GLTFDocumentExtension> 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<GLTFState> p_state);
double _filter_number(double p_float);