As someone new to game development, Viewports were a concept I had a hard time wrapping my head around. For me, it was easier to imagine a Viewport as a projection screen, or a canvas, onto which the pixels of the game objects in the world are drawn. As soon as pixels are drawn in a Viewport, we can do a variety of things with a Viewport’s contents, such as displaying them with a Camera or using them like Textures. We can leverage the latter to dynamically create Images from ViewportTextures that we can fill our photo album with. This post aims to provide a high level overview of a basic in-game camera and photo album implementation.
Taking a screenshot in-game is fairly straightforward — since we can access a Viewport’s texture, we can simply invoke the Viewport’s (in our case, this will be the root Viewport)
get_texture() method to grab the Viewport’s contents as a Texture. Note, however, that due to OpenGL internals, the resulting texture from the capture will be flipped vertically. Using
Texture.get_data(), we can obtain an Image of our screen capture and simply use
Image.flip_y() to flip it back to the orientation that we expected it to be in.
# Calling get_data() on the result gives us an Image we can call flip_y() on. var img = get_viewport().get_texture().get_data() img.flip_y()
We can create a new Texture from the flipped Image and use it however we want. For example, you could set it as the texture of a Sprite. These examples come from, by the way, from the Godot Documentation itself!
var texture = ImageTexture.new() texture.create_from_image(img) $Sprite.texture = texture # Sprite will show our viewport capture!
So far we’re only working with a screen capture of the entire Viewport. But what if we want to crop it, so that we can save just a small area of the Viewport?
One solution would be to create a rectangular “zone” that we can use to cut out the area we want from the full Viewport capture. We can save this “zone” as a separate scene that we can dynamically instantiate in the game whenever the player activates the camera.
In Godot, this crop zone can be easily represented by a Rect2. To properly instantiate a Rect2, we need to give it a position and a size.
The size of the Rect2 would be the desired dimensions of the cropped image. I wanted the images to fit into the photo frames I had drawn for the game, so I set the size of the rectangular zone to match the dimensions of the photo frame.
To represent the zone visually in-game, we’ll use a TextureRect that will represent the camera’s boundaries and will follow the player’s cursor when the camera is activated. The TextureRect will match the dimensions of our Rect2, with the 4 corners of the rectangle are drawn in black.
As for the Rect2’s position, we need to track the player’s mouse position as soon as they activate the camera in-game. That’s quite simple! Our “zone” scene should be set up with an
_input() method that looks for InputEventMouseMotion events and updates the positions of our TextureRect (so the player sees the camera boundary moving) as well as our Rect2 to match the InputEvent’s position every time the player moves their mouse.
Putting that all together, we’d roughly have the following —
# Dimensions of the crop_zone match our camera TextureRect's size var crop_zone:Rect2 = Rect2(Vector2.ZERO, rect_size) func _input(event): if event is InputEventMouseMotion: # Update our TextureRect's position to the event position. # An additional adjustment is made to put the rectangle at the # center of the cursor, instead of at the top-left corner. rect_position = -0.5 * rect_size + event.position crop_zone.position = rect_position
Note, due to how the scene is set up, rect_position can be used here since the zone scene is instanced under a CanvasLayer
Lastly, we want the camera to react when the player clicks on an area in the game. This is when we do the cropping magic – taking the initial (full) screenshot, cutting out the “cropped” area, and copying the cropped data into a new Image.
With our cropping zone set up, we can use
Image.blit_rect() to “cut” out the cropped area of the Viewport capture into a new Image. The crop zone’s position should have been regularly updated by our
_input() method, so it should already be up-to-date once the player clicks the mouse.
Then, on mouse click, we:
- Take a screenshot
- Create a new Image with the cropped dimensions
Image.blit_rect()to copy the area under the crop zone from the screenshot
That corresponds roughly to following snippet —
# Our Viewport capture from earlier var screenshot_img = get_viewport().get_texture().get_data() screenshot_img.flip_y() var cropped_img:Image = Image.new() cropped_img.create(crop_zone.size.x, crop_zone.size.y, false, screenshot_img.get_format()) cropped_img.blit_rect(screenshot_img, crop_zone, Vector2.ZERO)
Ta-da! Now we have a cropped image we can use however we like.
Displaying and Saving Photos
Now that our cropped Images are accessible, it’s fairly straightforward to store them in a data structure where we can retrieve or manipulate them. The photo album is implemented as a Godot Resource that contains a simple array of Images (which are also Resources!), which makes it trivial to save and load the photo album from disk.
To present the contents of the photo album to the player, we can create a separate photo album scene (not to be confused with the photo album array) that contains a GridContainer whose size will correspond to the max number of slots in the photo album. The elements of the grid (i.e., its children) are individual photo frames, implemented as a separate scene. This separate photo frame scene will have an empty TextureRect (which we’ll refer to as
$Photo) placed in the center to dynamically display the Images we have saved in the photo album.
Each photo frame’s
_ready() function basically queries its place in the grid by calling
get_index(), which returns its position under its GridContainer parent. Using its index, we can check to see if the corresponding index in our photo album array contains an Image. If so, we can update the texture of the empty TextureRect to be the photo!
func _ready(): var idx = get_index() var photo = photo_album.photos[idx] if photo is Image: var tex = ImageTexture.new() tex.create_from_image(photo) $Photo.texture = tex # Update the empty TextureRect
Once the photo album scene is instanced into the tree, all the photo frames update themselves automatically based data from on the photo album array. Hooray!
Last but not least, we want to save the photos to disk after exiting the game. Luckily, the Image class already provides a useful method for this —
Image.save_png(). Additionally, since our photo album is a Resource, saving both our photo album data and PNG versions of our photos then becomes quite trivial!
# Save our photo album resource to disk ResourceSaver.save(photo_album_save_path, photo_album) # Save each photo individually as a PNG file for idx in range(0, photo_album.photos.size()): var photo = photo_album.photos[idx] if photo is Image: photo.save_png(photo_album_save_path + str(idx) + ".png")
This post only scratches the surface of what we can do with an in-game photo album – for example photo deletion wasn’t covered (that would make this post even longer!), in addition to photo swapping, and extra features like sharing to social media can be implemented.
The Resource-based photo album implementation is heavily based off of Heartbeast’s Inventory tutorial. Even though the tutorial is about creating a grid-based inventory, it can be generalized to implement any sort of Resource-based grid display, so please do check it out for a step-by-step guide!
That’s all for now. Thanks for reading!