So far, our shader programs that we have been using in previous posts, have been static. By that, I mean that you compile them and all of the variables in the shader are fixed and never change (The only thing that can change is the vertex data itself). In this part on our journey of learning opengl, we are going to be controlling some parameters in the shader program so that we can change values over time by sending them *into* the shader from our c++ code, every frame. The are basically variables that we can tweak, and they are very fast and easy to change as opposed to having to change vertex data.

The way we do this is by using “uniform parameters”. These are variables in the shader that are exposed to the external c++ code and can be set dynamically. The reason they are called uniform, is because they have the same value across all invocations of the shader on all of your vertices/fragments.

There isn’t much we actually need to do for this to work. First lets deal with what we have to do in our shader. Previously we have some shader variables which were just set in the program itself to a hard coded value. Now we need to declare them as uniform. As we just said before, the word uniform itself describes how the value is going to be the same for all vertices/fragments that the shader program operates on. Instead of *varying* (which means different per vertex/fragment, like attributes), it is the same for each vertex/fragment invocation.

#include "error_handling.hpp" #include <array> #include <chrono> // current time #include <cmath> // sin & cos #include <cstdlib> // for std::exit() #include <fmt/core.h> // for fmt::print(). implements c++20 std::format #include <unordered_map> // this is really important to make sure that glbindings does not clash with // glfw's opengl includes. otherwise we get ambigous overloads. #define GLFW_INCLUDE_NONE #include <GLFW/glfw3.h> #include <glbinding/gl/gl.h> #include <glbinding/glbinding.h> #include <glbinding-aux/debug.h> #include "glm/glm.hpp" using namespace gl; using namespace std::chrono; int main() { auto startTime = system_clock::now(); const int width = 1280; const int height = 720; auto windowPtr = [](int w, int h) { if (!glfwInit()) { fmt::print("glfw didnt initialize!\n"); std::exit(EXIT_FAILURE); } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6); /* Create a windowed mode window and its OpenGL context */ auto windowPtr = glfwCreateWindow(w, h, "Chapter 9 - Full Screen Effects (Diy Shadertoy!)", nullptr, nullptr); if (!windowPtr) { fmt::print("window doesn't exist\n"); glfwTerminate(); std::exit(EXIT_FAILURE); } glfwSetWindowPos(windowPtr, 520, 180); glfwMakeContextCurrent(windowPtr); glbinding::initialize(glfwGetProcAddress, false); return windowPtr; }(width, height); // debugging { glEnable(GL_DEBUG_OUTPUT); glDebugMessageCallback(errorHandler::MessageCallback, 0); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_OTHER, GL_DEBUG_SEVERITY_NOTIFICATION, 0, nullptr, false); } auto createProgram = [](const char* vertexShaderSource, const char* fragmentShaderSource) -> GLuint { auto vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr); glCompileShader(vertexShader); errorHandler::checkShader(vertexShader, "Vertex"); auto fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr); glCompileShader(fragmentShader); errorHandler::checkShader(fragmentShader, "Fragment"); auto program = glCreateProgram(); glAttachShader(program, vertexShader); glAttachShader(program, fragmentShader); glLinkProgram(program); return program; }; auto program = createProgram(R"( #version 450 core const vec4 vertices[] = vec4[]( vec4(-1.f, -1.f, 0.0, 1.0), vec4( 3.f, -1.f, 0.0, 1.0), vec4(-1.f, 3.f, 0.0, 1.0)); void main(){ gl_Position = vertices[gl_VertexID]; } )", R"( #version 450 core // The MIT License // Copyright © 2013 Inigo Quilez // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and // associated documentation files (the "Software"), to deal in the Software without restriction, // including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", // WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // // I've not seen anybody out there computing correct cell interior distances for Voronoi // patterns yet. That's why they cannot shade the cell interior correctly, and why you've // never seen cell boundaries rendered correctly. // // However, here's how you do mathematically correct distances (note the equidistant and non // degenerated grey isolines inside the cells) and hence edges (in yellow): // // http://www.iquilezles.org/www/articles/voronoilines/voronoilines.htm // // More Voronoi shaders: // // Exact edges: https://www.shadertoy.com/view/ldl3W8 // Hierarchical: https://www.shadertoy.com/view/Xll3zX // Smooth: https://www.shadertoy.com/view/ldB3zc // Voronoise: https://www.shadertoy.com/view/Xd23Dh

out vec4 fragColor; vec4 fragCoord = gl_FragCoord; vec2 iMouse = vec2(960.f,0.f);

What we have done here is that we have created the same declarations of the variables that the shader program requires (that were previously provided by shadertoy behind the scenes) but now we want to change two of them (iTime and iResolution) to be exposed to and controlled by our c++ code. Here is how we do that…

uniform float iTime; uniform vec2 iResolution;

We have declared them as *uniform* variables. This is pretty much like declaring any other variable, but it has the “uniform” specifier in front of it. Now when the shader gets compiled, it will take that into account and provide a way for us to set the value of that variable any time we want.

mat2 rot(in float a){float c = cos(a), s = sin(a);return mat2(c,s,-s,c);} const mat3 m3 = mat3(0.33338, 0.56034, -0.71817, -0.87887, 0.32651, -0.15323, 0.15162, 0.69596, 0.61339)*1.93; float mag2(vec2 p){return dot(p,p);} float linstep(in float mn, in float mx, in float x){ return clamp((x - mn)/(mx - mn), 0., 1.); } float prm1 = 0.; vec2 bsMo = vec2(0); vec2 disp(float t){ return vec2(sin(t*0.22)*1., cos(t*0.175)*1.)*2.; } vec2 map(vec3 p) { vec3 p2 = p; p2.xy -= disp(p.z).xy; p.xy *= rot(sin(p.z+iTime)*(0.1 + prm1*0.05) + iTime*0.09); float cl = mag2(p2.xy); float d = 0.; p *= .61; float z = 1.; float trk = 1.; float dspAmp = 0.1 + prm1*0.2; for(int i = 0; i < 5; i++) { p += sin(p.zxy*0.75*trk + iTime*trk*.8)*dspAmp; d -= abs(dot(cos(p), sin(p.yzx))*z); z *= 0.57; trk *= 1.4; p = p*m3; } d = abs(d + prm1*3.)+ prm1*.3 - 2.5 + bsMo.y; return vec2(d + cl*.2 + 0.25, cl); } vec4 render( in vec3 ro, in vec3 rd, float time ) { vec4 rez = vec4(0); const float ldst = 8.; vec3 lpos = vec3(disp(time + ldst)*0.5, time + ldst); float t = 1.5; float fogT = 0.; for(int i=0; i<130; i++) { if(rez.a > 0.99)break; vec3 pos = ro + t*rd; vec2 mpv = map(pos); float den = clamp(mpv.x-0.3,0.,1.)*1.12; float dn = clamp((mpv.x + 2.),0.,3.); vec4 col = vec4(0); if (mpv.x > 0.6) { col = vec4(sin(vec3(5.,0.4,0.2) + mpv.y*0.1 +sin(pos.z*0.4)*0.5 + 1.8)*0.5 + 0.5,0.08); col *= den*den*den; col.rgb *= linstep(4.,-2.5, mpv.x)*2.3; float dif = clamp((den - map(pos+.8).x)/9., 0.001, 1. ); dif += clamp((den - map(pos+.35).x)/2.5, 0.001, 1. ); col.xyz *= den*(vec3(0.005,.045,.075) + 1.5*vec3(0.033,0.07,0.03)*dif); } float fogC = exp(t*0.2 - 2.2); col.rgba += vec4(0.06,0.11,0.11, 0.1)*clamp(fogC-fogT, 0., 1.); fogT = fogC; rez = rez + col*(1. - rez.a); t += clamp(0.5 - dn*dn*.05, 0.09, 0.3); } return clamp(rez, 0.0, 1.0); } float getsat(vec3 c) { float mi = min(min(c.x, c.y), c.z); float ma = max(max(c.x, c.y), c.z); return (ma - mi)/(ma+ 1e-7); } //from my "Will it blend" shader (https://www.shadertoy.com/view/lsdGzN) vec3 iLerp(in vec3 a, in vec3 b, in float x) { vec3 ic = mix(a, b, x) + vec3(1e-6,0.,0.); float sd = abs(getsat(ic) - mix(getsat(a), getsat(b), x)); vec3 dir = normalize(vec3(2.*ic.x - ic.y - ic.z, 2.*ic.y - ic.x - ic.z, 2.*ic.z - ic.y - ic.x)); float lgt = dot(vec3(1.0), ic); float ff = dot(dir, normalize(ic)); ic += 1.5*dir*sd*ff*lgt; return clamp(ic,0.,1.); } void main() { vec2 q = fragCoord.xy/iResolution.xy; vec2 p = (gl_FragCoord.xy - 0.5*iResolution.xy)/iResolution.y; bsMo = (iMouse.xy - 0.5*iResolution.xy)/iResolution.y; float time = iTime*3.; vec3 ro = vec3(0,0,time); ro += vec3(sin(iTime)*0.5,sin(iTime*1.)*0.,0); float dspAmp = .85; ro.xy += disp(ro.z)*dspAmp; float tgtDst = 3.5; vec3 target = normalize(ro - vec3(disp(time + tgtDst)*dspAmp, time + tgtDst)); ro.x -= bsMo.x*2.; vec3 rightdir = normalize(cross(target, vec3(0,1,0))); vec3 updir = normalize(cross(rightdir, target)); rightdir = normalize(cross(updir, target)); vec3 rd=normalize((p.x*rightdir + p.y*updir)*1. - target); rd.xy *= rot(-disp(time + 3.5).x*0.2 + bsMo.x); prm1 = smoothstep(-0.4, 0.4,sin(iTime*0.3)); vec4 scn = render(ro, rd, time); vec3 col = scn.rgb; col = iLerp(col.bgr, col.rgb, clamp(1.-prm1,0.05,1.)); col = pow(col, vec3(.55,0.65,0.6))*vec3(1.,.97,.9); col *= pow( 16.0*q.x*q.y*(1.0-q.x)*(1.0-q.y), 0.12)*0.7+0.3; //Vign fragColor = vec4( col, 1.0 ); } )"); GLuint vao; glCreateVertexArrays(1, &vao); glBindVertexArray(vao); glUseProgram(program);

int timeUniformLocation = glGetUniformLocation(program, "iTime"); int resolutionUniformLocation = glGetUniformLocation(program, "iResolution");

Now in our c++ code, we need a way to be able to refer to the location of that variable from the shader. This is done with the glGetUniformLocation function call. We ask the shader “program” where it’s “iTime” variable is and OpenGL will return us back an integer location that will be something like 0, 1, 2 etc depending on what OpenGL itself decided it would be. It doesn’t actually really matter what it is, because we are storing it as a variable and we never need to know what the actual value is itself. We just need to know that we can use it later on to set the value of the variable in the shader code using that integer ‘location’.

So now that we have that location, then we can use it to set the value of the shader variable at that location. We do that with a family of functions that all start with

glProgramUniform* (the star/wildcard here states that there is some text missing that dictates the type we want to change).

So for example, if we want to change the uniform variable that has a type in the shader that is a vec2, then we want to use the glProgramUniform2f function. And for a uniform variable that is a single ‘float’, then we want to use glProgramUniform1f.

glProgramUniform2f(program, resolutionUniformLocation, width, height); while (!glfwWindowShouldClose(windowPtr)) { auto currentTime = duration<float>(system_clock::now() - startTime).count(); glProgramUniform1f(program, timeUniformLocation, currentTime); // draw full screen triangle glDrawArrays(GL_TRIANGLES, 0, 3); glfwSwapBuffers(windowPtr); glfwPollEvents(); } glfwTerminate(); }

Here, just outside our main loop, we are setting the shader variable using the location “resolutionUniformLocation” that we previously got from asking where the “iResolution” variable was.= in the shader. We pass the two float values in as arguments to the function and they will get passed into the shader for us. This function is being called outside the loop because we only really want to set it once. But it still needs to be called because the resolution might have been set at program start up based on some user input (like command line parameters) and we still need to tell the shader what that is. If the window was changed size and we had written a function to handle that (which glfw allows up to do, although we haven’t explored that yet), then we would be able to update the shader accordingly.

Then we are calling the next glProgramUniform1f function inside the loop. This is because we want to update the value of the “iTime” in the shader with the current value from the runtime of the program. It is “1f” because it is a single float and this is something that is realtime and needs to update every frame.

That is pretty much it then for uniform variables. In the next chapter we will be tackling the ultimate graphics programming rite of passage… writing an obj loader from scratch to be able to load meshes into our programs and have them rendered on the screen!