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!
@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
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
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)));
}
}
]]
}
}