src/perlin-test.cc) | src/perlin-test.cc) | ||||
target_link_libraries(perlin-test libswan PNG::PNG ${libraries}) | target_link_libraries(perlin-test libswan PNG::PNG ${libraries}) | ||||
add_executable(lighting-test EXCLUDE_FROM_ALL | |||||
src/lighting-test.cc) | |||||
target_link_libraries(lighting-test libswan PNG::PNG ${libraries}) | |||||
set(assets | set(assets | ||||
assets/icon.png | assets/icon.png | ||||
assets/music/happy-1.wav) | assets/music/happy-1.wav) |
src/gfxutil.cc | src/gfxutil.cc | ||||
src/Item.cc | src/Item.cc | ||||
src/ItemStack.cc | src/ItemStack.cc | ||||
src/LightingThread.cc | |||||
src/Mod.cc | src/Mod.cc | ||||
src/OS.cc | src/OS.cc | ||||
src/Resource.cc | src/Resource.cc |
#pragma once | |||||
#include <thread> | |||||
#include <vector> | |||||
#include <unordered_map> | |||||
#include <mutex> | |||||
#include <condition_variable> | |||||
#include <utility> | |||||
#include <bitset> | |||||
#include "common.h" | |||||
namespace Swan { | |||||
struct NewLightChunk { | |||||
std::bitset<CHUNK_WIDTH * CHUNK_HEIGHT> blocks; | |||||
std::map<std::pair<int, int>, uint8_t> light_sources; | |||||
}; | |||||
struct LightChunk { | |||||
LightChunk() = default; | |||||
LightChunk(NewLightChunk &&ch): | |||||
blocks(std::move(ch.blocks)), light_sources(std::move(ch.light_sources)) {} | |||||
std::bitset<CHUNK_WIDTH * CHUNK_HEIGHT> blocks; | |||||
uint8_t light_levels[CHUNK_WIDTH * CHUNK_HEIGHT] = { 0 }; | |||||
uint8_t blocks_line[CHUNK_WIDTH] = { 0 }; | |||||
std::map<std::pair<int, int>, uint8_t> light_sources; | |||||
bool was_updated = false; | |||||
}; | |||||
class LightingCallback { | |||||
public: | |||||
virtual void onLightChunkUpdated(const LightChunk &chunk, Vec2i pos) = 0; | |||||
}; | |||||
class LightingThread { | |||||
public: | |||||
LightingThread(LightingCallback &cb); | |||||
~LightingThread(); | |||||
void onSolidBlockAdded(TilePos pos); | |||||
void onSolidBlockRemoved(TilePos pos); | |||||
void onLightAdded(TilePos pos, uint8_t level); | |||||
void onLightRemoved(TilePos pos, uint8_t level); | |||||
void onChunkAdded(Vec2i pos, NewLightChunk &&chunk); | |||||
void onChunkRemoved(Vec2i pos); | |||||
private: | |||||
struct Event { | |||||
enum class Tag { | |||||
BLOCK_ADDED, BLOCK_REMOVED, LIGHT_ADDED, LIGHT_REMOVED, | |||||
CHUNK_ADDED, CHUNK_REMOVED, | |||||
} tag; | |||||
TilePos pos; | |||||
union { | |||||
size_t num; | |||||
}; | |||||
}; | |||||
bool tileIsSolid(TilePos pos); | |||||
LightChunk *getChunk(Vec2i cpos); | |||||
int recalcTile(LightChunk &chunk, Vec2i cpos, Vec2i rpos, TilePos base); | |||||
void processUpdatedChunk(LightChunk &chunk, Vec2i cpos); | |||||
void processEvent(const Event &event, std::vector<NewLightChunk> &newChunks); | |||||
void run(); | |||||
LightingCallback &cb_; | |||||
bool running_ = true; | |||||
std::map<std::pair<int, int>, LightChunk> chunks_; | |||||
std::set<std::pair<int, int>> updated_chunks_; | |||||
LightChunk *cached_chunk_ = nullptr; | |||||
Vec2i cached_chunk_pos_; | |||||
int buffer_ = 0; | |||||
std::vector<Event> buffers_[2] = { {}, {} }; | |||||
std::vector<NewLightChunk> new_chunk_buffers_[2] = { {}, {} }; | |||||
std::thread thread_; | |||||
std::condition_variable cond_; | |||||
std::mutex mut_; | |||||
}; | |||||
inline void LightingThread::onSolidBlockAdded(TilePos pos) { | |||||
std::lock_guard<std::mutex> lock(mut_); | |||||
buffers_[buffer_].push_back({ Event::Tag::BLOCK_ADDED, pos, { 0 } }); | |||||
cond_.notify_one(); | |||||
} | |||||
inline void LightingThread::onSolidBlockRemoved(TilePos pos) { | |||||
std::lock_guard<std::mutex> lock(mut_); | |||||
buffers_[buffer_].push_back({ Event::Tag::BLOCK_REMOVED, pos, { 0 } }); | |||||
cond_.notify_one(); | |||||
} | |||||
inline void LightingThread::onLightAdded(TilePos pos, uint8_t level) { | |||||
std::lock_guard<std::mutex> lock(mut_); | |||||
buffers_[buffer_].push_back({ Event::Tag::LIGHT_ADDED, pos, { .num = level } }); | |||||
cond_.notify_one(); | |||||
} | |||||
inline void LightingThread::onLightRemoved(TilePos pos, uint8_t level) { | |||||
std::lock_guard<std::mutex> lock(mut_); | |||||
buffers_[buffer_].push_back({ Event::Tag::LIGHT_REMOVED, pos, { .num = level } }); | |||||
cond_.notify_one(); | |||||
} | |||||
inline void LightingThread::onChunkAdded(Vec2i pos, NewLightChunk &&chunk) { | |||||
std::lock_guard<std::mutex> lock(mut_); | |||||
buffers_[buffer_].push_back({ Event::Tag::CHUNK_ADDED, pos, | |||||
{ .num = new_chunk_buffers_[buffer_].size() } }); | |||||
new_chunk_buffers_[buffer_].push_back(std::move(chunk)); | |||||
cond_.notify_one(); | |||||
} | |||||
inline void LightingThread::onChunkRemoved(Vec2i pos) { | |||||
std::lock_guard<std::mutex> lock(mut_); | |||||
buffers_[buffer_].push_back({ Event::Tag::CHUNK_ADDED, pos, { 0 } }); | |||||
cond_.notify_one(); | |||||
} | |||||
} |
#include "WorldGen.h" | #include "WorldGen.h" | ||||
#include "Entity.h" | #include "Entity.h" | ||||
#include "Collection.h" | #include "Collection.h" | ||||
#include "LightingThread.h" | |||||
namespace Swan { | namespace Swan { | ||||
class World; | class World; | ||||
class Game; | class Game; | ||||
class WorldPlane: NonCopyable { | |||||
class WorldPlane final: NonCopyable, public LightingCallback { | |||||
public: | public: | ||||
using ID = uint16_t; | using ID = uint16_t; | ||||
void debugBox(TilePos pos); | void debugBox(TilePos pos); | ||||
// LightingCallback implementation | |||||
void onLightChunkUpdated(const LightChunk &chunk, Vec2i pos) final { /* TODO */ }; | |||||
ID id_; | ID id_; | ||||
World *world_; | World *world_; | ||||
std::unique_ptr<WorldGen> gen_; | std::unique_ptr<WorldGen> gen_; | ||||
void addLight(TilePos pos, uint8_t level); | void addLight(TilePos pos, uint8_t level); | ||||
void removeLight(TilePos pos, uint8_t level); | void removeLight(TilePos pos, uint8_t level); | ||||
std::unique_ptr<LightingThread> lighting_; | |||||
std::map<std::pair<int, int>, Chunk> chunks_; | std::map<std::pair<int, int>, Chunk> chunks_; | ||||
std::vector<Chunk *> active_chunks_; | std::vector<Chunk *> active_chunks_; | ||||
std::vector<std::pair<ChunkPos, Chunk *>> tick_chunks_; | std::vector<std::pair<ChunkPos, Chunk *>> tick_chunks_; |
#include "LightingThread.h" | |||||
#include "log.h" | |||||
namespace Swan { | |||||
static Vec2i lightChunkPos(TilePos pos) { | |||||
// Same logic as in WorldPlane.cc | |||||
return Vec2i( | |||||
((size_t)pos.x + (LLONG_MAX / 2) + 1) / CHUNK_WIDTH - | |||||
((LLONG_MAX / 2) / CHUNK_WIDTH) - 1, | |||||
((size_t)pos.y + (LLONG_MAX / 2) + 1) / CHUNK_HEIGHT - | |||||
((LLONG_MAX / 2) / CHUNK_HEIGHT) - 1); | |||||
} | |||||
static Vec2i lightRelPos(TilePos pos) { | |||||
// Same logic as in WorldPlane.cc | |||||
return Vec2i( | |||||
(pos.x + (size_t)CHUNK_WIDTH * ((LLONG_MAX / 2) / | |||||
CHUNK_WIDTH)) % CHUNK_WIDTH, | |||||
(pos.y + (size_t)CHUNK_HEIGHT * ((LLONG_MAX / 2) / | |||||
CHUNK_HEIGHT)) % CHUNK_HEIGHT); | |||||
} | |||||
LightingThread::LightingThread(LightingCallback &cb): | |||||
cb_(cb), thread_(&LightingThread::run, this) {} | |||||
LightingThread::~LightingThread() { | |||||
running_ = false; | |||||
cond_.notify_one(); | |||||
thread_.join(); | |||||
} | |||||
bool LightingThread::tileIsSolid(TilePos pos) { | |||||
Vec2i cpos = lightChunkPos(pos); | |||||
LightChunk *chunk = getChunk(cpos); | |||||
if (chunk == nullptr) { | |||||
return true; | |||||
} | |||||
Vec2i rpos = lightRelPos(pos); | |||||
return chunk->blocks[rpos.y * CHUNK_WIDTH + rpos.x]; | |||||
} | |||||
LightChunk *LightingThread::getChunk(Vec2i cpos) { | |||||
if (cached_chunk_ && cached_chunk_pos_ == cpos) { | |||||
return cached_chunk_; | |||||
} | |||||
auto it = chunks_.find(cpos); | |||||
if (it != chunks_.end()) { | |||||
cached_chunk_ = &it->second; | |||||
cached_chunk_pos_ = cpos; | |||||
return &it->second; | |||||
} | |||||
return nullptr; | |||||
} | |||||
void LightingThread::processEvent(const Event &evt, std::vector<NewLightChunk> &newChunks) { | |||||
info << "event " << (int)evt.tag; | |||||
if (evt.tag == Event::Tag::CHUNK_ADDED) { | |||||
chunks_.emplace(std::piecewise_construct, | |||||
std::forward_as_tuple(evt.pos), | |||||
std::forward_as_tuple(std::move(newChunks[evt.num]))); | |||||
LightChunk &ch = chunks_[evt.pos]; // Create and default initialize | |||||
ch.was_updated = true; | |||||
updated_chunks_.insert(evt.pos); | |||||
return; | |||||
} else if (evt.tag == Event::Tag::CHUNK_REMOVED) { | |||||
chunks_.erase(evt.pos); | |||||
return; | |||||
} | |||||
Vec2i cpos = lightChunkPos(evt.pos); | |||||
LightChunk *ch = getChunk(cpos); | |||||
if (!ch) return; | |||||
ch->was_updated = true; | |||||
updated_chunks_.insert(cpos); | |||||
Vec2i rpos = lightRelPos(evt.pos); | |||||
// TODO: Mark neighbouring chunks as updated | |||||
switch (evt.tag) { | |||||
case Event::Tag::BLOCK_ADDED: | |||||
ch->blocks.set(rpos.y * CHUNK_WIDTH + rpos.x, true); | |||||
ch->blocks_line[rpos.x] += 1; | |||||
break; | |||||
case Event::Tag::BLOCK_REMOVED: | |||||
ch->blocks.set(rpos.y * CHUNK_WIDTH + rpos.x, false); | |||||
ch->blocks_line[rpos.x] -= 1; | |||||
break; | |||||
case Event::Tag::LIGHT_ADDED: | |||||
ch->light_sources[rpos] += evt.num; | |||||
break; | |||||
case Event::Tag::LIGHT_REMOVED: | |||||
ch->light_sources[rpos] -= evt.num; | |||||
break; | |||||
// These were handled earlier | |||||
case Event::Tag::CHUNK_ADDED: | |||||
case Event::Tag::CHUNK_REMOVED: | |||||
break; | |||||
} | |||||
} | |||||
int LightingThread::recalcTile(LightChunk &chunk, Vec2i cpos, Vec2i rpos, TilePos base) { | |||||
std::vector<std::pair<Vec2i, uint8_t>> lights; | |||||
TilePos pos = rpos + base; | |||||
// TODO: Gather light sources from other chunks oo | |||||
for (auto &[lightrel, level]: chunk.light_sources) { | |||||
TilePos lightpos = base + Vec2i(lightrel.first, lightrel.second); | |||||
Vec2i diff = lightpos - pos; | |||||
if (diff.x * diff.x + diff.y * diff.y > level * level) { | |||||
continue; | |||||
} | |||||
lights.push_back({ lightpos, level }); | |||||
} | |||||
chunk.light_levels[rpos.y * CHUNK_WIDTH + rpos.x] = 0; | |||||
constexpr int accuracy = 4; | |||||
auto raycast = [&](Vec2 from, Vec2 to) { | |||||
auto diff = to - from; | |||||
float dist = ((Vec2)diff).length(); | |||||
Vec2 step = (Vec2)diff / (dist * accuracy); | |||||
Vec2 currpos = from; | |||||
Vec2i currtile = Vec2i(floor(currpos.x), floor(currpos.y)); | |||||
auto proceed = [&]() { | |||||
Vec2i t; | |||||
while ((t = Vec2i(floor(currpos.x), floor(currpos.y))) == currtile) { | |||||
currpos += step; | |||||
} | |||||
currtile = t; | |||||
}; | |||||
proceed(); | |||||
bool hit = false; | |||||
while ((currpos - from).squareLength() <= diff.squareLength()) { | |||||
if (tileIsSolid(currtile)) { | |||||
hit = true; | |||||
break; | |||||
} | |||||
proceed(); | |||||
} | |||||
return hit; | |||||
}; | |||||
int acc = 0; | |||||
for (auto &[lightpos, level]: lights) { | |||||
if (lightpos == pos) { | |||||
acc += level; | |||||
continue; | |||||
} | |||||
float dist = ((Vec2)(lightpos - pos)).length(); | |||||
int light = level - (int)dist; | |||||
int hit = | |||||
raycast( | |||||
Vec2(pos.x + 0.3, pos.y + 0.3), | |||||
Vec2(lightpos.x + 0.3, lightpos.y + 0.3)) + | |||||
raycast( | |||||
Vec2(pos.x + 0.7, pos.y + 0.3), | |||||
Vec2(lightpos.x + 0.7, lightpos.y + 0.3)) + | |||||
raycast( | |||||
Vec2(pos.x + 0.3, pos.y + 0.7), | |||||
Vec2(lightpos.x + 0.3, lightpos.y + 0.7)) + | |||||
raycast( | |||||
Vec2(pos.x + 0.7, pos.y + 0.7), | |||||
Vec2(lightpos.x + 0.7, lightpos.y + 0.7)); | |||||
acc += (light * (4 - hit)) / 4; | |||||
if (acc >= 255) { | |||||
return 255; | |||||
} | |||||
} | |||||
return acc; | |||||
} | |||||
void LightingThread::processUpdatedChunk(LightChunk &chunk, Vec2i cpos) { | |||||
auto start = std::chrono::steady_clock::now(); | |||||
TilePos base = cpos * Vec2i(CHUNK_WIDTH * CHUNK_HEIGHT); | |||||
for (int y = 0; y < CHUNK_HEIGHT; ++y) { | |||||
for (int x = 0; x < CHUNK_WIDTH; ++x) { | |||||
chunk.light_levels[y * CHUNK_WIDTH + x] = | |||||
recalcTile(chunk, cpos, Vec2i(x, y), base); | |||||
} | |||||
} | |||||
auto end = std::chrono::steady_clock::now(); | |||||
auto dur = std::chrono::duration<double, std::milli>(end - start); | |||||
info << "Generating light for " << cpos << " took " << dur.count() << "ms"; | |||||
cb_.onLightChunkUpdated(chunk, cpos); | |||||
} | |||||
void LightingThread::run() { | |||||
std::unique_lock<std::mutex> lock(mut_, std::defer_lock); | |||||
while (running_) { | |||||
lock.lock(); | |||||
cond_.wait(lock, [&] { return buffers_[buffer_].size() > 0 || !running_; }); | |||||
std::vector<Event> &buf = buffers_[buffer_]; | |||||
std::vector<NewLightChunk> &newChunks = new_chunk_buffers_[buffer_]; | |||||
buffer_ = (buffer_ + 1) % 2; | |||||
lock.unlock(); | |||||
updated_chunks_.clear(); | |||||
for (auto &evt: buf) { | |||||
processEvent(evt, newChunks); | |||||
} | |||||
buf.clear(); | |||||
newChunks.clear(); | |||||
for (auto &pos: updated_chunks_) { | |||||
auto ch = chunks_.find(pos); | |||||
if (ch != chunks_.end()) { | |||||
processUpdatedChunk(ch->second, Vec2i(pos.first, pos.second)); | |||||
} | |||||
} | |||||
updated_chunks_.clear(); | |||||
} | |||||
} | |||||
} |
WorldPlane::WorldPlane( | WorldPlane::WorldPlane( | ||||
ID id, World *world, std::unique_ptr<WorldGen> gen, | ID id, World *world, std::unique_ptr<WorldGen> gen, | ||||
std::vector<std::unique_ptr<EntityCollection>> &&colls): | std::vector<std::unique_ptr<EntityCollection>> &&colls): | ||||
id_(id), world_(world), gen_(std::move(gen)), ent_colls_(std::move(colls)) { | |||||
id_(id), world_(world), gen_(std::move(gen)), | |||||
lighting_(std::make_unique<LightingThread>(*this)), | |||||
ent_colls_(std::move(colls)) { | |||||
for (auto &coll: ent_colls_) { | for (auto &coll: ent_colls_) { | ||||
ent_colls_by_type_[coll->type()] = coll.get(); | ent_colls_by_type_[coll->type()] = coll.get(); | ||||
chunk.setTileID(rp, id, newTile.image_.texture_.get()); | chunk.setTileID(rp, id, newTile.image_.texture_.get()); | ||||
chunk.markModified(); | chunk.markModified(); | ||||
if (oldTile.light_level_ > 0) { | |||||
removeLight(pos, oldTile.light_level_); | |||||
if (!oldTile.is_solid_ && newTile.is_solid_) { | |||||
lighting_->onSolidBlockAdded(pos); | |||||
} else if (oldTile.is_solid_ && !newTile.is_solid_) { | |||||
lighting_->onSolidBlockRemoved(pos); | |||||
} | } | ||||
if (newTile.light_level_ > 0) { | |||||
addLight(pos, newTile.light_level_); | |||||
if (newTile.light_level_ != oldTile.light_level_) { | |||||
if (oldTile.light_level_ > 0) { | |||||
lighting_->onLightRemoved(pos, oldTile.light_level_); | |||||
removeLight(pos, oldTile.light_level_); | |||||
} | |||||
if (newTile.light_level_ > 0) { | |||||
lighting_->onLightAdded(pos, newTile.light_level_); | |||||
addLight(pos, newTile.light_level_); | |||||
} | |||||
} | } | ||||
} | } | ||||
} | } |
#include <swan/LightingThread.h> | |||||
#include <swan/log.h> | |||||
#include <png++/png.hpp> | |||||
#include <chrono> | |||||
class CB final: public Swan::LightingCallback { | |||||
public: | |||||
void onLightChunkUpdated(const Swan::LightChunk &chunk, Swan::Vec2i pos) final { | |||||
Swan::info << "light chunk at " << pos; | |||||
chunk_ = chunk; | |||||
done_ = true; | |||||
cond_.notify_one(); | |||||
} | |||||
Swan::LightChunk chunk_; | |||||
bool done_ = false; | |||||
std::mutex mut_; | |||||
std::condition_variable cond_; | |||||
}; | |||||
int main() { | |||||
CB cb; | |||||
Swan::LightingThread lt(cb); | |||||
Swan::NewLightChunk nc; | |||||
auto set = [&](int x, int y) { nc.blocks[y * Swan::CHUNK_WIDTH + x] = true; }; | |||||
set(0, 0); | |||||
set(18, 3); | |||||
set(12, 13); | |||||
set(28, 22); | |||||
set(22, 12); | |||||
for (int x = 4; x < 28; ++x) { | |||||
set(x, 24); | |||||
} | |||||
for (int x = 12; x < 20; ++x) { | |||||
set(x, 26); | |||||
} | |||||
nc.light_sources = { | |||||
{ { 20, 10 }, 20 }, | |||||
{ { 16, 30 }, 20 }, | |||||
{ { 5, 27 }, 20 }, | |||||
}; | |||||
lt.onChunkAdded({0, 0}, std::move(nc)); | |||||
std::unique_lock<std::mutex> lock(cb.mut_); | |||||
cb.cond_.wait(lock, [&] { return cb.done_; }); | |||||
png::image<png::rgb_pixel> image(Swan::CHUNK_WIDTH, Swan::CHUNK_HEIGHT); | |||||
for (int y = 0; y < Swan::CHUNK_HEIGHT; ++y) { | |||||
for (int x = 0; x < Swan::CHUNK_WIDTH; ++x) { | |||||
uint8_t light = cb.chunk_.light_levels[y * Swan::CHUNK_WIDTH + x]; | |||||
bool block = false; | |||||
if (cb.chunk_.blocks[y * Swan::CHUNK_WIDTH + x]) { | |||||
block = true; | |||||
} | |||||
bool isLight = | |||||
(x == 20 && y == 10) || | |||||
(x == 16 && y == 30) || | |||||
(x == 5 && y == 27); | |||||
unsigned char lightcol = (unsigned char)(sqrt(light) * 30); | |||||
if (block) { | |||||
image[y][x] = { | |||||
lightcol, lightcol, lightcol }; | |||||
} else if (isLight) { | |||||
image[y][x] = { | |||||
255, 255, 64 }; | |||||
} else { | |||||
image[y][x] = { | |||||
lightcol, 0, 0 }; | |||||
} | |||||
} | |||||
} | |||||
image.write("lighting-test.png"); | |||||
} |