Search code examples
pythonopenglglslpyopengl

GLSL, SDF based Rounding Rectangle


The app is based on PyOpenGL (core profile) and using orthographic projection. I have to draw several different 2d shapes on a quad(2 triangles).

I have found a really great article on rendering 2d/3d shapes using SDF. The first shape I'm trying is a rounded rectangle with border. This Shadertoy example perfectly fits to my requirement. Here are my two shaders:

VERTEX SHADER

#version 330 core
// VERTEX SHADER
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 tex_coord;
uniform mat4 mvp;

void main()
{
    gl_Position = mvp * vec4(aPos, 1.0);
    tex_coord = aTexCoord;
}

FRAGMENT SHADER

#version 330 core
// FRAGMENT SHADER 

uniform vec4 in_color;
in vec2 tex_coord;
vec2 resolution = vec2(800, 600);
float aspect = resolution.x / resolution.y;
const float borderThickness = 0.01;
const vec4 borderColor = vec4(1.0, 1.0, 0.0, 1.0);
const vec4 fillColor = vec4(1.0, 0.0, 0.0, 1.0);
const float radius = 0.05;

float RectSDF(vec2 p, vec2 b, float r)
{
    vec2 d = abs(p) - b + vec2(r);
    return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;   
}

void main() {
    
    // https://www.shadertoy.com/view/ltS3zW
    vec2 centerPos = tex_coord - vec2(0.5, 0.5); // <-0.5,0.5>
    //vec2 centerPos = (tex_coord/resolution - vec2(0.5)) * 2.0;
    //centerPos *= aspect; // fix aspect ratio
    //centerPos = (centerPos - resolution.xy) * 2.0;
    
    float fDist = RectSDF(centerPos, vec2(0.5, 0.5), radius);
    
    vec4 v4FromColor = borderColor; // Always the border color. If no border, this still should be set
    vec4 v4ToColor = vec4(0.0, 0.0, 1.0, 1.0); // Outside color
    
    if (borderThickness > 0.0)
    {
        if (fDist < 0.0)
        {
            v4ToColor = fillColor;   
        } 
        
        fDist = abs(fDist) - borderThickness;
    }
    
    float fBlendAmount = smoothstep(-0.01, 0.01, fDist);
    
    // final color
    gl_FragColor = mix(v4FromColor, v4ToColor, fBlendAmount);
}

And the difference between two outputs:

enter image description here

Problem 1 In Shadertoy example, border is neat and there is no blurring, mine is blurred.

Problem 2 I am using ndc coordinates to specify borderThickness and radius, because of this I'm not getting a consistent border. If you see in the image, horizontal border is slightly wider then vertical one. I would prefer to use borderThickness and radius in pixel size. The idea is to get a consistent border around the rectangle irrespective of screen dimension.

Problem 3 Make outside blue color transparent.

Problem 4 As I've mentioned that I've recently started to learn GLSL, Some where I've read that too many "if" conditions would greatly affect the shader performance and chances are you might be using them unnecessary. There are already two "if" conditions exists in this code and I'm not sure if they can be omitted.


Solution

  • Use a Uniform (rectSize) to to specify the size of the rectangle in pixel. The texture coordinates (tex_coord) need to be in range [0.0, 1.0]. Compute the pixel position in the rectangle (rectSize * tex_coord). Now you can specify the radius and the edge thickness in pixels:

    in vec2 tex_coord;
    uniform vec2 rectSize;
    
    const float borderThickness = 10.0;
    const float radius = 30.0;
    
    // [...]
    
    void main() 
    {
        vec2 pos = rectSize * tex_coord;
            
        float fDist = RectSDF(pos-rectSize/2.0, rectSize/2.0 - borderThickness/2.0-1.0, radius);
        float fBlendAmount = smoothstep(-1.0, 1.0, abs(fDist) - borderThickness / 2.0);
    
        vec4 v4FromColor = borderColor;
        vec4 v4ToColor = (fDist < 0.0) ? fillColor : vec4(0.0);
        gl_FragColor = mix(v4FromColor, v4ToColor, fBlendAmount);
    }
    
    rect_loc = glGetUniformLocation(program, "rectSize")
    glUniform2f(rect_loc, width, height)
    

    Use Blending to make the outside transparent. For this, the alpha channel of the outer color must be 0. (e.g. vec4(0.0))

    glEnable(GL_BLEND)
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
    

    Minimal example:

    from OpenGL.GLUT import *
    from OpenGL.GLU import *
    from OpenGL.GL import *
    import OpenGL.GL.shaders
    from ctypes import c_void_p
    import glm
    
    sh_vert = """
    #version 330 core
    // VERTEX SHADER
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec2 aTexCoord;
    
    out vec2 tex_coord;
    uniform mat4 mvp;
    
    void main()
    {
        gl_Position = mvp * vec4(aPos, 1.0);
        tex_coord = aTexCoord;
    }
    """
    
    sh_frag = """
    #version 330 core
    // FRAGMENT SHADER 
    
    in vec2 tex_coord;
    uniform vec2 rectSize;
    
    const vec4 borderColor = vec4(1.0, 1.0, 0.0, 1.0);
    const vec4 fillColor = vec4(1.0, 0.0, 0.0, 1.0);
    const float borderThickness = 10.0;
    const float radius = 30.0;
    
    float RectSDF(vec2 p, vec2 b, float r)
    {
        vec2 d = abs(p) - b + vec2(r);
        return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;   
    }
    
    void main() 
    {
        vec2 pos = rectSize * tex_coord;
            
        float fDist = RectSDF(pos-rectSize/2.0, rectSize/2.0 - borderThickness/2.0-1.0, radius);
        float fBlendAmount = smoothstep(-1.0, 1.0, abs(fDist) - borderThickness / 2.0);
    
        vec4 v4FromColor = borderColor;
        vec4 v4ToColor = (fDist < 0.0) ? fillColor : vec4(0.0);
        gl_FragColor = mix(v4FromColor, v4ToColor, fBlendAmount);
    }
    """
    
    def display():
        glClear(GL_COLOR_BUFFER_BIT)
        
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, None)
    
        glutSwapBuffers()
        glutPostRedisplay()
    
    def reshape(width, height):
        glViewport(0, 0, width, height)
        
    
    resolution = (640, 480)
    rect = (50, 50, 350, 250)
    attributes = (GLfloat * 20)(*[rect[0],rect[1],0,0,1, rect[2],rect[1],0,1,1, rect[2],rect[3],0,1,0, rect[0],rect[3],0,0,0])
    indices = (GLuint * 6)(*[0,1,2, 0,2,3])
    
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB)
    glutInitWindowSize(*resolution)
    glutCreateWindow(b"OpenGL Window")
    glutDisplayFunc(display)
    glutReshapeFunc(reshape)
    
    vao = glGenVertexArrays(1)
    vbo = glGenBuffers(1)
    ebo = glGenBuffers(1)
    glBindVertexArray(vao)
    glBindBuffer(GL_ARRAY_BUFFER, vbo)
    glBufferData(GL_ARRAY_BUFFER, attributes, GL_STATIC_DRAW)
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo)
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW)
    glVertexAttribPointer(0, 3, GL_FLOAT, False, 5 * 4, None)
    glEnableVertexAttribArray(0)
    glVertexAttribPointer(1, 2, GL_FLOAT, False, 5 * 4, c_void_p(3 * 4))
    glEnableVertexAttribArray(1)
    
    program = OpenGL.GL.shaders.compileProgram(
        OpenGL.GL.shaders.compileShader(sh_vert, GL_VERTEX_SHADER),
        OpenGL.GL.shaders.compileShader(sh_frag, GL_FRAGMENT_SHADER)
    )
    glUseProgram(program)
    mvp_loc = glGetUniformLocation(program, "mvp")
    mvp = glm.ortho(0, *resolution, 0, -1, 1)
    glUniformMatrix4fv(mvp_loc, 1, GL_FALSE, glm.value_ptr(mvp))
    rect_loc = glGetUniformLocation(program, "rectSize")
    glUniform2f(rect_loc, rect[2]-rect[0], rect[3]-rect[1])
    
    glClearColor(0.5, 0.5, 0.5, 0.0)
    glutMainLoop()