Python Moderngl Rain Ripple Shader

126 Views Asked by At

I am trying to recreate the ripple effect from the following article in python with glsl shaders and window managment via pygame https://web.archive.org/web/20160418004149/http://freespace.virgin.net/hugo.elias/graphics/x_water.htm I've found a few other sources which I've pulled from: https://www.youtube.com/watch?v=qm5cDNbtGig https://www.youtube.com/watch?v=BZUdGqeOD0w

The shader is supposed to create water ripples onto the screen were I click by changing the rgb values of the pixels between tw o buffers while also applying a dampening effect.

Theory is that there is a current buffer which stores pixel data of the current frame and a previous buffer which contains texel data from the previous frame. You change the rgb value for each pixel depending on the values of surrounding pixels while applying a dampening factor which reduces the brightness of the pixel.

Then you display the current buffer and swap the buffers

The implementation isn't supposed to use cosine or sine which makes it very intersting.

My problem is that the ripples do not get displayed.

I've tried two versions which are both unsucsessful: The first try mostly follows the 1st video where I use numpy arrays (float32) which are written into moderngl textures. The fragment shader graps the pixel of the shader and modifies it like in the video. Then spits out the result the same.

The second mostly abandons the fragment shader It uses two numpy arrays of float32 which are written into moderngl textures only for rendering. I follow the article to handle changing the pixels and re-write to the texture

first attempt

import numpy
import pygame, moderngl
from sys import exit

class Main():
    def __init__(self):
        self.vertex_shader: str = """
# version 460 core

in vec2 aPosition;
in vec2 aTexCoord;
out vec2 pos;

void main(){
    pos = aTexCoord;
    pos.y = 1.0 - pos.y;

    gl_Position = vec4(aPosition, 0.0, 1.0);
}
"""

        self.fragment_shader: str = """
#version 460 core

uniform vec2 resolution;
uniform float dampening;
uniform sampler2D currBuffer;
uniform sampler2D prevBuffer;

in vec2 pos;

out vec4 f_colour;

void main(){
    vec2 pix = 1.0/resolution;

    float prev = texture(prevBuffer, pos).r;

    float u = texture(currBuffer, pos + vec2(0.0, pix.y)).r;
    float d = texture(currBuffer, pos - vec2(0.0, pix.y)).r;
    float r = texture(currBuffer, pos + vec2(pix.x, 0.0)).r;
    float l = texture(currBuffer, pos - vec2(pix.x, 0.0)).r;

    float next = ((u + d + l + r) / 2.0) - prev;
    next = next * dampening;

    f_colour = vec4(next, next/2.0 + 0.5, 1.0, 1.0);
}
"""

        pygame.init()
        self.screen: pygame.Surface   = pygame.display.set_mode((1000,600), pygame.RESIZABLE | pygame.OPENGL | pygame.DOUBLEBUF)
        self.clock: pygame.time.Clock = pygame.time.Clock()
        pygame.display.set_caption("Render Engine Test")

        self.ctx: moderngl.Context = moderngl.create_context()

        self.program: moderngl.Program     = self.ctx.program(vertex_shader=self.vertex_shader, fragment_shader=self.fragment_shader)
        self.vbo_1:   moderngl.Buffer      = self.ctx.buffer(data=numpy.array([-1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0], dtype="f4"))
        self.vbo_2:   moderngl.Buffer      = self.ctx.buffer(data=numpy.array([-1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0], dtype="f4"))
        self.vao:     moderngl.VertexArray = self.ctx.vertex_array(self.program, [(self.vbo_1, "2f", "aPosition"), (self.vbo_2, "2f", "aTexCoord")])
        self.current_buffer  = numpy.array([[0.0 for x in range(self.screen.get_size()[0])] for y in range(self.screen.get_size()[1])], dtype="f4")
        self.previous_buffer = numpy.array([[0.0 for x in range(self.screen.get_size()[0])] for y in range(self.screen.get_size()[1])], dtype="f4")
        
        self.current_buffer[500][300] = 0.1

        self.program["resolution"] = self.screen.get_size()
        self.program["dampening"]  = 0.9

        self.current_texture:  moderngl.Texture = self.ctx.texture(size=self.screen.get_size(), components=4)
        self.previous_texture: moderngl.Texture = self.ctx.texture(size=self.screen.get_size(), components=4)

        self.current_texture.use(location=0)
        self.previous_texture.use(location=1)
        self.program["currBuffer"] = 0
        self.program["prevBuffer"] = 1

    def update(self):

        self.current_texture.write(data=self.current_buffer)
        self.previous_texture.write(data=self.previous_buffer)

        temp_buffer = self.previous_buffer
        self.previous_buffer = self.current_buffer
        self.current_buffer  = temp_buffer


    def draw(self):
        self.ctx.clear(0.0, 0.0, 0.0)
        self.vao.render(mode=moderngl.TRIANGLE_STRIP)
        pygame.display.flip()

    def check_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.exit_garbage()
                pygame.quit()
                exit()


    def exit_garbage(self):
        self.vao.release()
        self.program.release()
        self.vbo_1.release()
        self.vbo_2.release()
        self.current_texture.release()
        self.previous_texture.release()

    def run(self):
        while True:
            self.check_events()
            self.update()
            self.draw()
            self.clock.tick(60)

if __name__ == "__main__":
    main: Main = Main()
    main.run()

second attempt

import numpy
import pygame, moderngl
from sys import exit

class Main():
    def __init__(self):
        self.vertex_shader: str = """
# version 460 core

in vec2 aPosition;

void main(){
    gl_Position = vec4(aPosition, 0.0, 1.0);
}
"""

        self.fragment_shader: str = """
#version 460 core

uniform vec2 resolution;
uniform sampler2D myTexture;
uniform float dampening;
out vec4 f_colour;

void main(){

    vec2 pos = vec2(gl_FragCoord.xy / resolution.xy);

    float next = texture(myTexture, pos).r * dampening;
    f_colour   = vec4(next, next/2.0 + 0.5, 1.0, 1.0);
}
"""

        pygame.init()
        self.screen: pygame.Surface   = pygame.display.set_mode((500,500), pygame.RESIZABLE | pygame.OPENGL | pygame.DOUBLEBUF)
        self.clock: pygame.time.Clock = pygame.time.Clock()
        pygame.display.set_caption("Render Engine Test")

        self.ctx: moderngl.Context = moderngl.create_context()

        self.program: moderngl.Program     = self.ctx.program(vertex_shader=self.vertex_shader, fragment_shader=self.fragment_shader)
        self.vbo_1:   moderngl.Buffer      = self.ctx.buffer(data=numpy.array([-1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0], dtype="f4"))
        self.vao:     moderngl.VertexArray = self.ctx.vertex_array(self.program, [(self.vbo_1, "2f", "aPosition")])

        # self.buffer_1 = numpy.array([1.0 for x in range(self.screen.get_width()) for x in range(self.screen.get_height())], dtype="f4")
        self.buffer_1 = numpy.zeros(shape=(self.screen.get_height(), self.screen.get_width()), dtype="f4")
        self.buffer_2 = numpy.zeros(shape=(self.screen.get_height(), self.screen.get_width()), dtype="f4")
        
        self.texture: moderngl.Texture = self.ctx.texture(size=self.screen.get_size(), components=4)
        self.texture.write(data=self.buffer_1.tobytes())
        self.texture.use(location=0)

        self.program["dampening"]  = 0.9
        self.program["resolution"] = self.screen.get_size()
        self.program["myTexture"]  = 0
    
    def update(self):
        try:
            for y in range(len(self.buffer_2)):
                for x in range(len(self.buffer_2)):
                    self.buffer_2[y][x] = (
                        self.buffer_1[y][x-1] +
                        self.buffer_1[y][x+1] +
                        self.buffer_1[y+1][x] +
                        self.buffer_1[y-1][x]
                    ) / 2 - self.buffer_2[y][x]
        except: pass

    def draw(self):
        self.ctx.clear(0.0, 0.0, 0.0)
        self.vao.render(mode=moderngl.TRIANGLE_STRIP)
        pygame.display.flip()

        temp_buffer = self.buffer_2
        self.buffer_2 = self.buffer_1
        self.buffer_1 = temp_buffer

        self.texture.write(data=self.buffer_1.tobytes())

    def check_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.exit_garbage()
                pygame.quit()
                exit()
        if pygame.mouse.get_pressed()[0]:
            x: int; y: int
    
            x, y = pygame.mouse.get_pos()
            self.buffer_1[y-1][x-1] = 1


    def exit_garbage(self):
        self.ctx.release()
        self.vao.release()
        self.vbo_1.release()
        self.texture.release()

    def run(self):
        while True:
            self.check_events()
            self.update()
            self.draw()
            self.clock.tick(60)

if __name__ == "__main__":
    main: Main = Main()
    main.run()

Could I have some assistance?

0

There are 0 best solutions below