Ren'Py tint entire screen with im.MatrixColor()

f95zoneuser463

Member
Game Developer
Aug 14, 2017
219
1,024
I'm in the process of implementing a day/night cycle into .

Since I do not have the computing-power to do additional night-renders I have decided to use color-tinting with im.MatrixColor(). This also has the positive side-effect that it's not increasing the size of the game by adding many images.

The color-matrix is fed with dynamic values that are based on the ingame-time in the real code.

background at night example:
Code:
image i_bg = im.MatrixColor("images/bg.png",im.matrix.saturation(0.5)*im.matrix.tint(.75,.75,1.0)*im.matrix.brightness(-0.2))
scene i_bg
The problem:
I have many screens that hold overlay-images. These need to be tinted as well. I could just add the im.MatrixColor-code for every image but that is a lot of fiddling. What I am looking for is a solution that can tint an entire screen will all it's content.

I've search the web up- and down and could only find this post from 2015 ... not yet possible it seems:


Since there is a preview of Ren'Py 7 out ... maybe some people know more about it.

Btw. I tried the very cheap way of just placing a screen with only a translucent background-color over the scene ... no way ... it looks very bad ... using im.MatrixColor() is required to make this look good. Placing a translucent-layer of the scene can never look that good because it will just add colors together.

Edit: im.MatrixColor() is not GPU hardware accelerated and runs on the CPU, careful with the performance when using it.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,976
16,232
MatrixColor is for complex computing, while your problem is more simple. So, why not simply using a screen ?

Something like :
Code:
screen myTint:
   zorder 1000
   add Frame( Solid( "#00000055" ), xsize=config.screen_width, ysize=config.screen_height )
The value of Solid is RRGGBBAA (red, green, blue, alpha channel). So you can chose the color of your tint and with the alpha channel control it's opacity.

With this:
Code:
screen myTint( alpha ):
   zorder 1000
   add Frame( Solid( "#000000"+alpha ), xsize=config.screen_width, ysize=config.screen_height )
and things like this:
Code:
    show screen myTint( "50" ) 
    pause 5
    show screen myTint( "80" ) 
    pause 5
    show screen myTint( "A0" )
You can even use a single screen to simulate different lighting.

And going full custom, with this:
Code:
screen myTint( color, alpha ):
   zorder 1000
   add Frame( Solid( color+alpha ), xsize=config.screen_width, ysize=config.screen_height )
and this :
Code:
init python:
    nightTint = "#000000"
    fogTint = "#FFFFFF"

label someLabel:
    show screen myTint( nightTint, "50" )
You can even use the same screen to handle a lot of different lighting case.
 
  • Like
Reactions: f95zoneuser463

f95zoneuser463

Member
Game Developer
Aug 14, 2017
219
1,024
That is what I did already and what I meant with this:
Btw. I tried the very cheap way of just placing a screen with only a translucent background-color over the scene ... no way ... it looks very bad ... using im.MatrixColor() is required to make this look good. Placing a translucent-layer of the scene can never look that good because it will just add colors together.
The quality of the screen-method is just to low due to the additive color-blending. By now I already gave up on the idea to have an entire screen tinted directly ... this does not seem to be possible.

I just have accepted the fact that I have to use im.MatrixColor() on every image that I want to tint. Meanwhile I've added a tiny tint()-function to wrap im.MatrixColor() and some kind of time&color-look-up-dictionary that I generate with a little C#-console program.

Code:
def tint(imagepath="images/wheelhover.png"):
    # store.tintmatrix hold colordata that gets updated based on the ingame-time
    # store.tintmatrix data comes from generated 't_dictionary' (see spoilers)
    # condition to be able to switch of tint (may be useful on very low end system)
    if use_tint:
        return im.MatrixColor(imagepath,store.tintmatrix)
    return imagepath
used like this for example:
Code:
screen bg():
    tag master
    layer "master"
    frame:
        background tint("images/bg.png")
    # more tinted stuff here
    image "my_anim"
You don't have permission to view the spoiler content. Log in or register now.

You don't have permission to view the spoiler content. Log in or register now.

I got everything working except for one big problem now:
ATL-animation do not update tinted images when the ingame-time ticks/advances, this is where I'm stuck right now. The result: the background and overlays are tinted correct, but the animation does not update. My ingame-time advances at a fixed tick-rate/interval of 5 seconds and this is when all images must be re-tinted.
Code:
image my_anim:
    tint("drink1.png")
    pause 4.0
    tint("drink2.png")
    pause 2.0
    repeat
If this animation is placed inside a screen it will tint the images only the first time when it's initially displayed.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,976
16,232
Code:
image my_anim:
    tint("drink1.png")
    pause 4.0
    tint("drink2.png")
    pause 2.0
    repeat
If this animation is placed inside a screen it will tint the images only the first time when it's initially displayed.
Between prediction and refresh, a screen is updated (way to) many times. Try something simple:
Code:
init python:
    logFile = renpy.os.path.join( config.basedir, 'logFile.txt' )
    def myLog( msg ):
        FH = open( logFile, "a" )
        FH.write( "{}\n".format( msg ) )
        FH.close()

screen control:
    $ myLog( "called" )

label start:
    "wait a little before clicking"
    $ myLog( "screen will be shown now" )
    show screen control
    "now wait again some time"
    return
If you wait long enough (let's say 30 seconds to be sure), you'll see that the screen have been predicted, and so logged on the file before the "screen will be shown now". And wait again long enough and you'll found way more than one other "called" on the log file.
Each time the screen is predicted/refreshed, everything is done again, so here your "tint" function is called, and the screen will show the update. If you have some button on the screen, the whole screen is even refreshed each time a button change it's state because of the mouse movements.

At the opposite you've image which works as an assignation. It's played once and just once. Normally you can make them dynamic with something like :
Code:
image myImage: "path/[dynamic].jpg"
But there's two limitations. Firstly dynamic can't be an array nor a function, because Ren'py don't like(ed?) [] and () here. Secondly, if I remember correctly (it's 3AM here) the value will be refreshed only at the moment when myImage is displayed.

I'm not totally sure of what I'll say, but one solution could be to dynamically create the image with the . Don't care about the, "It is an error to run this function once the game has started." It's an error if you do it without real reason, which isn't the case here.
Something like :
Code:
 def myTint( imageName, path2pict ):
     if time == 1:
         renpy.image( imageName, [...] )
     elif [...]

     return imageName
and instead of showing the image, you show myTint() with the right parameters.
It should do the trick, but like I said, I'm not sure about this, never played with renpy.image before, so I don't know the possible side effects.
If it works, try to add a dict as cache-like to shortcut the useless creation of an image which is already correct :
Code:
  myTintCache = {}
  for n in [ imageName1, imageName2 ]:
      myTintCache.update( { n: 0 } )

  def myTint( imageName, path2pict ):
     if myTintCache[imageName] == time: 
        pass
     elif time == 1:
         renpy.image( imageName, [...] )
     elif [...]
     else:
        [...]

     myTintCache[imageName] = time
     return imageName
You can even don't use path2pict and have another dict to replace it if you want.

And if renpy.image really don't like to be called outside of init time, you still can use more or less the same trick. Instead of creating the image a runtime, you create all the tint variations at init time, and use myTint() to return the right name according to the actual time.
 
  • Like
Reactions: f95zoneuser463

f95zoneuser463

Member
Game Developer
Aug 14, 2017
219
1,024
I got a working solution after struggling almost 2 days with this! It is far from perfect, but it works. I tried so many things to fix this.

I tinkered around in my scripts with your suggestions. Your link to the Python equivalent-functions made me try something completely different:

In the screen, instead of using image "my_anim" I tried $ renpy.show("my_anim"). This does update the tint correctly ... but it does not show up instantly for some reason (don't know why). It requires on little extra *interaction* tick by the user to be shown. Hovering over a HUD-button for example will make it appear. It also does have the negative side that it is not actually tied to the screen ... more on that later.

To fix the animation not showing I added on "show":
Code:
screen bg():
    tag master
    layer "master"

    on "show":
        action Function(renpy.restart_interaction())

    frame:
        background tint("images/bg.png")

    $ renpy.show("my_anim", layer="master", tag="anim")
There are two things that need to be considered when using this:
  • animations will reset with each screen-update, due to the timer updating my screens every 5 seconds the animation cannot be longer than 5 seconds (they can be longer in the script but they will only ever show the first 5 seconds due to the reset)
  • hiding the screen will not hide the animation, easiest solution for that is to use an empty scene to clear everything when they player changes his location for example
I'm not really happy with this solution but I will use it for now. When moving around in the world from location to location it is noticeable slower because all tinted screen images will be tinted twice initially. Yes I could cache this somehow but I'd like to avoid that. It would be better to find a solution that works without using renpy.restart_interaction() at all.

To improve this I could...
  • try to tint and update ONLY the animation from the screen instead of updating everything with renpy.restart_interaction(), but how?
  • try to improve the speed of im.MatrixColor()
  • try to find other solution
  • try to build my own image-class that does always use my tint-matrix by default internally, that's some expert level stuff
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,976
16,232
In the screen, instead of using image "my_anim" I tried $ renpy.show("my_anim"). This does update the tint correctly ...
It's surprising since it shouldn't at all happen. The show statement end with a call to renpy.show. So, if one should do more than the other, it should be the statement because of the pre-processing part, not the Python equivalent. Or is it because you put the call in a screen that it works, I'm not really sure here.
Anyway, be careful if you update your version of Ren'py, control that it's still working like that.


but it does not show up instantly for some reason (don't know why). It requires on little extra *interaction* tick by the user to be shown.
You already have the answer ; basically speaking, it's stacked and will be performed with the next interaction. But there's simpler than what you used. Try this :
Code:
    def myShow( *args, **kwargs ):
        renpy.show( *args, **kwargs )
        renpy.restart_interaction()
Use it in directly in your code place of renpy.show.
Code:
label myLabel:
    [...]
    $ myShow("my_anim", layer="master", tag="anim")
    [...]
    hide my_anim
If I correctly understood your problem, it should do the same, minus the side effects.
You can also use it directly inside the screen, still in place of renpy.show. In this case, the side effects should stay present, but it will simplify the screen since you don't need anymore the on "show" part.


When moving around in the world from location to location it is noticeable slower because all tinted screen images will be tinted twice initially.
It's way more than twice. Because of the prediction and screen refresh, they'll be probably tinted every five/six seconds when the screen is displayed and the player do nothing, and can be tinted every single second if he move the mouse.
If you want, use my with a game which use way too many variables, like by example. Just open the tool's screen to see all the variables, then move the mouse over the data. You'll see how screen refresh can strongly slow down something (yet it's now optimized and two times faster than a regular code). It's an extreme case, but still a good example.
So, if the myShow trick works, I recommend you to prefer it over your screen solution. It will imply less prediction and no screen refresh, and so shouldn't slow down your game.


Yes I could cache this somehow but I'd like to avoid that. It would be better to find a solution that works without using renpy.restart_interaction() at all.
It's not possible. Whatever you'll use, either it will be explicitly called by yourself, or implicitly called by Ren'py.


  • try to tint and update ONLY the animation from the screen instead of updating everything with renpy.restart_interaction(), but how?
Using a could perhaps do it with the help of . But you'll have to fight against the small documentation on this subject.
You'll also have to find a way to discriminate the call to the event method to only redraw when it's really this displayable which need to be updated (which is almost never done with Ren'py displayable, that's why my "slow down" example above happen. The whole viewport is redrawn each time one of the text displayable is updated because the mouse have moved).
And still you'll have to deal with the fact that the tint should be changed.

To my knowledge, it's the only way to redraw just a displayable.
 

f95zoneuser463

Member
Game Developer
Aug 14, 2017
219
1,024
Another solution without renpy.restart_interaction() and instead of ATL now python:

Code:
def my_anim(st, at):
    # modulo to repeat endless
    if st % 5 < 2.5:
        return tint("drink1.png"), 1
    else:
        return tint("drink2.png"), 1

# inside the master screen:
image DynamicDisplayable(my_anim)
No interaction-tricks, it is tied to the screen like it should be and the animation will not reset.

Tomorrow I will look into im.MatrixColor(). The performance annoys me. I had the idea to make a customized version of this function that does ignore the alpha-channel ... it could then be used on the backgrounds. Why the f*ck does renpy not use a GPU shaders to do this stuff, it's 2018 and every little cheap smartphone from 5 years ago can do that, argh.
I really need to sleep now...

Edit: cropping all transparent overlay images and position them manually in RenPy-script helps a lot to reduce the amount of pixels im.MatrixColor() has to process -> better performance.
I made .bat-files to do this automatically,