Godot Version
4.7.2
Context
Hello, I’m trying to add and remove data from an image like a digital brush would. The intention is to use the output image data as the base for my smoothly interpolated marching squares algorithm. I have already managed to hook them up, and I am very pleased with the result:
For the interested, this is the script (beware that it uses data from an image instead of a dictionary)
extends Node2D
# Value on grid
@onready var image_manipulation = $Image_Manipulation
var cell_state: int # Marching squares "state"
const threshold: float = 0.4 # Threshold for point activation
# Resolution of the grid
@onready var res = $Image_Manipulation.res
@onready var rows = $Image_Manipulation.rows
@onready var cols = $Image_Manipulation.cols
@onready var screen_size = $Image_Manipulation.screen_size
# Circle for visualization
var circle_color: Color = Color.WHITE
const circle_size: float = 0.1
# Lines for visualization
const line_width: float = -1
const line_color: Color = Color.WHITE
#---------Built-in functions-------#
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
pass
# Called every frame. 'delta' is the elapsed time since the previous frame.
@warning_ignore("unused_parameter")
func _process(delta: float) -> void:
# Mouse location
var mouse_location: Vector2 = Vector2(round(get_local_mouse_position() / res))
if Input.is_action_pressed("mouse_left"): # Clicking left-mouse button
image_manipulation.add_paint(mouse_location)
queue_redraw()
if Input.is_action_pressed("mouse_right"): # Clicking right-mouse button
image_manipulation.remove_paint(mouse_location)
queue_redraw()
func _draw() -> void:
# draw_circles() # Draw circles to visualize value data
draw_lines() # Draw stuff with the value data
#---------Custom functions-------#
func value_get(key):
var key_int: Vector2i = Vector2i(key)
key_int = key_int.clamp(Vector2(0,0), Vector2(rows, cols) - Vector2(1.0, 1.0))
return image_manipulation.field.get_pixelv(
Vector2i(key_int.x, key_int.y)
).get_luminance() * threshold + 0.1
func draw_circles():
for c_num in range(cols):
for r_num in range(rows):
var dict_key = Vector2(r_num, c_num) # Positional key.
# Circle position, size and color
circle_color.v = value_get(dict_key) * 5 # Block color
draw_circle(
dict_key * res,
circle_size * res,
circle_color,
true,
-1,
false,
)
func draw_lines(): # Lines master function
for c_num in range(cols-1):
for r_num in range(rows-1):
# Defining corner positions for line draw.
# r_num and c_num are x, and y, and they start in the top-left.
var p1 := Vector2(r_num, c_num) # Top-left
var p2 := Vector2(r_num + 1, c_num) # Top-right
var p3 := Vector2(r_num + 1, c_num + 1) # Bottom-right
var p4 := Vector2(r_num, c_num +1) # Bottom_left
# Get an output ranging from 0 to 15 (marching squares states)
cell_state = get_cell_state(p1, p2, p3, p4)
# Links cell-states to certain lines
if cell_state == 1:
construct_line(p4, p3, p4, p1)
elif cell_state == 2:
construct_line(p3, p4, p3, p2)
elif cell_state == 3:
construct_line(p3, p2, p4, p1)
elif cell_state == 4:
construct_line(p2, p1, p2, p3)
elif cell_state == 5:
construct_line(p2, p1, p4, p1)
construct_line(p4, p3, p2, p3)
elif cell_state == 6:
construct_line(p2, p1, p3, p4)
elif cell_state == 7:
construct_line(p2, p1, p4, p1)
elif cell_state == 8:
construct_line(p1, p2, p1, p4)
elif cell_state == 9:
construct_line(p1, p2, p4, p3)
elif cell_state == 10:
construct_line(p1, p2, p3, p2)
construct_line(p3, p4, p1, p4)
elif cell_state == 11:
construct_line(p1, p2, p3, p2)
elif cell_state == 12:
construct_line(p1, p4, p2, p3)
elif cell_state == 13:
construct_line(p4, p3, p2, p3)
elif cell_state == 14:
construct_line(p3, p4, p1, p4)
elif cell_state == 15:
pass
# Returns number from binary, e.g. 0100 = 4 and 0010 = 2
func get_cell_state(p1, p2, p3, p4):
return int(
t_check(value_get(p1)) * 8
+ t_check(value_get(p2)) * 4
+ t_check(value_get(p3)) * 2
+ t_check(value_get(p4)) * 1)
func t_check(field_value): # Activation threshold check
if field_value > threshold:
return 1
else:
return 0
# Interpolate end and start, then draw line between a and b.
func construct_line(start_a, end_a, start_b, end_b):
var start: Vector2 = start_a
start = start.lerp(
end_a,
lerpf(1 - threshold / value_get(start_a), 1.0, value_get(end_a) / threshold)
)
var end: Vector2 = start_b
end = end.lerp(
end_b,
lerpf(1 - threshold / value_get(start_b), 1.0, value_get(end_b) / threshold)
)
draw_line(start * res, end * res, line_color, line_width)
Where the question comes in
The problem with my method blending a brush image onto the canvas seems that it is continuously making it brighter while I am holding the mouse. I am using Image.blend_rect(), which uses alpha-blending. All the blending-related methods of the Image class seem to be alpha-blending, and blit seems to be straight-up copying data without blending. So what should I do?
My idea to solve this, requires a lighten/max blend on top of the image.
Here is a small video of how I intend this to work:
Here is the current image manipulation script:
extends Node2D
# Screen
var screen_size: Vector2
# Grid-related
const res: int = 15
var cols: int
var rows: int
# Data objects and properties
var field: Image
var white_brush: Image
var black_brush: Image
var brush_rectangle: Rect2i
var brush_size: Vector2
# A texture just here to help me debug
var debug_texture: ImageTexture
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
screen_size = get_viewport_rect().size # Screen size
rows = int(floor(screen_size.x / res)) + 1 # Max rows amount.
cols = int(floor(screen_size.y / res)) + 1 # Max columns amount
# Create canvas
# var field_path := "res://Assets/debug_image.png"
# field = load(field_path)
# field.convert(Image.FORMAT_LA8)
field = Image.create_empty(rows, cols, false, Image.FORMAT_LA8)
field.fill(Color.BLACK)
create_brushes()
func create_brushes():
# Create white white_brush
var white_brush_path := "res://Assets/brush_white.png"
# I want to edit the pixels so I assign this to an image
white_brush = load(white_brush_path)
# Convert to simple format
white_brush.convert(Image.FORMAT_LA8)
# Create black white_brush
var black_brush_path := "res://Assets/brush_black.png"
# I want to edit the pixels so I assign this to an image
black_brush = load(black_brush_path)
# Convert to simple format
black_brush.convert(Image.FORMAT_LA8)
# Defining white_brush boundary used in blending
brush_size = white_brush.get_size() # Used for offsetting the mouse
brush_rectangle = Rect2i(Vector2i(0, 0), brush_size)
# print(field.get_size())
# Called every frame. 'delta' is the elapsed time since the previous frame.
@warning_ignore("unused_parameter")
func _process(delta: float) -> void:
pass
func add_paint(mouse_location): # Use mouse to paint values
field.blend_rect(white_brush, brush_rectangle, mouse_location - brush_size / 2)
# display_image()
func remove_paint(mouse_location): # Use mouse to remove values
field.blend_rect(black_brush, brush_rectangle, mouse_location - brush_size / 2)
# display_image()
# print(field.get_pixelv(Vector2i(rows-1, cols-1)).get_luminance())
func display_image():
debug_texture = ImageTexture.create_from_image(field)
$Canvas.position = Vector2i(screen_size / 2)
$Canvas.scale = Vector2(1, 1) * res
$Canvas.texture = debug_texture
Should I write a shader for this? For this question I’ve been researching shaders a bit and it seems that it is possible to save a shader output to an image (which in turn can be used as data values for my marching squares)?
By the way, this is a continuation of this question post I made before: What approach for "smooth" 2D terrain that can be dug into / subtracted from - #5 by Puzzling_Detective
If you have better ideas regarding the terrain removal method (as a drilling vehicle would), I am keen to hear them!