Automated Side Images with Circular Cropping

A place for Ren'Py tutorials and reusable Ren'Py code.
Forum rules
Do not post questions here!

This forum is for example code you want to show other people. Ren'Py questions should be asked in the Ren'Py Questions and Announcements forum.
Post Reply
Message
Author
cisco_donovan
Newbie
Posts: 16
Joined: Wed Apr 06, 2022 7:31 am
itch: ciscodonovan
Contact:

Automated Side Images with Circular Cropping

#1 Post by cisco_donovan »

This tutorial will explain how to:

* Automatically generate side images (which appear next to the speaker)
* Crop those side images into a circle

It'll help if you know a bit about images, side images and even layered images - but I'll walk you through it all from scratch.

Image

Side Image Basics

Side images are great: they can be used to automatically show a picture of the speaking character next to the dialog window. Like a little portrait of the speaker. There's a bit of set up to do - but after that, the side image will appear magically when needed, and will use the same tags as the main speaker, so expressions and outfits and stuff will all sync.

Usually, side images are created as separate images. You'd create a main sprite called "sylvie smile.webp", and a smaller side image called "sylvie smile side.webp". Ren'Py will find this side image and show it next to the speaker for you.

Now, the thing is, I love laziness and I hate duplication. So I don't want to manually create two versions of every character pose/expression/outfit. I want to just define one sprite set and use Ren'Py's excellent functionality to automate the generation of side images. And guess what - I can!

So, I'm going to use transforms, and later layered images, to reduce the amount of manual effort I have to put into my code.

Transforming Side Images

In my game, I want to show a side image in the bottom left of the screen, slightly offset from it's default position. Something like this:

Image

To get this positioning, I've actually had to tweak the say statement in screens.rpy. You may or may not need to do this in your project. Here's my say statement code - I've literally only changed the xoffset down on the last line:

Code: Select all

screen say(who, what):
    style_prefix "say"

    window:
        id "window"

        if who is not None:

            window:
                id "namebox"
                style "namebox"
                text who id "who"

        text what id "what"


    ## If there's a side image, display it above the text. Do not display on the
    ## phone variant - there's no room.
    if not renpy.variant("small"):
        add SideImage() xoffset 60 yalign 1.0  #< ----- I've edited here
Here's some script for a game which lets us test side images:

Code: Select all

define s = Character("Sylvie", image="sylvie")

image side sylvie normal = Transform("sylvie normal", crop=(80,0,160,180), zoom=1.2)
image side sylvie smile = Transform("sylvie smile", crop=(80,0,160,180), zoom=1.2)

label start:

    scene bg uni

    show sylvie normal

    s "I'm so happy!"

    show sylvie smile

    s "I have a circular side image!"

    jump start
 
Whenever we change sylvie's tags, like show sylvie happy, Ren'Py will automatically update the side image to match the active tags. Neat!

This is mostly standard Ren'Py such as code you'll find in the side images docs. I've defined a character and associated an image with it. I've defined side images with different tags. I've used show and say statements to display my game.

The magic is that I've used a Transform in my image statement. The transform will take whichever image I'm pointing at - in this case, my full-sized image of sylvie - and, well, transform it. In this case, I'm zooming it up and cropping it down to only show the character's face. Ren'Py supports many transforms - you've probably used a few. Here's a full list in docs.

The Crop part is a little complicate. It takes a tuple with 4 values: (x, y, width, height). Imagine the crop as a rectangular area drawn inside the original image. The x and y represent the top-left corner of that rectangle. Width and height then define how long and tall the crop area should be.

Working out your crop square isn't easy. I know it's tempting but don't try and guess it. Load the image up into an image editor and use that to work out the bounds of the crop. I use the GIMP to draw a rectangular selection, and read the right values back out from there.

Radial Crops

In my game, I wanted a radial crop of the side image. I.e, I want to see a circle of the face, not a square.

Ren'Py doesn't support a radial crop transform. But Unsleppen[url], my new personal hero, wrote me one! H ... all layers plugin.

Here's a really simple layered image:

Code: Select all

layeredimage eileen:
  group everything:
    attribute happy "eileen happy"
    attribute concerned "eileen concerned"
Now, to set up the side image, we need to use what's called a Layered Image Proxy. We'll use this to copy the layered image so that we can manipulate it without affecting the original.

Code: Select all

image side eileen = LayeredImageProxy("eileen",
  [
    Transform(
      crop=(50, 20, 220, 220),
      xoffset=-20
    ),
    Transform(
      shader="circle_crop",
      mesh=True
    )
  ]
)

Like before, I'm using two transforms which makes life a bit easier. I've also put an offset on my transform here to alter it's position in the side image - this is straight up a hack for the tutorial.

And that's it! Once I've set up my side images to proxy and crop my main sprite images, everything in the game just works. Any changes via a show statement are instantly and automatically reflected in both the main sprite and the side image, for a more immersive experience all around.
Last edited by cisco_donovan on Tue May 10, 2022 12:15 pm, edited 1 time in total.

cisco_donovan
Newbie
Posts: 16
Joined: Wed Apr 06, 2022 7:31 am
itch: ciscodonovan
Contact:

Re: Automated Side Images with Circular Cropping

#2 Post by cisco_donovan »

The shader I've shared above will always crop from the center to the edge.

Here's an alternative shader you can use if you want to pass in your own radius and anchor point:

Code: Select all

init -50 python:
    renpy.register_shader(
        "circle_crop2",
        variables="""
        attribute vec4 a_position;

        uniform vec2 u_model_size;
        uniform float u_radius;
        uniform vec2 u_anchor;

        varying vec4 v_position;
        """,
        vertex_functions="""
        """,
        fragment_functions="""
        const vec4 c_transparent_color = vec4(0.0, 0.0, 0.0, 0.0);

        // Checks if a point is within a circle
        // IN:
        //    point, center, radius
        // OUT:
        //    int:
        //        2 - within, 1 - on, 0 - outside
        int is_inside_circle(vec2 point, vec2 center, float radius)
        {
            float distance_sqr = pow(point.x - center.x, 2.0) + pow(point.y - center.y, 2.0);
            float radius_sqr = pow(radius, 2.0);

            if (distance_sqr < radius_sqr)
            {
                return 2;
            }
            if (distance_sqr == radius_sqr)
            {
                return 1;
            }
            return 0;
        }
        """,
        vertex_300="""
        v_position = a_position;
        """,
        fragment_300="""
        vec2 center = u_model_size * u_anchor;
        float radius = max(u_model_size.x, u_model_size.y) / 2.0 * u_radius;

        if (is_inside_circle(v_position.xy, center, radius) == 0)
        {
            gl_FragColor.rgba = c_transparent_color;
        }
        """
    )

transform circle_crop_2(radius=1.0, anchor=(0.5, 0.5)):
    u_radius radius
    u_anchor anchor
    shader "circle_crop2"
    mesh True
Note that when you call this from a transform, you have to pass in a radius and anchor point (as floats between 0 and 1).

Code: Select all

image side eileen = LayeredImageProxy("eileen",
  [
    Transform(
      crop=(50, 20, 220, 220),
      zoom=1.0,
      xoffset=-20
    ),
    Transform(
      shader="circle_crop",
      radius=0.5,
      anchor=[0.5, 0.5],
      mesh=True
    )
  ]
)

Post Reply

Who is online

Users browsing this forum: No registered users