Soft Shadows - Meshes and Framebuffers

Hi all,

I wanted to share a small demo I’ve been working on over the last couple of days.
It uses 2 custom shaders, the first to render the scene from the point of view of the light and generate a shadow map (encoded in the rgba of the framebuffer) and the second shader to render the final scene using the shadow map to cast soft shadows with penumbra. The shadow should start hard at contact points between the caster and receiver and soften as it gets further away.

The code itself is a little rough and ready but should run just fine. You can display plain old hard shadows by reducing the sample counts as far as the sliders will allow. I haven’t tested this on older devices so it may not run brilliantly on some.

It’s available here or to download using WebRepo

Screenshot

sweeeet

so would this work with craft models?

@UberGoober I’m really not sure unfortunately. I haven’t really experimented with Craft much.

any way to make this work on 2d projection? like for a side scroller

@skar I’m not entirely sure what you’re meaning but I suspect it may be pretty different to what I’m doing here.

I’m sure an effect like this is possible in 2D I’m just not aware of it.

basically see the attached images, if we have a light source in 2d can we draw a shadow projected from the 2d asset and basically just fake it and draw that shape skewed/stretched and rotated

do you think you can take a stab at it?

@skar I suspect so yes, looks more of a pseudo 3D effect rather than all in 2D space.

If you wanted a detailed silhouette of the shadow caster (as seen in your images) I think you’d have to calculate the shadow map much the same as I have anyway and your ground plane would need to be 3D so it can accurately query the shadow map.

If you wanted it purely in 2D I really wouldn’t know where to begin to be honest.

@skar not sure about rotated, but how about this for a quick fake shadow for skew/stretch using meshes?


-- Shadowcast
--by West

-- Use this function to perform your initial setup
function setup()
--source image - ussume bottom of the image aligns with the ground
  srcimg=readImage(asset.builtin.Platformer_Art.Guy_Look_Right)
  w=srcimg.width
  h=srcimg.height
  --create a flipped black and white shadow image
  shadow=image(w,h)
  setContext(shadow)
  for i=1,w do
    for j=1,h do
      local r,g,b,a=srcimg:get(i,j)
      if a~=0 then
      shadow:set(i,h-j,color(0,255))
      end  
    end
  end
  setContext()

  shadowMesh=mesh()
  shadowMesh.texture=shadow
  shadowMesh.texCoords =  {vec2(0,1),vec2(0,0),vec2(1,1),vec2(1,0),vec2(1,1),vec2(0,0)}

  parameter.number("xoff",-5,5,0)
  parameter.number("yoff",-1,4,0)
  
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(94, 94, 203)
  
  suny=HEIGHT-(HEIGHT/8*yoff)
  sunx=WIDTH/2-xoff*WIDTH/8
  fill(255,255,0)
  ellipse(sunx,suny,100)
  
  noStroke()
fill(95, 129, 32)
  rect(0,0,WIDTH,HEIGHT/2+h/2)
  local tr = vec2(w/2, h/2)  
  local tl = vec2(-w/2,h/2)
  local br = vec2(w/2+w*xoff,-h/2-h*yoff)
  local bl = vec2(-w/2+w*xoff,-h/2-h*yoff)
  
  shadowMesh.vertices={tl,bl,tr,br,tr,bl}
  pushMatrix()
  translate(WIDTH/2, HEIGHT/2)
  sprite(srcimg,0,h)
  shadowMesh:draw()
  popMatrix()
end


@West wow that nearly perfect, i’ll play around with it some and see if i can get some additional functionality out into of it

@West - very neat - I like it. I put 50% transparency on, think it looks a little better.

I am not a great coder, so there’s probably ways to do this in, like, two extra lines of code, but this is how I did it, and it works, at least.

Anyway it’s just a dumb little addition but it lets you control the amount that the shadow fades out the farther away it gets from the source.


-- Shadowcast
--by West

-- Use this function to perform your initial setup
function setup()
    --source image - ussume bottom of the image aligns with the ground
    srcimg=readImage(asset.builtin.Platformer_Art.Guy_Look_Right)
    w=srcimg.width
    h=srcimg.height
    
    shadowMesh=mesh()
    shadowMesh.texture=makeShadow(readImage(asset.builtin.Platformer_Art.Guy_Look_Right), 170)
    shadowMesh.texCoords =  {vec2(0,1),vec2(0,0),vec2(1,1),vec2(1,0),vec2(1,1),vec2(0,0)}
    
    parameter.number("xoff",-5,5,0)
    parameter.number("yoff",-1,4,0)
    parameter.number("alpha", 0, 255, 170, function(value)
        local newShadow = makeShadow(readImage(asset.builtin.Platformer_Art.Guy_Look_Right), value)
        shadowMesh=mesh()
        shadowMesh.texture = newShadow
        shadowMesh.texCoords =  {vec2(0,1),vec2(0,0),vec2(1,1),vec2(1,0),vec2(1,1),vec2(0,0)}
        print("h")
    end)
    end
    
function makeShadow(srcimg, fade)
    local w, h, shadow
    w=srcimg.width
    h=srcimg.height
    --create a flipped black and white shadow image
    shadow=image(w,h)
    setContext(shadow)
    local alpha = fade       
    for j=1,h do
        for i=1,w do
            local r,g,b,a=srcimg:get(i,j)
            local useAlpha = alpha - (j * 1)
            if useAlpha < 0 then useAlpha = 0 end
            if a~=0 then
                shadow:set(i,h-j,color(0,useAlpha))
            end 
        end
    end
    return shadow
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(94, 94, 203)
    
    suny=HEIGHT-(HEIGHT/8*yoff)
    sunx=WIDTH/2-xoff*WIDTH/8
    fill(255,255,0)
    ellipse(sunx,suny,100)
    
    noStroke()
    fill(95, 129, 32)
    rect(0,0,WIDTH,HEIGHT/2+h/2)
    local tr = vec2(w/2, h/2)  
    local tl = vec2(-w/2,h/2)
    local br = vec2(w/2+w*xoff,-h/2-h*yoff)
    local bl = vec2(-w/2+w*xoff,-h/2-h*yoff)
    
    shadowMesh.vertices={tl,bl,tr,br,tr,bl}
    pushMatrix()
    translate(WIDTH/2, HEIGHT/2)
    sprite(srcimg,0,h)
    shadowMesh:draw()
    popMatrix()
end 

@Bri_G hehe - yes, I did nearly the same thing after I’d posted it.

@UberGoober nice addition. One function I learnt of here which has been really useful is math.max function. In your code:

      local useAlpha = alpha - (j * 1)
      if useAlpha < 0 then useAlpha = 0 end

Can be condensed down to:

      local useAlpha = math.max(0,alpha - (j * 1))

@Steppers you may be interested in this fairly insanely impressive demo that was made by MMGames.

@UberGoober Wow, very impressive! Perhaps a little complicated for me to understand though :o

It did seem a little unstable and Codea crashed a couple of times running it but I’ll get that added to WebRepo at some point.

@Steppers

Yeah it crashed for me too, but as recently as the beginning of this year it was running fine and not crashing at all.

@Simeon any clue on this?

It would be a shame for such a nice piece of work to be unusable by anyone else.

@West so i got to the crux of this code, it’s really here that controls the skew and stretch, the other parts (black and flip) i can replicate with a shader


  local tr = vec2(w/2, h/2)  
  local tl = vec2(-w/2,h/2)
  local br = vec2(w/2+w*xoff,-h/2-h*yoff)
  local bl = vec2(-w/2+w*xoff,-h/2-h*yoff)

  shadowMesh.vertices={tl,bl,tr,br,tr,bl}

can you explain this? like what do the variable names mean? (tr, tl, br, bl) and why are there 6 vertices? a visual diagram would help me the best but any insight would be appreciated

@skar I’ll have a go - or at least the way I view things. Firstly textures are mapped on to meshes in triangles not rectangles - Codea provides shortcuts with setRecText and such like but I think under the hood these map a rectangle on to two triangles.

What is important is that the each texture coordinate (texCoords) on the source texture maps to the equivalent point in”mesh space". In the example, this is:


shadowMesh.texCoords =  {vec2(0,1),vec2(0,0),vec2(1,1),vec2(1,0),vec2(1,1),vec2(0,0)}

In texCoords, the values run from 0 to 1 and represent the percentage of the width/height of the source texture image. So above we are going from top left corner to bottom left corner to top right corner to form the first triangle and then from bottom right corner to top right corner to bottom left corner for the second triangle.

To visulise this, you could use colours instead of a texture. Comment out the above line and replace with


  shadowMesh.colors={color(255,0,0),color(255,0,0),color(255,0,0),color(0,0,255),color(0,0,255),color(0,0,255)}

To see this represented as red and blue triangles.

When it comes to the mesh code, my variable names are:
tr=top right corner of the mesh rectangle
tl=top left corner of the mesh rectangle
br=bottom right corner of the mesh rectangle
bl=bottom left corner of the mesh rectangle
w=width of the on screen mesh
h=height of the on screen mesh

My bad - should always chose meaningful variable names!

Note how the order {tl,bl,tr,br,tr,bl} follows the order of the texCoords.

The xoff and yoff are just how much you want to translate the bottom corners of the mesh to get the skew.

@UberGoober The Global Illumination demo should be available on WebRepo now.

@west thanks a lot for the helpful breakdown. i’m getting really close to a simplified version (at least in my opinion) of this, just been feeling sick and unproductive

@west @ubergoober

i’ve completed the rework here so it’s using a shader, setRect instead of coords, and it falls behind and in front of the character depending on the Yoffset, you can also set the shadow color to anything other than black

@steppers P.S. thanks for letting me and us use your thread to piggy back this work


-- Shadowcast by West, modified by skar

function setup()
  srcimg = readImage(asset.builtin.Platformer_Art.Guy_Look_Right)
  w=srcimg.width
  h=srcimg.height

  characterMesh = mesh()
  characterMesh.texture = srcimg
  characterMesh:addRect(WIDTH/2, HEIGHT/2, w, h, 0)

  shadowMesh = mesh()
  shadowMesh.texture = srcimg
  shadowMesh:addRect(WIDTH/2, HEIGHT/2, w, h, 0)
  shadowMesh.shader = shader(Shadow.v, Shadow.f)
  shadowMesh.shader.modColor = vec4(0, 0, 0, 1)
  shadowMesh.shader.shadowAlpha = 0.5
  
  parameter.number("xoff", -5, 5, 2)
  parameter.number("yoff", -2, 4, 0.3)
  parameter.number("shadowAlphaP", 0.0, 1.0, 0.5)
end

function draw()
  background(94, 94, 203)  

  local tl = vec2((WIDTH/2) + (-w/2+w*xoff), (HEIGHT/2) + (h/2-h*yoff))
  local bl = vec2((WIDTH/2) + (-w/2), (HEIGHT/2) + (-h/2))
  local tr = vec2((WIDTH/2) + (w/2+w*xoff), (HEIGHT/2) + (h/2-h*yoff))  
  local br = vec2((WIDTH/2) + (w/2), (HEIGHT/2) + (-h/2))
  
  shadowMesh.vertices={tl,bl,br, tl,br,tr}
  shadowMesh.shader.shadowAlpha = shadowAlphaP
  shadowMesh:draw()
  characterMesh:draw()
end 

Shadow = {
  v = [[
    uniform mat4 modelViewProjection;
    attribute vec4 position;
    attribute vec4 color;
    attribute vec2 texCoord;
  
    uniform vec4 modColor;
  
    varying lowp vec4 vColor;
    varying highp vec2 vTexCoord;
    void main() {
      vColor = vec4(color.rgb * modColor.rgb, color.a * modColor.a);
      vTexCoord = texCoord;
      gl_Position = modelViewProjection * position;
    }
  ]],
  f = [[
    precision highp float;
    uniform lowp sampler2D texture;

    varying lowp vec4 vColor;
    varying highp vec2 vTexCoord;
    uniform float shadowAlpha;

  
    void main() {

      lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
      float useAlpha = shadowAlpha * col.a * (1.0 - vTexCoord.y);
      gl_FragColor = vec4(col.r, col.g, col.b, useAlpha);
    }
  ]]
}