@@ -73,6 +73,10 @@ add_executable(perlin-test EXCLUDE_FROM_ALL | |||
src/perlin-test.cc) | |||
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 | |||
assets/icon.png | |||
assets/music/happy-1.wav) |
@@ -11,6 +11,7 @@ add_library(libswan SHARED | |||
src/gfxutil.cc | |||
src/Item.cc | |||
src/ItemStack.cc | |||
src/LightingThread.cc | |||
src/Mod.cc | |||
src/OS.cc | |||
src/Resource.cc |
@@ -0,0 +1,124 @@ | |||
#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(); | |||
} | |||
} |
@@ -16,13 +16,14 @@ | |||
#include "WorldGen.h" | |||
#include "Entity.h" | |||
#include "Collection.h" | |||
#include "LightingThread.h" | |||
namespace Swan { | |||
class World; | |||
class Game; | |||
class WorldPlane: NonCopyable { | |||
class WorldPlane final: NonCopyable, public LightingCallback { | |||
public: | |||
using ID = uint16_t; | |||
@@ -74,6 +75,9 @@ public: | |||
void debugBox(TilePos pos); | |||
// LightingCallback implementation | |||
void onLightChunkUpdated(const LightChunk &chunk, Vec2i pos) final { /* TODO */ }; | |||
ID id_; | |||
World *world_; | |||
std::unique_ptr<WorldGen> gen_; | |||
@@ -82,6 +86,8 @@ private: | |||
void addLight(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::vector<Chunk *> active_chunks_; | |||
std::vector<std::pair<ChunkPos, Chunk *>> tick_chunks_; |
@@ -0,0 +1,239 @@ | |||
#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(); | |||
} | |||
} | |||
} |
@@ -45,7 +45,9 @@ Context WorldPlane::getContext() { | |||
WorldPlane::WorldPlane( | |||
ID id, World *world, std::unique_ptr<WorldGen> gen, | |||
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_) { | |||
ent_colls_by_type_[coll->type()] = coll.get(); | |||
@@ -108,12 +110,22 @@ void WorldPlane::setTileID(TilePos pos, Tile::ID id) { | |||
chunk.setTileID(rp, id, newTile.image_.texture_.get()); | |||
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_); | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,78 @@ | |||
#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"); | |||
} |