Extract a region of an image and translate it into a puzzle shape

I’m currently creating a jigsaw puzzle game and I’m a bit stuck on something apparently easy, I want to get a new image with a predetermined shape using another image as mask (an svg) but the resulting image is zero and I get the following error:

jigsaw_core.gd:38 @ _ready(): Condition “dsize == 0” is true. (line 38 is the line where I use blit_rect_mask)

Basically I want to extract a piece of image and display it in a jigsaw piece.

This is the svg image:
image

@export var puzzle_width := 9
@export var puzzle_height := 5
@export var puzzle_scale := Vector2i.ONE

@onready var puzzle_image: TextureRect = $PuzzleImage
@onready var puzzle_image_dimensions := Utilities.get_texture_dimensions(image_to_puzzle)


const PUZZLE_SHAPE = preload("res://components/algorithms/jigsaw_puzzle/assets/pieces/puzzle_shape.svg")


func _ready():
    puzzle_image.texture = image_to_puzzle
    puzzle_image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
    puzzle_image.custom_minimum_size = puzzle_image_dimensions.size * puzzle_scale
    
    divide_image_into_pieces()
    
    var current_puzzle_image = puzzle_image.texture.get_image()
    var puzzle_shape_image = PUZZLE_SHAPE.get_image() 
    var puzzle_shape_size = puzzle_shape_image.get_size()

    var piece_shape = Image.new()
    piece_shape.create(puzzle_shape_size.x, puzzle_shape_size.y, true, Image.FORMAT_RGBA8)
    piece_shape.fill(Color.TRANSPARENT)
    piece_shape.blit_rect_mask(current_puzzle_image, puzzle_shape_image, Rect2(Vector2.ZERO, puzzle_shape_size), Vector2.ZERO)
    
    ## TEST PURPOSES
    var sprite = Sprite2D.new()
    sprite.texture = ImageTexture.create_from_image(piece_shape)
    add_child(sprite)

You should get a warning when running this:

The function “create()” is a static function but was called from an instance. Instead, it should be directly called from the type: “Image.create()”.

So instead of doing this:

var piece_shape = Image.new()
piece_shape.create(puzzle_shape_size.x, puzzle_shape_size.y, true, Image.FORMAT_RGBA8)

simply do this:

var piece_shape = Image.create(puzzle_shape_size.x, puzzle_shape_size.y, true, Image.FORMAT_RGBA8)
1 Like

Hi @njamster, I appreciate your help!

In the end I was able to get it and this is the final code I got:

func _ready():
	var puzzle_image = image_to_puzzle.get_image()
	var mask_image: Image = load("res://components/algorithms/jigsaw_puzzle/assets/pieces/puzzle_shape.svg").get_image()
	puzzle_image.decompress()
	puzzle_image.convert(mask_image.get_format())

	var puzzle_shape_image = Image.create(mask_image.get_width(), mask_image.get_height(), false, mask_image.get_format())
	puzzle_shape_image.fill(Color.TRANSPARENT)
	puzzle_shape_image.blit_rect_mask(
		puzzle_image.get_region(Rect2i(Vector2i.ZERO, mask_image.get_size())), # Trozo de imagen original misma size que la mask (pieza de puzzle)
		mask_image,
		Rect2i(Vector2i.ZERO, mask_image.get_size()), 
		Vector2i.ZERO
	)

	### TEST PURPOSES
	var sprite = Sprite2D.new()
	sprite.texture = ImageTexture.create_from_image(puzzle_shape_image)
	sprite.position = get_viewport().size / 2
	sprite.scale *= 0.5
	add_child(sprite)

victory

One thing to think about doing is to use your own canvas material shader in a material to do this, where it would have two texture inputs, the whole puzzle image, then a slot for the shape mask, then with a few exposed parameters, you would feed that material the offset coordinates to shift the mapping of the whole puzzle image, etc.

Then you wouldn’t have to do the extra image creation, which I imagine you’d need to do for all pieces, which could be time consuming in ms to bake, and memory intensive especially if you want high resolution pieces.

1 Like

Hey @roc4u1000 thanks for your contribution. For now to generate about 500 pieces, there is not a big memory consumption since I have the parts I use as a mask preloaded.

I have also tried your alternative with assigning a ShaderMaterial where I pass the mask as a texture instead of creating it by code.

shader_type canvas_item;

uniform sampler2D mask;

void fragment() {
	vec4 col = texture(TEXTURE, UV);
	vec4 vmask = texture(mask, UV);
	
	col.a = vmask.a;
	COLOR = col;
}

I don’t see any remarkable differences for now in terms of performance but certainly assigning a ShaderMaterial is more direct and readable.

1 Like

For the memory part I was referring to the cost of the generated images in runtime memory, the generated piece image puzzle_shape_image that are assigned to all of those 500 sprites. All of that would be in memory during the game, as well as the full puzzle_image source texture and then all of the source mask_image textures for all of pieces.

If you just had the puzzle_image texture in memory and all of the mask_image textures it should be much less runtime memory overall, but that would depend on the resulting resolution of those generated piece images and how many you make.

For the perf, drawing them either way is cheap and performant, I was thinking how much ms it would take to generate all of those puzzle piece images, but it is still minimal though I wonder about generating 500 of them and if there are any delays in gameplay that would be a consideration, and the ways around doing that if it is a problem.