Reverse-triangle mesh

Hi all,
I am currently trying to design a ‘hide and seek’ sort of game. The only part I cannot figure out is how to graphically represent a flashlight, as in drawing a black mesh over every part that is not lit up by the flashlight beam. I am truly sorry if someone has asked this question already, but I could not find an answer. Example code would be greatly appreciated and would help me better to learn, but all I really want is a quick explanation of how I would calculate the position of the mesh vertices. Thank you!

If you want a simple circle of light, the fastest approach may be to prepare a large black image with a round hole in the middle. Then simply draw this image centred where you want the flashlight to be, and it will cover everything except the hole in the middle…

Otherwise you probably need to use a shader.

@TheSolderKing If you don’t mind a square flashlight, this works. Just slide your finger anywhere around the screen.

EDIT: This has bugs, but it’s just a quick example.


displayMode(FULLSCREEN)
supportedOrientations(PORTRAIT_ANY)

function setup()
    rectMode(CENTER)
    img1=readImage("Cargo Bot:Startup Screen")
    cx=WIDTH/2
    cy=HEIGHT/2
    img2=image(100,100)
    size=100
end

function draw()
    background(0)
    img2=img1:copy(cx,cy,size,size)
    sprite(img2,cx,cy)
end

function touched(t)
    if t.state==MOVING then
        cx=cx+t.deltaX
        cy=cy+t.deltaY
    end
end

I was going to suggest the shader approach - start with the ellipse shader and invert the results.

Thank you all for answering! Sorry for not elaborating-by flashlight I did in fact mean the inverse of the ‘arc’ shader. The one question I have is would I invert this? Thank you! :smiley:

@TheSolderKing - if you’re comfortable with shaders, try this

https://coolcodea.wordpress.com/2014/09/17/163-2d-platform-game-8-lighting/

Thank you everyone! Thanks to you, I was able to make this:

-- Shader

-- Use this function to perform your initial setup
function setup()
    supportedOrientations(LANDSCAPE_RIGHT)
    print("Hello World!")
    m=mesh()
    m:addRect(0,0,WIDTH*4,HEIGHT*4)
    myShader=shader("Patterns:Arc")
    m.shader=myShader
    m:setColors(0,100,100)
    rotat=0
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(255, 255, 255, 255)
    sprite("Small World:Mine Large", WIDTH/2, HEIGHT/2, WIDTH, HEIGHT)
    -- This sets the line thickness
    strokeWidth(5)
    rotat = rotat + RotationRate.z
    -- Do your drawing here
    m.shader.size=0
    m.shader.color=color(0,0,0)
    m.shader.a1=math.rad(15)
    m.shader.a2=math.rad(-15)
    translate(WIDTH/2, HEIGHT/2)
    rotate(rotat)
    m:draw()
end

Hi @TheSolderKing,

Looks good but can’t see the effect without the shader ‘Patterns:Arc’ !

Bri_G

:((

Oh dear - my apologies. Just found the patterns entries under the Assets menu. Still the app didn’t work - but I was running it from Aircode.

Sorry about that.

Bri_G

:open_mouth: :open_mouth: :open_mouth:

Hi @TheSolderKing,

My version using @dave1707 code above:


displayMode(FULLSCREEN)
supportedOrientations(PORTRAIT_ANY)

function setup()
    rectMode(CENTER)
    img1=readImage("Cargo Bot:Startup Screen")
    cx, w2 = WIDTH/2, WIDTH/2
    cy, h2 = HEIGHT/2, HEIGHT/2
    img2 = image(WIDTH,HEIGHT)
    setContext(img2)
        rectMode(CENTER)
        fill(10,10,10,229)
        rect(w2,h2,WIDTH,HEIGHT)
    setContext()
    img3=image(100,100)
    size=100
end

function draw()
    background(0)
    img3=img1:copy(cx,cy,size,size)
    sprite(img1,w2,h2)
    sprite(img2,w2,h2)
    sprite(img3,cx,cy)
end

function touched(t)
    if t.state==MOVING then
        cx=cx+t.deltaX
        cy=cy+t.deltaY
    end
end

You can set the transparency in the fill to give the shadows effect the depth you need. If you need an elliptical shape you could draw a sprite the same size as the copy area from img3 above with the same level of colouring and transparency as used for the fill above. |The using setContext(img3) you could sprite it onto that image.

Hope that makes sense and helps.

Bri_G

:slight_smile:

Here’s an updated version of my code that uses a circular mask. Just in case someone doesn’t want to use shaders.


displayMode(FULLSCREEN)
supportedOrientations(PORTRAIT_ANY)

function setup()
    img1=readImage("Cargo Bot:Startup Screen")  -- background image
    size=75 -- size of flashlight circle
    xc=size+5   -- x,y position for center of mask
    yc=size+5
    mask=image(size*2+10,size*2+10)    -- create image for circular mask
    for x=1,size*2+10 do
        for y=1,size*2+10 do
            if (x-xc)^2/size^2+(y-yc)^2/size^2 > 1 then 
                mask:set(x,y,0,0,0)
            end
        end
    end  
    -- create 2nd image of flashlight circle
    cx=WIDTH/2
    cy=HEIGHT/2
    img2=image(size,size)
    img2=img1:copy(cx-size,cy-size,size*2,size*2)
end

function draw()
    background(0)
    sprite(img2,cx-1,cy-1)  -- draw square image copy 
    sprite(mask,cx,cy) -- cover square image copy with circle mask 
end

function touched(t)
    if t.state==MOVING then
        cx=cx+t.deltaX
        cy=cy+t.deltaY
        -- keep image copy on screen
        if cx<size then
            cx=size
        end
        if cx>WIDTH-size then
            cx=WIDTH-size
        end
        if cy<size then
            cy=size
        end
        if cy>HEIGHT-size then
            cy=HEIGHT-size
        end  
        -- copy image     
        img2=img1:copy(cx-size,cy-size,size*2,size*2)
    end
end

I modified one of my godray shaders to make a 2D ray-casted light effect, if you want to
try it. A little laggy, but you can turn down the settings for better performance.


--# 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, 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.godraySamples = 2
    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 / 5.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)));
    }
}

]]
}
}