├── .luacheckrc ├── README.md ├── init.lua ├── libs ├── humpclass.lua ├── licenses │ ├── OneLoneCoder_PGE_ShadowCasting2D_license.txt │ └── shash_LICENSE.txt ├── shash.lua └── vector-light.lua ├── media └── default_light.png └── screenshot.jpg /.luacheckrc: -------------------------------------------------------------------------------- 1 | globals = {"love"} 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lighter 2 | 3 | A dynamic light / shadow casting library for Löve. 4 | 5 | ## Installation 6 | Clone the repo into your libs directory and require with 7 | 8 | require 'path.to.lighter' 9 | 10 | ## Usage 11 | local Lighter = require 'path.to.lighter' 12 | 13 | local lighter = Lighter() 14 | 15 | local wall = { 16 | 100, 100, 17 | 300, 100, 18 | 300, 300, 19 | 100, 300 20 | } 21 | 22 | lighter:addPolygon(wall) 23 | 24 | local lightX, lightY = 500, 500 25 | 26 | -- addLight signature: (x,y,radius,r,g,b,a) 27 | local light = lighter:addLight(lightX, lightY, 500, 1, 0.5, 0.5) 28 | 29 | function love.update(dt) 30 | lightX, lightY = love.mouse.getPosition() 31 | lighter:updateLight(light, lightX, lightY) 32 | end 33 | 34 | function love.draw() 35 | love.graphics.polygon('fill', wall) 36 | lighter:drawLights() 37 | end 38 | 39 | -- Clean up 40 | -- lighter:removeLight(light) 41 | -- lighter:removePolygon(wall) 42 | 43 | ### How to use in your game context with a darker global illumination: 44 | #### (remember you can use any combination of additive & multiplicative blending to achieve a result you like) 45 | -- Call after your light positions have been updated 46 | function preDrawLights() 47 | love.graphics.setCanvas({ lightCanvas, stencil = true}) 48 | love.graphics.clear(0.4, 0.4, 0.4) -- Global illumination level 49 | self.lighter:drawLights() 50 | love.graphics.setCanvas() 51 | end 52 | 53 | -- Call after you have drawn your scene (but before UI) 54 | function drawLights() 55 | love.graphics.setBlendMode("multiply", "premultiplied") 56 | love.graphics.draw(lightCanvas) 57 | love.graphics.setBlendMode("alpha") 58 | end 59 | 60 | ### Function signatures of the public API: 61 | ``` 62 | init = function(self, options) 63 | addLight = function(self, x, y, radius, r, g, b, a, gradientImage) 64 | updateLight = function(self, light, x, y, radius, r, g, b, a, gradientImage) 65 | removeLight = function(self, light) 66 | addPolygon = function(self, polygon) 67 | removePolygon = function(self, polygon) 68 | drawVisibilityPolygon = function(self, light) 69 | drawLights = function(self) 70 | ``` 71 | 72 | ## Credits: 73 | * Helped me understand the premise and their code provided me with tons of helpful tips for getting this implemented: [https://github.com/OneLoneCoder](https://github.com/OneLoneCoder) 74 | * The actual page I ended up following to really get the idea: [https://ncase.me/sight-and-light/](https://ncase.me/sight-and-light/) 75 | 76 | 77 | ### Screenshot of the library in use in a game 78 | ![Screenshot of the library in use in a game](./screenshot.jpg) 79 | 80 | 81 | ## How it works under the hood 82 | For each light we cast rays towards wall polygon corners (aka map features). Checks intersections. 83 | Builds a visibility polygon and uses it as the stencil for drawing the light. 84 | 85 | As an optimization it uses a spatial map and calculates polygon bounding boxes on the fly so that only polygons within light radius get included in the visibility polygon calculations. 86 | 87 | 88 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | -- MIT License 2 | -- 3 | -- Copyright (c) 2020 Jesse Viikari 4 | -- 5 | -- Permission is hereby granted, free of charge, to any person obtaining a copy 6 | -- of this software and associated documentation files (the "Software"), to deal 7 | -- in the Software without restriction, including without limitation the rights 8 | -- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | -- copies of the Software, and to permit persons to whom the Software is 10 | -- furnished to do so, subject to the following conditions: 11 | -- 12 | -- The above copyright notice and this permission notice shall be included in all 13 | -- copies or substantial portions of the Software. 14 | -- 15 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | -- SOFTWARE. 22 | 23 | local PATH = (...):gsub('%.init$', '') 24 | 25 | local vector = require(PATH .. '.libs.vector-light') 26 | local Class = require(PATH .. '.libs.humpclass') 27 | local shash = require(PATH .. '.libs.shash') 28 | 29 | local MEDIAPATH = PATH:gsub("%.", "/") 30 | 31 | local defaultGradientImage = love.graphics.newImage(MEDIAPATH .. '/media/default_light.png') 32 | 33 | -- This is a local array that will be used in calculateVisibilityPolygon 34 | local _angles = {} 35 | 36 | -- PRIVATE FUNCTIONS START 37 | 38 | local function angleSortFunc(a, b) 39 | return a.angle < b.angle 40 | end 41 | 42 | local function getLineIntersectionPoint(Ax1, Ay1, Ax2, Ay2, Bx1, By1, Bx2, By2) 43 | local intersectX, intersectY 44 | local s1x, s1y = vector.sub(Ax2, Ay2, Ax1, Ay1) 45 | local s2x, s2y = vector.sub(Bx2, By2, Bx1, By1) 46 | 47 | local b = (-s2x * s1y + s1x * s2y) 48 | local s = ( -s1y * (Ax1 - Bx1) + s1x * (Ay1 - By1)) / b 49 | local t = ( s2x * (Ay1 - By1) - s2y * (Ax1 - Bx1)) / b 50 | 51 | -- There was an intersection 52 | if s >= 0 and s <= 1 and t >= 0 and t <= 1 then 53 | intersectX = Ax1 + (t * s1x) 54 | intersectY = Ay1 + (t * s1y) 55 | end 56 | 57 | return intersectX, intersectY 58 | end 59 | 60 | -- Get bounding box of all polygons passed in. This is used by the algorithm 61 | -- to have a non-intersecting polygon that surrounds all geometry, which is 62 | -- used to ensure light rays always have something to intersect 63 | local function getMinMaxFromPolygons(originX, originY, radius, polygons) 64 | local halfRadius = radius / 2 65 | local minX, minY, maxX, maxY = math.huge, math.huge, -math.huge, -math.huge 66 | 67 | for _, polygon in ipairs(polygons) do 68 | for i=1,#polygon,2 do 69 | local x = polygon[i] 70 | local y = polygon[i+1] 71 | if x > maxX then maxX = x end 72 | if y > maxY then maxY = y end 73 | if x < minX then minX = x end 74 | if y < minY then minY = y end 75 | end 76 | end 77 | 78 | minX = math.min(originX - halfRadius, minX) 79 | minY = math.min(originY - halfRadius, minY) 80 | maxX = math.max(originX + halfRadius, maxX) 81 | maxY = math.max(originY + halfRadius, maxY) 82 | 83 | return minX, minY, maxX, maxY 84 | end 85 | 86 | local function calculateVisibilityPolygon(originX, originY, radius, polygons) 87 | local visibilityPolygon = {} 88 | 89 | local minX, minY, maxX, maxY = getMinMaxFromPolygons(originX, originY, radius, polygons) 90 | 91 | -- Required for the lines to always have something to intersect 92 | local surroundPolygon = { 93 | minX, minY, 94 | maxX, minY, 95 | maxX, maxY, 96 | minX, maxY, 97 | minX, minY 98 | } 99 | 100 | local allPolygons = {} 101 | 102 | for _, polygon in ipairs(polygons) do 103 | local copiedPolygon = {} 104 | for i, coord in ipairs(polygon) do copiedPolygon[i] = coord end 105 | 106 | -- Self close if start and end points don't match 107 | if copiedPolygon[1] ~= copiedPolygon[#copiedPolygon-1] or copiedPolygon[2] ~= copiedPolygon[#copiedPolygon] then 108 | table.insert(copiedPolygon, copiedPolygon[1]) 109 | table.insert(copiedPolygon, copiedPolygon[2]) 110 | end 111 | 112 | table.insert(allPolygons, copiedPolygon) 113 | end 114 | 115 | table.insert(allPolygons, surroundPolygon) 116 | 117 | local len1 = vector.len(vector.sub(minX, minY, originX, originY)) 118 | local len2 = vector.len(vector.sub(maxX, maxY, originX, originY)) 119 | 120 | -- Change actual raycasting radius here to ensure it reaches the bounding polygon limits 121 | -- TODO: The 100 is a magic number to ensure to ray is long enough. Figure out a good way 122 | -- to calculate this without it. 123 | radius = math.max(len1, len2) + 100 124 | 125 | for _, polygon in ipairs(allPolygons) do 126 | -- Go through all points (x,y) 127 | for i=1,#polygon-2,2 do 128 | local x = polygon[i] 129 | local y = polygon[i+1] 130 | local a1, a2 = vector.sub(x, y, originX, originY) 131 | 132 | local angleA = math.atan2(a2, a1) 133 | 134 | _angles[1] = angleA 135 | _angles[2] = angleA + 0.0001 136 | _angles[3] = angleA - 0.0001 137 | 138 | -- Go through all 3 angles as rays cast from originX, originY 139 | for j=1,3 do 140 | local angle = _angles[j] 141 | 142 | -- The ray we cast is originX, originY, rayX2, rayY2 143 | -- rayX2, rayY2 are origin + angle*radius 144 | local dirX, dirY = math.cos(angle), math.sin(angle) 145 | local rayX2, rayY2 = vector.add(originX, originY, vector.mul(radius, vector.normalize(dirX, dirY))) 146 | 147 | -- Next up we find the shortest intersection point for each ray. 148 | -- We store the shortest so far in the min* variables defined below 149 | local minX, minY, minAngle 150 | local minLength = math.huge 151 | local found = false 152 | 153 | -- Go through all the points as line segments (x1,y1,x2,y2). 154 | -- See where the ray intersects (if it does at all) with the line 155 | -- segment. If it does, check if it's the shortest length from 156 | -- origin so far. If it is, then that's the point we want to store 157 | -- in visibilityPolygon 158 | for _, polygon2 in ipairs(allPolygons) do 159 | for u=1,#polygon2-2,2 do 160 | local segmentX1 = polygon2[u] 161 | local segmentY1 = polygon2[u+1] 162 | local segmentX2 = polygon2[u+2] 163 | local segmentY2 = polygon2[u+3] 164 | 165 | -- Now check for actual intersection between 166 | -- the ray cast from the origin point and the line segment. 167 | local intersectX, intersectY = getLineIntersectionPoint( 168 | originX, originY, rayX2, rayY2, 169 | segmentX1, segmentY1, segmentX2, segmentY2 170 | ) 171 | 172 | if intersectX and intersectY then 173 | local length = vector.len2(vector.sub(intersectX, intersectY, originX, originY)) 174 | if length < minLength then 175 | minX, minY, minAngle = intersectX, intersectY, angle 176 | minLength = length 177 | found = true 178 | end 179 | end 180 | end 181 | end 182 | 183 | if found then 184 | table.insert(visibilityPolygon, { 185 | x = minX, y = minY, angle = minAngle 186 | }) 187 | end 188 | end 189 | end 190 | end 191 | 192 | table.sort(visibilityPolygon, angleSortFunc) 193 | return visibilityPolygon 194 | end 195 | 196 | local function getPolygonBoundingBox(polygon) 197 | local minX, minY = math.huge, math.huge 198 | local maxX, maxY = -math.huge, -math.huge 199 | 200 | for i=1,#polygon-1,2 do 201 | local x = polygon[i] 202 | local y = polygon[i+1] 203 | if x > maxX then maxX = x end 204 | if x < minX then minX = x end 205 | if y > maxY then maxY = y end 206 | if y < minY then minY = y end 207 | end 208 | 209 | return minX, minY, maxX - minX, maxY - minY 210 | end 211 | 212 | local function getLightBoundingBox(light) 213 | local halfRadius = light.radius/2 214 | return light.x - halfRadius, light.y - halfRadius, light.radius, light.radius 215 | end 216 | 217 | local function updateLight(self, light) 218 | self.lightHash:update(light, getLightBoundingBox(light)) 219 | local polygons = {} 220 | 221 | local x,y,w,h = getLightBoundingBox(light) 222 | -- Get polygons only within the reach of the light 223 | self.polygonHash:each(x,y,w,h, function(polygon) 224 | table.insert(polygons, polygon) 225 | end) 226 | 227 | local visibilityPolygon = calculateVisibilityPolygon(light.x, light.y, light.radius, polygons) 228 | self.visibilityPolygons[light] = visibilityPolygon 229 | self.stencilFunctions[light] = function() 230 | self:drawVisibilityPolygon(light) 231 | 232 | if self.litPolygons then 233 | love.graphics.setColor(0,0,0,1) 234 | for _, polygon in ipairs(polygons) do 235 | love.graphics.polygon('fill', polygon) 236 | end 237 | love.graphics.setColor(1,1,1,1) 238 | end 239 | end 240 | end 241 | 242 | -- PRIVATE FUNCTIONS END 243 | 244 | local Lighter = Class{ 245 | init = function(self, options) 246 | self.polygonHash = shash.new() 247 | self.lightHash = shash.new() 248 | self.lights = {} 249 | self.polygons = {} 250 | self.visibilityPolygons = {} 251 | self.stencilFunctions = {} 252 | 253 | if options then 254 | self.litPolygons = options.litPolygons 255 | end 256 | end, 257 | addLight = function(self, x, y, radius, r, g, b, a, gradientImage) 258 | local light = { 259 | x = x, y = y, radius = radius, 260 | r = r or 1, g = g or 1, b = b or 1, a = a or 1, 261 | gradientImage = gradientImage or defaultGradientImage 262 | } 263 | table.insert(self.lights, light) 264 | self.lightHash:add(light, getLightBoundingBox(light)) 265 | updateLight(self, light) 266 | 267 | return light 268 | end, 269 | updateLight = function(self, light, x, y, radius, r, g, b, a, gradientImage) 270 | light.x = x or light.x 271 | light.y = y or light.y 272 | light.radius = radius or light.radius 273 | light.r = r or light.r 274 | light.g = g or light.g 275 | light.b = b or light.b 276 | light.a = a or light.a 277 | light.gradientImage = gradientImage or light.gradientImage 278 | 279 | updateLight(self, light) 280 | end, 281 | removeLight = function(self, light) 282 | for i, existingLight in ipairs(self.lights) do 283 | if existingLight == light then 284 | table.remove(self.lights, i) 285 | self.visibilityPolygons[light] = nil 286 | self.lightHash:remove(light) 287 | return 288 | end 289 | end 290 | end, 291 | addPolygon = function(self, polygon) 292 | local newPolygon = {} 293 | newPolygon.original = polygon 294 | 295 | for i, coordinate in ipairs(polygon) do 296 | newPolygon[i] = coordinate 297 | end 298 | 299 | local x, y, w, h = getPolygonBoundingBox(newPolygon) 300 | 301 | table.insert(self.polygons, newPolygon) 302 | self.polygonHash:add(newPolygon, x, y, w, h) 303 | 304 | self.lightHash:each(x, y, w, h, function(light) 305 | updateLight(self, light) 306 | end) 307 | end, 308 | removePolygon = function(self, polygon) 309 | local x, y, w, h = getPolygonBoundingBox(polygon) 310 | for i, existingPolygon in ipairs(self.polygons) do 311 | if existingPolygon.original == polygon then 312 | self.polygonHash:remove(existingPolygon) 313 | table.remove(self.polygons, i) 314 | goto continue 315 | end 316 | end 317 | 318 | ::continue:: 319 | 320 | self.lightHash:each(x, y, w, h, function(light) 321 | updateLight(self, light) 322 | end) 323 | end, 324 | drawVisibilityPolygon = function(self, light) 325 | local x, y, _ = light.x, light.y, light.radius 326 | local visibilityPolygon = self.visibilityPolygons[light] 327 | if #visibilityPolygon == 0 then return end 328 | 329 | love.graphics.setColor(1,1,1) 330 | 331 | for i=1,#visibilityPolygon-1 do 332 | local point1 = visibilityPolygon[i] 333 | local point2 = visibilityPolygon[i+1] 334 | love.graphics.polygon('fill', { 335 | x, y, 336 | point1.x, point1.y, 337 | point2.x, point2.y 338 | }) 339 | end 340 | 341 | local firstPoint = visibilityPolygon[1] 342 | local lastPoint = visibilityPolygon[#visibilityPolygon] 343 | love.graphics.polygon('fill', { 344 | x, y, 345 | lastPoint.x, lastPoint.y, 346 | firstPoint.x, firstPoint.y 347 | }) 348 | end, 349 | drawLights = function(self) 350 | for _, light in ipairs(self.lights) do 351 | love.graphics.stencil(self.stencilFunctions[light], "replace", 1) 352 | love.graphics.setStencilTest("greater", 0) 353 | local w, h = light.gradientImage:getDimensions() 354 | local scale = light.radius / w 355 | love.graphics.setColor(light.r, light.g, light.b, light.a) 356 | love.graphics.draw(light.gradientImage, light.x, light.y, 0, scale, scale, w/2, h/2) 357 | love.graphics.setColor(1,1,1,1) 358 | love.graphics.setStencilTest() 359 | end 360 | end 361 | } 362 | 363 | return Lighter 364 | -------------------------------------------------------------------------------- /libs/humpclass.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2010-2013 Matthias Richter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | Except as contained in this notice, the name(s) of the above copyright holders 15 | shall not be used in advertising or otherwise to promote the sale, use or 16 | other dealings in this Software without prior written authorization. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | ]]-- 26 | 27 | local function include_helper(to, from, seen) 28 | if from == nil then 29 | return to 30 | elseif type(from) ~= 'table' then 31 | return from 32 | elseif seen[from] then 33 | return seen[from] 34 | end 35 | 36 | seen[from] = to 37 | for k,v in pairs(from) do 38 | k = include_helper({}, k, seen) -- keys might also be tables 39 | if to[k] == nil then 40 | to[k] = include_helper({}, v, seen) 41 | end 42 | end 43 | return to 44 | end 45 | 46 | -- deeply copies `other' into `class'. keys in `other' that are already 47 | -- defined in `class' are omitted 48 | local function include(class, other) 49 | return include_helper(class, other, {}) 50 | end 51 | 52 | -- returns a deep copy of `other' 53 | local function clone(other) 54 | return setmetatable(include({}, other), getmetatable(other)) 55 | end 56 | 57 | local function new(class) 58 | -- mixins 59 | class = class or {} -- class can be nil 60 | local inc = class.__includes or {} 61 | if getmetatable(inc) then inc = {inc} end 62 | 63 | for _, other in ipairs(inc) do 64 | if type(other) == "string" then 65 | other = _G[other] 66 | end 67 | include(class, other) 68 | end 69 | 70 | -- class implementation 71 | class.__index = class 72 | class.init = class.init or class[1] or function() end 73 | class.include = class.include or include 74 | class.clone = class.clone or clone 75 | 76 | -- constructor call 77 | return setmetatable(class, {__call = function(c, ...) 78 | local o = setmetatable({}, c) 79 | o:init(...) 80 | return o 81 | end}) 82 | end 83 | 84 | -- interface for cross class-system compatibility (see https://github.com/bartbes/Class-Commons). 85 | if class_commons ~= false and not common then 86 | common = {} 87 | function common.class(name, prototype, parent) 88 | return new{__includes = {prototype, parent}} 89 | end 90 | function common.instance(class, ...) 91 | return class(...) 92 | end 93 | end 94 | 95 | 96 | -- the module 97 | return setmetatable({new = new, include = include, clone = clone}, 98 | {__call = function(_,...) return new(...) end}) 99 | -------------------------------------------------------------------------------- /libs/licenses/OneLoneCoder_PGE_ShadowCasting2D_license.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/speakk/lighter/93e059c8c890423fdc908156d3b2db3c4948745a/libs/licenses/OneLoneCoder_PGE_ShadowCasting2D_license.txt -------------------------------------------------------------------------------- /libs/licenses/shash_LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 rxi 2 | 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /libs/shash.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- shash.lua 3 | -- 4 | -- Copyright (c) 2017 rxi 5 | -- 6 | -- This library is free software; you can redistribute it and/or modify it 7 | -- under the terms of the MIT license. See LICENSE for details. 8 | -- 9 | 10 | local shash = { _version = "0.1.1" } 11 | shash.__index = shash 12 | 13 | 14 | function shash.new(cellsize) 15 | local self = setmetatable({}, shash) 16 | cellsize = cellsize or 64 17 | self.cellsize = cellsize 18 | self.tablepool = {} 19 | self.cells = {} 20 | self.entities = {} 21 | return self 22 | end 23 | 24 | 25 | local function coord_to_key(x, y) 26 | return x + y * 1e7 27 | end 28 | 29 | 30 | local function cell_position(cellsize, x, y) 31 | return math.floor(x / cellsize), math.floor(y / cellsize) 32 | end 33 | 34 | 35 | local function each_overlapping_cell(self, e, fn, ...) 36 | local cellsize = self.cellsize 37 | local sx, sy = cell_position(cellsize, e[1], e[2]) 38 | local ex, ey = cell_position(cellsize, e[3], e[4]) 39 | for y = sy, ey do 40 | for x = sx, ex do 41 | local idx = coord_to_key(x, y) 42 | fn(self, idx, ...) 43 | end 44 | end 45 | end 46 | 47 | 48 | local function add_entity_to_cell(self, idx, e) 49 | if not self.cells[idx] then 50 | self.cells[idx] = { e } 51 | else 52 | table.insert(self.cells[idx], e) 53 | end 54 | end 55 | 56 | 57 | local function remove_entity_from_cell(self, idx, e) 58 | local t = self.cells[idx] 59 | local n = #t 60 | -- Only one entity? Remove entity from cell and remove cell 61 | if n == 1 then 62 | self.cells[idx] = nil 63 | return 64 | end 65 | -- Find and swap-remove entity 66 | for i, v in ipairs(t) do 67 | if v == e then 68 | t[i] = t[n] 69 | t[n] = nil 70 | return 71 | end 72 | end 73 | end 74 | 75 | 76 | function shash:add(obj, x, y, w, h) 77 | -- Create entity. The table is used as an array as this offers a noticable 78 | -- performance increase on LuaJIT; the indices are as follows: 79 | -- [1] = left, [2] = top, [3] = right, [4] = bottom, [5] = object 80 | local e = { x, y, x + w, y + h, obj } 81 | -- Add to main entities table 82 | self.entities[obj] = e 83 | -- Add to cells 84 | each_overlapping_cell(self, e, add_entity_to_cell, e) 85 | end 86 | 87 | 88 | function shash:remove(obj) 89 | -- Get entity of obj 90 | local e = self.entities[obj] 91 | -- Remove from main entities table 92 | self.entities[obj] = nil 93 | -- Remove from cells 94 | each_overlapping_cell(self, e, remove_entity_from_cell, e) 95 | end 96 | 97 | 98 | function shash:update(obj, x, y, w, h) 99 | -- Get entity from obj 100 | local e = self.entities[obj] 101 | -- No width/height specified? Get width/height from existing bounding box 102 | w = w or e[3] - e[1] 103 | h = h or e[4] - e[2] 104 | -- Check the entity has actually changed cell-position, if it hasn't we don't 105 | -- need to touch the cells at all 106 | local cellsize = self.cellsize 107 | local ax1, ay1 = cell_position(cellsize, e[1], e[2]) 108 | local ax2, ay2 = cell_position(cellsize, e[3], e[4]) 109 | local bx1, by1 = cell_position(cellsize, x, y) 110 | local bx2, by2 = cell_position(cellsize, x + w, y + h) 111 | local dirty = ax1 ~= bx1 or ay1 ~= by1 or ax2 ~= bx2 or ay2 ~= by2 112 | -- Remove from old cells 113 | if dirty then 114 | each_overlapping_cell(self, e, remove_entity_from_cell, e) 115 | end 116 | -- Update entity 117 | e[1], e[2], e[3], e[4] = x, y, x + w, y + h 118 | -- Add to new cells 119 | if dirty then 120 | each_overlapping_cell(self, e, add_entity_to_cell, e) 121 | end 122 | end 123 | 124 | 125 | function shash:clear() 126 | -- Clear all cells and entities 127 | for k in pairs(self.cells) do 128 | self.cells[k] = nil 129 | end 130 | for k in pairs(self.entities) do 131 | self.entities[k] = nil 132 | end 133 | end 134 | 135 | 136 | local function overlaps(e1, e2) 137 | return e1[3] > e2[1] and e1[1] < e2[3] and e1[4] > e2[2] and e1[2] < e2[4] 138 | end 139 | 140 | 141 | local function each_overlapping_in_cell(self, idx, e, set, fn, ...) 142 | local t = self.cells[idx] 143 | if not t then 144 | return 145 | end 146 | for i, v in ipairs(t) do 147 | if e ~= v and overlaps(e, v) and not set[v] then 148 | fn(v[5], ...) 149 | set[v] = true 150 | end 151 | end 152 | end 153 | 154 | 155 | local function each_overlapping_entity(self, e, fn, ...) 156 | -- Init set for keeping track of which entities have already been handled 157 | local set = table.remove(self.tablepool) or {} 158 | -- Do overlap checks 159 | each_overlapping_cell(self, e, each_overlapping_in_cell, e, set, fn, ...) 160 | -- Clear set and return to pool 161 | for v in pairs(set) do 162 | set[v] = nil 163 | end 164 | table.insert(self.tablepool, set) 165 | end 166 | 167 | 168 | function shash:each(x, y, w, h, fn, ...) 169 | local e = self.entities[x] 170 | if e then 171 | -- Got object, use its entity 172 | each_overlapping_entity(self, e, y, w, h, fn, ...) 173 | else 174 | -- Got bounding box, make temporary entity 175 | each_overlapping_entity(self, { x, y, x + w, y + h }, fn, ...) 176 | end 177 | end 178 | 179 | 180 | function shash:info(opt, ...) 181 | if opt == "cells" or opt == "entities" then 182 | local n = 0 183 | for k in pairs(self[opt]) do 184 | n = n + 1 185 | end 186 | return n 187 | end 188 | if opt == "cell" then 189 | local t = self.cells[ coord_to_key(...) ] 190 | return t and #t or 0 191 | end 192 | error( string.format("invalid opt '%s'", opt) ) 193 | end 194 | 195 | 196 | return shash 197 | -------------------------------------------------------------------------------- /libs/vector-light.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Copyright (c) 2012-2013 Matthias Richter 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | Except as contained in this notice, the name(s) of the above copyright holders 15 | shall not be used in advertising or otherwise to promote the sale, use or 16 | other dealings in this Software without prior written authorization. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | ]]-- 26 | 27 | local sqrt, cos, sin, atan2 = math.sqrt, math.cos, math.sin, math.atan2 28 | 29 | local function str(x,y) 30 | return "("..tonumber(x)..","..tonumber(y)..")" 31 | end 32 | 33 | local function mul(s, x,y) 34 | return s*x, s*y 35 | end 36 | 37 | local function div(s, x,y) 38 | return x/s, y/s 39 | end 40 | 41 | local function add(x1,y1, x2,y2) 42 | return x1+x2, y1+y2 43 | end 44 | 45 | local function sub(x1,y1, x2,y2) 46 | return x1-x2, y1-y2 47 | end 48 | 49 | local function permul(x1,y1, x2,y2) 50 | return x1*x2, y1*y2 51 | end 52 | 53 | local function dot(x1,y1, x2,y2) 54 | return x1*x2 + y1*y2 55 | end 56 | 57 | local function det(x1,y1, x2,y2) 58 | return x1*y2 - y1*x2 59 | end 60 | 61 | local function eq(x1,y1, x2,y2) 62 | return x1 == x2 and y1 == y2 63 | end 64 | 65 | local function lt(x1,y1, x2,y2) 66 | return x1 < x2 or (x1 == x2 and y1 < y2) 67 | end 68 | 69 | local function le(x1,y1, x2,y2) 70 | return x1 <= x2 and y1 <= y2 71 | end 72 | 73 | local function len2(x,y) 74 | return x*x + y*y 75 | end 76 | 77 | local function len(x,y) 78 | return sqrt(x*x + y*y) 79 | end 80 | 81 | local function fromPolar(angle, radius) 82 | radius = radius or 1 83 | return cos(angle)*radius, sin(angle)*radius 84 | end 85 | 86 | local function randomDirection(len_min, len_max) 87 | len_min = len_min or 1 88 | len_max = len_max or len_min 89 | 90 | assert(len_max > 0, "len_max must be greater than zero") 91 | assert(len_max >= len_min, "len_max must be greater than or equal to len_min") 92 | 93 | return fromPolar(math.random()*2*math.pi, 94 | math.random() * (len_max-len_min) + len_min) 95 | end 96 | 97 | local function toPolar(x, y) 98 | return atan2(y,x), len(x,y) 99 | end 100 | 101 | local function dist2(x1,y1, x2,y2) 102 | return len2(x1-x2, y1-y2) 103 | end 104 | 105 | local function dist(x1,y1, x2,y2) 106 | return len(x1-x2, y1-y2) 107 | end 108 | 109 | local function normalize(x,y) 110 | local l = len(x,y) 111 | if l > 0 then 112 | return x/l, y/l 113 | end 114 | return x,y 115 | end 116 | 117 | local function rotate(phi, x,y) 118 | local c, s = cos(phi), sin(phi) 119 | return c*x - s*y, s*x + c*y 120 | end 121 | 122 | local function perpendicular(x,y) 123 | return -y, x 124 | end 125 | 126 | local function project(x,y, u,v) 127 | local s = (x*u + y*v) / (u*u + v*v) 128 | return s*u, s*v 129 | end 130 | 131 | local function mirror(x,y, u,v) 132 | local s = 2 * (x*u + y*v) / (u*u + v*v) 133 | return s*u - x, s*v - y 134 | end 135 | 136 | -- ref.: http://blog.signalsondisplay.com/?p=336 137 | local function trim(maxLen, x, y) 138 | local s = maxLen * maxLen / len2(x, y) 139 | s = s > 1 and 1 or math.sqrt(s) 140 | return x * s, y * s 141 | end 142 | 143 | local function angleTo(x,y, u,v) 144 | if u and v then 145 | return atan2(y, x) - atan2(v, u) 146 | end 147 | return atan2(y, x) 148 | end 149 | 150 | -- the module 151 | return { 152 | str = str, 153 | 154 | fromPolar = fromPolar, 155 | toPolar = toPolar, 156 | randomDirection = randomDirection, 157 | 158 | -- arithmetic 159 | mul = mul, 160 | div = div, 161 | idiv = idiv, 162 | add = add, 163 | sub = sub, 164 | permul = permul, 165 | dot = dot, 166 | det = det, 167 | cross = det, 168 | 169 | -- relation 170 | eq = eq, 171 | lt = lt, 172 | le = le, 173 | 174 | -- misc operations 175 | len2 = len2, 176 | len = len, 177 | dist2 = dist2, 178 | dist = dist, 179 | normalize = normalize, 180 | rotate = rotate, 181 | perpendicular = perpendicular, 182 | project = project, 183 | mirror = mirror, 184 | trim = trim, 185 | angleTo = angleTo, 186 | } 187 | -------------------------------------------------------------------------------- /media/default_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/speakk/lighter/93e059c8c890423fdc908156d3b2db3c4948745a/media/default_light.png -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/speakk/lighter/93e059c8c890423fdc908156d3b2db3c4948745a/screenshot.jpg --------------------------------------------------------------------------------