Search code examples
glslkivypost-processing

How to change the fragment shader of a kivy RenderContext?


I have a kivy app where I want everything that is drawn on a particular RenderContext to have a shader post-processing effect applied to it (similar to the effects demonstrated in the EffectWidget example in the kivy-examples in the documentation).

In the constructor of the world Widget I create the RenderContext

self.prc = RenderContext()

set its projection matrix (this works)

self.prc['projection_mat'] = proj_mat

and then try to set its fragment shader to a minimal copy of the default fragment shader that should just make everything one tenth as opaque (basically dimming the screen).

self.prc.shader.fs = """
#ifdef GL_ES
    precision highp float;
#endif

/* Outputs from the vertex shader */
varying vec4 frag_color;
varying vec2 tex_coord0;

/* uniform texture samplers */
uniform sampler2D texture0;

void main (void){
    gl_FragColor = 0.1*frag_color * texture2D(texture0, tex_coord0);
}
"""

If the code for this shader is incorrect, the program doesn't run, complaining of a shader compile error, which indicates that the shader is being compiled. But, I don't see any effect of the shader. Everything rendered on to prc is drawn, but at normal opacity. What am I doing wrong? Thanks for your time!


EDIT!

I have been asked to provide a complete runnable example. The following program draws two rectangles. The Rectangle on the left has its own RenderContext and is not affected by the grayscale postprocessing affect (it is drawn as Red). The Rectangle on the right does not have its own RenderContext and it is correctly affected by the postprocessing affect.

Here is the code:

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.floatlayout import FloatLayout
from kivy.graphics import *
from kivy.graphics.opengl import *
from kivy.graphics.shader import *
from kivy.core.window import Window
from kivy.graphics.transformation import Matrix

from kivy.logger import Logger

class World(Widget) :
    def __init__(self, **kwargs):
        Logger.debug('world.init()')

        # Parent RenderContext for subsuming all other render contexts
        self.prc=RenderContext()
        proj_mat = Matrix()
        proj_mat.look_at(0.,0.,1., # eye position coords
                         0.,0.,0.,  # looking at these coords
                         0,1.,0)    # a vector that points up

        if Window.height > Window.width :
            self.xRadius = float(Window.width)/Window.height
            self.yRadius = 1.0
            proj_mat.scale(1.0/self.xRadius,1.0,1.0)
        else :
            self.xRadius = 1.0
            self.yRadius = float(Window.height)/Window.width
            proj_mat.scale(1.0,1.0/self.yRadius,1.0)

        self.prc['projection_mat'] = proj_mat

        ## an effect shader used to make objects monochromatic (grayscale)
        self.prc.shader.fs = """
#ifdef GL_ES
precision highp float;
#endif

/* Outputs from the vertex shader */
varying vec4 frag_color;
varying vec2 vTexCoords0;

/* uniform texture samplers */
uniform sampler2D texture0;

uniform vec2 resolution;
uniform float time;

void main() {
  vec4 rgb = texture2D(texture0, vTexCoords0);
  float c = (rgb.x + rgb.y + rgb.z) * 0.3333;
  gl_FragColor = vec4(c, c, c, 1.0);
}
"""

        if not self.prc.shader.success :
            raise Exception('Effect shader compile failed.')

        self.canvas = self.prc

        ## Left Rectangle drawn with its own RenderContext
        ## this is not affected by the effect shader (if it were, it would be drawn as white)
        ## but it is affected by the parent's projection matrix
        self.spriteRC = RenderContext(use_parent_projection=True)
        self.spriteRC.add(Color(1,0,0,1))
        self.spriteRC.add(Rectangle(pos=(-0.25,0.0),size=(0.1,0.1)))

        ## Right Rectangle object drawn directly to the canvas
        ## this **is** affected by the effect shader
        self.canvas.add(Color(1,0,0,1))
        self.canvas.add(Rectangle(pos=(0.25,0),size=(0.1,0.1)))
        self.canvas.add(self.spriteRC)



        super(World, self).__init__(**kwargs)

class GameApp(App):
    def build(self):
        w = World()
        fl = FloatLayout()
        fl.add_widget(w)
        return fl

if __name__ == '__main__':
    GameApp().run()

Solution

  • Shader cannot be stacked in the pipeline. Only the latest binded will be used, it's not a Kivy limitation, but that's how OpenGL works. Ie:

    self.c1 = RenderContext()
    self.c2 = RenderContext()
    self.c2.add(Rectangle())
    self.c1.add(self.c2)
    

    The rectangle will be processed only by the latest shader, the one in c2.

    In order to have a specific shader for the rectangle, and then process the output with the c1 shader as well, use Framebuffer (Fbo are a RenderContext subclass)!

    self.c1 = RenderContext()
    self.c2 = Fbo(size=...)
    self.c2.add(Rectangle())
    self.c1.add(self.c2)  # this is just for triggering the render from c1 when c2 content changes
    self.c1.add(Rectangle(size=self.c2.size, texture=self.c2.texture))
    

    I missed all the Color and others parameters here, but it's just for the demonstration. You can change the shader on the Fbo the same way you do on RenderContext.