Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Soft Shadows - Meshes and Framebuffers

edited May 19 in Examples Posts: 199

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

Comments

  • edited May 19 Posts: 1,307

    sweeeet

    so would this work with craft models?

  • Posts: 199

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

  • Posts: 172

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

  • Posts: 199

    @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.

  • Posts: 172

    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?

  • Posts: 199

    @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.

  • Posts: 892

    @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
  • Posts: 172

    @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

  • Posts: 2,366

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

  • edited May 20 Posts: 1,307

    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
  • Posts: 892

    @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))
    
  • Posts: 1,307

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

  • Posts: 199

    @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.

  • Posts: 1,307

    @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.

  • Posts: 172

    @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

  • edited May 21 Posts: 892

    @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.

  • Posts: 199

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

  • Posts: 172

    @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

  • Posts: 172

    @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); } ]] }
  • Posts: 199

    @skar No worries. That looks pretty nice!

Sign In or Register to comment.