1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 17:19:50 +00:00

[GLES 3 only] Add OmniScale for scaling and rotation

This commit is contained in:
Emmanouil Papadeas 2023-01-11 17:54:33 +02:00
parent 8423ce7d42
commit 08e00d3c31
5 changed files with 411 additions and 12 deletions

View file

@ -1,7 +1,15 @@
extends Node
enum GradientDirection { TOP, BOTTOM, LEFT, RIGHT }
# Continuation from Image.Interpolation
enum Interpolation { SCALE3X = 5, CLEANEDGE = 6, OMNISCALE = 7 }
var clean_edge_shader: Shader = preload("res://src/Shaders/Rotation/cleanEdge.gdshader")
var omniscale_shader: Shader
func _ready() -> void:
if OS.get_current_video_driver() == OS.VIDEO_DRIVER_GLES3:
omniscale_shader = load("res://src/Shaders/Rotation/OmniScale.gdshader")
# Algorithm based on http://members.chello.at/easyfilter/bresenham.html
@ -428,7 +436,7 @@ func scale_image(width: int, height: int, interpolation: int) -> void:
continue
var sprite := Image.new()
sprite.copy_from(f.cels[i].image)
if interpolation == 5: # scale3x
if interpolation == Interpolation.SCALE3X:
var times: Vector2 = Vector2(
ceil(width / (3.0 * sprite.get_width())),
ceil(height / (3.0 * sprite.get_height()))
@ -436,10 +444,14 @@ func scale_image(width: int, height: int, interpolation: int) -> void:
for _j in range(max(times.x, times.y)):
sprite.copy_from(scale_3x(sprite))
sprite.resize(width, height, 0)
elif interpolation == 6: # cleanEdge
elif interpolation == Interpolation.CLEANEDGE:
var params := {"angle": 0, "slope": true, "cleanup": true, "preview": false}
var gen := ShaderImageEffect.new()
gen.generate_image(sprite, clean_edge_shader, params, Vector2(width, height))
elif interpolation == Interpolation.OMNISCALE and omniscale_shader:
var params := {"angle": 0, "preview": false}
var gen := ShaderImageEffect.new()
gen.generate_image(sprite, omniscale_shader, params, Vector2(width, height))
else:
sprite.resize(width, height, interpolation)
Global.current_project.undo_redo.add_do_property(f.cels[i].image, "data", sprite.data)

View file

@ -0,0 +1,350 @@
// No AA version from https://github.com/deakcor/godot-omniscale/blob/5dfee6e89cd955dd01dccfe70c9979f9b55bb1bf/OmniScale.shader
// Edited slightly by Overloaded to add rotation support for Pixelorama
shader_type canvas_item;
//#version 130
// OmniScale
// by Lior Halphon
// original GLSL code from hunterk by way of RetroArch
// ported to Godot3 shader language by Nobuyuki
//
// MIT License
//
// Copyright (c) 2015-2016 Lior Halphon
//
// 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.
//
uniform int ScaleMultiplier : hint_range(0, 100) = 4;
// vertex compatibility #defines
// #define vTexCoord TEX0.xy
// #define outsize vec4(OutputSize, 1.0 / OutputSize)
// Pixelorama-specific uniforms
uniform float angle;
uniform sampler2D selection_tex;
uniform vec2 selection_pivot;
uniform vec2 selection_size;
uniform bool preview;
bool is_different(vec4 a, vec4 b)
{
return distance(a,b)>0.1;
}
// This define could've made code a ton more readable if godot shaders supported it, but it doesn't.
// 55 occurances of this in code; use regexp to turn it back once supported?
// #define P(m, r) ((pattern & (m)) == (r))
vec4 scale(sampler2D image, vec2 coord, vec2 pxSize) {
vec2 OutputSize = vec2(float(ScaleMultiplier), float(ScaleMultiplier));
vec2 textureDimensions = vec2(1.0,1.0);
// o = offset, the width of a pixel
vec2 o = pxSize;
vec2 texCoord = coord;
// We always calculate the top left quarter. If we need a different quarter, we flip our co-ordinates */
// p = the position within a pixel [0...1]
vec2 p = fract(texCoord / pxSize);
if (p.x > 0.5) {
o.x = -o.x;
p.x = 1.0 - p.x;
}
if (p.y > 0.5) {
o.y = -o.y;
p.y = 1.0 - p.y;
}
vec4 w0 = texture(image, texCoord + vec2( -o.x, -o.y));
vec4 w1 = texture(image, texCoord + vec2( 0, -o.y));
vec4 w2 = texture(image, texCoord + vec2( o.x, -o.y));
vec4 w3 = texture(image, texCoord + vec2( -o.x, 0));
vec4 w4 = texture(image, texCoord + vec2( 0, 0));
vec4 w5 = texture(image, texCoord + vec2( o.x, 0));
vec4 w6 = texture(image, texCoord + vec2( -o.x, o.y));
vec4 w7 = texture(image, texCoord + vec2( 0, o.y));
vec4 w8 = texture(image, texCoord + vec2( o.x, o.y));
int pattern = 0;
if (is_different(w0, w4)) pattern |= 1 << 0;
if (is_different(w1, w4)) pattern |= 1 << 1;
if (is_different(w2, w4)) pattern |= 1 << 2;
if (is_different(w3, w4)) pattern |= 1 << 3;
if (is_different(w5, w4)) pattern |= 1 << 4;
if (is_different(w6, w4)) pattern |= 1 << 5;
if (is_different(w7, w4)) pattern |= 1 << 6;
if (is_different(w8, w4)) pattern |= 1 << 7;
if ((((pattern & (191)) == (55)) || ((pattern & (219)) == (19))) && is_different(w1, w5))
return mix(w4, w3, 0.5 - p.x>0.5?1.0:0.0);
if ((((pattern & (219)) == (73)) || ((pattern & (239)) == (109))) && is_different(w7, w3))
return mix(w4, w1, 0.5 - p.y>0.5?1.0:0.0);
if ((((pattern & (11)) == (11)) || ((pattern & (254)) == (74)) || ((pattern & (254)) == (26))) && is_different(w3, w1))
return w4;
if ((((pattern & (111)) == (42)) || ((pattern & (91)) == (10)) || ((pattern & (191)) == (58)) || ((pattern & (223)) == (90)) ||
((pattern & (159)) == (138)) || ((pattern & (207)) == (138)) || ((pattern & (239)) == (78)) || ((pattern & (63)) == (14)) ||
((pattern & (251)) == (90)) || ((pattern & (187)) == (138)) || ((pattern & (127)) == (90)) || ((pattern & (175)) == (138)) ||
((pattern & (235)) == (138))) && is_different(w3, w1))
return mix(w4, mix(w4, w0, 0.5 - p.x>0.5?1.0:0.0), 0.5 - p.y>0.5?1.0:0.0);
if (((pattern & (11)) == (8)))
return mix(mix(w0 * 0.375 + w1 * 0.25 + w4 * 0.375, w4 * 0.5 + w1 * 0.5, p.x * 2.0>0.5?1.0:0.0), w4, p.y * 2.0>0.5?1.0:0.0);
if (((pattern & (11)) == (2)))
return mix(mix(w0 * 0.375 + w3 * 0.25 + w4 * 0.375, w4 * 0.5 + w3 * 0.5, p.y * 2.0>0.5?1.0:0.0), w4, p.x * 2.0>0.5?1.0:0.0);
if (((pattern & (47)) == (47))) {
float dist = length(p - vec2(0.5));
float pixel_size = length(1.0 / (OutputSize / textureDimensions));
if (dist < 0.5 - pixel_size / 2.0) {
return w4;
}
vec4 r;
if (is_different(w0, w1) || is_different(w0, w3)) {
r = mix(w1, w3, p.y - p.x + 0.5>0.5?1.0:0.0);
}
else {
r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0>0.5?1.0:0.0), w1, p.x * 2.0>0.5?1.0:0.0);
}
if (dist > 0.5 + pixel_size / 2.0) {
return r;
}
return mix(w4, r, (dist - 0.5 + pixel_size / 2.0) / pixel_size>0.5?1.0:0.0);
}
if (((pattern & (191)) == (55)) || ((pattern & (219)) == (19))) {
float dist = p.x - 2.0 * p.y;
float pixel_size = length(1.0 / (OutputSize / textureDimensions)) * sqrt(5.0);
if (dist > pixel_size / 2.0) {
return w1;
}
vec4 r = mix(w3, w4, p.x + 0.5>0.5?1.0:0.0);
if (dist < -pixel_size / 2.0) {
return r;
}
return mix(r, w1, (dist + pixel_size / 2.0) / pixel_size>0.5?1.0:0.0);
}
if (((pattern & (219)) == (73)) || ((pattern & (239)) == (109))) {
float dist = p.y - 2.0 * p.x;
float pixel_size = length(1.0 / (OutputSize / textureDimensions)) * sqrt(5.0);
if (p.y - 2.0 * p.x > pixel_size / 2.0) {
return w3;
}
vec4 r = mix(w1, w4, p.x + 0.5>0.5?1.0:0.0);
if (dist < -pixel_size / 2.0) {
return r;
}
return mix(r, w3, (dist + pixel_size / 2.0) / pixel_size>0.5?1.0:0.0);
}
if (((pattern & (191)) == (143)) || ((pattern & (126)) == (14))) {
float dist = p.x + 2.0 * p.y;
float pixel_size = length(1.0 / (OutputSize / textureDimensions)) * sqrt(5.0);
if (dist > 1.0 + pixel_size / 2.0) {
return w4;
}
vec4 r;
if (is_different(w0, w1) || is_different(w0, w3)) {
r = mix(w1, w3, p.y - p.x + 0.5>0.5?1.0:0.0);
}
else {
r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0>0.5?1.0:0.0), w1, p.x * 2.0>0.5?1.0:0.0);
}
if (dist < 1.0 - pixel_size / 2.0) {
return r;
}
return mix(r, w4, (dist + pixel_size / 2.0 - 1.0) / pixel_size>0.5?1.0:0.0);
}
if (((pattern & (126)) == (42)) || ((pattern & (239)) == (171))) {
float dist = p.y + 2.0 * p.x;
float pixel_size = length(1.0 / (OutputSize / textureDimensions)) * sqrt(5.0);
if (p.y + 2.0 * p.x > 1.0 + pixel_size / 2.0) {
return w4;
}
vec4 r;
if (is_different(w0, w1) || is_different(w0, w3)) {
r = mix(w1, w3, p.y - p.x + 0.5>0.5?1.0:0.0);
}
else {
r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0>0.5?1.0:0.0), w1, p.x * 2.0>0.5?1.0:0.0);
}
if (dist < 1.0 - pixel_size / 2.0) {
return r;
}
return mix(r, w4, (dist + pixel_size / 2.0 - 1.0) / pixel_size>0.5?1.0:0.0);
}
if (((pattern & (27)) == (3)) || ((pattern & (79)) == (67)) || ((pattern & (139)) == (131)) || ((pattern & (107)) == (67)))
return mix(w4, w3, 0.5 - p.x>0.5?1.0:0.0);
if (((pattern & (75)) == (9)) || ((pattern & (139)) == (137)) || ((pattern & (31)) == (25)) || ((pattern & (59)) == (25)))
return mix(w4, w1, 0.5 - p.y>0.5?1.0:0.0);
if (((pattern & (251)) == (106)) || ((pattern & (111)) == (110)) || ((pattern & (63)) == (62)) || ((pattern & (251)) == (250)) ||
((pattern & (223)) == (222)) || ((pattern & (223)) == (30)))
return mix(w4, w0, (1.0 - p.x - p.y) / 2.0>0.5?1.0:0.0);
if (((pattern & (79)) == (75)) || ((pattern & (159)) == (27)) || ((pattern & (47)) == (11)) ||
((pattern & (190)) == (10)) || ((pattern & (238)) == (10)) || ((pattern & (126)) == (10)) || ((pattern & (235)) == (75)) ||
((pattern & (59)) == (27))) {
float dist = p.x + p.y;
float pixel_size = length(1.0 / (OutputSize / textureDimensions));
if (dist > 0.5 + pixel_size / 2.0) {
return w4;
}
vec4 r;
if (is_different(w0, w1) || is_different(w0, w3)) {
r = mix(w1, w3, p.y - p.x + 0.5>0.5?1.0:0.0);
}
else {
r = mix(mix(w1 * 0.375 + w0 * 0.25 + w3 * 0.375, w3, p.y * 2.0>0.5?1.0:0.0), w1, p.x * 2.0>0.5?1.0:0.0);
}
if (dist < 0.5 - pixel_size / 2.0) {
return r;
}
return mix(r, w4, (dist + pixel_size / 2.0 - 0.5) / pixel_size>0.5?1.0:0.0);
}
if (((pattern & (11)) == (1)))
return mix(mix(w4, w3, 0.5 - p.x>0.5?1.0:0.0), mix(w1, (w1 + w3) / 2.0, 0.5 - p.x>0.5?1.0:0.0), 0.5 - p.y>0.5?1.0:0.0);
if (((pattern & (11)) == (0)))
return mix(mix(w4, w3, 0.5 - p.x>0.5?1.0:0.0), mix(w1, w0, 0.5 - p.x>0.5?1.0:0.0), 0.5 - p.y>0.5?1.0:0.0);
float dist = p.x + p.y;
float pixel_size = length(1.0 / (OutputSize / textureDimensions));
if (dist > 0.5 + pixel_size / 2.0)
return w4;
/* We need more samples to "solve" this diagonal */
vec4 x0 = texture(image, texCoord + vec2( -o.x * 2.0, -o.y * 2.0));
vec4 x1 = texture(image, texCoord + vec2( -o.x , -o.y * 2.0));
vec4 x2 = texture(image, texCoord + vec2( 0.0 , -o.y * 2.0));
vec4 x3 = texture(image, texCoord + vec2( o.x , -o.y * 2.0));
vec4 x4 = texture(image, texCoord + vec2( -o.x * 2.0, -o.y ));
vec4 x5 = texture(image, texCoord + vec2( -o.x * 2.0, 0.0 ));
vec4 x6 = texture(image, texCoord + vec2( -o.x * 2.0, o.y ));
if (is_different(x0, w4)) pattern |= 1 << 8;
if (is_different(x1, w4)) pattern |= 1 << 9;
if (is_different(x2, w4)) pattern |= 1 << 10;
if (is_different(x3, w4)) pattern |= 1 << 11;
if (is_different(x4, w4)) pattern |= 1 << 12;
if (is_different(x5, w4)) pattern |= 1 << 13;
if (is_different(x6, w4)) pattern |= 1 << 14;
int diagonal_bias = -7;
while (pattern != 0) {
diagonal_bias += pattern & 1;
pattern >>= 1;
}
if (diagonal_bias <= 0) {
vec4 r = mix(w1, w3, p.y - p.x + 0.5>0.5?1.0:0.0);
if (dist < 0.5 - pixel_size / 2.0) {
return r;
}
return mix(r, w4, (dist + pixel_size / 2.0 - 0.5) / pixel_size>0.5?1.0:0.0);
}
return w4;
}
vec2 rotate(vec2 uv, vec2 pivot, float ratio) { // Taken from NearestNeighbour shader
// Scale and center image
uv.x -= pivot.x;
uv.x *= ratio;
uv.x += pivot.x;
// Rotate image
uv -= pivot;
uv = vec2(cos(angle) * uv.x + sin(angle) * uv.y,
-sin(angle) * uv.x + cos(angle) * uv.y);
uv.x /= ratio;
uv += pivot;
return uv;
}
void fragment()
{
vec4 original = texture(TEXTURE, UV);
float selection = texture(selection_tex, UV).a;
vec2 size = 1.0 / TEXTURE_PIXEL_SIZE;
vec2 pivot = selection_pivot / size; // Normalize pivot position
float ratio = size.x / size.y; // Resolution ratio
vec2 pixelated_uv = floor(UV * size) / (size - 1.0); // Pixelate UV to fit resolution
vec2 rotated_uv;
if (preview) {
rotated_uv = rotate(pixelated_uv, pivot, ratio);
}
else {
rotated_uv = rotate(UV, pivot, ratio);
}
vec4 c;
c = scale(TEXTURE, rotated_uv, TEXTURE_PIXEL_SIZE);
// Taken from NearestNeighbour shader
c.a *= texture(selection_tex, rotated_uv).a; // Combine with selection mask
// Make a border to prevent stretching pixels on the edge
vec2 border_uv = rotated_uv;
// Center the border
border_uv -= 0.5;
border_uv *= 2.0;
border_uv = abs(border_uv);
float border = max(border_uv.x, border_uv.y); // This is a rectangular gradient
border = floor(border - TEXTURE_PIXEL_SIZE.x); // Turn the grad into a rectangle shape
border = 1.0 - clamp(border, 0.0, 1.0); // Invert the rectangle
float mask = mix(selection, 1.0, 1.0 - ceil(original.a)); // Combine selection mask with area outside original
// Combine original and rotated image only when intersecting, otherwise just pure rotated image.
COLOR.rgb = mix(mix(original.rgb, c.rgb, c.a * border), c.rgb, mask);
COLOR.a = mix(original.a, 0.0, selection); // Remove alpha on the selected area
COLOR.a = mix(COLOR.a, 1.0, c.a * border); // Combine alpha of original image and rotated
//c.a = step(0.5,c.a);
//COLOR = c;
}

View file

@ -1,5 +1,7 @@
extends ImageEffect
enum { ROTXEL_SMEAR, CLEANEDGE, OMNISCALE, NNS, NN, ROTXEL, URD }
var live_preview: bool = true
var rotxel_shader: Shader
var nn_shader: Shader = preload("res://src/Shaders/Rotation/NearestNeighbour.shader")
@ -20,15 +22,16 @@ onready var wait_time_slider: ValueSlider = $VBoxContainer/WaitTime
func _ready() -> void:
# Algorithms are arranged according to their speed
if OS.get_name() != "HTML5":
type_option_button.add_item("Rotxel with Smear")
type_option_button.add_item("Rotxel with Smear", ROTXEL_SMEAR)
rotxel_shader = load("res://src/Shaders/Rotation/SmearRotxel.shader")
type_option_button.add_item("cleanEdge")
type_option_button.add_item("Nearest neighbour (Shader)")
type_option_button.add_item("Nearest neighbour")
type_option_button.add_item("Rotxel")
type_option_button.add_item("Upscale, Rotate and Downscale")
type_option_button.add_item("cleanEdge", CLEANEDGE)
type_option_button.add_item("OmniScale", OMNISCALE)
type_option_button.set_item_disabled(OMNISCALE, not DrawingAlgos.omniscale_shader)
type_option_button.add_item("Nearest neighbour (Shader)", NNS)
type_option_button.add_item("Nearest neighbour", NN)
type_option_button.add_item("Rotxel", ROTXEL)
type_option_button.add_item("Upscale, Rotate and Downscale", URD)
type_option_button.emit_signal("item_selected", 0)
@ -56,6 +59,7 @@ func decide_pivot() -> void:
if (
type_option_button.text != "Nearest neighbour (Shader)"
and type_option_button.text != "cleanEdge"
and type_option_button.text != "OmniScale"
):
if int(size.x) % 2 == 0:
pivot.x -= 0.5
@ -71,6 +75,7 @@ func decide_pivot() -> void:
if (
type_option_button.text != "Nearest neighbour (Shader)"
and type_option_button.text != "cleanEdge"
and type_option_button.text != "OmniScale"
):
# Pivot correction in case of even size
if int(selection_rectangle.end.x - selection_rectangle.position.x) % 2 == 0:
@ -145,6 +150,22 @@ func commit_action(cel: Image, _project: Project = Global.current_project) -> vo
var gen := ShaderImageEffect.new()
gen.generate_image(cel, clean_edge_shader, params, _project.size)
yield(gen, "done")
"OmniScale":
var params := {
"angle": angle,
"selection_tex": selection_tex,
"selection_pivot": pivot,
"selection_size": selection_size,
"preview": true
}
if !confirmed:
for param in params:
preview.material.set_shader_param(param, params[param])
else:
params["preview"] = false
var gen := ShaderImageEffect.new()
gen.generate_image(cel, DrawingAlgos.omniscale_shader, params, _project.size)
yield(gen, "done")
"Nearest neighbour (Shader)":
var params := {
"angle": angle,
@ -177,6 +198,7 @@ func _type_is_shader() -> bool:
type_option_button.text == "Nearest neighbour (Shader)"
or type_option_button.text == "Rotxel with Smear"
or type_option_button.text == "cleanEdge"
or type_option_button.text == "OmniScale"
)
@ -191,6 +213,11 @@ func _on_TypeOptionButton_item_selected(_id: int) -> void:
sm.shader = clean_edge_shader
preview.set_material(sm)
smear_options.visible = false
elif type_option_button.text == "OmniScale":
var sm := ShaderMaterial.new()
sm.shader = DrawingAlgos.omniscale_shader
preview.set_material(sm)
smear_options.visible = false
elif type_option_button.text == "Nearest neighbour (Shader)":
var sm := ShaderMaterial.new()
sm.shader = nn_shader

View file

@ -10,6 +10,19 @@ onready var interpolation_type: OptionButton = find_node("InterpolationType")
onready var ratio_box: BaseButton = find_node("AspectRatioButton")
func _ready() -> void:
interpolation_type.add_item("Nearest", Image.INTERPOLATE_NEAREST)
interpolation_type.add_item("Bilinear", Image.INTERPOLATE_BILINEAR)
interpolation_type.add_item("Cubic", Image.INTERPOLATE_CUBIC)
interpolation_type.add_item("Trilinear", Image.INTERPOLATE_TRILINEAR)
interpolation_type.add_item("Lanczos", Image.INTERPOLATE_LANCZOS)
interpolation_type.add_item("Scale3X", DrawingAlgos.Interpolation.SCALE3X)
interpolation_type.add_item("cleanEdge", DrawingAlgos.Interpolation.CLEANEDGE)
interpolation_type.add_item("OmniScale", DrawingAlgos.Interpolation.OMNISCALE)
if not DrawingAlgos.omniscale_shader:
interpolation_type.set_item_disabled(DrawingAlgos.Interpolation.OMNISCALE, true)
func _on_ScaleImage_about_to_show() -> void:
Global.canvas.selection.transform_content_confirm()
aspect_ratio = Global.current_project.size.x / Global.current_project.size.y

View file

@ -207,9 +207,6 @@ margin_right = 234.0
margin_bottom = 20.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
text = "Nearest"
items = [ "Nearest", null, false, 0, null, "Bilinear", null, false, 1, null, "Cubic", null, false, 2, null, "Trilinear", null, false, 3, null, "Lanczos", null, false, 4, null, "Scale3X", null, false, 5, null, "cleanEdge", null, false, 6, null ]
selected = 0
[connection signal="about_to_show" from="." to="." method="_on_ScaleImage_about_to_show"]
[connection signal="confirmed" from="." to="." method="_on_ScaleImage_confirmed"]