Need help with custom warp shader [SOLVED]

Discuss how to use the Ren'Py engine to create visual novels and story-based games. New releases are announced in this section.
Forum rules
This is the right place for Ren'Py help. Please ask one question per thread, use a descriptive subject like 'NotFound error in option.rpy' , and include all the relevant information - especially any relevant code and traceback messages. Use the code tag to format scripts.
Post Reply
Message
Author
Lubnewski
Newbie
Posts: 3
Joined: Sat Nov 27, 2021 2:53 am
Contact:

Need help with custom warp shader [SOLVED]

#1 Post by Lubnewski »

Decided to dabble in shaders so that I could maybe give the game I'm currently working on a bit of extra visual flair.

Unsurprisingly, shaders are hard.

I'm trying to make a warp shader (specifically, to adapt this specific shader by AmazingThew on Shadertoy into Ren'Py: https://www.shadertoy.com/view/XlS3Wm; you can see the preview of what I'm going for there, too). And currently, I'm failing.
renpy1.PNG
Anyways, here's my code. I have no idea what I'm doing, but I'm trying my best.

Code: Select all

    renpy.register_shader("mygame.warp", variables=
    """
    uniform sampler2D tex0;
    uniform sampler2D tex1;
    uniform vec2 res0;
    uniform float u_time;
    uniform vec2 u_model_size;
    uniform vec2 uv;
    uniform float power;
    uniform float warpSpeed;
    uniform float warpDistance;
    attribute vec4 a_position;
    varying vec2 v_position;
    """, vertex_200="""
        v_position = a_position.xy / u_model_size.xy;
    """, fragment_200=
    """
        float warpSpeed = 0.3;
        float warpDistance = 0.02;

    	vec2 uv = v_position.xy / res0.xy;
        uv.y *= -1.0;

        vec3 offTexX = texture2D(tex1, uv).rgb;
        vec3 luma = vec3(0.299, 0.587, 0.114);
        float power = dot(offTexX, luma);

        power = sin(3.1415927*2.0 * mod(power + u_time * warpSpeed, 1.0));

        gl_FragColor=texture2D(tex0, uv+vec2(0, power)*warpDistance);
    """)
Here's the custom displayable code I borrowed from the Discord in order to be able to pass an image to tex1:

Code: Select all

init python:
    class ShaderDisplayable(renpy.Displayable):

        def __init__(self, *args, **kwargs):
            self.shader_kws, kwargs = renpy.split_properties(kwargs, "u_", "")
            self.shader = kwargs.pop("shader")
            self.mipmap = kwargs.pop("mipmap", None)

            # Pass additional properties on to the constructor.
            super(ShaderDisplayable, self).__init__(**kwargs)

            self.shader_textures = [renpy.easy.displayable(k) for k in args]

        def render(self, width, height, st, at):
            mr = renpy.display.render.render(
                self.shader_textures[0], width, height, st, at)
            w, h = mr.get_size()

            rv = renpy.display.render.Render(w, h, opaque=False)

            rv.blit(mr, (0,0))

            for k in self.shader_textures[1:]:
                cr = renpy.display.render.render(k, w, h, st, at)
                rv.blit(cr, (0,0), focus=False, main=False)

            rv.mesh = True
            rv.add_shader(self.shader)
            for k,v in self.shader_kws.items():
                rv.add_uniform("u_{}".format(k), v)
            rv.add_property("mipmap",
                renpy.config.mipmap_dissolves if (self.mipmap is None)
                else self.mipmap)

            return rv

        def event(self, ev, x, y, st):
            return None

        def visit(self):
            return self.shader_textures
Here's the bit of code I use to show it ingame:

Code: Select all

    show expression ShaderDisplayable("images/panorama.jpg","images/bayer.png", shader="mygame.warp")
The image I'm using for tex0:
panorama.jpg
The image I'm using for tex1:
bayer.png
What am I doing wrong? What should I change in order for the code to work the way I want it to? :(
Last edited by Lubnewski on Sat Nov 27, 2021 7:09 pm, edited 1 time in total.

strayerror
Regular
Posts: 159
Joined: Fri Jan 04, 2019 3:44 pm
Contact:

Re: Need help with custom warp shader

#2 Post by strayerror »

I believe there were two primary causes for your problems. The first was the calculation of the uv value, which I believe was being done twice with the result that the shader was only ever sampling a single pixel, explaining your blue screen. The second was that the the ShaderDisplayable class never seems to call redraw, making it dependent on other things in order to update, which tends to result in a very choppy animation.

The first can be solved by switching to using v_tex_coord to seed the uv variable, which follows the convention of how Ren'Py generates the uv equivalent value in it's own shaders, using the a_tex_coord attribute from the standard mesh. The second can be solved by using ATL to repeatedly render the shader as quick as it can up to Ren'Py's frame cap resulting in a smooth animation.

Additionally managing texture wrapping can make the edges behave a little nicer. See the code comments for more details.

Hope this helps! :)

Code: Select all

init python:
    # To avoid vertical bands at the top and bottom of the warp (where the
    # shader is sampling outside of the texture space due to the offsets), we
    # use the mirrored repeat texture wrapping. Think of it as putting mirrors
    # at all the edges of your texture. More information can be found at the
    # link below under gl_texture_wrap.
    # https://www.renpy.org/doc/html/model.html#gl-properties
    from renpy.uguu import GL_MIRRORED_REPEAT

    # This is just for convenience later, a short hand for mirroring in both
    # the x- and y-axes.
    reflect = (GL_MIRRORED_REPEAT, GL_MIRRORED_REPEAT)


    # Main issue with the shader was the uv calculation:
    # - Switched to Ren'Py's typical v_tex_coord removing the need for res0
    # - Removed all uniforms that were not being used as uniforms
    # - Moved control constants so they can be declared const
    renpy.register_shader('mygame.warp', variables='''
        uniform sampler2D tex0;
        uniform sampler2D tex1;
        uniform float u_time;
        attribute vec2 a_tex_coord;
        varying vec2 v_tex_coord;
    ''', vertex_200='''
        v_tex_coord = a_tex_coord;
    ''', fragment_functions='''
        const float warpSpeed = 0.3;
        const float warpDistance = 0.02;
    ''', fragment_200='''
        vec2 uv = v_tex_coord.xy;
        uv.y *= -1.0;

        vec3 offTexX = texture2D(tex1, uv).rgb;
        vec3 luma = vec3(0.299, 0.587, 0.114);
        float power = dot(offTexX, luma);

        power = sin(3.1415927 * 2.0 * mod(power + u_time * warpSpeed, 1.0));

        gl_FragColor = texture2D(tex0, uv + vec2(0, power) * warpDistance);
    ''')


image panowarp:
    # Use built in Model class to set up the shader and textures
    # https://www.renpy.org/doc/html/model.html#model-displayable
    (Model().child('panorama.jpg', fit=True)
            .property('texture_wrap', reflect)
            .shader('mygame.warp')
            .texture('bayer.png'))

    # This is a special case in Ren'Py that ensures a single frame is rendered
    # before immediately redrawing. This will try to run the shader at Ren'Py's
    # maximum frame rate.
    pause 0
    repeat


label start:
    show panowarp
    'Wibbly wobbly!'
    return

Lubnewski
Newbie
Posts: 3
Joined: Sat Nov 27, 2021 2:53 am
Contact:

Re: Need help with custom warp shader

#3 Post by Lubnewski »

strayerror wrote: Sat Nov 27, 2021 2:30 pm I believe there were two primary causes for your problems. The first was the calculation of the uv value, which I believe was being done twice with the result that the shader was only ever sampling a single pixel, explaining your blue screen. The second was that the the ShaderDisplayable class never seems to call redraw, making it dependent on other things in order to update, which tends to result in a very choppy animation.

The first can be solved by switching to using v_tex_coord to seed the uv variable, which follows the convention of how Ren'Py generates the uv equivalent value in it's own shaders, using the a_tex_coord attribute from the standard mesh. The second can be solved by using ATL to repeatedly render the shader as quick as it can up to Ren'Py's frame cap resulting in a smooth animation.

Additionally managing texture wrapping can make the edges behave a little nicer. See the code comments for more details.

Hope this helps! :)

Code: Select all

init python:
    # To avoid vertical bands at the top and bottom of the warp (where the
    # shader is sampling outside of the texture space due to the offsets), we
    # use the mirrored repeat texture wrapping. Think of it as putting mirrors
    # at all the edges of your texture. More information can be found at the
    # link below under gl_texture_wrap.
    # https://www.renpy.org/doc/html/model.html#gl-properties
    from renpy.uguu import GL_MIRRORED_REPEAT

    # This is just for convenience later, a short hand for mirroring in both
    # the x- and y-axes.
    reflect = (GL_MIRRORED_REPEAT, GL_MIRRORED_REPEAT)


    # Main issue with the shader was the uv calculation:
    # - Switched to Ren'Py's typical v_tex_coord removing the need for res0
    # - Removed all uniforms that were not being used as uniforms
    # - Moved control constants so they can be declared const
    renpy.register_shader('mygame.warp', variables='''
        uniform sampler2D tex0;
        uniform sampler2D tex1;
        uniform float u_time;
        attribute vec2 a_tex_coord;
        varying vec2 v_tex_coord;
    ''', vertex_200='''
        v_tex_coord = a_tex_coord;
    ''', fragment_functions='''
        const float warpSpeed = 0.3;
        const float warpDistance = 0.02;
    ''', fragment_200='''
        vec2 uv = v_tex_coord.xy;
        uv.y *= -1.0;

        vec3 offTexX = texture2D(tex1, uv).rgb;
        vec3 luma = vec3(0.299, 0.587, 0.114);
        float power = dot(offTexX, luma);

        power = sin(3.1415927 * 2.0 * mod(power + u_time * warpSpeed, 1.0));

        gl_FragColor = texture2D(tex0, uv + vec2(0, power) * warpDistance);
    ''')


image panowarp:
    # Use built in Model class to set up the shader and textures
    # https://www.renpy.org/doc/html/model.html#model-displayable
    (Model().child('panorama.jpg', fit=True)
            .property('texture_wrap', reflect)
            .shader('mygame.warp')
            .texture('bayer.png'))

    # This is a special case in Ren'Py that ensures a single frame is rendered
    # before immediately redrawing. This will try to run the shader at Ren'Py's
    # maximum frame rate.
    pause 0
    repeat


label start:
    show panowarp
    'Wibbly wobbly!'
    return
Thank you! It works now :)

Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot], Google [Bot]