1. Hey! Guest! The 36th GMC Jam will take place between February 27th, 12:00 UTC - March 2nd, 12:00 UTC. Why not join in! Click here to find out more!
    Dismiss Notice

Code practices - managing shaders

Discussion in 'Programming' started by kupo15, Jan 31, 2020.

  1. kupo15

    kupo15 Member

    Joined:
    Jun 20, 2016
    Posts:
    925
    The better I get at shaders, the less I understand how to manage it all. So many uniforms that have to be assigned to specific shaders. Also the logical way to organize things seems not good for shaders given the nature.

    For example, if I have a palette swap shader and a contrast shader, my normal instinct would be to make them both separately and then apply them independently. But this won't be good because then you are double processing the pixels with two passes. Then if you want to also have a pixelation shader as well, that's 3 passes.

    Does this mean its better to essentially create shaders with every single combination possible and have a ton of uniform sets to match then pick which one to apply in the draw event?

    Code:
    // DRAW
    shader = pixel
    shader_set(shader)
    switch shader
    {
    case pixel: set pixel uniforms break
    case pixel palette: set pixel palette unforms break
    }
    
    draw_sprites
    end shader
    
    Or would it be better to only have ONE shader only that does it all and you pass uniforms that toggle on and off the different steps of the different effects? And somehow doing it without if statements in the Shader.

    Here are my stray thoughts I had how to wrap my head around this as I was talking with Hopeless. Just pasting my spitballing from our chat:
    hmm well looking at the code, perhaps you do have to have each object set/get it in the create so both can have different values. i think what's confusing is that it seems like you are working backwards

    I was always confused why its uniform_GET in the create event....not set

    so with scripts, you call the script and set variables in it. but with shaders, I think the uniforms inside the shader are where they are initialized...aka the true create event is there. and the objects create event is getting the reference to those uniforms kinda like pixel_pointer = shader.pixSize

    do we have to do it in the create event? Can we call that every frame in the draw? otherwise if you have two shaders that use the same uniform and you want to use the same variable you have to do like texSize1 = get (shader0) texSize2 = get (shader1)

    blahh, its so fragmented dealing with 3 different places lol I wish I could just pass them in like you do with scripts

    Its definitely a clusterF to figure out how to organize the code

    that's where my 2 hour time waste struggle happened. I think I overrode the same variable with another shader because I wanted one shader to draw the distortion, and another one which basically is the same to draw the normal map somewhere else for previewing

    in that case it would be great to be able to run shader code inside of a shader

    @Xor @The Reverend @GMWolf
     
    Last edited: Jan 31, 2020
    Cpaz likes this.
  2. Ido-f

    Ido-f Member

    Joined:
    Feb 19, 2018
    Posts:
    145
    Both ways are doable (although different passes is probably inferior, as it also requires extra coding in addition to being less optimised).
    Personally I simply look at it as a limitation of GM as an engine and not worry so much about it.

    One tip I do have Is organising the different shader actions in functions, which you can define above the main function.
    for example:
    Code:
    //get a grayscale value of a color
    float LumOf (vec3 col)
    {
        return dot(col, vec3(0.299, 0.587, 0.14));
    }
    It's easier to remember what each part of the code does that way, as well as copying them into a combined shader.
     
    Last edited: Feb 1, 2020
    kupo15 likes this.
  3. kupo15

    kupo15 Member

    Joined:
    Jun 20, 2016
    Posts:
    925
    Sorry for the late reply I was away all weekend. I didn't know you could use the return function, how does that work inside the shader? I though the main function was where you loop through the pixels, how does that code above the main work? What variable is the return going to?
     
  4. Ido-f

    Ido-f Member

    Joined:
    Feb 19, 2018
    Posts:
    145
    You do loop through the pixels in the main function. Any other function you define (above the main function in code) is just a tool you can use to organise code. The return is acting the same way it does in gml scripts.
    So for example, a simple grayscale shader could be coded that way:

    Code:
    varying vec2 v_vTexcoord;
    varying vec4 v_vColour;
    
    float LumOf (vec3 col) //float in this line defines the return type, vec3 defines the argument type
    {
        return dot(col, vec3(0.299, 0.587, 0.14));
    }
    
    void main()
    {
        vec4 org_col = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
    
        gl_FragColor = vec4( vec3(LumOf(org_col.rgb)), 1.0);
    }
    
    In this example it might seem to overcomplicate things, but when your shader does many different things it's nice to have them listed in functions, and it makes it easier to import utilities between shaders.
     
    kupo15, Bart, Rob and 1 other person like this.
  5. GMWolf

    GMWolf aka fel666

    Joined:
    Jun 21, 2016
    Posts:
    3,518
    It is indeed better for performance to merge your shaders (for per pixel effects. If you are doing multiple taps it may be worth separating your shaders to avoid having to do too many taps)

    You *could* have all the different combinations of shaders, but I don't see a good way to do that in GM.

    If you don't know at compile time what combinations of effects you will use, or there are simply too many, then a mega shader with uniforms to toggle effects on and off is perfectly fine.
    Branching on a uniform is quite cheap, especially on modern hardware. (Unlike branching on per-fragment values, that's very slow).

    As for uniform get/set:

    In order to set a uniform, you need to k ow it's "position". A uniform position is its binding point. It's a shared index between your game code and shader code.
    In order to get the position of the uniform, you need to get it using its name. This does a (relatively slow) table lookup. So we only do it once.
    The position can be different for every shader so you have to get it per shader.

    And in case you missed it earlier:

    If statements in shaders are ok, provided they are applied to dynamically uniform values. A dynamically uniform value is a value that is the same across all threads (a wave).

    Uniforms are dynamically uniform. So it's ok to use ifs with uniforms as conditions. (Especially on modern hardware).
    It won't be as fast as generating every permutation of shaders but the difference is probably negligible in your case.

    GPUs achieve their parallelism by having super wide registers and ALUs.
    On modern hardware, those are usually 64 elements wide, or 2048 bits wide.
    Each "core" processes many pixels at a time, applying the same operation to each pixel at a time.
    When you branch with a non dynamically uniform value, some pixels might go down one path, and others down the other.
    So the core will evaluate the condition for each pixel, and apply both branch to each pixel (disabling lanes respectively).

    However with a dynamically uniform value, it knows all pixels will go down the same branch, so it can execute only the instructions you go down.
     
    Last edited: Feb 11, 2020
    kupo15 likes this.
  6. kupo15

    kupo15 Member

    Joined:
    Jun 20, 2016
    Posts:
    925
    Thanks for the info! So applying the if statements to the dynamically uniform values would be in the section above the main like in ido-f's example?
     
  7. GMWolf

    GMWolf aka fel666

    Joined:
    Jun 21, 2016
    Posts:
    3,518
    You could do something like so:
    Code:
    uniform float applyEffect1;
    uniform float applyEffect2;
    uniform float applyEffect3;
    
    vec3 effect1(vec3 c) {
     //Whatever
    }
    vec3 effect2(vec3 c) {
     //Whatever
    }
    
    vec3 effect3(vec3 c) {
     //Whatever
    }
    
    void main()
    {
    ....
    if (applyEffect1)
    {
      col = effect1(col);
    }
    
    if (applyEffect2)
    {
      col = effect2(col);
    }
    
    if (applyEffect3)
    {
      col = effect3(col);
    }
    ...
    }
    
    
    Then you can have whatever combination of the three effects you want with one shader (and, on modern hardware at least, little more performance cost compared to having all 6 different combinations of shaders.
     
    Neptune, Ido-f and kupo15 like this.
  8. kupo15

    kupo15 Member

    Joined:
    Jun 20, 2016
    Posts:
    925
    oh I think I get how that works now. Cool, thanks!
     

Share This Page