summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/classes/Line2D.xml13
-rw-r--r--modules/gltf/doc_classes/GLTFDocument.xml11
-rw-r--r--modules/gltf/doc_classes/GLTFDocumentExtension.xml55
-rw-r--r--modules/gltf/extensions/gltf_document_extension.cpp34
-rw-r--r--modules/gltf/extensions/gltf_document_extension.h10
-rw-r--r--modules/gltf/extensions/gltf_document_extension_texture_webp.cpp39
-rw-r--r--modules/gltf/extensions/gltf_document_extension_texture_webp.h5
-rw-r--r--modules/gltf/gltf_document.cpp118
-rw-r--r--modules/gltf/gltf_document.h11
-rw-r--r--scene/2d/line_2d.cpp63
-rw-r--r--scene/2d/line_2d.h4
-rw-r--r--scene/2d/line_builder.cpp344
-rw-r--r--scene/2d/line_builder.h2
13 files changed, 469 insertions, 240 deletions
diff --git a/doc/classes/Line2D.xml b/doc/classes/Line2D.xml
index 7e02a520a0..4c444721f4 100644
--- a/doc/classes/Line2D.xml
+++ b/doc/classes/Line2D.xml
@@ -63,16 +63,21 @@
[b]Note:[/b] [Line2D] is not accelerated by batching when being anti-aliased.
</member>
<member name="begin_cap_mode" type="int" setter="set_begin_cap_mode" getter="get_begin_cap_mode" enum="Line2D.LineCapMode" default="0">
- The style of the beginning of the polyline. Use [enum LineCapMode] constants.
+ The style of the beginning of the polyline, if [member closed] is [code]false[/code]. Use [enum LineCapMode] constants.
+ </member>
+ <member name="closed" type="bool" setter="set_closed" getter="is_closed" default="false">
+ If [code]true[/code] and the polyline has more than 2 points, the last point and the first one will be connected by a segment.
+ [b]Note:[/b] The shape of the closing segment is not guaranteed to be seamless if a [member width_curve] is provided.
+ [b]Note:[/b] The joint between the closing segment and the first segment is drawn first and it samples the [member gradient] and the [member width_curve] at the beginning. This is an implementation detail that might change in a future version.
</member>
<member name="default_color" type="Color" setter="set_default_color" getter="get_default_color" default="Color(1, 1, 1, 1)">
The color of the polyline. Will not be used if a gradient is set.
</member>
<member name="end_cap_mode" type="int" setter="set_end_cap_mode" getter="get_end_cap_mode" enum="Line2D.LineCapMode" default="0">
- The style of the end of the polyline. Use [enum LineCapMode] constants.
+ The style of the end of the polyline, if [member closed] is [code]false[/code]. Use [enum LineCapMode] constants.
</member>
<member name="gradient" type="Gradient" setter="set_gradient" getter="get_gradient">
- The gradient is drawn through the whole line from start to finish. The default color will not be used if a gradient is set.
+ The gradient is drawn through the whole line from start to finish. The [member default_color] will not be used if this property is set.
</member>
<member name="joint_mode" type="int" setter="set_joint_mode" getter="get_joint_mode" enum="Line2D.LineJointMode" default="0">
The style of the connections between segments of the polyline. Use [enum LineJointMode] constants.
@@ -93,7 +98,7 @@
The style to render the [member texture] of the polyline. Use [enum LineTextureMode] constants.
</member>
<member name="width" type="float" setter="set_width" getter="get_width" default="10.0">
- The polyline's width
+ The polyline's width.
</member>
<member name="width_curve" type="Curve" setter="set_curve" getter="get_curve">
The polyline's width curve. The width of the polyline over its length will be equivalent to the value of the width curve over its domain.
diff --git a/modules/gltf/doc_classes/GLTFDocument.xml b/modules/gltf/doc_classes/GLTFDocument.xml
index 9b760a997a..9b84397c7e 100644
--- a/modules/gltf/doc_classes/GLTFDocument.xml
+++ b/modules/gltf/doc_classes/GLTFDocument.xml
@@ -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>
diff --git a/modules/gltf/doc_classes/GLTFDocumentExtension.xml b/modules/gltf/doc_classes/GLTFDocumentExtension.xml
index bae980fb56..dbfbf63da6 100644
--- a/modules/gltf/doc_classes/GLTFDocumentExtension.xml
+++ b/modules/gltf/doc_classes/GLTFDocumentExtension.xml
@@ -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>
@@ -63,7 +63,7 @@
<param index="1" name="gltf_node" type="GLTFNode" />
<param index="2" name="scene_parent" type="Node" />
<description>
- Part of the import process. This method is run after [method _parse_node_extensions] and before [method _import_post_parse].
+ Part of the import process. This method is run after [method _import_post_parse] and before [method _import_node].
Runs when generating a Godot scene node from a GLTFNode. The returned node will be added to the scene tree. Multiple nodes can be generated in this step if they are added as a child of the returned node.
</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>
@@ -87,7 +94,7 @@
<param index="2" name="json" type="Dictionary" />
<param index="3" name="node" type="Node" />
<description>
- Part of the import process. This method is run after [method _import_post_parse] and before [method _import_post].
+ Part of the import process. This method is run after [method _generate_scene_node] and before [method _import_post].
This method can be used to make modifications to each of the generated Godot scene nodes.
</description>
</method>
@@ -104,7 +111,7 @@
<return type="int" enum="Error" />
<param index="0" name="state" type="GLTFState" />
<description>
- Part of the import process. This method is run after [method _generate_scene_node] and before [method _import_node].
+ Part of the import process. This method is run after [method _parse_node_extensions] and before [method _generate_scene_node].
This method can be used to modify any of the data imported so far, including any scene nodes, before running the final per-node import step.
</description>
</method>
@@ -134,7 +141,7 @@
<param index="1" name="gltf_node" type="GLTFNode" />
<param index="2" name="extensions" type="Dictionary" />
<description>
- Part of the import process. This method is run after [method _get_supported_extensions] and before [method _generate_scene_node].
+ Part of the import process. This method is run after [method _get_supported_extensions] and before [method _import_post_parse].
Runs when parsing the node extensions of a GLTFNode. This method can be used to process the extension JSON data into a format that can be used by [method _generate_scene_node]. The return value should be a member of the [enum Error] enum.
</description>
</method>
@@ -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>
diff --git a/modules/gltf/extensions/gltf_document_extension.cpp b/modules/gltf/extensions/gltf_document_extension.cpp
index 11718ba78a..e16e366692 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<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);
diff --git a/modules/gltf/extensions/gltf_document_extension.h b/modules/gltf/extensions/gltf_document_extension.h
index 0a631bb6c5..512b7aba91 100644
--- a/modules/gltf/extensions/gltf_document_extension.h
+++ b/modules/gltf/extensions/gltf_document_extension.h
@@ -47,14 +47,18 @@ public:
virtual Error parse_image_data(Ref<GLTFState> p_state, const PackedByteArray &p_image_data, const String &p_mime_type, Ref<Image> r_image);
virtual String get_image_file_extension();
virtual Error parse_texture_json(Ref<GLTFState> p_state, const Dictionary &p_texture_json, Ref<GLTFTexture> r_gltf_texture);
- virtual Node3D *generate_scene_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Node *p_scene_parent);
virtual Error import_post_parse(Ref<GLTFState> p_state);
+ virtual Node3D *generate_scene_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Node *p_scene_parent);
virtual Error import_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Dictionary &r_json, Node *p_node);
virtual Error import_post(Ref<GLTFState> p_state, Node *p_node);
// Export process.
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>);
};
diff --git a/modules/gltf/extensions/gltf_document_extension_texture_webp.cpp b/modules/gltf/extensions/gltf_document_extension_texture_webp.cpp
index 73c869be3b..f8bd6d57cf 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<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;
+}
diff --git a/modules/gltf/extensions/gltf_document_extension_texture_webp.h b/modules/gltf/extensions/gltf_document_extension_texture_webp.h
index d2654aae8c..2113bd4768 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<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
diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp
index 984052d2ca..761db165ee 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<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;
@@ -5821,6 +5878,10 @@ void GLTFDocument::_generate_scene_node(Ref<GLTFState> p_state, const GLTFNodeIn
current_node = _generate_spatial(p_state, p_node_index);
}
}
+ String gltf_node_name = gltf_node->get_name();
+ if (!gltf_node_name.is_empty()) {
+ current_node->set_name(gltf_node_name);
+ }
// Add the node we generated and set the owner to the scene root.
p_scene_parent->add_child(current_node, true);
if (current_node != p_scene_root) {
@@ -5829,7 +5890,6 @@ void GLTFDocument::_generate_scene_node(Ref<GLTFState> p_state, const GLTFNodeIn
current_node->propagate_call(StringName("set_owner"), args);
}
current_node->set_transform(gltf_node->xform);
- current_node->set_name(gltf_node->get_name());
p_state->scene_nodes.insert(p_node_index, current_node);
for (int i = 0; i < gltf_node->children.size(); ++i) {
@@ -6443,7 +6503,7 @@ float GLTFDocument::get_max_component(const Color &p_color) {
return MAX(MAX(r, g), b);
}
-void GLTFDocument::_process_mesh_instances(Ref<GLTFState> p_state, Node *p_scene_root) {
+void GLTFDocument::_process_mesh_instances(Ref<GLTFState> p_state) {
for (GLTFNodeIndex node_i = 0; node_i < p_state->nodes.size(); ++node_i) {
Ref<GLTFNode> node = p_state->nodes[node_i];
@@ -7158,6 +7218,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"),
@@ -7266,15 +7334,28 @@ Error GLTFDocument::write_to_filesystem(Ref<GLTFState> p_state, const String &p_
return OK;
}
+Node *GLTFDocument::_generate_scene_node_tree(Ref<GLTFState> p_state) {
+ Node *single_root = memnew(Node3D);
+ for (int32_t root_i = 0; root_i < p_state->root_nodes.size(); root_i++) {
+ _generate_scene_node(p_state, p_state->root_nodes[root_i], single_root, single_root);
+ }
+ // Assign the scene name and single root name to each other
+ // if one is missing, or do nothing if both are already set.
+ if (unlikely(p_state->scene_name.is_empty())) {
+ p_state->scene_name = single_root->get_name();
+ } else if (single_root->get_name() == StringName()) {
+ single_root->set_name(_gen_unique_name(p_state, p_state->scene_name));
+ }
+ return single_root;
+}
+
Node *GLTFDocument::generate_scene(Ref<GLTFState> p_state, float p_bake_fps, bool p_trimming, bool p_remove_immutable_tracks) {
ERR_FAIL_NULL_V(p_state, nullptr);
ERR_FAIL_INDEX_V(0, p_state->root_nodes.size(), nullptr);
Error err = OK;
- GLTFNodeIndex gltf_root = p_state->root_nodes.write[0];
- Node *gltf_root_node = p_state->get_scene_node(gltf_root);
- Node *root = gltf_root_node->get_parent();
+ Node *root = _generate_scene_node_tree(p_state);
ERR_FAIL_NULL_V(root, nullptr);
- _process_mesh_instances(p_state, root);
+ _process_mesh_instances(p_state);
if (p_state->get_create_animations() && p_state->animations.size()) {
AnimationPlayer *ap = memnew(AnimationPlayer);
root->add_child(ap, true);
@@ -7454,11 +7535,6 @@ Error GLTFDocument::_parse_gltf_state(Ref<GLTFState> p_state, const String &p_se
/* ASSIGN SCENE NAMES */
_assign_node_names(p_state);
- Node3D *root = memnew(Node3D);
- for (int32_t root_i = 0; root_i < p_state->root_nodes.size(); root_i++) {
- _generate_scene_node(p_state, p_state->root_nodes[root_i], root, root);
- }
-
return OK;
}
diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h
index f2e36a0457..febe04b55f 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<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);
@@ -306,7 +314,8 @@ public:
Error _parse_gltf_state(Ref<GLTFState> p_state, const String &p_search_path);
Error _parse_asset_header(Ref<GLTFState> p_state);
Error _parse_gltf_extensions(Ref<GLTFState> p_state);
- void _process_mesh_instances(Ref<GLTFState> p_state, Node *p_scene_root);
+ void _process_mesh_instances(Ref<GLTFState> p_state);
+ Node *_generate_scene_node_tree(Ref<GLTFState> p_state);
void _generate_scene_node(Ref<GLTFState> p_state, const GLTFNodeIndex p_node_index, Node *p_scene_parent, Node *p_scene_root);
void _generate_skeleton_bone_node(Ref<GLTFState> p_state, const GLTFNodeIndex p_node_index, Node *p_scene_parent, Node *p_scene_root);
void _import_animation(Ref<GLTFState> p_state, AnimationPlayer *p_animation_player,
diff --git a/scene/2d/line_2d.cpp b/scene/2d/line_2d.cpp
index 3a9473d76c..be58875e0e 100644
--- a/scene/2d/line_2d.cpp
+++ b/scene/2d/line_2d.cpp
@@ -42,12 +42,12 @@ Rect2 Line2D::_edit_get_rect() const {
return Rect2(0, 0, 0, 0);
}
Vector2 d = Vector2(_width, _width);
- Rect2 aabb = Rect2(_points[0] - d, 2 * d);
+ Rect2 bounding_rect = Rect2(_points[0] - d, 2 * d);
for (int i = 1; i < _points.size(); i++) {
- aabb.expand_to(_points[i] - d);
- aabb.expand_to(_points[i] + d);
+ bounding_rect.expand_to(_points[i] - d);
+ bounding_rect.expand_to(_points[i] + d);
}
- return aabb;
+ return bounding_rect;
}
bool Line2D::_edit_use_rect() const {
@@ -59,7 +59,14 @@ bool Line2D::_edit_is_selected_on_click(const Point2 &p_point, double p_toleranc
const Vector2 *points = _points.ptr();
for (int i = 0; i < _points.size() - 1; i++) {
Vector2 p = Geometry2D::get_closest_point_to_segment(p_point, &points[i]);
- if (p.distance_to(p_point) <= d) {
+ if (p_point.distance_to(p) <= d) {
+ return true;
+ }
+ }
+ if (_closed && _points.size() > 2) {
+ const Vector2 closing_segment[2] = { points[0], points[_points.size() - 1] };
+ Vector2 p = Geometry2D::get_closest_point_to_segment(p_point, closing_segment);
+ if (p_point.distance_to(p) <= d) {
return true;
}
}
@@ -73,6 +80,15 @@ void Line2D::set_points(const Vector<Vector2> &p_points) {
queue_redraw();
}
+void Line2D::set_closed(bool p_closed) {
+ _closed = p_closed;
+ queue_redraw();
+}
+
+bool Line2D::is_closed() const {
+ return _closed;
+}
+
void Line2D::set_width(float p_width) {
if (p_width < 0.0) {
p_width = 0.0;
@@ -86,14 +102,12 @@ float Line2D::get_width() const {
}
void Line2D::set_curve(const Ref<Curve> &p_curve) {
- // Cleanup previous connection if any
if (_curve.is_valid()) {
_curve->disconnect_changed(callable_mp(this, &Line2D::_curve_changed));
}
_curve = p_curve;
- // Connect to the curve so the line will update when it is changed
if (_curve.is_valid()) {
_curve->connect_changed(callable_mp(this, &Line2D::_curve_changed));
}
@@ -156,14 +170,12 @@ Color Line2D::get_default_color() const {
}
void Line2D::set_gradient(const Ref<Gradient> &p_gradient) {
- // Cleanup previous connection if any
if (_gradient.is_valid()) {
_gradient->disconnect_changed(callable_mp(this, &Line2D::_gradient_changed));
}
_gradient = p_gradient;
- // Connect to the gradient so the line will update when the Gradient is changed
if (_gradient.is_valid()) {
_gradient->connect_changed(callable_mp(this, &Line2D::_gradient_changed));
}
@@ -264,20 +276,10 @@ void Line2D::_draw() {
return;
}
- // TODO Is this really needed?
- // Copy points for faster access
- Vector<Vector2> points;
- points.resize(len);
- {
- const Vector2 *points_read = _points.ptr();
- for (int i = 0; i < len; ++i) {
- points.write[i] = points_read[i];
- }
- }
-
// TODO Maybe have it as member rather than copying parameters and allocating memory?
LineBuilder lb;
- lb.points = points;
+ lb.points = _points;
+ lb.closed = _closed;
lb.default_color = _default_color;
lb.gradient = *_gradient;
lb.texture_mode = _texture_mode;
@@ -306,13 +308,10 @@ void Line2D::_draw() {
lb.uvs, Vector<int>(), Vector<float>(),
texture_rid);
- // DEBUG
- // Draw wireframe
- // if(lb.indices.size() % 3 == 0) {
- // Color col(0,0,0);
- // for(int i = 0; i < lb.indices.size(); i += 3) {
- // int vi = lb.indices[i];
- // int lbvsize = lb.vertices.size();
+ // DEBUG: Draw wireframe
+ // if (lb.indices.size() % 3 == 0) {
+ // Color col(0, 0, 0);
+ // for (int i = 0; i < lb.indices.size(); i += 3) {
// Vector2 a = lb.vertices[lb.indices[i]];
// Vector2 b = lb.vertices[lb.indices[i+1]];
// Vector2 c = lb.vertices[lb.indices[i+2]];
@@ -320,9 +319,9 @@ void Line2D::_draw() {
// draw_line(b, c, col);
// draw_line(c, a, col);
// }
- // for(int i = 0; i < lb.vertices.size(); ++i) {
+ // for (int i = 0; i < lb.vertices.size(); ++i) {
// Vector2 p = lb.vertices[i];
- // draw_rect(Rect2(p.x-1, p.y-1, 2, 2), Color(0,0,0,0.5));
+ // draw_rect(Rect2(p.x - 1, p.y - 1, 2, 2), Color(0, 0, 0, 0.5));
// }
// }
}
@@ -350,6 +349,9 @@ void Line2D::_bind_methods() {
ClassDB::bind_method(D_METHOD("clear_points"), &Line2D::clear_points);
+ ClassDB::bind_method(D_METHOD("set_closed", "closed"), &Line2D::set_closed);
+ ClassDB::bind_method(D_METHOD("is_closed"), &Line2D::is_closed);
+
ClassDB::bind_method(D_METHOD("set_width", "width"), &Line2D::set_width);
ClassDB::bind_method(D_METHOD("get_width"), &Line2D::get_width);
@@ -387,6 +389,7 @@ void Line2D::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_antialiased"), &Line2D::get_antialiased);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_VECTOR2_ARRAY, "points"), "set_points", "get_points");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "closed"), "set_closed", "is_closed");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "width", PROPERTY_HINT_NONE, "suffix:px"), "set_width", "get_width");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "width_curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve"), "set_curve", "get_curve");
ADD_PROPERTY(PropertyInfo(Variant::COLOR, "default_color"), "set_default_color", "get_default_color");
diff --git a/scene/2d/line_2d.h b/scene/2d/line_2d.h
index aa873ca642..1d750ca456 100644
--- a/scene/2d/line_2d.h
+++ b/scene/2d/line_2d.h
@@ -76,6 +76,9 @@ public:
void add_point(Vector2 pos, int atpos = -1);
void remove_point(int i);
+ void set_closed(bool p_closed);
+ bool is_closed() const;
+
void set_width(float width);
float get_width() const;
@@ -127,6 +130,7 @@ private:
LineJointMode _joint_mode = LINE_JOINT_SHARP;
LineCapMode _begin_cap_mode = LINE_CAP_NONE;
LineCapMode _end_cap_mode = LINE_CAP_NONE;
+ bool _closed = false;
float _width = 10.0;
Ref<Curve> _curve;
Color _default_color = Color(1, 1, 1);
diff --git a/scene/2d/line_builder.cpp b/scene/2d/line_builder.cpp
index 0970325502..e898dc7fab 100644
--- a/scene/2d/line_builder.cpp
+++ b/scene/2d/line_builder.cpp
@@ -30,75 +30,25 @@
#include "line_builder.h"
-//----------------------------------------------------------------------------
-// Util
-//----------------------------------------------------------------------------
-
-enum SegmentIntersectionResult {
- SEGMENT_PARALLEL = 0,
- SEGMENT_NO_INTERSECT = 1,
- SEGMENT_INTERSECT = 2
-};
-
-static SegmentIntersectionResult segment_intersection(
- Vector2 a, Vector2 b, Vector2 c, Vector2 d,
- Vector2 *out_intersection) {
- // http://paulbourke.net/geometry/pointlineplane/ <-- Good stuff
- Vector2 cd = d - c;
- Vector2 ab = b - a;
- float div = cd.y * ab.x - cd.x * ab.y;
-
- if (Math::abs(div) > 0.001f) {
- float ua = (cd.x * (a.y - c.y) - cd.y * (a.x - c.x)) / div;
- float ub = (ab.x * (a.y - c.y) - ab.y * (a.x - c.x)) / div;
- *out_intersection = a + ua * ab;
- if (ua >= 0.f && ua <= 1.f &&
- ub >= 0.f && ub <= 1.f) {
- return SEGMENT_INTERSECT;
- }
- return SEGMENT_NO_INTERSECT;
- }
-
- return SEGMENT_PARALLEL;
-}
-
-static float calculate_total_distance(const Vector<Vector2> &points) {
- float d = 0.f;
- for (int i = 1; i < points.size(); ++i) {
- d += points[i].distance_to(points[i - 1]);
- }
- return d;
-}
-
-static inline Vector2 rotate90(const Vector2 &v) {
- // Note: the 2D referential is X-right, Y-down
- return Vector2(v.y, -v.x);
-}
+#include "core/math/geometry_2d.h"
+// Utility method.
static inline Vector2 interpolate(const Rect2 &r, const Vector2 &v) {
return Vector2(
Math::lerp(r.position.x, r.position.x + r.get_size().x, v.x),
Math::lerp(r.position.y, r.position.y + r.get_size().y, v.y));
}
-//----------------------------------------------------------------------------
-// LineBuilder
-//----------------------------------------------------------------------------
-
LineBuilder::LineBuilder() {
}
-void LineBuilder::clear_output() {
- vertices.clear();
- colors.clear();
- indices.clear();
- uvs.clear();
-}
-
void LineBuilder::build() {
- // Need at least 2 points to draw a line
+ // Need at least 2 points to draw a line, so clear the output and return.
if (points.size() < 2) {
- clear_output();
+ vertices.clear();
+ colors.clear();
+ indices.clear();
+ uvs.clear();
return;
}
@@ -107,14 +57,21 @@ void LineBuilder::build() {
const float hw = width / 2.f;
const float hw_sq = hw * hw;
const float sharp_limit_sq = sharp_limit * sharp_limit;
- const int len = points.size();
+ const int point_count = points.size();
+ const bool wrap_around = closed && point_count > 2;
+
+ _interpolate_color = gradient != nullptr;
+ const bool retrieve_curve = curve != nullptr;
+ const bool distance_required = _interpolate_color || retrieve_curve ||
+ texture_mode == Line2D::LINE_TEXTURE_TILE ||
+ texture_mode == Line2D::LINE_TEXTURE_STRETCH;
// Initial values
Vector2 pos0 = points[0];
Vector2 pos1 = points[1];
Vector2 f0 = (pos1 - pos0).normalized();
- Vector2 u0 = rotate90(f0);
+ Vector2 u0 = f0.orthogonal();
Vector2 pos_up0 = pos0;
Vector2 pos_down0 = pos0;
@@ -124,32 +81,37 @@ void LineBuilder::build() {
float current_distance0 = 0.f;
float current_distance1 = 0.f;
float total_distance = 0.f;
+
float width_factor = 1.f;
- _interpolate_color = gradient != nullptr;
- bool retrieve_curve = curve != nullptr;
- bool distance_required = _interpolate_color ||
- retrieve_curve ||
- texture_mode == Line2D::LINE_TEXTURE_TILE ||
- texture_mode == Line2D::LINE_TEXTURE_STRETCH;
+ float modified_hw = hw;
+ if (retrieve_curve) {
+ width_factor = curve->sample_baked(0.f);
+ modified_hw = hw * width_factor;
+ }
+
if (distance_required) {
- total_distance = calculate_total_distance(points);
- //Adjust totalDistance.
- // The line's outer length will be a little higher due to begin and end caps
- if (begin_cap_mode == Line2D::LINE_CAP_BOX || begin_cap_mode == Line2D::LINE_CAP_ROUND) {
- if (retrieve_curve) {
- total_distance += width * curve->sample_baked(0.f) * 0.5f;
- } else {
- total_distance += width * 0.5f;
- }
+ // Calculate the total distance.
+ for (int i = 1; i < point_count; ++i) {
+ total_distance += points[i].distance_to(points[i - 1]);
}
- if (end_cap_mode == Line2D::LINE_CAP_BOX || end_cap_mode == Line2D::LINE_CAP_ROUND) {
- if (retrieve_curve) {
- total_distance += width * curve->sample_baked(1.f) * 0.5f;
- } else {
- total_distance += width * 0.5f;
+ if (wrap_around) {
+ total_distance += points[point_count - 1].distance_to(pos0);
+ } else {
+ // Adjust the total distance.
+ // The line's outer length may be a little higher due to the end caps.
+ if (begin_cap_mode == Line2D::LINE_CAP_BOX || begin_cap_mode == Line2D::LINE_CAP_ROUND) {
+ total_distance += modified_hw;
+ }
+ if (end_cap_mode == Line2D::LINE_CAP_BOX || end_cap_mode == Line2D::LINE_CAP_ROUND) {
+ if (retrieve_curve) {
+ total_distance += hw * curve->sample_baked(1.f);
+ } else {
+ total_distance += hw;
+ }
}
}
}
+
if (_interpolate_color) {
color0 = gradient->get_color(0);
} else {
@@ -159,34 +121,31 @@ void LineBuilder::build() {
float uvx0 = 0.f;
float uvx1 = 0.f;
- if (retrieve_curve) {
- width_factor = curve->sample_baked(0.f);
- }
-
- pos_up0 += u0 * hw * width_factor;
- pos_down0 -= u0 * hw * width_factor;
+ pos_up0 += u0 * modified_hw;
+ pos_down0 -= u0 * modified_hw;
// Begin cap
- if (begin_cap_mode == Line2D::LINE_CAP_BOX) {
- // Push back first vertices a little bit
- pos_up0 -= f0 * hw * width_factor;
- pos_down0 -= f0 * hw * width_factor;
-
- current_distance0 += hw * width_factor;
- current_distance1 = current_distance0;
- } else if (begin_cap_mode == Line2D::LINE_CAP_ROUND) {
- if (texture_mode == Line2D::LINE_TEXTURE_TILE) {
- uvx0 = width_factor * 0.5f / tile_aspect;
- } else if (texture_mode == Line2D::LINE_TEXTURE_STRETCH) {
- uvx0 = width * width_factor / total_distance;
+ if (!wrap_around) {
+ if (begin_cap_mode == Line2D::LINE_CAP_BOX) {
+ // Push back first vertices a little bit.
+ pos_up0 -= f0 * modified_hw;
+ pos_down0 -= f0 * modified_hw;
+
+ current_distance0 += modified_hw;
+ current_distance1 = current_distance0;
+ } else if (begin_cap_mode == Line2D::LINE_CAP_ROUND) {
+ if (texture_mode == Line2D::LINE_TEXTURE_TILE) {
+ uvx0 = width_factor * 0.5f / tile_aspect;
+ } else if (texture_mode == Line2D::LINE_TEXTURE_STRETCH) {
+ uvx0 = width * width_factor / total_distance;
+ }
+ new_arc(pos0, pos_up0 - pos0, -Math_PI, color0, Rect2(0.f, 0.f, uvx0 * 2, 1.f));
+ current_distance0 += modified_hw;
+ current_distance1 = current_distance0;
}
- new_arc(pos0, pos_up0 - pos0, -Math_PI, color0, Rect2(0.f, 0.f, uvx0 * 2, 1.f));
- current_distance0 += hw * width_factor;
- current_distance1 = current_distance0;
+ strip_begin(pos_up0, pos_down0, color0, uvx0);
}
- strip_begin(pos_up0, pos_down0, color0, uvx0);
-
/*
* pos_up0 ------------- pos_up1 --------------------
* | |
@@ -200,19 +159,30 @@ void LineBuilder::build() {
// http://labs.hyperandroid.com/tag/opengl-lines
// (not the same implementation but visuals help a lot)
+ // If the polyline wraps around, then draw two more segments with joints:
+ // The last one, which should normally end with an end cap, and the one that matches the end and the beginning.
+ int segments_count = wrap_around ? point_count : (point_count - 2);
+ // The wraparound case starts with a "fake walk" from the end of the polyline
+ // to its beginning, so that its first joint is correct, without drawing anything.
+ int first_point = wrap_around ? -1 : 1;
+
+ // If the line wraps around, these variables will be used for the final segment.
+ Vector2 first_pos_up, first_pos_down;
+ bool is_first_joint_sharp = false;
+
// For each additional segment
- for (int i = 1; i < len - 1; ++i) {
- pos1 = points[i];
- Vector2 pos2 = points[i + 1];
+ for (int i = first_point; i <= segments_count; ++i) {
+ pos1 = points[(i == -1) ? point_count - 1 : i % point_count]; // First point.
+ Vector2 pos2 = points[(i + 1) % point_count]; // Second point.
Vector2 f1 = (pos2 - pos1).normalized();
- Vector2 u1 = rotate90(f1);
+ Vector2 u1 = f1.orthogonal();
- // Determine joint orientation
- const float dp = u0.dot(f1);
+ // Determine joint orientation.
+ float dp = u0.dot(f1);
const Orientation orientation = (dp > 0.f ? UP : DOWN);
- if (distance_required) {
+ if (distance_required && i >= 1) {
current_distance1 += pos0.distance_to(pos1);
}
if (_interpolate_color) {
@@ -220,15 +190,14 @@ void LineBuilder::build() {
}
if (retrieve_curve) {
width_factor = curve->sample_baked(current_distance1 / total_distance);
+ modified_hw = hw * width_factor;
}
- Vector2 inner_normal0, inner_normal1;
- if (orientation == UP) {
- inner_normal0 = u0 * hw * width_factor;
- inner_normal1 = u1 * hw * width_factor;
- } else {
- inner_normal0 = -u0 * hw * width_factor;
- inner_normal1 = -u1 * hw * width_factor;
+ Vector2 inner_normal0 = u0 * modified_hw;
+ Vector2 inner_normal1 = u1 * modified_hw;
+ if (orientation == DOWN) {
+ inner_normal0 = -inner_normal0;
+ inner_normal1 = -inner_normal1;
}
/*
@@ -245,18 +214,18 @@ void LineBuilder::build() {
* /
*/
- // Find inner intersection at the joint
+ // Find inner intersection at the joint.
Vector2 corner_pos_in, corner_pos_out;
- SegmentIntersectionResult intersection_result = segment_intersection(
+ bool is_intersecting = Geometry2D::segment_intersects_segment(
pos0 + inner_normal0, pos1 + inner_normal0,
pos1 + inner_normal1, pos2 + inner_normal1,
&corner_pos_in);
- if (intersection_result == SEGMENT_INTERSECT) {
- // Inner parts of the segments intersect
+ if (is_intersecting) {
+ // Inner parts of the segments intersect.
corner_pos_out = 2.f * pos1 - corner_pos_in;
} else {
- // No intersection, segments are either parallel or too sharp
+ // No intersection, segments are too sharp or they overlap.
corner_pos_in = pos1 + inner_normal0;
corner_pos_out = pos1 - inner_normal0;
}
@@ -273,8 +242,8 @@ void LineBuilder::build() {
Line2D::LineJointMode current_joint_mode = joint_mode;
Vector2 pos_up1, pos_down1;
- if (intersection_result == SEGMENT_INTERSECT) {
- // Fallback on bevel if sharp angle is too high (because it would produce very long miters)
+ if (is_intersecting) {
+ // Fallback on bevel if sharp angle is too high (because it would produce very long miters).
float width_factor_sq = width_factor * width_factor;
if (current_joint_mode == Line2D::LINE_JOINT_SHARP && corner_pos_out.distance_squared_to(pos1) / (hw_sq * width_factor_sq) > sharp_limit_sq) {
current_joint_mode = Line2D::LINE_JOINT_BEVEL;
@@ -288,57 +257,78 @@ void LineBuilder::build() {
// Bevel or round
if (orientation == UP) {
pos_up1 = corner_pos_up;
- pos_down1 = pos1 - u0 * hw * width_factor;
+ pos_down1 = pos1 - u0 * modified_hw;
} else {
- pos_up1 = pos1 + u0 * hw * width_factor;
+ pos_up1 = pos1 + u0 * modified_hw;
pos_down1 = corner_pos_down;
}
}
} else {
// No intersection: fallback
if (current_joint_mode == Line2D::LINE_JOINT_SHARP) {
- // There is no fallback implementation for LINE_JOINT_SHARP so switch to the LINE_JOINT_BEVEL
+ // There is no fallback implementation for LINE_JOINT_SHARP so switch to the LINE_JOINT_BEVEL.
current_joint_mode = Line2D::LINE_JOINT_BEVEL;
}
pos_up1 = corner_pos_up;
pos_down1 = corner_pos_down;
}
- // Add current line body quad
- // Triangles are clockwise
+ // Triangles are clockwise.
if (texture_mode == Line2D::LINE_TEXTURE_TILE) {
uvx1 = current_distance1 / (width * tile_aspect);
} else if (texture_mode == Line2D::LINE_TEXTURE_STRETCH) {
uvx1 = current_distance1 / total_distance;
}
- strip_add_quad(pos_up1, pos_down1, color1, uvx1);
-
- // Swap vars for use in the next line
+ // Swap vars for use in the next line.
color0 = color1;
u0 = u1;
f0 = f1;
pos0 = pos1;
- if (intersection_result == SEGMENT_INTERSECT) {
+ if (is_intersecting) {
if (current_joint_mode == Line2D::LINE_JOINT_SHARP) {
pos_up0 = pos_up1;
pos_down0 = pos_down1;
} else {
if (orientation == UP) {
pos_up0 = corner_pos_up;
- pos_down0 = pos1 - u1 * hw * width_factor;
+ pos_down0 = pos1 - u1 * modified_hw;
} else {
- pos_up0 = pos1 + u1 * hw * width_factor;
+ pos_up0 = pos1 + u1 * modified_hw;
pos_down0 = corner_pos_down;
}
}
} else {
- pos_up0 = pos1 + u1 * hw * width_factor;
- pos_down0 = pos1 - u1 * hw * width_factor;
+ pos_up0 = pos1 + u1 * modified_hw;
+ pos_down0 = pos1 - u1 * modified_hw;
+ }
+
+ // End the "fake pass" in the closed line case before the drawing subroutine.
+ if (i == -1) {
+ continue;
+ }
+
+ // For wrap-around polylines, store some kind of start positions of the first joint for the final connection.
+ if (wrap_around && i == 0) {
+ Vector2 first_pos_center = (pos_up1 + pos_down1) / 2;
+ float lerp_factor = 1.0 / width_factor;
+ first_pos_up = first_pos_center.lerp(pos_up1, lerp_factor);
+ first_pos_down = first_pos_center.lerp(pos_down1, lerp_factor);
+ is_first_joint_sharp = current_joint_mode == Line2D::LINE_JOINT_SHARP;
+ }
+
+ // Add current line body quad.
+ if (wrap_around && retrieve_curve && !is_first_joint_sharp && i == segments_count) {
+ // If the width curve is not seamless, we might need to fetch the line's start points to use them for the final connection.
+ Vector2 first_pos_center = (first_pos_up + first_pos_down) / 2;
+ strip_add_quad(first_pos_center.lerp(first_pos_up, width_factor), first_pos_center.lerp(first_pos_down, width_factor), color1, uvx1);
+ return;
+ } else {
+ strip_add_quad(pos_up1, pos_down1, color1, uvx1);
}
- // From this point, bu0 and bd0 concern the next segment
- // Add joint geometry
+ // From this point, bu0 and bd0 concern the next segment.
+ // Add joint geometry.
if (current_joint_mode != Line2D::LINE_JOINT_SHARP) {
/* ________________ cbegin
* / \
@@ -358,64 +348,68 @@ void LineBuilder::build() {
cend = pos_up0;
}
- if (current_joint_mode == Line2D::LINE_JOINT_BEVEL) {
+ if (current_joint_mode == Line2D::LINE_JOINT_BEVEL && !(wrap_around && i == segments_count)) {
strip_add_tri(cend, orientation);
- } else if (current_joint_mode == Line2D::LINE_JOINT_ROUND) {
+ } else if (current_joint_mode == Line2D::LINE_JOINT_ROUND && !(wrap_around && i == segments_count)) {
Vector2 vbegin = cbegin - pos1;
Vector2 vend = cend - pos1;
strip_add_arc(pos1, vbegin.angle_to(vend), orientation);
}
- if (intersection_result != SEGMENT_INTERSECT) {
+ if (!is_intersecting) {
// In this case the joint is too corrupted to be re-used,
// start again the strip with fallback points
strip_begin(pos_up0, pos_down0, color1, uvx1);
}
}
}
- // Last (or only) segment
- pos1 = points[points.size() - 1];
- if (distance_required) {
- current_distance1 += pos0.distance_to(pos1);
- }
- if (_interpolate_color) {
- color1 = gradient->get_color(gradient->get_point_count() - 1);
- }
- if (retrieve_curve) {
- width_factor = curve->sample_baked(1.f);
- }
+ // Draw the last (or only) segment, with its end cap logic.
+ if (!wrap_around) {
+ pos1 = points[point_count - 1];
- Vector2 pos_up1 = pos1 + u0 * hw * width_factor;
- Vector2 pos_down1 = pos1 - u0 * hw * width_factor;
+ if (distance_required) {
+ current_distance1 += pos0.distance_to(pos1);
+ }
+ if (_interpolate_color) {
+ color1 = gradient->get_color(gradient->get_point_count() - 1);
+ }
+ if (retrieve_curve) {
+ width_factor = curve->sample_baked(1.f);
+ modified_hw = hw * width_factor;
+ }
- // End cap (box)
- if (end_cap_mode == Line2D::LINE_CAP_BOX) {
- pos_up1 += f0 * hw * width_factor;
- pos_down1 += f0 * hw * width_factor;
+ Vector2 pos_up1 = pos1 + u0 * modified_hw;
+ Vector2 pos_down1 = pos1 - u0 * modified_hw;
- current_distance1 += hw * width_factor;
- }
+ // Add extra distance for a box end cap.
+ if (end_cap_mode == Line2D::LINE_CAP_BOX) {
+ pos_up1 += f0 * modified_hw;
+ pos_down1 += f0 * modified_hw;
- if (texture_mode == Line2D::LINE_TEXTURE_TILE) {
- uvx1 = current_distance1 / (width * tile_aspect);
- } else if (texture_mode == Line2D::LINE_TEXTURE_STRETCH) {
- uvx1 = current_distance1 / total_distance;
- }
-
- strip_add_quad(pos_up1, pos_down1, color1, uvx1);
+ current_distance1 += modified_hw;
+ }
- // End cap (round)
- if (end_cap_mode == Line2D::LINE_CAP_ROUND) {
- // Note: color is not used in case we don't interpolate...
- Color color = _interpolate_color ? gradient->get_color(gradient->get_point_count() - 1) : Color(0, 0, 0);
- float dist = 0;
if (texture_mode == Line2D::LINE_TEXTURE_TILE) {
- dist = width_factor / tile_aspect;
+ uvx1 = current_distance1 / (width * tile_aspect);
} else if (texture_mode == Line2D::LINE_TEXTURE_STRETCH) {
- dist = width * width_factor / total_distance;
+ uvx1 = current_distance1 / total_distance;
+ }
+
+ strip_add_quad(pos_up1, pos_down1, color1, uvx1);
+
+ // Custom drawing for a round end cap.
+ if (end_cap_mode == Line2D::LINE_CAP_ROUND) {
+ // Note: color is not used in case we don't interpolate.
+ Color color = _interpolate_color ? gradient->get_color(gradient->get_point_count() - 1) : Color(0, 0, 0);
+ float dist = 0;
+ if (texture_mode == Line2D::LINE_TEXTURE_TILE) {
+ dist = width_factor / tile_aspect;
+ } else if (texture_mode == Line2D::LINE_TEXTURE_STRETCH) {
+ dist = width * width_factor / total_distance;
+ }
+ new_arc(pos1, pos_up1 - pos1, Math_PI, color, Rect2(uvx1 - 0.5f * dist, 0.f, dist, 1.f));
}
- new_arc(pos1, pos_up1 - pos1, Math_PI, color, Rect2(uvx1 - 0.5f * dist, 0.f, dist, 1.f));
}
}
diff --git a/scene/2d/line_builder.h b/scene/2d/line_builder.h
index 044ff93b42..3706352d5c 100644
--- a/scene/2d/line_builder.h
+++ b/scene/2d/line_builder.h
@@ -41,6 +41,7 @@ public:
Line2D::LineJointMode joint_mode = Line2D::LINE_JOINT_SHARP;
Line2D::LineCapMode begin_cap_mode = Line2D::LINE_CAP_NONE;
Line2D::LineCapMode end_cap_mode = Line2D::LINE_CAP_NONE;
+ bool closed = false;
float width = 10.0;
Curve *curve = nullptr;
Color default_color = Color(0.4, 0.5, 1);
@@ -61,7 +62,6 @@ public:
LineBuilder();
void build();
- void clear_output();
private:
enum Orientation {