3

I would like to dynamically invoke an easing based on the data passed into the shader. So in pseudocode:

var easing = easings[easingId]
var value = easing(point)

I'm wondering the best way to accomplish this in GLSL. I could use a switch statement in some way, or could perhaps put the easings into an array and use them like that. Or maybe there is a way to create a hashtable and use it like the above example.

easingsArray = [
  cubicIn,
  cubicOut,
  ...
]

uniform easingId

main() {
  easing = easingsArray[easingId]
  value = easing(point)
}

That would be the potential array approach. Another approach is the switch statement. Maybe there are others. Wondering what the recommended way is of doing this. Maybe I could use a struct somehow...

user10869858
  • 481
  • 1
  • 3
  • 16
  • 1
    What is an "easing"? It's not really clear what it is you're trying to do. Is an "easing" a function? – Nicol Bolas Mar 14 '19 at 23:57
  • Easings are the [easing functions](https://gist.github.com/gre/1650294) for animation. I want to pick one dynamically based on the data. – user10869858 Mar 15 '19 at 00:00

2 Answers2

9

If you need conditional branching in GLSL (in your case for selecting an easing function based on a variable) you'll need to use if or switch statements.

For example

if (easingId == 0) {
    result = cubicIn();
} else if (easingId == 1) {
    result = cubicOut();
}

or

switch (easingId) {
case 0:
    result = cubicIn();
    break;
case 1:
    result = cubicOut();
    break;
}

GLSL has no support for function pointers, so the kind of dynamic dispatch solutions you are considering (tables of function pointers etc.) will unfortunately not be possible.

Whilst your question was explicitly about data being passed into the shader, I would also like to point out that if the value controlling the branch is being passed into the shader as a uniform, then you could instead compile multiple variants of your shader, and then dynamically select the right one (i.e. the one using the right easing function) from the application itself. This would save the cost of branching in the shader.

Laurie
  • 186
  • 1
  • 3
1

Apart from conditional branching with if-else or switch, you may also want to have a look at the GLSL subroutines, this allows you to control the shader code branching on the C++ side, without touching the shader. Theoretically, this approach should be more efficient than using if-else, but the downside is that subroutines are not supported in SPIR-V, which is the future of shaders.

A subroutine is very much like a function pointer in C, you define multiple functions of the same "subroutine" type, and then use a subroutine uniform to control which function should be called at runtime. Here's an example of debug drawing a framebuffer using subroutines.

#version 460

// this shader is used by: `FBO::DebugDraw()`
////////////////////////////////////////////////////////////////////////////////

#ifdef vertex_shader

layout(location = 0) in vec3 position;
layout(location = 1) in vec3 normal;
layout(location = 2) in vec2 uv;
layout(location = 0) out vec2 _uv;

void main() {
    _uv = uv;
    gl_Position = vec4(position.xy, 0.0, 1.0);
}

#endif

////////////////////////////////////////////////////////////////////////////////

#ifdef fragment_shader

layout(location = 0) in vec2 _uv;
layout(location = 0) out vec4 color;

layout(binding = 0) uniform sampler2D color_depth_texture;
layout(binding = 1) uniform usampler2D stencil_texture;

const float near = 0.1;
const float far = 100.0;

subroutine vec4 draw_buffer(void);  // typedef the subroutine (like a function pointer)
layout(location = 0) subroutine uniform draw_buffer buffer_switch;

layout(index = 0)
subroutine(draw_buffer)
vec4 DrawColorBuffer() {
    return texture(color_depth_texture, _uv);
}

layout(index = 1)
subroutine(draw_buffer)
vec4 DrawDepthBuffer() {
    // sampling the depth texture format should return a float
    float depth = texture(color_depth_texture, _uv).r;

    // depth in screen space is non-linear, the precision is high for small z-values
    // and low for large z-values, we need to linearize depth values before drawing
    float ndc_depth = depth * 2.0 - 1.0;
    float z = (2.0 * near * far) / (far + near - ndc_depth * (far - near));
    float linear_depth = z / far;

    return vec4(vec3(linear_depth), 1.0);
}

layout(index = 2)
subroutine(draw_buffer)
vec4 DrawStencilBuffer() {
    uint stencil = texture(stencil_texture, _uv).r;
    return vec4(vec3(stencil), 1.0);
}

void main() {
    color = buffer_switch();
}

#endif

neo-mashiro
  • 422
  • 4
  • 13