Skip to content

Instantly share code, notes, and snippets.

@methanoliver
Last active June 17, 2023 08:30
Show Gist options
  • Save methanoliver/98fe9304b63fff1f7d3419120e2c11f5 to your computer and use it in GitHub Desktop.
Save methanoliver/98fe9304b63fff1f7d3419120e2c11f5 to your computer and use it in GitHub Desktop.
Neat RenPy rain effect
#!/usr/bin/python3
# This generates the rain images that serve as the base for the rain effect,
# and isn't expected to be used at runtime.
#
# You will need an installation of Python 3 with Pillow library in it.
from PIL import Image, ImageDraw, ImageFilter
import random
# Background color, serves as a shadow.
BLANK = (0, 0, 0)
# Foreground color.
RAIN = (255, 255, 255)
# Width and height must be even divisors of screen width and height,
# respectively, but they can (and should) be smaller than the screen.
# For my 1920x1080 screen, 6x3 makes a reasonable 320x360 texture.
WIDTH = 1920 // 6
HEIGHT = 1080 // 3
# Playing with these values and re-generating the images
# will be required to get good results for a specific use case.
# Thickness affects how dense the rain is.
THICKNESS = 0.3
# Angle of rain: multiplier for y offset.
ANGLE = 8
# ---------------
# These constants were arrived at through trial and error.
SHORT = 350
MEDIUM = 250
LONG = 150
MARGIN = int((max(WIDTH, HEIGHT) / 100) * 20)
def drawlines(image, number, scale):
number = int(number)
draw = ImageDraw.Draw(image)
for line in range(0, int(number)):
x = random.randint(0 - MARGIN, WIDTH + MARGIN)
y = random.randint(0 - MARGIN, HEIGHT + MARGIN)
scaleoff = random.random() * 0.5
xoff = x - int(5 * (scale + scaleoff))
yoff = y + int(5 * ANGLE * (scale + scaleoff))
# This trick makes the generated texture tile seamlessly
# by drawing each line four extra times, so that if
# it crosses any of the edges, it definitely has a match
# on the other end.
draw.line((x, y, xoff, yoff), fill=RAIN)
draw.line((x - WIDTH, y, xoff - WIDTH, yoff), fill=RAIN)
draw.line((x + WIDTH, y, xoff + WIDTH, yoff), fill=RAIN)
draw.line((x, y - HEIGHT, xoff, yoff - HEIGHT), fill=RAIN)
draw.line((x, y + HEIGHT, xoff, yoff + HEIGHT), fill=RAIN)
return image
def generate(multiplier, step):
plane = Image.new("RGB", (WIDTH, HEIGHT), BLANK)
# Shortest lines
if step == 1:
plane = drawlines(plane, SHORT * multiplier, 1)
# shorter lines
if step == 2:
plane = drawlines(plane, MEDIUM * multiplier, 3)
# Longer lines
if step == 3:
plane = drawlines(plane, LONG * multiplier, 6)
return plane
random.seed()
img = generate(THICKNESS, 1)
img.save("_rain-short.png")
img = generate(THICKNESS, 2)
img.save("_rain-medium.png")
img = generate(THICKNESS, 3)
img.save("_rain-long.png")
init:
python:
# RainBlur is how much to blur rain.
RainBlur = 4
# RainAlpha is the total alpha level of the entire rain sheet.
RainAlpha = 0.75
# RainY is rain speed, basically how long does it take
# for the rain sheet to fall down by one tile
RainY = 0.16
# RainX is the same for horizontal movement,
# and needs to be manually adjusted to fit the chosen raindrop angle
# for the rain to fall naturally.
RainX = RainY * 9
# Total alpha of the rain is the sum of the alpha of all three layers,
# so each sheet has a third of it.
RainLayerAlpha = RainAlpha / 3
# Speed of the medium sheet of rain is two times faster than the front sheet.
RainYM = RainY / 2
RainXM = RainX / 2
# Speed of the futhest sheet of rain is two times faster than that.
RainYF = RainYM / 2
RainXF = RainXM / 2
######################################
# Automatically find our rain files, which are assumed to live in the module directory:
RainFiles = renpy.os.path.dirname(renpy.get_filename_line()[0]).split("game/")[-1]
# We're making three displayables, each bigger than the screen by the size of one rain
# tile, in both directions.
#
# Check generate-rain.py for how the raindrops are made.
RainTileSizeX, RainTileSizeY = renpy.image_size(RainFiles+"/_rain-long.png")
# Amazingly, using ATLs xtile/ytile to tile the rain images actually results
# in a lot more CPU usage.
RainsheetLong = Composite(
(config.screen_width + RainTileSizeX, config.screen_height + RainTileSizeY),
(0, 0), Tile(RainFiles+"/_rain-long.png"))
RainsheetMedium = Composite(
(config.screen_width + RainTileSizeX, config.screen_height + RainTileSizeY),
(0,0), Tile(RainFiles+"/_rain-medium.png"))
RainsheetShort = Composite(
(config.screen_width + RainTileSizeX, config.screen_height + RainTileSizeY),
(0,0), Tile(RainFiles+"/_rain-short.png"))
# This defines the far sheet of the rain.
# You show this one /behind/ character sprites.
# It has the shortest (more distant) and fastest moving raindrops.
# In theory you can split it and put sprites between each of the three sheets,
# but I didn't need that.
image rainback scroll:
# Distant drops
contains:
RainsheetShort
blur RainBlur
alpha RainLayerAlpha
subpixel True
parallel:
ypos -RainTileSizeY
linear RainYF ypos 0
repeat
parallel:
xpos 0
linear RainXF xpos -RainTileSizeX
repeat
# Medium drops
contains:
RainsheetMedium
blur RainBlur
alpha RainLayerAlpha
subpixel True
parallel:
ypos -RainTileSizeY
linear RainYM ypos 0
repeat
parallel:
xpos 0
linear RainXM xpos -RainTileSizeX
repeat
# This is the front sheet of the rain, it goes /above/ the
# character sprites.
image rainfront scroll:
contains:
RainsheetLong
blur RainBlur
alpha RainLayerAlpha
subpixel True
parallel:
ypos -RainTileSizeY
linear RainY ypos 0
repeat
parallel:
xpos 0
linear RainX xpos -RainTileSizeX
repeat
# Static sheets of rain for use when the rain does not need to animate,
# e.g. when the time has stopped.
image rainback static:
contains:
RainsheetShort
alpha RainLayerAlpha
blur RainBlur
contains:
RainsheetMedium
alpha RainLayerAlpha
blur RainBlur
image rainfront static:
contains:
RainsheetLong
alpha RainLayerAlpha
blur RainBlur
init -50 python hide:
# We also need to forbid build code from picking up our generator script,
# which I like to keep with my project.
build.classify("**/generate-rain.py", None)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment