Mirrage/Heat Shimmer/Kagerou Effect Displayable

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
Unin
Regular
Posts: 54
Joined: Wed Sep 01, 2010 8:08 pm
Location: Florida
Contact:

Mirrage/Heat Shimmer/Kagerou Effect Displayable

#1 Post by Unin »

Scanline code flagrantly stolen adapted from Human Bolt Diary's Perspective Displayable.

The following will take a displayable and show a screen consisting of that displayable with animated "heat haze" style mirage. It accomplishes this by slicing the image into horizontal lines, and then stretching and offsetting this lines by an oscillating amount, that shifts in phase as time passes

I originally developed it for providing the illusion of a heavily magnified view of a far away object on the horizon, but see it as more useful for anything requiring a heat shimmer, great for fiery ovens, searing deserts, scorching alien stars, and the occasional steamy onsen or noxious smog. Those indulging in stumbling intoxication may also find the effect appropriate.

it could also conceivably be used at low amplitudes in combination with a raster scanline texture to give the illusion of an retro tv/computer monitor

The magnitude of offset and stretch can be manipulated with the "amplitude" variable.
The frequency of the wave is determined by the inverse of the "wavelength" variable
The cycle time is determined by the number of slices and shown timebase. As this is a resource intensive process, please don't expect this to be a rapid animation

Code: Select all

init python:
    import math
    import pygame
    ######################################## Mirage Heat Shimmer Distortion Effect #####################################    
    test_image = Image("example.jpg") # Whatever image we're distorting
    
    class Mirage(renpy.Displayable):
        def __init__(self,image):
            super(renpy.Displayable,self).__init__()
            self.start_image = image
            self.W2 = config.screen_width/2
            self.H2 = config.screen_height/2
            self.amplitude = 0.02
            self.wavelength = 10
        
        # Stretch each scanline horizontally, oscillating from +amplitude to -amplitude across specified wavelength
        # Shift oscillation over time by st
        def render(self,width,height,st,at):
            render = renpy.Render(0, 0)
            self.img = self.start_image
            self.img = self.makeScanlines(self.img)   
            h = 1.0
            for scanline in self.img:
                zoom_factor = 1.0-self.amplitude
                # math.sin(x) returns the sine of x radians
                self.t = Transform(scanline, xzoom=(1/zoom_factor)+(math.sin(h/self.wavelength + st)*self.amplitude), yzoom=(1.01))
                h +=1.0
                child_render = renpy.render(self.t, 0, 0, st, at)
                cW,cH = child_render.get_size()
                #final ammount subtracted from h sets y placement
                render.blit(child_render, ((self.W2)-(cW/2),h - 0))
            renpy.redraw(self, 0.1)
            return render

        # Cut image into scanlines 
        def makeScanlines(self, base_image):
            cut = []
            child_render = renpy.render(base_image, config.screen_width, config.screen_height, 0, 0)
            width = int(child_render.get_size()[0]) #Prevents get_size from returning a float
            height = int(child_render.get_size()[1])
            cut = [Transform(base_image, crop=(0,i,width,1)) for i in range(height)]
            return cut



init:
    define Distortion = Mirage(test_image)
    image before = test_image

# Screen drawn onto   
screen after:
    add Distortion



# The game starts here.
label start:
    show before
    "before"
    show screen after
    "after"
    return

Human Bolt Diary
Regular
Posts: 111
Joined: Fri Oct 11, 2013 12:46 am
Contact:

Re: Mirrage/Heat Shimmer/Kagerou Effect Displayable

#2 Post by Human Bolt Diary »

Interesting. To be fair, I plundered the scanline code from: http://lemmasoft.renai.us/forums/viewto ... 51&t=15329

I played around with this a bit, and made some changes to reduce the strain this places on the CPU.

The first thing I did was remove the makeScanlines function, getting the call out of render() since we only need the operation done once. I removed get_size() and required providing the width and height as arguments. I think this is a fair trade. Likewise, I pulled the zoom_factor into __init__ since it's never changing. Anything to get stuff out of that for loop inside render(). Finally, I set amplitude and wavelength as arguments, so different Mirage effects can be created without needing to modify the parent object.

Code: Select all

init python:
    import math


    class Mirage(renpy.Displayable):
        def __init__(self, image, width=0, height=0, amplitude=0, wavelength=0):
            super(renpy.Displayable, self).__init__()
            self.image = [Transform(Image(image), crop=(0, i, width, 1)) for i in range(height)]
            self.amplitude = amplitude
            self.wavelength = wavelength
            self.W2 = config.screen_width * 0.5

            zoom_factor = 1.0 - self.amplitude
            self.x_zoom_factor = 1 / zoom_factor
            
        # Stretch each scanline horizontally, oscillating from +amplitude to -amplitude across specified wavelength
        # Shift oscillation over time by st
        def render(self, width, height, st, at):
            render = renpy.Render(0, 0)
             
            h = 1.0
            for scanline in self.image:    
                # math.sin(x) returns the sine of x radians
                t = Transform(scanline, xzoom = self.x_zoom_factor + (math.sin(h / self.wavelength + st) * self.amplitude), yzoom = (1.01))
                h += 1.0
                child_render = renpy.render(t, 0, 0, st, at)
                cW, cH = child_render.get_size()
                # final amount subtracted from h sets y placement
                render.blit(child_render, ((self.W2) - (cW * 0.5), h - 0))
            renpy.redraw(self, 0.1)
            return render


init:
    define Distortion = Mirage("test.jpg", width=590, height=300, amplitude=0.02, wavelength=10)
    image before = Image("test.jpg")

# Screen drawn onto   
screen after:
    add Distortion


# The game starts here.
label start:
    show before
    "before"
    show screen after
    "after"
    return

User avatar
vollschauer
Veteran
Posts: 231
Joined: Sun Oct 11, 2015 9:38 am
Github: vollschauer
Contact:

Re: Mirrage/Heat Shimmer/Kagerou Effect Displayable

#3 Post by vollschauer »

I would like to have this as kind of "smooth" wrap transition :) like in some movies if they switch TV channels ....

User avatar
firecat
Miko-Class Veteran
Posts: 540
Joined: Sat Oct 25, 2014 6:20 pm
Completed: The Unknowns Saga series
Projects: The Unknown Saga series
Tumblr: bigattck
Deviantart: bigattck
Skype: bigattck firecat
Soundcloud: bigattck-firecat
Contact:

Re: Mirrage/Heat Shimmer/Kagerou Effect Displayable

#4 Post by firecat »

what makes this better then making it on photoshop or gimp? the code suggest that its not animated or have anything useful to add.
Image


Image


special thanks to nantoka.main.jp and iichan_lolbot

User avatar
xela
Lemma-Class Veteran
Posts: 2481
Joined: Sun Sep 18, 2011 10:13 am
Contact:

Re: Mirrage/Heat Shimmer/Kagerou Effect Displayable

#5 Post by xela »

firecat wrote:the code suggest that its not animated
There is always an off chance that you don't have the experience to know what the code does just from reading it...
Like what we're doing? Support us at:
Image

User avatar
firecat
Miko-Class Veteran
Posts: 540
Joined: Sat Oct 25, 2014 6:20 pm
Completed: The Unknowns Saga series
Projects: The Unknown Saga series
Tumblr: bigattck
Deviantart: bigattck
Skype: bigattck firecat
Soundcloud: bigattck-firecat
Contact:

Re: Mirrage/Heat Shimmer/Kagerou Effect Displayable

#6 Post by firecat »

xela wrote:
firecat wrote:the code suggest that its not animated
There is always an off chance that you don't have the experience to know what the code does just from reading it...
the op also stated "please don't expect this to be a rapid animation" so yes its no better than repeat command.
Image


Image


special thanks to nantoka.main.jp and iichan_lolbot

User avatar
xela
Lemma-Class Veteran
Posts: 2481
Joined: Sun Sep 18, 2011 10:13 am
Contact:

Re: Mirrage/Heat Shimmer/Kagerou Effect Displayable

#7 Post by xela »

firecat wrote:the op also stated "please don't expect this to be a rapid animation" so yes its no better than repeat command.
(you didn't mention your concerns about speed)

With any degree of animation involved, you'll get two main advantages over images prepared in image editing software (as with any code that animates/manipulates images instead of using animations/manipulations made with image editing software):

1) You need just the one image which saves space (potentially a lot of it in this case since you almost always do this with a background/large part of a background). In order to get the same/similar effect 5 - 20 (more?) images could be required depending on settings before they can be repeated.

2) You can reuse/test this using different settings to get it just right for your project better than you could in image editing software (at least it's always like that for me).

Edit: ===>>
3) One of the ways I use this in my project is a huge fire based attack in the BE so it creates "Heat Shimmer" effects for a lot of different backgrounds. This is another huge advantage since code can do it for any image automatically.

===>>
Speed can be improved (significantly) by tweaking the code a little bit (if it is actually causing issues, it shouldn't on modern machines with reasonable settings). Prolly simplest tweak is to make scanlines larger than one pixel sacrificing quality for performance, there are many settings where it's close to impossible to tell the difference between scanlines up to 3 pixels. 5 and above deterioration becomes more noticeable but I used 8 (1280x720 res) in some cases and it still looked acceptable, standalone or in VN mode this should just work with 1, but you could get into trouble when running another 5 - 6 animation sequences or heavy logic at the same time.

Obviously if speed is a real issue and fails to get better for the settings/quality that are right for the project, separate images will be the best option but about the same thing can be said for any advanced animation/manipulation.
Like what we're doing? Support us at:
Image

User avatar
vollschauer
Veteran
Posts: 231
Joined: Sun Oct 11, 2015 9:38 am
Github: vollschauer
Contact:

Re: Mirrage/Heat Shimmer/Kagerou Effect Displayable

#8 Post by vollschauer »

We are not on track any more, actually my question was: is it possible to have this as transition like "with pixellate" ???

User avatar
xela
Lemma-Class Veteran
Posts: 2481
Joined: Sun Sep 18, 2011 10:13 am
Contact:

Re: Mirrage/Heat Shimmer/Kagerou Effect Displayable

#9 Post by xela »

vollschauer wrote:We are not on track any more,
We're not :oops:
vollschauer wrote:actually my question was: is it possible to have this as transition like "with pixellate" ???
Maybe, but I expect that it would require a bit of extra coding. There is nothing that should prevent that but if just doing over one displayable is not enough, you'll have to work with everything that is currently being displayed on all layers instead, I am afraid that I never done something like that before and although there should be clear examples of how that's done in Ren'Py code and I even recall Asceai doing something like that at one point, this will prolly take time to figure out.

So the answer is: It's very likely that it's possible but (at least) I am not sure how exactly. You may be better off with some other approach, the reason I didn't respond to the:
I would like to have this as kind of "smooth" wrap transition :) like in some movies if they switch TV channels ....
is that I wasn't sure what you've meant exactly. You should be able to do a "old tv channel switching" transition with ImageDissolve, maybe in combo with MultiTransition (or whatever that's called) instead, it will require less know how and may even turn out to look better.
Like what we're doing? Support us at:
Image

User avatar
Pando
Regular
Posts: 29
Joined: Wed Oct 08, 2014 7:57 am
Projects: Crossfire
Organization: Agape Studios
IRC Nick: Pando
Github: Scylardor
Contact:

Re: Mirrage/Heat Shimmer/Kagerou Effect Displayable

#10 Post by Pando »

Hey yall,

necroposting this a bit, since I've thought it could be interesting to share my experience...

First of all: this code is great.
It really works as expected and the visual effect is right what you'd want for any kind of hallucination or dizziness effect...

Unfortunately, as one could imagine by reading the code, it is pretty CPU intensive :roll:
Actually, I want to apply it to a background, i.e. a rather big image, so that takes quite a processing toll.
The effect seems to work without too much stutter, but I was indeed frightened to see that applying to a background makes python (Renpy) CPU use to a whopping 10% when doing this... which may not be Ok for some end-user machines.

So I wondered... Can we do better ?

And so I came up with something like this...

Code: Select all

init python:
    import math


    class MyMirage2(renpy.Displayable):
        def __init__(self, img, posx, posy, amplitude=0, wavelength=0):
            super(renpy.Displayable, self).__init__()
            # You can specify a path OR a predefined image
            if type(img) is not Image:
                img = Image(img)
            width, height = renpy.image_size(img)  # Now we're sure img is an image
            # Cut our image into N scanlines where N is the height
            self.scan_lines = [Transform(Image(img), crop=(0, i, width, 1)) for i in range(height)]
            self.width = width
            self.height = height
            self.amplitude = amplitude
            self.wavelength = wavelength
            self.pos = (posx, posy)
            self.x_zoom_factor = 1 / (1.0 - self.amplitude)
            self.y_zoom_factor = 1.01


        def render(self, width, height, st, at):
            """
            Stretch each scanline horizontally, oscillating from +amplitude to -amplitude across specified wavelength
            Shift oscillation over time by st.
            """
            render = renpy.Render(self.width, self.height)

            h = 0.0
            for scanline in self.scan_lines:
                # math.sin(x) returns the sine of x radians
                scanline_x_zoom = self.x_zoom_factor + (math.sin(h / self.wavelength + st) * self.amplitude)
                scanline_trsf = Transform(scanline, xzoom=scanline_x_zoom, yzoom=self.y_zoom_factor)
                render.place(scanline_trsf, x=0, y=h, width=self.width, height=1)
                h += 1.0

            renpy.redraw(self, 0.01)
            return render

# Screen drawn onto
screen after(img_path):
    #add Mirage(img_path, width=590, height=300, amplitude=0.1, wavelength=15)
    add MyMirage2(img_path, 0,0, amplitude=0.05, wavelength=30)
It works nearly in the same way as before (but you give it a position since I'd like to be able to position what is rendered on the screen), the main difference is that instead of blitting each scan line one by one in the render function, I preallocate a render of the final image size at the beginning, and then just "glue" my transformed scanlines back together line by line in it with render.place.

The good news is that it works as intended, just like the first version...
The bad news is that performance-wise the gains are inexistent :lol:
This solution, according to my rough estimates, is just as CPU intensive as the first one !

So I'm still wondering... Can we actually do better ? :mrgreen:

While this post is a sort of ask for help (if I don't find something better, I'm going to stick with the first version and hope it doesn't lag on anyone computer...), it is also a testimony to other developers that I've tried this approach and that so far, it isn't worth trying to do it this way, in case you had the idea too.

edit: The code pasted here does not really take the position you give it into account, but, well, you get the idea. :oops:
Image

User avatar
xela
Lemma-Class Veteran
Posts: 2481
Joined: Sun Sep 18, 2011 10:13 am
Contact:

Re: Mirrage/Heat Shimmer/Kagerou Effect Displayable

#11 Post by xela »

Pando wrote:So I'm still wondering... Can we actually do better ? :mrgreen:
Reduce the amount of scanlines by vertically cropping the image and load on the CPU will drop, it will still look amazing, I use a lot of different settings for different backgrounds but with a lot of images, even a width of 8 lines looks perfectly fine, 2 - 3 lines looks almost the same as one:

Code: Select all

    class Mirage(renpy.Displayable):
        def __init__(self, displayable, resize=(1280, 720), ycrop=8, amplitude=0, wavelength=0, **kwargs):
            super(renpy.Displayable, self).__init__(**kwargs)
            displayable = Transform(displayable, size=resize)
            self.displayable = list()
            for r in xrange(resize[1]/ycrop):
                y = r * ycrop
                self.displayable.append((Transform(displayable, crop=(0, y, resize[0], ycrop)), y))
                
            # self.image = [Transform(renpy.easy.displayable(image), crop=(0, i+1, width, 2)) for i in range(height/2)]
            self.amplitude = amplitude
            self.wavelength = wavelength
            self.W2 = config.screen_width * 0.5

            zoom_factor = 1.0 - self.amplitude
            self.x_zoom_factor = 1 / zoom_factor
           
        # Stretch each scanline horizontally, oscillating from +amplitude to -amplitude across specified wavelength
        # Shift oscillation over time by st
        def render(self, width, height, st, at):
            math = store.math
            render = renpy.Render(width, height)
             
            h = 1.0
            for scanline in self.displayable:   
                # math.sin(x) returns the sine of x radians
                t = Transform(scanline[0], xzoom = self.x_zoom_factor + (math.sin(h / self.wavelength + st) * self.amplitude), yzoom = (1.01))
                h += 1.0
                child_render = t.render(0, 0, st, at)
                cW, cH = child_render.get_size()
                # final amount subtracted from h sets y placement
                render.blit(child_render, ((self.W2) - (cW * 0.5), scanline[1]))
            renpy.redraw(self, 0)
            return render
Like what we're doing? Support us at:
Image

Human Bolt Diary
Regular
Posts: 111
Joined: Fri Oct 11, 2013 12:46 am
Contact:

Re: Mirrage/Heat Shimmer/Kagerou Effect Displayable

#12 Post by Human Bolt Diary »

You feel like getting crazy? Let's get crazy.

Code: Select all

init python:
    import math
    from itertools import cycle

    sin = math.sin
    
    class Mirage(renpy.Displayable):
        def __init__(self, image, width=0, height=0, amplitude=0,
                     wavelength=0, **kwargs):
            super(renpy.Displayable, self).__init__(**kwargs)

            # Cut image into scanlines
            Image(image)

            cut_image = [
                Transform(image, crop=(0, i, width, 1)) for i in range(height)
            ]

            self.half_width = renpy.config.screen_width * 0.5

            zoom_factor = 1.0 - amplitude
            x_zoom_factor = 1 / zoom_factor
            
            tf_list = []
            h = 1.0
            st = 0.0
             
            for scanline in cut_image:
                tmp_list = []
                for _ in range(12):
                    t = Transform(
                        scanline,
                        xzoom=x_zoom_factor + (sin(h / wavelength + st) * amplitude),
                        yzoom=1.01
                    )
                    tmp_list.append(t)
                    st += 0.5
                
                h += 1.0
                
                tf_list.append(cycle(tmp_list))
                
                if st >= 1.0:
                    st = 0
            
            self.tf_list = tf_list

        def render(self, width, height, st, at):
            render = renpy.Render(0, 0)

            hw = self.half_width
            r = renpy.render 

            for i, item in enumerate(self.tf_list):
                t = item.next()
                
                child_render = r(t, 0, 0, st, at)
                cW = child_render.get_size()[0]
                render.blit(child_render, (hw - (cW * 0.5), i))
            
            renpy.redraw(self, 0.15)
            
            return render
By my estimates, this cuts CPU usage by 40-50% from the last one I posted.

Before drawing, we calculate every possible Transform for every sliced line. As a result the render function no longer needs to build new Transform objects, it just cycles through a list.

The RAM usage is higher, however. The effect also is no longer based on the st value from the render, so there's no real-time control over the update speed.

Post Reply

Who is online

Users browsing this forum: No registered users