I posed a YouTube video on one of the AppGameKit Facebook groups and people wanted me to share the code for my 2D shadow effects that don't require shaders. Obviously, shaders are the way to go if you want to get really serious, but this method has okay performance on a modern PC. The bottleneck seems to be the SetImageMask() function which the documentation recommends you not use every frame. However, by working on an image that is smaller than the screen it still gives good results.
https://youtu.be/Up49DWoG7HA
// show all errors
SetErrorMode(2)
// set window properties
SetWindowTitle("2D Shadows Demo")
SetWindowSize(1600, 900, 1)
SetWindowAllowResize( 1 ) // allow the user to resize the window
// set display properties
SetVirtualResolution(1920, 1080) // doesn't have to match the window
SetOrientationAllowed( 1, 1, 1, 1 ) // allow both portrait and landscape on mobile devices
SetSyncRate(30, 0) // 30fps instead of 60 to save battery
SetVSync(1)
SetScissor(0, 0, 0, 0) // use the maximum available screen space, no black borders
UseNewDefaultFonts( 1 ) // since version 2.0.22 we can use nicer default fonts
SetAntialiasMode(1)
dim lines[] as Line
dim rays[] as Ray
global pixelImage as integer
global px as float
global py as float
global shadowScaleX as float
global shadowScaleY as float
global mask as integer
global stencil as integer
global maskSprite as integer
shadowScaleX = 0.5
shadowScaleY = 0.5
px = 960
py = 540
LoadAssets()
CreateDefaultLines(32.0)
do
px = GetPointerX()
py = GetPointerY()
ft# = GetFrameTime()
GenerateShadows()
c = MakeColor(127, 127, 255)
DrawEllipse(px, py, 4, 4, c, c, 1)
Print(ScreenFPS())
Sync()
loop
function GenerateShadows()
ow# = GetVirtualWidth()
oh# = GetVirtualHeight()
width# = ow# * shadowScaleX
height# = oh# * shadowScaleY
rays.Length = -1
if GetImageExists(mask) = 0
mask = CreateRenderImage(width#, height#, 0, 0)
endif
SetRenderToImage(mask, 0)
SetVirtualResolution(width#, height#)
SetClearColor(255, 0, 0)
ClearScreen()
local r1 as Ray
local r2 as Ray
local r3 as Ray
local r4 as Ray
for i = 0 to lines.Length
sw# = GetSpriteWidth(lines[i].Sprite) / 2.0
sh# = GetSpriteHeight(lines[i].Sprite) / 2.0
r1 = CreateRay(px, py, GetWorldXFromSprite(lines[i].Sprite, -sw#, -sh#), GetWorldYFromSprite(lines[i].Sprite, -sw#, -sh#))
r2 = CreateRay(px, py, GetWorldXFromSprite(lines[i].Sprite, sw#, -sh#), GetWorldYFromSprite(lines[i].Sprite, sw#, -sh#))
r3 = CreateRay(px, py, GetWorldXFromSprite(lines[i].Sprite, -sw#, sh#), GetWorldYFromSprite(lines[i].Sprite, -sw#, sh#))
r4 = CreateRay(px, py, GetWorldXFromSprite(lines[i].Sprite, sw#, sh#), GetWorldYFromSprite(lines[i].Sprite, sw#, sh#))
rays.Insert(r1)
rays.Insert(r2)
rays.Insert(r3)
rays.Insert(r4)
next i
local tmp as Ray[]
for i = 0 to rays.Length
r1 = AdjustedRay(rays[i], -0.01, 1)
r2 = AdjustedRay(rays[i], 0.01, 1)
tmp.Insert(r1)
tmp.Insert(r2)
next i
for i = 0 to tmp.Length
rays.Insert(tmp[i])
next i
tmp.Length = -1
for i = 0 to rays.Length
UpdateRay(rays[i])
next i
rays.Sort()
local v1 as Vector2
local v2 as Vector2
local v3 as Vector2
for i = 1 to rays.Length
v1.X = px * shadowScaleX
v1.Y = py * shadowScaleY
v2.X = rays[i - 1].EX * shadowScaleX
v2.Y = rays[i - 1].EY * shadowScaleY
v3.X = rays[i].EX * shadowScaleX
v3.Y = rays[i].EY * shadowScaleY
DrawTriangle(v1, v2, v3, 0)
next i
v1.X = px * shadowScaleX
v1.Y = py * shadowScaleY
v2.X = rays[rays.Length].EX * shadowScaleX
v2.Y = rays[rays.Length].EY * shadowScaleY
v3.X = rays[0].EX * shadowScaleX
v3.Y = rays[0].EY * shadowScaleY
DrawTriangle(v1, v2, v3, 0)
if GetImageExists(stencil) = 0
stencil = CreateRenderImage(width#, height#, 0, 0)
endif
SetRenderToImage(stencil, 0)
SetClearColor(0, 0, 0)
ClearScreen()
SetImageMask(stencil, mask, 4, 1, 0, 0)
if GetSpriteExists(maskSprite) = 0
maskSprite = CreateSprite(stencil)
SetSpriteDepth(maskSprite, 20)
SetSpriteScale(maskSprite, 1.0 / shadowScaleX, 1.0 / shadowScaleY)
SetSpriteTransparency(maskSprite, 1)
endif
SetVirtualResolution(ow#, oh#)
SetRenderToScreen()
SetClearColor(15, 15, 31)
endfunction
function DrawSegment(index1, index2)
local v1 as Vector2
local v2 as Vector2
local v3 as Vector2
v1.X = px * shadowScaleX
v1.Y = py * shadowScaleY
v2.X = rays[index1].EX * shadowScaleX
v2.Y = rays[index1].EY * shadowScaleY
v3.X = rays[index2].EX * shadowScaleX
v3.Y = rays[index2].EY * shadowScaleY
DrawTriangle(v1, v2, v3, 0)
endfunction
function UpdateRay(r ref as Ray)
c = SpriteRayCast(r.SX, r.SY, r.EX, r.EY)
if c = 1
r.EX = GetRayCastX()
r.EY = GetRayCastY()
endif
endfunction
function AdjustedRay(r as Ray, adjustment as float, intensity as integer)
local nr as Ray
a# = r.Angle + adjustment
nr.SX = r.SX
nr.SY = r.SY
nr.EX = r.SX + (Sin(a#) * 2250)
nr.EY = r.SY + (-Cos(a#) * 2250)
nr.Angle = a#
nr.Intensity = intensity
endfunction nr
function DrawTriangle(v1 as Vector2, v2 as Vector2, v3 as Vector2, intensity)
local vt as Vector2[]
vt.Insert(v1)
vt.Insert(v2)
vt.Insert(v3)
vt.Sort()
if vt[1].Y = vt[2].Y
FillBottomFlatTriangle(vt[0].X, vt[0].Y, vt[1].X, vt[1].Y, vt[2].X, vt[2].Y, intensity)
elseif vt[0].Y = vt[1].Y
FillTopFlatTriangle(vt[0].X, vt[0].Y, vt[1].X, vt[1].Y, vt[2].X, vt[2].Y, intensity)
else
local v4 as Vector2
v4.X = vt[0].X + ((vt[1].Y - vt[0].Y) / (vt[2].Y - vt[0].Y)) * (vt[2].X - vt[0].X)
v4.Y = vt[1].Y
FillBottomFlatTriangle(vt[0].X, vt[0].Y, vt[1].X, vt[1].Y, v4.X, v4.Y, intensity)
FillTopFlatTriangle(vt[1].X, vt[1].Y, v4.X, v4.Y, vt[2].X, vt[2].Y, intensity)
endif
if intensity = 0
c = MakeColor(0, 0, 0)
else
c = MakeColor(127, 0, 0)
endif
DrawLine(v1.X, v1.Y, v2.X, v2.Y, c, c)
DrawLine(v1.X, v1.Y, v3.X, v3.Y, c, c)
DrawLine(v3.X, v3.Y, v2.X, v2.Y, c, c)
endfunction
function FillBottomFlatTriangle(x1#, y1#, x2#, y2#, x3#, y3#, intensity)
s1# = (x2# - x1#) / (y2# - y1#)
s2# = (x3# - x1#) / (y3# - y1#)
curx1# = x1#
curx2# = x1#
if intensity = 0
c = MakeColor(0, 0, 0)
else
c = MakeColor(127, 0, 0)
endif
for scan = y1# to y2# step 1
DrawLine(curx1#, scan, curx2#, scan, c, c)
curx1# = curx1# + s1#
curx2# = curx2# + s2#
next scan
endfunction
function FillTopFlatTriangle(x1#, y1#, x2#, y2#, x3#, y3#, intensity)
s1# = (x3# - x1#) / (y3# - y1#)
s2# = (x3# - x2#) / (y3# - y2#)
curx1# = x3#
curx2# = x3#
if intensity = 0
c = MakeColor(0, 0, 0)
else
c = MakeColor(127, 0, 0)
endif
for scan = y3# to y1# step -1
DrawLine(curx1#, scan, curx2#, scan, c, c)
curx1# = curx1# - s1#
curx2# = curx2# - s2#
next scan
endfunction
function CalculateRayAngle(r ref as Ray)
xx# = r.EX - r.SX
yy# = r.EY - r.SY
a# = ATanFull(xx#, yy#)
r.Angle = a#
endfunction
function LengthSq(a#, b#)
r# = (a# * a#) + (b# * b#)
endfunction r#
function CreateDefaultLines(lineHeight#)
w# = GetVirtualWidth() - 1
h# = GetVirtualHeight() - 1
// Top
lines.Insert(Createline(0, 0, w#, 0, lineHeight#))
// Right
lines.Insert(CreateLine(w#, 0, w#, h#, lineHeight#))
// Bottom
lines.Insert(Createline(0, h#, w#, h#, lineHeight#))
// Left
lines.Insert(CreateLine(0, h#, 0, 0, lineHeight#))
lines.Insert(CreateLine(200, 200, 400, 400, 200))
lines.Insert(CreateLine(800, 300, 800, 600, 200))
lines.Insert(CreateLine(1700, 600, 1500, 800, 300))
lines.Insert(CreateLine(1400, 200, 1600, 200, 200))
lines.Insert(CreateLine(300, 800, 400, 800, 100))
endfunction
function CreateRay(x1#, y1#, x2#, y2#)
local r as Ray
r.SX = x1#
r.SY = y1#
r.EX = x2#
r.EY = y2#
CalculateRayAngle(r)
endfunction r
function CreateLine(x1#, y1#, x2#, y2#, height#)
local l as Line
l.Height = height#
l.X1 = x1#
l.Y1 = y1#
l.X2 = x2#
l.Y2 = y2#
ll# = Sqrt(LengthSq(x2# - x1#, y2# - y1#))
s = CreateSprite(pixelImage)
SetSpriteSize(s, ll#, height#)
SetSpriteShape(s, 2)
SetSpritePositionByOffset(s, (x1# + x2#) / 2.0, (y1# + y2#) / 2.0)
SetSpriteAngle(s, Atan2(y2# - y1#, x2# - x1#))
SetSpriteColor(s, 31, 31, 63, 255)
SetSpriteActive(s, 0)
SetSpriteDepth(s, 19)
l.Sprite = s
endfunction l
function LoadAssets()
pixelImage = LoadImage("pixel.png")
endfunction
type Vector2
Y as float
X as float
endtype
type Line
Sprite as integer
Height as float
X1 as float
Y1 as float
X2 as float
Y2 as float
endtype
type Ray
Angle as float
Intensity as integer
SX as float
SY as float
EX as float
EY as float
endtype