From 10b6c833e8c8b60317b7d827451e82a66e4c0420 Mon Sep 17 00:00:00 2001 From: lawnjelly Date: Mon, 1 Mar 2021 12:44:39 +0000 Subject: [PATCH] More snapping options as project settings Defaults are better (and separate) for stretch_mode viewport and stretch_mode 2D. All rounding settings can be overridden giving the user full control. --- core/engine.h | 10 +- core/snappers.cpp | 101 ++++++++++++++++++ core/snappers.h | 122 ++++++++++++++++++++++ doc/classes/ProjectSettings.xml | 40 ++++++- main/main.cpp | 8 +- scene/2d/animated_sprite.cpp | 7 +- scene/2d/sprite.cpp | 13 +-- servers/visual/visual_server_canvas.cpp | 14 ++- servers/visual/visual_server_canvas.h | 1 - servers/visual/visual_server_viewport.cpp | 16 +-- 10 files changed, 299 insertions(+), 33 deletions(-) create mode 100644 core/snappers.cpp create mode 100644 core/snappers.h diff --git a/core/engine.h b/core/engine.h index 660699161988..a37e9cf6df76 100644 --- a/core/engine.h +++ b/core/engine.h @@ -33,6 +33,7 @@ #include "core/list.h" #include "core/os/main_loop.h" +#include "core/snappers.h" #include "core/ustring.h" #include "core/vector.h" @@ -58,15 +59,17 @@ class Engine { float _fps; int _target_fps; float _time_scale; - bool _gpu_pixel_snap; - bool _snap_2d_transforms; - bool _snap_2d_viewports; uint64_t _physics_frames; float _physics_interpolation_fraction; uint64_t _idle_frames; bool _in_physics; + bool _gpu_pixel_snap; + bool _snap_2d_transforms; + bool _snap_2d_viewports; + Snappers _snappers; + List singletons; Map singleton_ptrs; @@ -111,6 +114,7 @@ class Engine { _FORCE_INLINE_ bool get_use_gpu_pixel_snap() const { return _gpu_pixel_snap; } bool get_snap_2d_transforms() const { return _snap_2d_transforms; } bool get_snap_2d_viewports() const { return _snap_2d_viewports; } + const Snappers &get_snappers() const { return _snappers; } #ifdef TOOLS_ENABLED _FORCE_INLINE_ void set_editor_hint(bool p_enabled) { editor_hint = p_enabled; } diff --git a/core/snappers.cpp b/core/snappers.cpp new file mode 100644 index 000000000000..f2e3f2122f70 --- /dev/null +++ b/core/snappers.cpp @@ -0,0 +1,101 @@ +/*************************************************************************/ +/* snappers.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "snappers.h" +#include "core/project_settings.h" + +void Snappers::snap_read_item(Vector2 &r_pos) const { + if (snapper_canvas_item_read.is_enabled()) { + snapper_canvas_item_read.snap(r_pos); + } else if (_gpu_snap_enabled) { + r_pos = r_pos.floor(); + } +} + +void Snappers::initialize(bool p_gpu_snap, bool p_snap_transforms, bool p_snap_viewports, bool p_stretch_mode_viewport) { + + _gpu_snap_enabled = p_gpu_snap; + + const char *sz_mode_selection = "Default,Disabled,Floor,Ceiling,Round"; + +#define GODOT_SNAP_DEFINE(MODE_STRING) \ + GLOBAL_DEF(MODE_STRING, 0); \ + ProjectSettings::get_singleton()->set_custom_property_info(MODE_STRING, PropertyInfo(Variant::INT, MODE_STRING, PROPERTY_HINT_ENUM, sz_mode_selection)) + + int item_pre_x = GODOT_SNAP_DEFINE("rendering/2d/snapping_modes/item_pre_x"); + int item_pre_y = GODOT_SNAP_DEFINE("rendering/2d/snapping_modes/item_pre_y"); + int item_post_x = GODOT_SNAP_DEFINE("rendering/2d/snapping_modes/item_post_x"); + int item_post_y = GODOT_SNAP_DEFINE("rendering/2d/snapping_modes/item_post_y"); + int item_read_x = GODOT_SNAP_DEFINE("rendering/2d/snapping_modes/item_read_x"); + int item_read_y = GODOT_SNAP_DEFINE("rendering/2d/snapping_modes/item_read_y"); + + int camera_pre_x = GODOT_SNAP_DEFINE("rendering/2d/snapping_modes/camera_pre_x"); + int camera_pre_y = GODOT_SNAP_DEFINE("rendering/2d/snapping_modes/camera_pre_y"); + int camera_post_x = GODOT_SNAP_DEFINE("rendering/2d/snapping_modes/camera_post_x"); + int camera_post_y = GODOT_SNAP_DEFINE("rendering/2d/snapping_modes/camera_post_y"); + int camera_parent_pre_x = GODOT_SNAP_DEFINE("rendering/2d/snapping_modes/camera_parent_pre_x"); + int camera_parent_pre_y = GODOT_SNAP_DEFINE("rendering/2d/snapping_modes/camera_parent_pre_y"); + + // defaults + if (p_snap_transforms) { + if (p_stretch_mode_viewport) { + snapper_canvas_item_pre.set_snap_modes(Snapper2D::SNAP_ROUND, Snapper2D::SNAP_CEILING); + } else { + snapper_canvas_item_post.set_snap_modes(Snapper2D::SNAP_ROUND, Snapper2D::SNAP_ROUND); + } + } + + if (p_snap_viewports) { + if (p_stretch_mode_viewport) { + snapper_viewport_pre.set_snap_modes(Snapper2D::SNAP_ROUND, Snapper2D::SNAP_ROUND); + } else { + snapper_viewport_post.set_snap_modes(Snapper2D::SNAP_ROUND, Snapper2D::SNAP_ROUND); + } + } + + // default actions for these derive from earlier types + snapper_canvas_item_read = snapper_canvas_item_pre; + snapper_viewport_parent_pre = snapper_viewport_pre; + + // custom user overrides + if (p_snap_transforms) { + snapper_canvas_item_pre.set_custom_snap_modes((Snapper2D::SnapMode)item_pre_x, (Snapper2D::SnapMode)item_pre_y); + snapper_canvas_item_post.set_custom_snap_modes((Snapper2D::SnapMode)item_post_x, (Snapper2D::SnapMode)item_post_y); + snapper_canvas_item_read.set_custom_snap_modes((Snapper2D::SnapMode)item_read_x, (Snapper2D::SnapMode)item_read_y); + } + + if (p_snap_viewports) { + snapper_viewport_pre.set_custom_snap_modes((Snapper2D::SnapMode)camera_pre_x, (Snapper2D::SnapMode)camera_pre_y); + snapper_viewport_post.set_custom_snap_modes((Snapper2D::SnapMode)camera_post_x, (Snapper2D::SnapMode)camera_post_y); + snapper_viewport_parent_pre.set_custom_snap_modes((Snapper2D::SnapMode)camera_parent_pre_x, (Snapper2D::SnapMode)camera_parent_pre_y); + } + +#undef GODOT_SNAP_DEFINE +} diff --git a/core/snappers.h b/core/snappers.h new file mode 100644 index 000000000000..9d46603baf26 --- /dev/null +++ b/core/snappers.h @@ -0,0 +1,122 @@ +/*************************************************************************/ +/* snappers.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef SNAPPERS_H +#define SNAPPERS_H + +#include "core/math/math_funcs.h" +#include "core/math/vector2.h" + +// generic class for handling 2d snapping +class Snapper2D { +public: + enum SnapMode { + SNAP_DEFAULT, + SNAP_DISABLED, + SNAP_FLOOR, + SNAP_CEILING, + SNAP_ROUND, + }; + + void snap(Vector2 &r_pos) const { + if (!_enabled) { + return; + } + + r_pos.x = snap_value(r_pos.x, _snap_mode_x); + r_pos.y = snap_value(r_pos.y, _snap_mode_y); + } + + void set_snap_modes(SnapMode p_mode_x, SnapMode p_mode_y) { + _snap_mode_x = p_mode_x; + _snap_mode_y = p_mode_y; + _enabled = !((_snap_mode_x == SNAP_DISABLED) && (_snap_mode_y == SNAP_DISABLED)); + } + + // disabled is user choosing default, so we will ignore the choice + void set_custom_snap_modes(SnapMode p_mode_x, SnapMode p_mode_y) { + if (p_mode_x != SNAP_DEFAULT) { + _snap_mode_x = p_mode_x; + } + if (p_mode_y != SNAP_DEFAULT) { + _snap_mode_y = p_mode_y; + } + _enabled = !((_snap_mode_x == SNAP_DISABLED) && (_snap_mode_y == SNAP_DISABLED)); + } + + bool is_enabled() const { return _enabled; } + +private: + real_t snap_value(real_t p_value, SnapMode p_mode) const { + switch (p_mode) { + default: + break; + case SNAP_FLOOR: { + return Math::floor(p_value); + } break; + case SNAP_CEILING: { + return Math::ceil(p_value); + } break; + case SNAP_ROUND: { + return Math::round(p_value); + } break; + } + return p_value; + } + + bool _enabled = false; + SnapMode _snap_mode_x = SNAP_DISABLED; + SnapMode _snap_mode_y = SNAP_DISABLED; +}; + +// All the 2D snapping in one place. +// This is called from the various places it needs to be introduced, but the logic +// can be self contained here to make it easier to change / debug. +class Snappers { +public: + void initialize(bool p_gpu_snap, bool p_snap_transforms, bool p_snap_viewports, bool p_stretch_mode_viewport); + + // for positioning of sprites etc, not the main draw call + void snap_read_item(Vector2 &r_pos) const; + + Snapper2D snapper_canvas_item_pre; + Snapper2D snapper_canvas_item_post; + Snapper2D snapper_canvas_item_read; + + Snapper2D snapper_viewport_pre; + Snapper2D snapper_viewport_post; + Snapper2D snapper_viewport_parent_pre; + +private: + // local version + bool _gpu_snap_enabled = false; +}; + +#endif // SNAPPERS_H diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index af85d8cbf3d3..63fcef737786 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -1045,7 +1045,7 @@ [b]Note:[/b] Antialiased software skinned polys are not supported, and will be rendered without antialiasing. - If [code]true[/code], forces snapping of 2D viewports to the nearest whole coordinate. + [b]Experimental[/b] If [code]true[/code], forces snapping of 2D viewports to the nearest whole coordinate. Can reduce unwanted camera relative movement in pixel art styles. @@ -1054,9 +1054,45 @@ Consider using the project setting [member rendering/batching/precision/uv_contract] to prevent artifacts. - If [code]true[/code], forces snapping of 2D object transforms to the nearest whole coordinate. + [b]Experimental[/b] If [code]true[/code], forces snapping of 2D object transforms to the nearest whole coordinate. Can help prevent unwanted relative movement in pixel art styles. + + [b]Experimental[/b] Canvas parent position x snapping mode. + + + [b]Experimental[/b] Canvas parent position y snapping mode. + + + [b]Experimental[/b] Canvas position x snapping mode (after applying global transform). + + + [b]Experimental[/b] Canvas position y snapping mode (after applying global transform). + + + [b]Experimental[/b] Canvas local position x snapping mode. + + + [b]Experimental[/b] Canvas local position y snapping mode. + + + [b]Experimental[/b] Item position x snapping mode when drawing (after applying parent transform). + + + [b]Experimental[/b] Item position y snapping mode when drawing (after applying parent transform). + + + [b]Experimental[/b] Item local position x snapping mode when drawing. + + + [b]Experimental[/b] Item local position y snapping mode when drawing. + + + [b]Experimental[/b] Item local position x snapping mode when reading. + + + [b]Experimental[/b] Item local position y snapping mode when reading. + When batching is on, this regularly prints a frame diagnosis log. Note that this will degrade performance. diff --git a/main/main.cpp b/main/main.cpp index 78257855943f..fe010a802ce1 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -1127,6 +1127,8 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph Engine::get_singleton()->_gpu_pixel_snap = GLOBAL_DEF("rendering/2d/snapping/use_gpu_pixel_snap", false); Engine::get_singleton()->_snap_2d_transforms = GLOBAL_DEF("rendering/2d/snapping/use_transform_snap", false); Engine::get_singleton()->_snap_2d_viewports = GLOBAL_DEF("rendering/2d/snapping/use_camera_snap", false); + Engine::get_singleton()->_snappers.initialize(Engine::get_singleton()->get_use_gpu_pixel_snap(), Engine::get_singleton()->get_snap_2d_transforms(), Engine::get_singleton()->get_snap_2d_viewports(), false); + OS::get_singleton()->_keep_screen_on = GLOBAL_DEF("display/window/energy_saving/keep_screen_on", true); if (rtm == -1) { rtm = GLOBAL_DEF("rendering/threads/thread_model", OS::RENDER_THREAD_SAFE); @@ -1826,9 +1828,13 @@ bool Main::start() { SceneTree::StretchMode sml_sm = SceneTree::STRETCH_MODE_DISABLED; if (stretch_mode == "2d") sml_sm = SceneTree::STRETCH_MODE_2D; - else if (stretch_mode == "viewport") + else if (stretch_mode == "viewport") { sml_sm = SceneTree::STRETCH_MODE_VIEWPORT; + // reset the snappers in viewport mode + Engine::get_singleton()->_snappers.initialize(Engine::get_singleton()->get_use_gpu_pixel_snap(), Engine::get_singleton()->get_snap_2d_transforms(), Engine::get_singleton()->get_snap_2d_viewports(), true); + } + SceneTree::StretchAspect sml_aspect = SceneTree::STRETCH_ASPECT_IGNORE; if (stretch_aspect == "keep") sml_aspect = SceneTree::STRETCH_ASPECT_KEEP; diff --git a/scene/2d/animated_sprite.cpp b/scene/2d/animated_sprite.cpp index 6769dcc0cdee..1c4e6897bc25 100644 --- a/scene/2d/animated_sprite.cpp +++ b/scene/2d/animated_sprite.cpp @@ -452,11 +452,8 @@ void AnimatedSprite::_notification(int p_what) { if (centered) ofs -= s / 2; - if (Engine::get_singleton()->get_snap_2d_transforms()) { - ofs = ofs.round(); - } else if (Engine::get_singleton()->get_use_gpu_pixel_snap()) { - ofs = ofs.floor(); - } + Engine::get_singleton()->get_snappers().snap_read_item(ofs); + Rect2 dst_rect(ofs, s); if (hflip) diff --git a/scene/2d/sprite.cpp b/scene/2d/sprite.cpp index 7040a1741ccb..fb8355d82a44 100644 --- a/scene/2d/sprite.cpp +++ b/scene/2d/sprite.cpp @@ -100,11 +100,7 @@ void Sprite::_get_rects(Rect2 &r_src_rect, Rect2 &r_dst_rect, bool &r_filter_cli if (centered) dest_offset -= frame_size / 2; - if (Engine::get_singleton()->get_snap_2d_transforms()) { - dest_offset = dest_offset.round(); - } else if (Engine::get_singleton()->get_use_gpu_pixel_snap()) { - dest_offset = dest_offset.floor(); - } + Engine::get_singleton()->get_snappers().snap_read_item(dest_offset); r_dst_rect = Rect2(dest_offset, frame_size); @@ -381,11 +377,8 @@ Rect2 Sprite::get_rect() const { Point2 ofs = offset; if (centered) ofs -= Size2(s) / 2; - if (Engine::get_singleton()->get_snap_2d_transforms()) { - ofs = ofs.round(); - } else if (Engine::get_singleton()->get_use_gpu_pixel_snap()) { - ofs = ofs.floor(); - } + + Engine::get_singleton()->get_snappers().snap_read_item(ofs); if (s == Size2(0, 0)) s = Size2(1, 1); diff --git a/servers/visual/visual_server_canvas.cpp b/servers/visual/visual_server_canvas.cpp index 060cd8b652e7..086635b62dd5 100644 --- a/servers/visual/visual_server_canvas.cpp +++ b/servers/visual/visual_server_canvas.cpp @@ -99,11 +99,18 @@ void VisualServerCanvas::_render_canvas_item(Item *p_canvas_item, const Transfor Rect2 rect = ci->get_rect(); Transform2D xform = ci->xform; - if (snap_2d_transforms) { - xform.elements[2] = xform.elements[2].round(); - } + + // opportunity to snap before AND after the p_transform is applied. + // for stretch_mode viewport, snapping before makes sense, + const Snappers &snappers = Engine::get_singleton()->get_snappers(); + snappers.snapper_canvas_item_pre.snap(xform.elements[2]); + xform = p_transform * xform; + // for stretch_mode 2d, we can snap after the scaling / zoom is applied, + // so we get sub-texel level snapping, which looks much better. + snappers.snapper_canvas_item_post.snap(xform.elements[2]); + Rect2 global_rect = xform.xform(rect); global_rect.position += p_clip_rect.position; @@ -1482,7 +1489,6 @@ VisualServerCanvas::VisualServerCanvas() { z_last_list = (RasterizerCanvas::Item **)memalloc(z_range * sizeof(RasterizerCanvas::Item *)); disable_scale = false; - snap_2d_transforms = Engine::get_singleton()->get_snap_2d_transforms(); } VisualServerCanvas::~VisualServerCanvas() { diff --git a/servers/visual/visual_server_canvas.h b/servers/visual/visual_server_canvas.h index 7503e9557256..19c3ff40b524 100644 --- a/servers/visual/visual_server_canvas.h +++ b/servers/visual/visual_server_canvas.h @@ -158,7 +158,6 @@ class VisualServerCanvas { RID_Owner canvas_light_owner; bool disable_scale; - bool snap_2d_transforms; private: void _render_canvas_item_tree(Item *p_canvas_item, const Transform2D &p_transform, const Rect2 &p_clip_rect, const Color &p_modulate, RasterizerCanvas::Light *p_lights); diff --git a/servers/visual/visual_server_viewport.cpp b/servers/visual/visual_server_viewport.cpp index 0c64be8cbd78..25c9551a25ba 100644 --- a/servers/visual/visual_server_viewport.cpp +++ b/servers/visual/visual_server_viewport.cpp @@ -41,23 +41,25 @@ static Transform2D _canvas_get_transform(VisualServerViewport::Viewport *p_viewp float scale = 1.0; - bool snap = Engine::get_singleton()->get_snap_2d_viewports(); + const Snappers &snappers = Engine::get_singleton()->get_snappers(); if (p_viewport->canvas_map.has(p_canvas->parent)) { Transform2D c_xform = p_viewport->canvas_map[p_canvas->parent].transform; - if (snap) { - c_xform.elements[2] = c_xform.elements[2].round(); - } + + snappers.snapper_viewport_parent_pre.snap(c_xform.elements[2]); + xf = xf * c_xform; scale = p_canvas->parent_scale; } Transform2D c_xform = p_canvas_data->transform; - if (snap) { - c_xform.elements[2] = c_xform.elements[2].round(); - } + + // opportunity to snap pre and post the transform being applied.. + // pre may be better for stretch_mode viewport, post better for stretch_mode 2D + snappers.snapper_viewport_pre.snap(c_xform.elements[2]); xf = xf * c_xform; + snappers.snapper_viewport_post.snap(xf.elements[2]); if (scale != 1.0 && !VSG::canvas->disable_scale) { Vector2 pivot = p_vp_size * 0.5;