sunLoadingImage
whowedImag
decoration left 1
decoration left 2
transhome
transprojects
transgallery
transarticles
decoration rigth
English

Show/Hide search bar
black cat logo variable logo
[3 Feb 2013]

Hatching and Gooch shading in GLSL

First part of this tutorial shows how to quickly create hatching shader in OpenGL and GLSL. Second extra part of the tutorial shows how to implement and use non-photorealistic rendering technique called Gooch shading. Following images show what we will get in the end:
Simple hatching with hatches and patterns
Gooch shading with different cool and warm colors
Combination of hatching and Gooch Shading
Hatching
Hatching is an artistic method which is used to create non-photorealistic effect of shading with help of multiple hatches. The darker the part of the object, the more hatches are drawn over that part. You can use hatches of different lenght, width, under different angles, with different distance between hatches and with different density of placement. When hatches are not parallel to each other then such hatching is called cross-hatching. You can use hatches under different angles to highlight different colors or parts of the object. You can also use hatching with curves to emphasize the shape of the object.
To produce hatching which gives a lot of information about shape of the object (like hatching along contours of the object), you have to find out directions of the hatches that depend on the shape of the object for each rendered fragment of the object. This information can be saved in texture and used very similarly to normal maps. For parametric objects, such as sphere or cylinder, you can use their parametric texture coordinates to determine direction of the hatches. So you don't need to calculate texture that stores information about directions of the hatches for parametric object. Just use gradient of parameter as direction of hatches. For other non-parametric objects (eg. polygonal mesh of the cat), you should precalculate directions in the texture, because texture coordinates of the mesh are not permanent in most cases and their gradients are not aligned with shape of the object.
Simple hatching in GLSL
But this tutorial shows only most simple version of GLSL hatching, which doesn't take in account only lighting of the object, but not directions of contours and shape of the object. Another disadvantage of the method is that hatching is not constant on the object, and depends on place on a screen, where the object is renderer. So hatches will be on the same place on the screen, even if the object is moved.
First you have to create textures with different levels of hatching. Each texture with hatches represents different level of light intensity. For example, texture with small number of hatches (nearly white) represents high intensity of the light, and texture with high number of hatches (nearly black) represents low intensity of the light. Following image shows set of 8 textures with different levels of hatching:
Textures for hatching
Then in fragment shader calculate standard Blinn-Phong lighting for each framgent of the object. The intensity of diffuse lighting in the fragment is used to select among different levels of hatching textures. The less the intensity, the darker texture should be selected. Use blending of textures to eliminate visible gaps in places where one hatching texture transforms into another hatching texture. For example, if intensity of the light corresponds to value of hatching between two hatching textures, then sample two hatching textures and mix the colors accordingly.
Also, to minimize visible gaps between textures, textures shouldn't contain elements of low frequency (large elements and with distinguishable shape). Textures are built in such a way, that each darker texture contains all elements of previos lighter texture. So first you have to draw texture that represents high light intensity with low number of hatches, and then next darker texture uses all elements of previos texture as base, and adds new hatches, and so on.
To implement hatching you can use multiple textures, and then in GLSL shader you have to select which hatching textures should be used with help of if statements. But if there're a lot of textures, then number of if statements rises proportionally, and this negatively influences on performance of GLSL shader. But there is alternative to usage of multiple textures.
You can use 3D textures, which is part of OpenGL. Actually for hatching you have to use only one 3D texture. 3D textures are multilayered textures, where each 2D texture has same width and height as other 2D textures. Sampling of such textures is similar to sampling of 2D textures, but uses third texture coordinate. S and T texture coordinates determines position on 2D texture, and third coordinate P is used to select which texture (among 2D textures in 3D texture) should be sampled. If value of P is between two textures, then GLSL will sample texture below P and texture above P, and then will automatically interpolate between colors. This feature of 3D textures allows to eliminate need for multiple textures and multiple if statement in GLSL shader while hatching. Here you can find out how to load 3D texture from array of image files.
You can also use different patterns instead of hatching textures. Of cource it won't be hatching anymore, but... :) Following textures are used to render first sample image in this tutorial:
Texture with hatchy pattern   Circles and dont   Checkboard   Vertical and horizontal parallel lines  
So here're steps required to implement hatching in OpenGL and GLSL
  • 1) create and load 3D texture with different levels of hatches
  • 2) for each fragment calculate light intensity with Blinn-Phong lighting
  • 3) use intensity of light and position of fragemnt on the screen to sample 3D texture
  • Vertex shader for hatching and Gooch shading
    Vertex shader for hatching and Gooch shading. Just pass all required vectors for lighting to fragment shader:
    #version 330

    // attributes
    layout(location = 0) in vec3 i_position; // xyz - position
    layout(location = 1) in vec3 i_normal; // xyz - normal
    layout(location = 1) in vec2 i_texcoord0; // xy - texture coords

    // uniforms
    uniform mat4 u_model_mat;
    uniform mat4 u_view_mat;
    uniform mat4 u_proj_mat;
    uniform mat3 u_normal_mat;
    uniform vec3 u_light_position;
    uniform vec3 u_camera_position;

    // output data from vertex to fragment shader
    out vec3 o_normal;
    out vec3 o_lightVector;
    out vec3 o_viewVector;
    out vec2 o_texcoords;

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

    void main(void)
    {
       // transform position and normal to world space
       vec4 positionWorld = u_model_mat * vec4(i_position, 1.0);
       vec3 normalWorld = u_normal_mat * i_normal;

       // calculate and pass vectors required for lighting
       o_lightVector = u_light_position - positionWorld.xyz;
       o_viewVector = u_camera_position - positionWorld.xyz;
       o_texcoords = i_texcoord0;
       o_normal = normalWorld;

       // project world space position to the screen and output it
       gl_Position = u_proj_mat * u_view_mat * positionWorld;
    }
    Fragment shader for hatching
    Fragment shader for hatching calculates lighting, uses light intensity to sample 3D texture and outputs final color:
    #version 330

    // data from vertex sahder
    in vec3 o_normal;
    in vec3 o_lightVector;
    in vec3 o_viewVector;
    in vec2 o_texcoords;

    // sampler for 3D texture
    layout(location = 0) uniform sampler3D u_colorTexture;

    // output from fragment shader to framebuffer
    out vec4 resultingColor;

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

    void main(void)
    {
       // normalize vectors for lighting
       vec3 normalVector = normalize(o_normal);
       vec3 lightVector = normalize(o_lightVector);
       vec3 viewVector = normalize(o_viewVector);

       // calculate intensity of lighting
       float ambient = 0.025f;
       float diffuse = clamp(dot(lightVector, normalVector), 0, 1);
       vec3 reflectedVector = reflect(-lightVector, normalVector);
       float specular = 0;
       if(diffuse > 0)
       {
          specular = pow(dot(viewVector, reflectedVector), 32);
       }
       float lightIntensity = clamp(diffuse + specular + ambient, 0, 1);

       // get size of 3D texture
       ivec3 sizeOfTex = textureSize(u_colorTexture, 0);

       // texture coordinates by XY as position of fragment on the screen
       // divided by width of the texture. So each pixel of the texture will
       // correspond to pixel on the screen

       vec2 texCoordXY = gl_FragCoord.xy/sizeOfTex.x;
       // sample depth of the texture by light intensity
       float texCoordZ = lightIntensity;

       // sample 3D texture to get hatching intensity
       vec3 hatching = texture(u_colorTexture, vec3(texCoordXY, texCoordZ)).rgb;

       // modulate hatching with lighting (not required)
       resultingColor.rgb = hatching * (1.0 + lightIntensity * 2)/3;
       resultingColor.a = 1;
    }
    You can also hatch objects in GLSL shader manually (without textures). For each fragment calculate whether fragment lies on hatch. Hatches are aligned with screen space. If fragment lies on hatch, then assign dark color to this fragment, otherwise assign white color:
    Hatching without textures in GLSL shader
    resultingColor = vec4(1.0, 1.0, 1.0, 1.0);

    if (lightIntensity < 0.85)
    {
       // hatch from left top corner to right bottom
       if (mod(gl_FragCoord.x + gl_FragCoord.y, 10.0) == 0.0)
       {
          resultingColor = vec4(0.0, 0.0, 0.0, 1.0);
       }
    }

    if (lightIntensity < 0.75)
    {
       // hatch from right top corner to left boottom
       if (mod(gl_FragCoord.x - gl_FragCoord.y, 10.0) == 0.0)
       {
          resultingColor = vec4(0.0, 0.0, 0.0, 1.0);
       }
    }

    if (lightIntensity < 0.5)
    {
       // hatch from left top to right bottom
       if (mod(gl_FragCoord.x + gl_FragCoord.y - 5.0, 10.0) == 0.0)
       {
          resultingColor = vec4(0.0, 0.0, 0.0, 1.0);
       }
    }

    if (lightIntensity < 0.25)
    {
       // hatch from right top corner to left bottom
       if (mod(gl_FragCoord.x - gl_FragCoord.y - 5.0, 10.0) == 0.0)
       {
          resultingColor = vec4(0.0, 0.0, 0.0, 1.0);
       }
    }
    Gooch shading
    Gooch shading is used to substitute photorealistic rendering by rendering that focuses on structore and shape of the object. Instead of usage of light and shadow, Gooch shading uses concept of warm and cool colors. Standard Blinn-Phong shading only modulates base color of the object. In Gooch shading intensity of diffuse lighting is used to determine how to blend warm and cold colors together. Warm color is, for example, yellow or red, and cold colors is blue or gray. Warm colors represents parts of the model that are in light, and cool colors - parts that are in shadow. It's better to fade colors a bit, to make them less intense. Warm and cool colors may be blended together with color of the object.
    Different cool and warm colors in Gooch shading
    For Gooch shading you can use same vertex shader as for hatching. Following fragment shader calculates and outputs color produced by Gooch shadeing. Shader contains comments that describes how to properly mix warm and cool colors together. You can also mix color of Gooch shading with hatching, and you will get something like result on third image in this lesson.
    Input uniforms for shader may be:
  • u_objectColor = vec3(1, 1, 1);
  • u_coolColor = vec3(159.0f/255, 148.0f/255, 255.0f/255);
  • u_warmColor = vec3(255.0f/255, 75.0f/255, 75.0f/255);
  • u_alpha = 0.25;
  • u_beta = 0.5;
  • #version 330

    // data from vertex shader
    in vec3 o_normal;
    in vec3 o_lightVector;
    in vec3 o_viewVector;
    in vec2 o_texcoords;

    // diffuse color of the object
    uniform vec3 u_objectColor;
    // cool color of gooch shading
    uniform vec3 u_coolColor;
    // warm color of gooch shading
    uniform vec3 u_warmColor;
    // how much to take from object color in final cool color
    uniform float u_alpha;
    // how much to take from object color in final warm color
    uniform float u_beta;

    // output to framebuffer
    out vec4 resultingColor;

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

    void main(void)
    {
       // normlize vectors for lighting
       vec3 normalVector = normalize(o_normal);
       vec3 lightVector = normalize(o_lightVector);
       // intensity of diffuse lighting [-1, 1]
       float diffuseLighting = dot(lightVector, normalVector);
       // map intensity of lighting from range [-1; 1] to [0, 1]
       float = (1.0 + diffuseLighting)/2;

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

       // cool color mixed with color of the object
       vec3 coolColorMod = u_coolColor + u_objectColor * u_alpha;
       // warm color mixed with color of the object
       vec3 warmColorMod = u_warmColor + u_objectColor * u_beta;
       // interpolation of cool and warm colors according
       // to lighting intensity. The lower the light intensity,
       // the larger part of the cool color is used

       vec3 colorOut = mix(coolColorMod, warmColorMod, interpolationValue);

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

       // save color
       resultingColor.rgb = colorOut;
       resultingColor.a = 1;
    }



    Sun and Black Cat- Igor Dykhta (igor dykhta email) 2007-2014