423 lines
14 KiB
Lua
423 lines
14 KiB
Lua
local shadows = {
|
|
map_params = {
|
|
decay_minimum_light = 3,
|
|
decay_light_threshold = 9,
|
|
decay_factor_bright = 0.8,
|
|
decay_factor_dark = 0.85,
|
|
follow_sun = false,
|
|
blocksize = 16,
|
|
},
|
|
max_distance = 7,
|
|
time_budget = 500,
|
|
log_counters = false,
|
|
hfov = 1.1,
|
|
vfov = 1.4,
|
|
vector = vector.new(-1, -2, 1),
|
|
transparency = {},
|
|
buffers = { light = {}, content = {} },
|
|
players = {},
|
|
counters = {},
|
|
}
|
|
|
|
local storage = minetest.get_mod_storage()
|
|
|
|
-- initialize
|
|
function shadows:initialize()
|
|
-- read from minetest.conf using initial settings as defaults
|
|
self.map_params.decay_minimum_light = tonumber(minetest.settings:get("shadows.decay_minimum_light")) or self.map_params.decay_minimum_light
|
|
self.map_params.decay_light_threshold = tonumber(minetest.settings:get("shadows.decay_light_threshold")) or self.map_params.decay_light_threshold
|
|
self.map_params.decay_factor_bright = tonumber(minetest.settings:get("shadows.decay_factor_bright")) or self.map_params.decay_factor_bright
|
|
self.map_params.decay_factor_dark = tonumber(minetest.settings:get("shadows.decay_factor_dark")) or self.map_params.decay_factor_dark
|
|
self.map_params.follow_sun = minetest.settings:get("shadows.follow_sun") == nil and self.map_params.follow_sun or minetest.settings:get("shadows.follow_sun") == "true"
|
|
|
|
self.max_distance = tonumber(minetest.settings:get("shadows.max_distance")) or self.max_distance
|
|
self.time_budget = tonumber(minetest.settings:get("shadows.time_budget")) or self.time_budget
|
|
self.log_counters = minetest.settings:get("shadows.log_counters") == nil and self.log_counters or minetest.settings:get("shadows.log_counters") == "true"
|
|
|
|
-- detect changed settings and set the generation
|
|
self.params_dirty = false
|
|
|
|
for key,value in pairs(self.map_params) do
|
|
if tostring(value) ~= storage:get(key) then
|
|
storage:set_string(key, tostring(value))
|
|
self.params_dirty = true
|
|
end
|
|
end
|
|
|
|
if storage:get("generation") ~= nil and not self.params_dirty then
|
|
self.generation = tonumber(storage:get("generation"))
|
|
end
|
|
end
|
|
|
|
shadows:initialize()
|
|
|
|
function shadows:load_definitions()
|
|
for name,node in pairs(minetest.registered_nodes) do
|
|
local id = minetest.get_content_id(name)
|
|
if node._transparency then
|
|
self.transparency[id] = node._transparency
|
|
elseif id == minetest.CONTENT_IGNORE or node.sunlight_propagates then
|
|
self.transparency[id] = 1
|
|
elseif node.drawtype == "airlike" or node.drawtype == "torchlike" or node.drawtype == "firelike" or node.drawtype == "plantlike" then
|
|
self.transparency[id] = 1
|
|
elseif node.drawtype == "glasslike" or node.drawtype == "glasslike_framed" or node.drawtype == "glasslike_framed_optional" then
|
|
self.transparency[id] = 0.95
|
|
elseif node.drawtype == "liquid" or node.drawtype == "flowingliquid" then
|
|
self.transparency[id] = 0.9
|
|
elseif node.drawtype == "allfaces" or node.drawtype == "allfaces_optional" then
|
|
self.transparency[id] = 0.8
|
|
elseif node.drawtype == "normal" then
|
|
self.transparency[id] = 0 -- solid
|
|
else
|
|
self.transparency[id] = 0.01 -- not fully solid but casts shadow
|
|
end
|
|
end
|
|
end
|
|
|
|
local function decay_light(light, min, th, fbright, fdark)
|
|
return math.max(min, light >= th and light * fbright or light * fdark)
|
|
end
|
|
|
|
|
|
function shadows:update_shadows(min, max)
|
|
local vm = minetest.get_voxel_manip(vector.add(min, vector.new(-1,-1,-1)), vector.add(max, vector.new(1,1,1)))
|
|
local minedge, maxedge = vm:get_emerged_area()
|
|
local va = VoxelArea:new{MinEdge = minedge, MaxEdge = maxedge}
|
|
|
|
local data = vm:get_data(self.buffers.content)
|
|
local maplight = vm:get_light_data(self.buffers.light) -- light from and to the map
|
|
local light = {} -- real daylight spread
|
|
|
|
local origin = va:indexp(min)
|
|
|
|
for i in va:iterp(vector.add(min, vector.new(-1,-1,-1)), vector.add(max, vector.new(1,1,1))) do
|
|
light[i] = maplight[i] % 16 -- copy daylight
|
|
end
|
|
|
|
local minlight = 99 -- large inital value
|
|
local maxlight = 0
|
|
local i,delta,source,transparency,ilight
|
|
|
|
-- shadows
|
|
for y = max.y-min.y+1,-1,-1 do
|
|
for z = -1,max.z-min.z+1 do
|
|
for x = -1,max.x-min.x+1 do
|
|
i = origin + x + y*va.ystride + z*va.zstride
|
|
if y >= 0 and y <= max.y - min.y and x >= 0 and x <= max.x - min.x and z >= 0 and z <= max.z - min.z then
|
|
delta = ((min.y + y)%(-self.vector.y) == 0 and 1 or 0) -- 2 to 1
|
|
source = i + va.ystride - self.vector.x * delta - self.vector.z * delta*va.zstride
|
|
-- take the smallest transparency of self, +x and +z. this is to handle edges of walls, houses and caves
|
|
transparency = math.min(self.transparency[ data[i] ], math.min(self.transparency[ data[i - self.vector.x * delta] ], self.transparency[ data[i - self.vector.z * delta * va.zstride] ]))
|
|
|
|
if light[source] > minetest.LIGHT_MAX then
|
|
ilight = light[source] * transparency -- project ray
|
|
else
|
|
ilight = 0
|
|
end
|
|
else
|
|
ilight = light[i]
|
|
end
|
|
|
|
|
|
if ilight < minlight then
|
|
minlight = ilight
|
|
end
|
|
if ilight > maxlight then
|
|
maxlight = ilight
|
|
end
|
|
|
|
light[i] = ilight
|
|
end
|
|
end
|
|
end
|
|
|
|
-- propagation / blur
|
|
if minlight ~= maxlight then
|
|
self:inc_counter("blur")
|
|
|
|
local points = {}
|
|
local strides = { 1, va.ystride, va.zstride }
|
|
for y = 0,max.y-min.y do
|
|
for z = 0,max.z-min.z do
|
|
for x = 0,max.x-min.x do
|
|
-- try current point and it's central reflection
|
|
points[1] = origin + x + y*va.ystride + z*va.zstride
|
|
points[2] = origin + max.x - min.x - x + (max.y - min.y - y)*va.ystride + (max.z - min.z - z)*va.zstride
|
|
|
|
for t = 1,2 do
|
|
i = points[t]
|
|
|
|
-- only propagate light through non-solid nodes
|
|
if self.transparency[ data[i] ] ~= 0 then
|
|
for d = -1,1,2 do
|
|
for stride = 1,#strides do
|
|
ilight = decay_light((light[i + d*strides[stride]] or 0),
|
|
self.map_params.decay_minimum_light,
|
|
self.map_params.decay_light_threshold,
|
|
self.map_params.decay_factor_bright,
|
|
self.map_params.decay_factor_dark)
|
|
if ilight > light[i] then
|
|
light[i] = ilight
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- write back to map
|
|
-- mark edges as dirty for chained processing
|
|
-- every edge has coordinates (x,y,z) and coords can take values 0 (coord == 0), 1 (coord > 0 and < max), 2 (coord == max)
|
|
-- index in the edges = x + 3*y + 9*z
|
|
local edges,ei = {}, 0
|
|
local dirty = false
|
|
for y = 0,max.y-min.y do
|
|
for z = 0,max.z-min.z do
|
|
for x = 0,max.x-min.x do
|
|
i = origin + x + y*va.ystride + z*va.zstride
|
|
ilight = math.max(light[i], math.floor(maplight[i] / 16)) -- copy nightlight / artificial light
|
|
ilight = math.floor(maplight[i] / 16) * 16 + math.floor(ilight)
|
|
if math.abs(maplight[i] - ilight) > 0.1 then
|
|
maplight[i] = ilight
|
|
|
|
-- calculate index of the edge
|
|
ei = (x > 0 and 1 or 0) + (x == max.x-min.x and 1 or 0) +
|
|
3 * ((y > 0 and 1 or 0) + (y == max.y-min.y and 1 or 0)) +
|
|
9 * ((z > 0 and 1 or 0) + (z == max.z-min.z and 1 or 0))
|
|
|
|
edges[ei] = true
|
|
dirty = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
self:inc_counter("calc")
|
|
|
|
if dirty then
|
|
vm:set_light_data(maplight)
|
|
vm:write_to_map(false)
|
|
self:inc_counter("update")
|
|
end
|
|
return dirty and edges or false
|
|
end
|
|
|
|
if shadows.log_counters then
|
|
function shadows:inc_counter(name)
|
|
self.counters[name] = (self.counters[name] or 0) + 1
|
|
end
|
|
|
|
function shadows:dump_counters()
|
|
if not self.log_counters then
|
|
return
|
|
end
|
|
|
|
local queue_length = 0
|
|
local queue
|
|
for _,state in pairs(self.players) do
|
|
queue = state.queue or {}
|
|
queue_length = queue_length + #queue - (queue.count or 0)
|
|
end
|
|
local s = "queue_length="..queue_length
|
|
local counters = self.counters
|
|
self.counters = {}
|
|
for _,name in ipairs({"ignore", "queue", "dequeue", "calc", "blur", "update", "skip", "reset", "requeue" }) do
|
|
s = s.." "..name.."="..(counters[name] or 0)
|
|
counters[name] = nil
|
|
end
|
|
for name,value in pairs(counters) do
|
|
s = s.." "..name.."="..value
|
|
end
|
|
s = s.." generation="..self.generation
|
|
s = s.." params_dirty="..tostring(self.params_dirty)
|
|
s = s.." params="..dump(self.map_params,"")
|
|
minetest.chat_send_all(s)
|
|
end
|
|
else
|
|
function shadows:inc_counter(name) end
|
|
function shadows:dump_counters() end
|
|
end
|
|
|
|
local function to_block_pos(pos, bs)
|
|
return vector.new(math.floor(pos.x / bs), math.floor(pos.y / bs), math.floor(pos.z / bs))
|
|
end
|
|
|
|
local function to_node_pos(pos, bs)
|
|
return vector.new(pos.x * bs, pos.y * bs, pos.z * bs)
|
|
end
|
|
|
|
function shadows:watch_players()
|
|
-- ensure generation is set
|
|
if self.generation == nil then
|
|
self.generation = 48*(minetest.get_day_count() + minetest.get_timeofday())
|
|
minetest.chat_send_all("setting new generation to "..self.generation)
|
|
storage:set_string("generation", tostring(self.generation))
|
|
end
|
|
|
|
-- scan players and update the queues
|
|
for name,previous in pairs(self.players) do
|
|
player = minetest.get_player_by_name(name)
|
|
local look_direction = vector.normalize(player:get_look_dir())
|
|
player_block = to_block_pos(player:get_pos(), self.map_params.blocksize)
|
|
self.players[name] = { block = player_block, direction = look_direction, generation = shadows.generation, queue = {} }
|
|
|
|
local side = vector.normalize(vector.cross(look_direction, vector.new(0, 1, 0)))
|
|
local up = vector.normalize(vector.cross(look_direction, side))
|
|
local block,node_pos
|
|
|
|
local blocks_queued = 0
|
|
local n = 4
|
|
local blocks_seen = {}
|
|
for d = 0,n*self.max_distance do
|
|
for u = 0,math.floor(self.hfov*d) do
|
|
for v = 0,math.floor(self.vfov*d) do
|
|
for su=-1,1,2 do
|
|
for sv=-1,1,2 do
|
|
if blocks_queued >= 100 then
|
|
break
|
|
end
|
|
|
|
block = vector.add(player_block, vector.multiply(look_direction, d/n))
|
|
block = vector.add(block, vector.multiply(side, su*u/n))
|
|
block = vector.add(block, vector.multiply(up, sv*v/n))
|
|
block = vector.floor(block)
|
|
|
|
if blocks_seen[minetest.hash_node_position(block)] then
|
|
break
|
|
end
|
|
blocks_seen[minetest.hash_node_position(block)] = true
|
|
|
|
node_pos = to_node_pos(block, self.map_params.blocksize)
|
|
|
|
|
|
if minetest.get_node_or_nil(node_pos) ~= nil and minetest.get_meta(node_pos):get_int("shadows:gen") < self.generation then
|
|
table.insert(self.players[name].queue, block)
|
|
blocks_queued = blocks_queued + 1
|
|
self:inc_counter("queue")
|
|
else
|
|
self:inc_counter("ignore")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function shadows:mark_block_dirty(block)
|
|
local chain_block_node = to_node_pos(block, self.map_params.blocksize)
|
|
if minetest.get_node_or_nil(chain_block_node) ~= nil then
|
|
local meta = minetest.get_meta(chain_block_node)
|
|
if meta:get_int("shadows:gen") ~= 0 then
|
|
minetest.get_meta(chain_block_node):set_int("shadows:gen", 0)
|
|
shadows:inc_counter("reset")
|
|
end
|
|
end
|
|
end
|
|
|
|
function shadows:update_blocks()
|
|
local queues = {}
|
|
for _, state in pairs(self.players) do
|
|
if state.queue ~= nil and #state.queue > 0 then
|
|
table.insert(queues, state.queue)
|
|
end
|
|
end
|
|
|
|
if #queues == 0 then
|
|
return
|
|
end
|
|
|
|
local start = os.clock()
|
|
local i = 1
|
|
local empty_queues = 0
|
|
-- loop for the configurd budget or until everything's processed
|
|
while os.clock() - start < self.time_budget / 1000 and empty_queues < #queues do
|
|
local block
|
|
if #queues[i] > (queues[i].count or 0) then
|
|
-- this is a fancy way to dequeue from the head of the queue
|
|
queues[i].count = (queues[i].count or 0) + 1
|
|
block = queues[i][queues[i].count]
|
|
queues[i][queues[i].count] = nil
|
|
self:inc_counter("dequeue")
|
|
elseif queues[i].count ~= nil then
|
|
empty_queues = empty_queues + 1
|
|
queues[i].count = nil
|
|
end
|
|
|
|
if block ~= nil then
|
|
local min = to_node_pos(block, self.map_params.blocksize)
|
|
if minetest.get_meta(min):get_int("shadows:gen") < self.generation then
|
|
local max = vector.add(min, vector.new(self.map_params.blocksize-1,self.map_params.blocksize-1,self.map_params.blocksize-1))
|
|
local edges = shadows:update_shadows(min, max)
|
|
minetest.get_meta(min):set_int("shadows:gen", shadows.generation)
|
|
if edges then
|
|
for y = 1,-1,-1 do
|
|
for x = -shadows.vector.x,shadows.vector.x,shadows.vector.x == 0 and 1 or shadows.vector.x do
|
|
for z = -shadows.vector.z,shadows.vector.z,shadows.vector.z == 0 and 1 or shadows.vector.z do
|
|
local ei = (x + 1) + 3 * (y + 1) + 9 * (z + 1)
|
|
if (x ~= 0 or y ~= 0 or z ~= 0) and edges[ei] then
|
|
self:mark_block_dirty(vector.add(block, vector.new(x, y, z)))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
else
|
|
self:inc_counter("skip")
|
|
end
|
|
end
|
|
i = i % #queues + 1
|
|
end
|
|
end
|
|
|
|
local m2pi = math.pi * 2
|
|
function shadows:update_vector()
|
|
if not self.map_params.follow_sun then
|
|
return
|
|
end
|
|
local time = math.floor(48 * minetest.get_timeofday()) / 48
|
|
local adj_time = math.min(19/24, 6/24 + math.max(0, time - 7/24) * 6/5)
|
|
local new_vector = vector.new(math.floor(0.5-math.sin(m2pi * adj_time)), -1 - math.abs(math.floor(2 * math.cos(m2pi * adj_time))), math.floor(math.cos(m2pi * adj_time)))
|
|
if not vector.equals(self.vector, new_vector) then
|
|
self.vector = new_vector
|
|
self.generation = math.floor(48 * (minetest.get_day_count() + time))
|
|
end
|
|
end
|
|
|
|
function step()
|
|
local time = minetest.get_timeofday()
|
|
if time >= 4/24 and time < 20/24 then
|
|
shadows:watch_players()
|
|
shadows:update_vector()
|
|
shadows:update_blocks()
|
|
shadows:dump_counters()
|
|
end
|
|
minetest.after(1, step)
|
|
end
|
|
|
|
minetest.after(1, step)
|
|
|
|
-- register node transparency
|
|
minetest.register_on_mods_loaded(function() shadows:load_definitions() end)
|
|
|
|
minetest.register_on_joinplayer(function(player)
|
|
shadows.players[player:get_player_name()] = to_block_pos(player:get_pos(), shadows.map_params.blocksize)
|
|
end)
|
|
|
|
minetest.register_on_leaveplayer(function(player)
|
|
shadows.players[player:get_player_name()] = nil
|
|
end)
|
|
|
|
minetest.register_on_dignode(function(pos)
|
|
local block = to_block_pos(pos, shadows.map_params.blocksize)
|
|
shadows:mark_block_dirty(block)
|
|
end)
|
|
|
|
minetest.register_on_placenode(function(pos)
|
|
local block = to_block_pos(pos, shadows.map_params.blocksize)
|
|
shadows:mark_block_dirty(block)
|
|
end)
|