Lighting

I’m working on what isn’t so much lighting as it is a way to make a field of view where a character can’t see through walls in a 2D game. This is the first test of it (didn’t even have time to test or fix any code) but it is functional.


--# Main
function setup()
    attempt = physics.rect(1,1)
    attempt2 = physics.rect(1,1)
    light = Lights(vec2(WIDTH/2,HEIGHT/2),{attempt,attempt2})
    frame = 0
    rectMode(CENTER)
    physics.gravity(0,0)
end
function draw() 
    background(255, 255, 255, 255)
    fill(0, 0, 0, 255)
    rect(attempt.position.x+1,attempt.position.y+1,51)
    rect(attempt2.position.x+1,attempt2.position.y+1,51)
    ellipse(WIDTH/2,HEIGHT/2,10)
    light:draw()
end

function touched(t)
    attempt.position = vec2(t.x,t.y)
    attempt.linearVelocity = vec2(0,0)
    attempt2.position = vec2(WIDTH-t.x,HEIGHT-t.y)
    attempt2.linearVelocity = vec2(0,0)
end

physics.rect = function(x,y,w,h)
    x = x or WIDTH/2
    y = y or HEIGHT/2
    w = w or 50
    h = h or 50
    return physics.body(POLYGON,
    vec2(x-w/2,y-h/2),vec2(x+w/2,y-h/2),
    vec2(x+w/2,y+h/2),vec2(x-w/2,y+h/2))
end

--# Lights
Lights = class()
displayMode(FULLSCREEN)
function Lights:init(center,bodies)
    self.p = center
    self.b = bodies
    self.edges =   
    {physics.body(EDGE,vec2(0,0),vec2(0,HEIGHT)),
    physics.body(EDGE,vec2(0,0),vec2(WIDTH,0)),
    physics.body(EDGE,vec2(WIDTH,HEIGHT),vec2(0,HEIGHT)),
    physics.body(EDGE,vec2(WIDTH,HEIGHT),vec2(WIDTH,0))}
    for i,v in ipairs(self.edges) do
        v.mask = {1}
    end
    self.mesh = mesh()
end

function Lights:draw()
    local b = self.p
    local verts = {}
    for i,v in pairs(self.b) do 
        local points = {}
        for j,z in pairs(self.b[i].points) do
            local z2 = z + v.position
            local m1 = (z2.y-b.y)
            local m2 = (z2.x-b.x)
            local dx = math.abs(WIDTH/m2)
            local dy = math.abs(HEIGHT/m1)
            local q = vec2((b.x+m2*50),(b.y+m1*50))
            table.insert(points,{z2,q})--ep.point})
        end
        if #points>3 then
        for i=1,#points/2 do
            local a = points[i]
            local b = points[i+2]
            local tri=triangulate({a[1],a[2],b[2],b[1]})
            for i=1,#tri do
                verts[#verts+1] = tri[i]
            end
        end
        self.mesh.vertices = verts
        self.mesh:setColors(0,0,0,255)
        end
    end
    self.mesh:draw()
    self.mesh:clear()
end

Comments and suggestions are welcome

@Monkeyman32123 - If you’re just peeking through doorways, maybe try this instead. Move the parameter to slide the wall left and right to simulate changes to field of view, and see the characters behind.

There are a couple of ways to make things invisible behind walls. One is obviously to draw them before you draw the walls, as I’ve done with the left hand character.

What isn’t well known is that you can set a depth value in 2D graphics (ie z as well as x and y), which will draw objects behind others. That’s what I’ve done with the character on the right. So if you draw room objects at a depth slightly behind the walls, then they will be drawn behind the walls, no matter what order you draw the in.

function setup()
    --move this to slide wall
    parameter.number("X",-100,100,0)
end

function draw()
    background(220)
    fill(180, 113, 95, 255)
    sprite("Planet Cute:Character Boy",350,280)
    rect(200+X,200,200,150)
    rect(500+X,200,200,150)
    pushMatrix()
    translate(0,0,-0.1) --draw this char BEHIND the walls
    sprite("Planet Cute:Character Pink Girl",550,280)
    popMatrix()
end

Oh, I’m sorry, I think I may have not explained what mine is for very well. Say you’re in a top-down 2-D game and there are walls and obstacles that a normal human couldn’t see through, so you use this so that everything behind those walls is blocked from the player’s view.

Someone asked a similar question a while ago, and I tweaked my WIP godray shader for it to work. How’s this?


--# Main
-- Skyrays Lighting

displayMode(OVERLAY)

function setup()
    print("----\
Drag to move the image\
Double-tap to zoom in/out\
Zoom is kinda broken, sorry\
----")
    sidebarTween = tween.delay(1.5, function()
        displayMode(FULLSCREEN)
    end)
    shouldSidebar = 0
    parameter.watch("FPS")
    parameter.number("Samples", 0.5, 2.0, 0.5, callback)-- The shader can have the samples set to any amount, but 5 is the most I would recommend. 2 is a good mumber IMO, not too little that it looks really weird, but not too many that it's super laggy
    parameter.boolean("Realistic Curve", true, callback) -- Whether or not the light effect should amplified realistically
    parameter.boolean("Visible Walls", true, callback) -- Whether the walls should be colored or solid black
    parameter.boolean("Overlay", false, callback) -- Sprite the image over where it would be so you can see it in the dark
    mult = 1.0 -- Multiplier for the resolution of the screen (you can crank it up to 2k resolution on retinas, set the mult to 2)
    screen = image(WIDTH * mult, HEIGHT * mult)
    objects = image(WIDTH * mult, HEIGHT * mult)
    m = mesh()
    rIdx = m:addRect(WIDTH / 2 * mult, HEIGHT / 2 * mult, WIDTH * mult, HEIGHT * mult)
    m.texture = screen
    m.shader = shader(Shaders.Skyrays.vS, Shaders.Skyrays.fS)
    m.shader.point = vec2(0.5, 0.5)
    m.shader.samples = 100
    pos = {x = WIDTH / 8, y = HEIGHT / 2, s = 1}
    img = readImage("Small World:Mine Large") -- The image that is put on the screen to block godrays
    
    bg = image(WIDTH, HEIGHT)
    setContext(bg, false)
    local xSize = WIDTH / 7
    local ySize = WIDTH / 7
    for x = WIDTH / 2 - (xSize + 100), WIDTH / 2 + (xSize + 100), 100 do
        for y = HEIGHT / 2 - (ySize + 100), HEIGHT / 2 + (ySize + 100), 100 do
            sprite("Platformer Art:Block Brick", x, y, 100)
        end
    end
    setContext()
    math.randomseed(0)
    obj = {}
    for i = 1, 25 do
        table.insert(obj, {math.random(0, WIDTH), math.random(0, HEIGHT)})
    end
end

function callback()
    if FPS ~= 0 then
        tween.stop(sidebarTween)
        shouldSidebar = 10
    end
end

function draw()
    --noSmooth()
    
    local prev = shouldSidebar
    shouldSidebar = math.max(0, shouldSidebar - DeltaTime)
    if prev ~= 0 and shouldSidebar == 0 then
        displayMode(FULLSCREEN)
    end
    
    m.shader.samples = Samples
    m.shader.realisticCurve = Realistic_Curve
    m.shader.visibleWalls = Visible_Walls
    --m.shader.objects = objects
    
    background(119, 182, 200, 255)
    
    strokeWidth(5)
    
    setContext(screen, false)
    background(0, 0)
    pushMatrix()
    translate(pos.x, pos.y)
    scale(pos.s)
    translate(-(pos.x), -(pos.y))
    sprite(img, pos.x, pos.y, WIDTH, HEIGHT)
    popMatrix()
    setContext()
    
    translate(WIDTH / 2, HEIGHT / 2)
    scale(pos.s)
    translate(WIDTH / -2, HEIGHT / -2)
    
    local xOff = (pos.x / 100 - math.floor(pos.x / 100)) * 100
    local yOff = (pos.y / 100 - math.floor(pos.y / 100)) * 100
    
    sprite(bg, WIDTH / 2 + xOff, HEIGHT / 2 + yOff)
    
    for k, v in ipairs(obj) do
        sprite("Cargo Bot:Title Large Crate 1", v[1] + pos.x, v[2] + pos.y, 50)
    end
    
    sprite(objects, WIDTH / 2, HEIGHT / 2, WIDTH, HEIGHT)
    
    scale(1 / mult)
    
    m:draw()
    
    resetMatrix()
    
    if Overlay then
        tint(255, 31)
        sprite(img, pos.x, pos.y, WIDTH, HEIGHT)
        noTint()
    end
    
    resetMatrix()
    
    fill(255)
    font("HelveticaNeue-UltraLight")
    fontSize(24)
    textMode(CORNER)
    local str = "FPS: " .. FPS
    local w, h = textSize(str)
    text(str, 5, HEIGHT - h - 5)
end

function touched(touch)
    if touch.state ~= ENDED and touch.state ~= CANCELLED then
        pos.x = pos.x + touch.deltaX
        pos.y = pos.y + touch.deltaY
        displayMode(FULLSCREEN)
    elseif touch.tapCount == 2 then
        tween(1, pos, {s = 3 - pos.s}, tween.easing.bounceOut)
        if shouldSidebar > 0 then
            displayMode(OVERLAY)
        end
    else
        if shouldSidebar > 0 then
            displayMode(OVERLAY)
        end
    end
end

-- FPS counter --

FPS = 0
local frames = 0
local time = 0
tween.delay(0, function()
    local d = draw
    draw = function()
        frames = frames + 1
        if math.floor(ElapsedTime) ~= math.floor(time) then
            FPS = frames - 1
            frames = 1
        end
        time = ElapsedTime
        d()
    end
end)
--# Shaders
Shaders = {
Skyrays = {
vS = [[
//
// A basic vertex shader
//

//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;

//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

//This is an output variable that will be passed to the fragment shader
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

void main()
{
    //Pass the mesh color to the fragment shader
    vColor = color;
    vTexCoord = texCoord;
    
    //Multiply the vertex position by our combined transform
    gl_Position = modelViewProjection * position;
}

]],
fS = [[
//
// A basic fragment shader
//

//Default precision qualifier
precision highp float;

//This represents the current texture on the mesh
uniform lowp sampler2D texture;

//uniform lowp sampler2D objects;

uniform vec2 point;

//The interpolated vertex color for this fragment
varying lowp vec4 vColor;

//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;

uniform bool realisticCurve;

uniform float samples;

uniform bool visibleWalls;

const float distx = 1.0 / 3.0;
const float texMult = 1.0 / distx;

void main()
{
    //Sample the texture at the interpolated coordinate
    lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
    
    if (col.a <= 0.5) {
        col = vec4(0.0);
    }
    
    float targetDist = distance(point, vTexCoord);
    lowp vec4 pointSample = texture2D(texture, point);
    bool startVisible = pointSample.a <= 0.5;
    
    float light = 0.0;
    
    float add = max(0.0, 1.0 - targetDist * texMult);
    float objLight = add * col.a;
    
    if (targetDist <= distx) {
        vec2 dir = normalize(point - vTexCoord);
        bool visible = startVisible || col.a <= 0.5;
        float visibleNum = 0.0;
        float mult;
        int c = 0;
        for (float dist = 0.0; dist <= targetDist; dist += targetDist / (samples * 10.0)) {
            lowp vec4 sample = texture2D(texture, vTexCoord + dir * dist);// + texture2D(objects, vTexCoord + dir * dist);
            mult = min(1.0, (1.0 - sample.a) + 0.2);
            float objMult;
            if (visibleWalls) objMult = min(1.0, (1.0 - sample.a) + 0.85);
            if (sample.a != 0.0) {
                if (sample.a <= 0.5) {
                    visibleNum += 1.0;
                    visible = true;
                }
                if ((visible && startVisible) || (!visible) || sample.a > 0.5) {
                    add *= mult;
                    if (visibleWalls) objLight *= objMult;
                    if (col.a > 0.5) {
                        add *= mult;
                        if (visibleWalls) objLight *= objMult;
                        c++;
                    }
                    if (c > 2) break;
                }
            }
        }
        
        if (visible) {
            if (realisticCurve) add = pow(add * 1.5, 2.0) / 1.5;
            light += add;
        }
    }
    
    //Set the output color to the texture color
    if (visibleWalls && col.a > 0.5) {
        //light = (light + 0.1) * 2.0 - 0.1;
        gl_FragColor = mix(vec4(vec3(0.0), 1.0), col, max(0.0, min(1.0, objLight)));
    } else {
        gl_FragColor = mix(vec4(vec3(0.0), 1.0), col, max(0.0, min(1.0, light)));
    }
}

]]
}
}

@Monkeyman32123 - the problem with a top down view as you are suggesting, is that you have to do all the work to figure out what is visible - normally OpenGL does this for you.

@SkyTheCoder’s approach appears to use a shader to test whether each point can be seen by the camera, by checking a series of sample points in between to see if anything is blocking the view. I use a similar sampling approach in my 3D dungeon.

What you might find simpler, is to use a tiled map, ie break your map into a grid of (for example) 10x10 pixels squares. Each square can contain one object, eg wall, character, weapon.

Then you have a 2D table where you store what is in each square (you could have a set of characters which stand for different objects like walls, doors etc).

Each time you draw, you look around in all directions, creating a table of which tiles are visible. This can be done using a flood fill algorithm (I have code for this). Then you draw the visible tiles.

I think there’s a huge misunderstanding here. My code is perfectly functional but still being worked on. It is for doing top-down views and it blocks what shouldn’t be able to be seen. This is made for 2-D games and that’s all it works for. I wasn’t asking a question, thus I put it in “code sharing”. Since I’m confused what you are saying and I don’t think I’ve expressed what this is for well enough I will whip up a demo over the next few days to show what I mean

Sorry about that. A demo would be good.

@Monkeyman32123 Sorry, I also thought you were asking for help. Looking back at your post, it seems obvious that you were sharing your own code…I guess I just read it too quickly :confused:

@Monkeyman32123 This is some good stuff, I’ve used it in my game to make a demo for it. Is it okay to use it for my game, I’m going to tweak a few things but I’ll give you credit!
@Ignatz here is your demo using it in my game:
http://www.youtube.com/watch?v=RjXV2WATN38

That little dude needs to teach his feet how to behave!

Looks fun

@Ignatz thank you, it’s a bit erratic when the fps is only 30 but it’s being fixed currently.

Of course you may Luatee :slight_smile: I post code because I want people to have a use for it. If you come back periodically I plan to make improvements and a slew of changes. Currently it only functions (properly) with convex even-vertexed polygons (as this was just a test of concept) but the changes are a-comin’

Oh and it’s quite alright Ignatz and Sky :stuck_out_tongue: it happens.

That’s awesome! It reminds me of Lasersquad (on the Commodore 64 and then the Amiga). I think the same developer went on to make UFO and then XCOM. Although when I played XCOM recently that point of view effect didn’t seem quite as pronounced as I remembered it in the days of Lasersquad. On iOS, the Hunters games from Rodeo are probably closest to the original Lasersquad. @Monkeyman32123 is this for something tactical and turn-based?

Okay, so I was completely redoing the code to use edges (much more difficult) rather than crisscrossed quadrilaterals. As you move the block, occasionally the shadow overlaps the square and it seems to be random, and I cannot figure out why

function setup()
    attempt = physics.rect(1,1)
    attempt.mask = {1}
    --attempt2 = physics.rect(1,1)
    --attempt3 = physics.rect(WIDTH/2,HEIGHT/4,WIDTH/2,20)
    light = Lights(vec2(WIDTH/2,HEIGHT/2),{attempt})--,attempt2,attempt3})
    frame = 0
    rectMode(CENTER)
    physics.gravity(0,0)
end
function draw() 
    --light.p = light.p + vec2(Gravity.x,Gravity.y)*15
    background(255, 255, 255, 255)
    fill(255, 0, 0, 255)
    rect(attempt.position.x+1,attempt.position.y+1,51)
    --rect(attempt2.position.x+1,attempt2.position.y+1,51)
    --rect(WIDTH/2,HEIGHT/4,WIDTH/2+1,20+1)
    ellipse(light.center.x,light.center.y,30)
    light:draw()
end

function touched(t)
    attempt.position = vec2(t.x,t.y)
    attempt.linearVelocity = vec2(0,0)
    --attempt2.position = vec2(WIDTH-t.x-2,HEIGHT-t.y-2)
    --attempt2.linearVelocity = vec2(0,0)
end

physics.rect = function(x,y,w,h)
    x = x or WIDTH/2
    y = y or HEIGHT/2
    w = w or 50
    h = h or 50
    return physics.body(POLYGON,
    vec2(x-w/2,y-h/2),vec2(x+w/2,y-h/2),
    vec2(x+w/2,y+h/2),vec2(x-w/2,y+h/2))
end

Lights = class()
function Lights:init(center,bodies)
    self.center = center
    self.bodies = bodies
    self.mesh = mesh()
end

function Lights:draw()
    local c = self.center
    local verts = {}
    for i,v in pairs(self.bodies) do
        local last = nil
        local keypoints = {}
        local points = {}
        local extrusions = {}
        local on = nil
        for j,r in pairs(v.points) do
            local z = v:getWorldPoint(r)
            local m = z-c
            local zt = self:isvis(z-(m*.001))
            if not zt and last then
                keypoints[1] = j-1
            elseif zt and not last then
                keypoints[2] = j
            end
            last = zt
        end
        if not keypoints[1] then 
            keypoints[1] = #v.points
        end
        if not keypoints[2] then
            keypoints[2] = 1
        end
        for g=keypoints[1],math.huge do
            local x = g
            if g > #v.points then
                x = g-#v.points
            end
            local poi = v:getWorldPoint(v.points[x or g])
            points[g-keypoints[1]+1] = poi
            local m = poi-c
            extrusions[g-keypoints[1]+1] = poi+(m*100)
            if x == keypoints[2] then
                break
            end
        end
        for l=#extrusions,1,-1 do
            points[#points+1] = extrusions[l]
        end
        local tri = triangulate(points)
        if tri then
            for q=1,#tri do
                verts[#verts+1] = tri[q]
            end
        end
    end
    self.mesh.vertices = verts
    self.mesh:setColors(0,0,0,255)
    self.mesh:draw()
    self.mesh:clear()
end

function Lights:isvis(p)
    isnotvis = physics.raycast(self.center,p)
    if isnotvis then
        return false
    else
        return true
    end
end

In the draw routine, won’t drawing the rect after the light:draw fix the problem.

Yes, it will, but I want to fix the bug because adding vertices to the triangulation seems like it could become a problem with more complex shapes. Also, I’d just like to know what is causing it so that the rects drawing doesn’t need to be order-specific

And I really only want to know as a mental excercise (just got back to coding so proposed it to myself as a way to challenge myself back into it). That version is probably scrapped ASAP.

@Monkeyman32123 I see what your problem is, but I’m not sure I can explain it to you. Add the line of code I show below as the last line in setup(). Rotate your iPad to portrait mode and run your code. The red square should be in the lower right area of the screen. The shadow will cover the lower right portion of the square. What’s happening is when you calculate the verts to draw your shadow, the 3 points of the red square being covered are part of the verts. I was able to print out the vert x,y values and the points x,y values and was able to see the mesh areas that are colored black. I’m not sure how to fix it yet or why as you mover the square around, certain positions will cause this problem.

    attempt.position=vec2(370,245)

@Monkeyman32123 Here’s your problem. Under some conditions, the triangulate function is taking 6 points you calculate and creates 4 meshes. One of those meshes is a triangular part of the red square that’s colored black.