Shaders For Beginners (Me) - Part 2
<< Home

Shaders For Beginners (Me) - Part 2

Well, I said there’s going to be a part 2, right? I fullfilled my promise!

Introduction

Shaders are good stuffs. You and I both know that. So today, I think “why don’t I go over those shaders and learn something new in the process?”, and then began reviewing those old shader knowledges. Well, so this time we are going to talk about all sorts of shaders, most of which are from here. Go check it out! If you didn’t read part 1, go here. I’ve updated the scene and now a diffuse texture is included as well! Download it here.

As per usual, it is important to get the scene up & running:

Default from last

Reinhard Tone Mapping

Reinhard Tone mapping is simple & straightforward. It is also part of the HDR technique where the extreme light and extreme dark could look more acceptable and uniform. (I don’t like it though). Anyway to use tone mapping, the texture must be using RGBA16F or more, so it would not get clamped. And using it is very straightforward:

vec3 toneMap(vec3 i) {
    return i / (i + vec3(1.0));
}

void main() {
    vec3 sampled = texture(tex, uv).xyz;
    color = vec4(gamma(toneMap(sampled)), 1.0);
}

Assuming we get an extremely light-as-hell scene, using tone map will yield plausible results: (not tone mapping on the left, tone mapping on the right). Take a look at the missing details because of the extreme light. It is back!

Tone mapping

Exposure Tone Mapping

Using a correct exposure value, Exposure Tone Mapping could perform better than Reinhard Tone Mapping. Just plug the HDR color into this:

vec3 exposure(vec3 i) {
    return vec3(1.0) - exp(-i * exposure);
}

Where could be any number you want. Just test it out! It is also quite useful for dark scene (compare to Reinhard Tone Mapping):

Exposure

Bloom

Bloom is a way to “make light bleed”. It should be done in an HDR buffer. It is handle and adds a some realism to the scene (again). As my scene does not really get lights, we are just gonna go ahead, and pretend we got it. The bloom effect as achieved by preserving the extremely bright part in the scene, discarding the rest, then blur those parts.

Bloom

You could use multiple ways to blur the scene (the blurrerm the better the bloom effect). However as I am a little bit lazy, I am just gonna go ahead and use box blur and call it a day. Even though the effect is horrible, you could also see the bright spot on the ball. Like the afternoon sun.

vec3 bloom = blur(texture(brightness, uv).rgb);
vec3 sample = texture(originalTexture, uv).rgb;
outputColor = sample + bloom; // Simple as that

You could also consider about a gamma correction and a tone mapping afterwards. No biggie! If you are not as lazy as I, you could check out LeanrOpenGL again. It is a cool site, and I just couldn’t emphasize enough! I am a bad student though.

Point shadow

Finally, a worthy enemy. I struggled with this for a whole day! Not that it is hard, but it is quite complex and you know, OpenGL. I am sooo desperate at last and I had to ask the good people on Khronos. Turns out I forgot to multiply the perspective matrix! Aaaaaagh!

Right, right. Point shadow. It looks quite like directional shadow, except that you gotta render the scene for 6 times and you gotta render it inside a cube map texture (or not, depends on you). Here’s how it’s gonna work:

Point shadow

Yeah, I am well aware that this is some quality shitty explaination. Sorry. My mind is at a fuzz now and I am not really suitable for explaining this. Please head to good ol’ LearnOpenGL to learn more. Anyway, as you are now rendering on a cube map, a geometry shader is mandatory:

#version 330 core

layout (triangles) in;
layout (triangle_strip, max_vertices = 18) out;

uniform mat4 view[6];
uniform mat4 perspective;

out vec4 fragPos;


void main() {
    for (int face = 0; face < 6; face++) {
        gl_Layer = face;
        for (int i = 0; i < 3; i++) {
            fragPos = gl_in[i].gl_Position;
            gl_Position = perspective * view[face] * fragPos;
            EmitVertex();
        }
        EndPrimitive();
    }
}

It takes in the original triangle (which is not multipled by perspective & view, mind you), and process it into 6 more triangles, each one belonging to a face of the cube map. Then the fragment shader would be responsible for calculating the fragment depth:

#version 330 core

in vec4 fragPos;

uniform float time;


void main() {
    vec3 lightPos = vec3(sin(time), 4.0, cos(time)); // I am just lazy. Make it an uniform! Seriously.
    float far = 25.0f; // Please uniform it
    float lightDist = length(fragPos.xyz - lightPos) / far; // Calculate the light distance divided by far plane,
                                                            // Because if it's too big it would get clamped
                                                            // and if it's too small it wouldl lose precision
    gl_FragDepth = lightDist;
}

Now that the cube map is rendered, the actual scene (the rendering with lighting & other calculations) should calculate the shadow like this:

float getShadow() {
    vec3 lightPos = vec3(sin(time), 4.0, cos(time)); // I am just lazy. Please don't do this.
    vec3 fragToLight = pos - lightPos;
    float closestDepth = texture(depth, fragToLight).r * far; // Multiply the depth value by the far plane to
                                                              // get back the correct depth value
    float bias = 0.05;
    float currentDepth = length(fragToLight);
    float shadow = currentDepth - bias < closestDepth ? 0.0 : 0.6; // Good old shadow calculation
    return shadow;
}

void main() {
    float shadow = getShadow();
    color = vec4((1.0 - shadow) * phong(), 1.0);
}

And here’s what you are going to get.

Point shadow rendered

For a moment there I think “is it worth it?” well nah I guess? If I could choose, I will only use directional shadow. Easy to implement, and way more intuitive than point shadow. There is also less erring space. Of course point shadow itself is built on directional shadow, so well, yeah. I lost my words.

Screen-Space Ambient Occlusion (SSAO)

Now this is a really cool stuff to put into your game, or whatever it is! Using screen space ambient occlusion really shows of the depth of certain object, and doing that is very cool. Below is an unblurred ambient occlusion graphics to give you a look & feel.

Unblurred

It is quite simple to know what’s going on now, even now all we get is this grayscale scene: a suzanne, four balls, one on top of the cube, three laying on the ground. It is however a little bit complex to implement, and would require half of the G-buffer: the position buffer, and the normal buffer. The position buffer should be positions in view space.

Now we are going to get a kernel: we gotta sample the position points around. So here’s what we’re going to do: we get a hemisphere, and then randomly shoot vertices out from the center:

Hemisphere

Now this kernel’s gonna be in tangent space. When we are inside the SSAO fragment shader, we are going to put this kernel back to view space, so we are going to multiply it with the TBN matrix. You might want to add a little bit of noice to make the process look more random:

Sampling G-buffer

And now’s the real meat. We are going to sample all points at the end of the arrow. And then start a counter named occlusion. Now think about it in this way:

Think, Dougal, think

As the sampling process draws near a corner, the arrows will shoot through walls. And when that happens, we call this is occluded, and increase the occlusion counter by 1. Now most tutorials won’t cover what’s gonna happen. Think about this:

The position is stored in G-buffer. If it is in G-buffer, that means it survived the depth test. Actually, it means it is the king of the depth test. How is it possible to locate any inside way vertices?

The fact is, we don’t! We are not going to do the method described above. However, here’s what we know. If something has been occluded, that means what occludes it must be closer to the camera. Which in turn means when we shoot those vertices out, we will know the expected depth in the vertices we shot. But when the depth changes violently, especially when the depth is far less (which means far closer to the camera), we would know occlusion happened here.

Example

As the stuffs pointing up is generally closer to the camera (duh), we will introduce a bias variable. So only stuffs outside the bias could count to the occlusion counter.

void main() {
    vec3 pos = texture(posTex, uv).rgb;
    vec3 normal = texture(nrmlTex, uv).rgb;
    vec3 rand = vec3(noise(uv), 0.0);

    vec3 tangent = normalize(rand - normal * dot(rand, normal));
    vec3 bitangent = cross(normal, tangent);
    mat3 tbn = mat3(tangent, bitangent, normal);
    float o = 0.0; // occlusion counter
    for (int i = 0; i < 16; i++) {
       vec3 sample = tbn * kernel[i];
       sample = pos + sample * radius;
       vec4 offset = vec4(sample, 1.0);
       offset = perspective * offset; // to clip space
       offset.xyz /= offset.w;
       offset.xyz = offset.xyz * 0.5 + 0.5;
       float sampleDepth = texture(posTex, offset.xy).z; // sample the depth in view space
       o += (sampleDepth > s.z + bias ? 1.0 : 0.0) * rangeCheck; // is it even closer than the expected depth + bias? 
    }
    o = 1.0 - (o / 16.0);
    occlusion = vec4(o, o, o, 1.0);
}

See, we are not comparing with the position itself, but its expected depth. As the ambient occlusion is being processed in view space, the closer things are to the camera, the greater they will be. So the > operator actually means greater.

After performing the operation, the greater the occlusion is, the greater the o is. However should it be an attenuation value, we know things should go the other way. So we normalize it, and then minus 1 by o.

Remember this is only the ambient occlusion calculation, and as noise had been included here, things will tend to be noisy. That’s why another pass is needed - to blur the result. Only after that could the occlusion value used to enhance the graphics.

I know what I am saying here is quite a bit mess, that’s because I’ve been struggling on this for days. If I get to know it better later, I might update it. For now, if you have absolutely no idea what the hell I am talking about, head to LearnOpenGL. More references there!

Conclusion

Well, I guess I am going to cut it here. That’s like, a lot of lighting techniques (oh yeah)? And a lot I still didn’t covered. Anyhoo, I’ve learned a lot during these days! Hopefully I can wreck a demoscene out using those stuffs. Just wait and see!

References

  1. HDR, LearnOpenGL
  2. Bloom, LearnOpenGL
  3. Point Shadow, LearnOpenGL
  4. SSAO, LearnOpenGL
  5. SSAO, Brian Will
  6. SSAO, Wikipedia
  7. SSAO, John Chapman
  8. Multipass Shadow Mapping With Point Lights, OGLDev
  9. Bloom Post Effect, Epic Games