Cel-Shading

The full source code for this part of the tutorial is available in the file root/tutorials/s3d/celshading.bs. You can run it using storm -f tutorials.s3d.celshading. As in the previous parts, the code is fairly well-documented, and it is likely possible to read it on its own.

This tutorial builds upon the code from the lighting part of the tutorial. As you will see, this part of the tutorial only makes fairly small modifications to the code in the previous step. To avoid name clashes, we will however rename the painter class and the pixel shader. The rest of the tutorial reuses the code from the light tutorial directly.

Updates to the Painter

As mentioned above, this part of the tutorial reuses a large part of the code from the previous step (the lighting tutorial). The provided file (root/tutorials/s3d/celshading.bs) does, however, contain a copy of the LightPainter and the lightPixelShader (named CelPainter and celPixelShader respectively). The CelPainter is the same, except that the pipeline is initialized with celPixelShader instead of lightPixelShader. The interesting differences are in the pixel shader.

Updating the Pixel Shader

This version of cel-shading (there are most likely other ones) simply splits the continuous output of the light calculation into a few discrete steps. Since this is an operation we need to do a couple of times, we create a shader function that does the discretization for us:

shader Float celShade(Float strength) {
    Float steps = 5.0;
    (strength * steps).floor / steps;
}

The function above assumes that the input value is in the range 0 to 1. To discretizise the steps, the function then multiplies the input with the number of desired steps and then removes the decimal part using the floor function. After that, it divides the resulting value with the number of steps again to bring the value back into the range 0 to 1.

As in shader functions and Basic Storm, the value of the last statement in the function is returned by default. You can of course use return as well if you want.

We can then use the celShade function to discertizise the scalars from the light computations as follows:

pixel shader for LightPipeline: Output celPixelShader(LightIntermediate point) {
    // Compute the direction vector to the light source and the normal. Note that we need to
    // normalize the normal at this point.
    Vector cameraDir = (cameraPos - point.world).normalized;
    Vector lightDir = (lightPos - point.world).normalized;
    Vector normal = point.normal.normalized;

    // Ambient light:
    Color light = point.color * ambientStrength;

    // Diffuse light, updated:
    light += point.color * celShade(max(normal * lightDir, 0.0));

    // Specular light:
    Vector reflectDir = lightDir.reflect(normal);
    Float specular = pow(max(cameraDir * reflectDir, 0.0), specularExponent);
    // Updated:
    light += specularColor * celShade(specularStrength * specular);

    Output(light);
}

And with these changes, we can run the program. At this time we will see a cel-shaded rotating teapot.