[SOLVED] Texture Mapping in Custom Fragment Shader for Shadow Mapping

Hello,
I have been using a shadow mapping shader posted in this old forum thread that I slightly modified to better suit the game I am making. My modifications include clamping the shadow darkness so colors don’t become negative, manual control over the color of global light, and control over the intensity of the shadows. It is a simple and versatile shader, but it has a huge limitation: it can’t render textures.
Here is the shader and surrounding processing code; credit goes to forum user Poersch, I’ve (mostly) marked where I changed things:

void updateDefaultShader() {
  // Bias matrix to move homogeneous shadowCoords into the UV texture space
  PMatrix3D shadowTransform = new PMatrix3D(
    0.5, 0.0, 0.0, 0.5, 
    0.0, 0.5, 0.0, 0.5, 
    0.0, 0.0, 0.5, 0.5, 
    0.0, 0.0, 0.0, 1.0
    );

  // Apply project modelview matrix from the shadow pass (light direction)
  shadowTransform.apply(((PGraphicsOpenGL)shadowMap).projmodelview);

  // Apply the inverted modelview matrix from the default pass to get the original vertex
  // positions inside the shader. This is needed because Processing is pre-multiplying
  // the vertices by the modelview matrix (for better performance).
  PMatrix3D modelviewInv = ((PGraphicsOpenGL)g).modelviewInv;
  shadowTransform.apply(modelviewInv);

  // Convert column-minor PMatrix to column-major GLMatrix and send it to the shader.
  // PShader.set(String, PMatrix3D) doesn't convert the matrix for some reason.
  defaultShader.set("shadowTransform", new PMatrix3D(
    shadowTransform.m00, shadowTransform.m10, shadowTransform.m20, shadowTransform.m30, 
    shadowTransform.m01, shadowTransform.m11, shadowTransform.m21, shadowTransform.m31, 
    shadowTransform.m02, shadowTransform.m12, shadowTransform.m22, shadowTransform.m32, 
    shadowTransform.m03, shadowTransform.m13, shadowTransform.m23, shadowTransform.m33
    ));

  // Calculate light direction normal, which is the transpose of the inverse of the
  // modelview matrix and send it to the default shader.
  float lightNormalX = lightDir.x * modelviewInv.m00 + lightDir.y * modelviewInv.m10 + lightDir.z * modelviewInv.m20;
  float lightNormalY = lightDir.x * modelviewInv.m01 + lightDir.y * modelviewInv.m11 + lightDir.z * modelviewInv.m21;
  float lightNormalZ = lightDir.x * modelviewInv.m02 + lightDir.y * modelviewInv.m12 + lightDir.z * modelviewInv.m22;
  float normalLength = sqrt(lightNormalX * lightNormalX + lightNormalY * lightNormalY + lightNormalZ * lightNormalZ);
  defaultShader.set("lightDirection", lightNormalX / -normalLength, lightNormalY / -normalLength, lightNormalZ / -normalLength);

  // Send the shadowmap to the default shader
  defaultShader.set("shadowMap", shadowMap);
  
  // environmental effects (added by me)
  defaultShader.set("ambientLight", map(red(skylightColor), 0.0, 255.0, -0.3, 0.0), map(green(skylightColor), 0.0, 255.0, -0.3, 0.0), map(blue(skylightColor), 0.0, 255.0, -0.3, 0.0));
  defaultShader.set("shadowIntensity", map(shadowTransparency, 0.0, 1.0, 1.0, 1.5));
}
public void initShadowPass() {
  shadowMap = createGraphics(int(shadowResolution.x), int(shadowResolution.y), P3D);
  String[] vertSource = {
    "uniform mat4 transform;", 

    "attribute vec4 vertex;", 

    "void main() {", 
    "gl_Position = transform * vertex;", 
    "}"
  };
  String[] fragSource = {

    // In the default shader we won't be able to access the shadowMap's depth anymore,
    // just the color, so this function will pack the 16bit depth float into the first
    // two 8bit channels of the rgba vector.
    "vec4 packDepth(float depth) {", 
    "float depthFrac = fract(depth * 255.0);", 
    "return vec4(depth - depthFrac / 255.0, depthFrac, 1.0, 1.0);", 
    "}", 

    "void main(void) {", 
    "gl_FragColor = packDepth(gl_FragCoord.z);", 
    "}"
  };
  shadowMap.noSmooth(); // Antialiasing on the shadowMap leads to weird artifacts
  //shadowMap.loadPixels(); // Will interfere with noSmooth() (probably a bug in Processing)
  shadowMap.beginDraw();
  shadowMap.noStroke();
  shadowMap.shader(new PShader(this, vertSource, fragSource));
  shadowMap.ortho(-sceneRadius, sceneRadius, -sceneRadius, sceneRadius, 10, 2000); // Setup orthogonal view matrix for the directional light
  shadowMap.endDraw();
  //The shader used in shadowMap creates a depth texture. Nothing much to change here.
}

public void initDefaultPass() {
  String[] vertSource = {
    "uniform mat4 transform;", 
    "uniform mat4 modelview;", 
    "uniform mat3 normalMatrix;", 
    "uniform mat4 shadowTransform;", 
    "uniform vec3 lightDirection;", 
    
    
    "attribute vec4 vertex;", 
    "attribute vec4 color;", 
    "attribute vec3 normal;", 

    "varying vec4 vertColor;", 
    "varying vec4 shadowCoord;", 
    "varying float lightIntensity;", 

    "void main() {", 
    "vertColor = color;", 
    "vec4 vertPosition = modelview * vertex;", // Get vertex position in model view space
    "vec3 vertNormal = normalize(normalMatrix * normal);", // Get normal direction in model view space
    "shadowCoord = shadowTransform * (vertPosition + vec4(vertNormal, 0.0));", // Normal bias removes the shadow acne
    "lightIntensity = 0.5 + max(0,dot(-lightDirection, vertNormal)) * 0.5;", 
    "gl_Position = transform * vertex;", 
    "}"
  };
  String[] fragSource = {
    "#version 120", 

    // Used a bigger poisson disk kernel than in the tutorial to get smoother results
    "const vec2 poissonDisk[9] = vec2[] (", 
    "vec2(0.95581, -0.18159), vec2(0.50147, -0.35807), vec2(0.69607, 0.35559),", 
    "vec2(-0.0036825, -0.59150), vec2(0.15930, 0.089750), vec2(-0.65031, 0.058189),", 
    "vec2(0.11915, 0.78449), vec2(-0.34296, 0.51575), vec2(-0.60380, -0.41527)", 
    ");",
    
    // Unpack the 16bit depth float from the first two 8bit channels of the rgba vector
    "float unpackDepth(vec4 color) {", 
    "return color.r + color.g / 255.0;", 
    "}", 

    //environmental information (added by me)
    "uniform vec3 ambientLight;",
    "uniform float shadowIntensity;",
    
    "uniform sampler2D shadowMap;", 

    "varying vec4 vertColor;", 
    "varying vec4 shadowCoord;", 
    "varying float lightIntensity;", 

    "void main(void) {", 

    // Project shadow coords, needed for a perspective light matrix (spotlight)
    "vec3 shadowCoordProj = shadowCoord.xyz / shadowCoord.w;", 

    // Only render shadow if fragment is facing the light
    "if(lightIntensity > 0.5) {", 
    "float visibility = 9.0;", 
    
    // use step() instead of branchin.mult(-1)g, should be much faster this way
    "for(int n = 0; n < 9; ++n)", 
    "visibility += step(shadowCoordProj.z, unpackDepth(texture2D(shadowMap, shadowCoordProj.xy + poissonDisk[n] / 512.0)));", 
    //shadow
    "gl_FragColor = vec4(vertColor.rgb * min(visibility * 0.05556 * shadowIntensity, lightIntensity) + ambientLight.rgb, vertColor.a);", 
    "} else",
    //no shadow
    "gl_FragColor = vec4(vertColor.rgb * lightIntensity + ambientLight.rgb, vertColor.a);", 

    "}"
  };
  shader(defaultShader = new PShader(this, vertSource, fragSource));
  noStroke();
}

//implementation in sketch
void setup(){
  size(1600, 900, P3D);
  initShadowPass();
  initDefaultPass();
}
void draw(){
  lightDir.set(lightDistance * sin(lightYaw) * cos(lightPitch), lightDistance * 0.5 * -sin(lightPitch), lightDistance * cos(lightYaw) * cos(lightPitch));
  if (renderShadows) {
    // Render shadow pass
    shadowMap.beginDraw();
    shadowMap.noLights();
    shadowMap.camera(lightDir.x, lightDir.y, lightDir.z, 0, 0, 0, 0, 1, 0);
    shadowMap.background(#FFFFFF);
    renderLandscape(shadowMap);
    shadowMap.endDraw();
    shadowMap.updatePixels();
    // Update the shadow transformation matrix and send it, the light
    // direction normal, and the shadow map to the default shader.
    updateDefaultShader();
  }
  //render the landscape normally
  renderLandscape(g);
}
void renderLandscape(PGraphics c){
  //render geometry here, it will be rendered twice, once by shadowMap and once by the default PGraphics g
  c.box(10, 1, 10);
  c.box(3);
}

The bulk of texture mapping would occur in the defaultShader's fragment shader. In that shader, the variable ‘vertColor’, an attribute variable that contains the fill color of a face, defines the color of the fragment. This works fine until you use textures because the reference page for texture() explicitly says: “When textures are in use, the fill color is ignored. Instead, use tint() to specify the color of the texture as it is applied to the shape.” The result when using a texture is the textured shape being filled with an opaque white, the color OpenGL uses when there is no vertex color data. The ideal solution I am thinking of is finding a way to avoid using the vertColor variable in the frag shader and replacing it with a texture color, the color of the geometry after a texture has been mapped, then doing the same calculations as seen at the end of the frag shader. I am only beginning to learn how to use OpenGL shaders, so I’m not sure how to re-implement textures into the shadow mapping shader. I know processing has a built-in texture mapping shader (how else would the texture() function work) so, I thought I could possibly reuse that shader code in my fragment shader to reintroduce the functionality, but I haven’t found a reference to it and I don’t know how to code one from scratch. Also, if I code my own texture mapping fragment shader, I worry that I’ll be unable to use the simple and easy built-in texture() functions. I would like some help.
Thanks,
Shwaa

[SOLUTION]
The shaders tutorial on the processing webpage actually provides a basic texture shader, which I implemented into my program.
Updated shader code:

void updateDefaultShader() {
  // Bias matrix to move homogeneous shadowCoords into the UV texture space
  PMatrix3D shadowTransform = new PMatrix3D(
    0.5, 0.0, 0.0, 0.5, 
    0.0, 0.5, 0.0, 0.5, 
    0.0, 0.0, 0.5, 0.5, 
    0.0, 0.0, 0.0, 1.0
    );

  // Apply project modelview matrix from the shadow pass (light direction)
  shadowTransform.apply(((PGraphicsOpenGL)shadowMap).projmodelview);

  // Apply the inverted modelview matrix from the default pass to get the original vertex
  // positions inside the shader. This is needed because Processing is pre-multiplying
  // the vertices by the modelview matrix (for better performance).
  PMatrix3D modelviewInv = ((PGraphicsOpenGL)g).modelviewInv;
  shadowTransform.apply(modelviewInv);

  // Convert column-minor PMatrix to column-major GLMatrix and send it to the shader.
  // PShader.set(String, PMatrix3D) doesn't convert the matrix for some reason.
  defaultShader.set("shadowTransform", new PMatrix3D(
    shadowTransform.m00, shadowTransform.m10, shadowTransform.m20, shadowTransform.m30, 
    shadowTransform.m01, shadowTransform.m11, shadowTransform.m21, shadowTransform.m31, 
    shadowTransform.m02, shadowTransform.m12, shadowTransform.m22, shadowTransform.m32, 
    shadowTransform.m03, shadowTransform.m13, shadowTransform.m23, shadowTransform.m33
    ));

  // Calculate light direction normal, which is the transpose of the inverse of the
  // modelview matrix and send it to the default shader.
  float lightNormalX = lightDir.x * modelviewInv.m00 + lightDir.y * modelviewInv.m10 + lightDir.z * modelviewInv.m20;
  float lightNormalY = lightDir.x * modelviewInv.m01 + lightDir.y * modelviewInv.m11 + lightDir.z * modelviewInv.m21;
  float lightNormalZ = lightDir.x * modelviewInv.m02 + lightDir.y * modelviewInv.m12 + lightDir.z * modelviewInv.m22;
  float normalLength = sqrt(lightNormalX * lightNormalX + lightNormalY * lightNormalY + lightNormalZ * lightNormalZ);
  defaultShader.set("lightDirection", lightNormalX / -normalLength, lightNormalY / -normalLength, lightNormalZ / -normalLength);
  // Send the shadowmap to the default shader
  defaultShader.set("shadowMap", shadowMap);
  // environmental effects
  defaultShader.set("ambientLight", map(red(skylightColor), 0.0, 255.0, -0.3, 0.0), map(green(skylightColor), 0.0, 255.0, -0.3, 0.0), map(blue(skylightColor), 0.0, 255.0, -0.3, 0.0));
  defaultShader.set("shadowIntensity", map(shadowTransparency, 0.0, 1.0, 1.0, 1.5));
}
public void initShadowPass() {
  shadowMap = createGraphics(int(shadowResolution.x), int(shadowResolution.y), P3D);
  String[] vertSource = {
    "uniform mat4 transform;", 

    "attribute vec4 vertex;", 

    "void main() {", 
    "gl_Position = transform * vertex;", 
    "}"
  };
  String[] fragSource = {

    // In the default shader we won't be able to access the shadowMap's depth anymore,
    // just the color, so this function will pack the 16bit depth float into the first
    // two 8bit channels of the rgba vector.
    "vec4 packDepth(float depth) {", 
    "float depthFrac = fract(depth * 255.0);", 
    "return vec4(depth - depthFrac / 255.0, depthFrac, 1.0, 1.0);", 
    "}", 

    "void main(void) {", 
    "gl_FragColor = packDepth(gl_FragCoord.z);", 
    "}"
  };
  shadowMap.noSmooth(); // Antialiasing on the shadowMap leads to weird artifacts
  //shadowMap.loadPixels(); // Will interfere with noSmooth() (probably a bug in Processing)
  shadowMap.beginDraw();
  shadowMap.noStroke();
  shadowMap.shader(new PShader(this, vertSource, fragSource));
  shadowMap.ortho(-sceneRadius, sceneRadius, -sceneRadius, sceneRadius, 10, 2000); // Setup orthogonal view matrix for the directional light
  shadowMap.endDraw();
}

public void initDefaultPass() {
  String[] vertSource = {
    "uniform mat4 transform;", 
    "uniform mat4 texMatrix;", 
    "uniform mat4 modelview;", 
    "uniform mat3 normalMatrix;", 
    "uniform mat4 shadowTransform;", 
    "uniform vec3 lightDirection;", 

    "attribute vec4 position;", 
    "attribute vec4 vertex;", 
    "attribute vec4 color;", 
    "attribute vec3 normal;", 
    "attribute vec2 texCoord;", 

    "varying vec4 vertColor;", 
    "varying vec4 vertTexCoord;", 
    "varying vec4 shadowCoord;", 
    "varying float lightIntensity;", 

    "void main() {", 
    "vertColor = color;", 
    "vec4 vertPosition = modelview * vertex;", // Get vertex position in model view space
    "vec3 vertNormal = normalize(normalMatrix * normal);", // Get normal direction in model view space
    "shadowCoord = shadowTransform * (vertPosition + vec4(vertNormal, 0.0));", // Normal bias removes the shadow acne
    "lightIntensity = 0.5 + max(0,dot(-lightDirection, vertNormal)) * 0.5;", 
    "vertTexCoord = texMatrix * vec4(texCoord, 1.0, 1.0);", 
    "gl_Position = transform * vertex;", 
    "}"
  };
  String[] fragSource = {
    "#version 120", 
    //"#define PROCESSING_TEXTURE_SHADER", 
    // Used a bigger poisson disk kernel than in the tutorial to get smoother results
    "const vec2 poissonDisk[9] = vec2[] (", 
    "vec2(0.95581, -0.18159), vec2(0.50147, -0.35807), vec2(0.69607, 0.35559),", 
    "vec2(-0.0036825, -0.59150), vec2(0.15930, 0.089750), vec2(-0.65031, 0.058189),", 
    "vec2(0.11915, 0.78449), vec2(-0.34296, 0.51575), vec2(-0.60380, -0.41527)", 
    ");", 

    // Unpack the 16bit depth float from the first two 8bit channels of the rgba vector
    "float unpackDepth(vec4 color) {", 
    "return color.r + color.g / 255.0;", 
    "}", 

    //environmental information
    "uniform vec3 ambientLight;", 
    "uniform float shadowIntensity;", 

    "uniform sampler2D shadowMap;", 
    "uniform sampler2D texture;", 

    "varying vec4 vertColor;", 
    "varying vec4 vertTexCoord;", 
    "varying vec4 shadowCoord;", 
    "varying float lightIntensity;", 

    "void main(void) {", 
    // Get texture color
    "vec4 curColor = texture2D(texture, vertTexCoord.st);",
    //if there is no texture color, use the fill color
    "if(curColor == vec4(0, 0, 0, 1)){",
    "curColor = vertColor;",
    "}",
    
    // Project shadow coords, needed for a perspective light matrix (spotlight)
    "vec3 shadowCoordProj = shadowCoord.xyz / shadowCoord.w;", 

    // Only render shadow if fragment is facing the light
    "if(lightIntensity > 0.5) {", 
    "float visibility = 9.0;", 
    // I used step() instead of branchin.mult(-1)g, should be much faster this way
    "for(int n = 0; n < 9; ++n)", 
    "visibility += step(shadowCoordProj.z, unpackDepth(texture2D(shadowMap, shadowCoordProj.xy + poissonDisk[n] / 512.0)));", 
    //in light
    "gl_FragColor = vec4(curColor.rgb * min(visibility * 0.05556 * shadowIntensity, lightIntensity) + ambientLight.rgb, curColor.a);", 
    "} else", 
    //in shadow
    "gl_FragColor = vec4(curColor.rgb * lightIntensity + ambientLight.rgb, curColor.a);", 
    "}"
  };
  shader(defaultShader = new PShader(this, vertSource, fragSource));
  noStroke();
  //perspective(60 * DEG_TO_RAD, (float)width / height, 10, 1000);
}

The important check is that if the texture has a completely black pixel, it means the texture likely is blank. So, if that is the case, use the fill color. It’s a bit limiting since none of my texture can have totally black pixels. That issue can only be solved if I can figure out how to properly use #define PROCESSING_TEXTURE_SHADER and #define PROCESSING_COLOR_SHADER in my shaders, because using them only gave me problems. This is mainly due to struggling to load two shaders at the same time. Because I can use colors pretty close to black in textures, things should be ok.
Here’s an example image:

2 Likes