#include "LightServer.h" #include #include "log.h" namespace Swan { static ChunkPos 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); } static uint8_t linToSRGB(float lin) { float s; if (lin <= 0.0031308) { s = lin / 12.92; } else { s = 1.055 * std::pow(lin, 1/2.4) - 0.055; } return std::clamp((int)round(s * 255), 0, 255); } static float attenuate(float dist, float squareDist) { return 1 / (1 + 1 * dist + 0.02 * squareDist); } static float attenuate(float dist) { return attenuate(dist, dist * dist); } static float attenuateSquared(float squareDist) { return attenuate(sqrt(squareDist), squareDist); } LightChunk::LightChunk(NewLightChunk &&ch): blocks(std::move(ch.blocks)), lightSources(std::move(ch.lightSources)) { for (int y = 0; y < CHUNK_HEIGHT; ++y) { for (int x = 0; x < CHUNK_WIDTH; ++x) { if (blocks[y * CHUNK_WIDTH + x]) { blocksLine[x] += 1; } } } } LightServer::LightServer(LightCallback &cb): cb_(cb), thread_(&LightServer::run, this) {} LightServer::~LightServer() { running_ = false; cond_.notify_one(); thread_.join(); } bool LightServer::tileIsSolid(TilePos pos) { ChunkPos 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 *LightServer::getChunk(ChunkPos cpos) { if (cachedChunk_ && cachedChunkPos_ == cpos) { return cachedChunk_; } auto it = chunks_.find(cpos); if (it != chunks_.end()) { cachedChunk_ = &it->second; cachedChunkPos_ = cpos; return &it->second; } return nullptr; } void LightServer::processEvent(const Event &evt, std::vector &newChunks) { auto markAdjacentChunksModified = [&](ChunkPos cpos) { for (int y = -1; y <= 1; ++y) { for (int x = -1; x <= 1; ++x) { updatedChunks_.insert(cpos + Vec2i(x, y)); } } }; auto markChunks = [&](ChunkPos cpos, Vec2i rpos, bool l, bool r, bool t, bool b) { updatedChunks_.insert(cpos); if (l) updatedChunks_.insert(cpos + Vec2i(-1, 0)); if (r) updatedChunks_.insert(cpos + Vec2i(1, 0)); if (t) updatedChunks_.insert(cpos + Vec2i(0, -1)); if (b) updatedChunks_.insert(cpos + Vec2i(0, 1)); if (l && t) updatedChunks_.insert(cpos + Vec2i(-1, -1)); if (r && t) updatedChunks_.insert(cpos + Vec2i(1, -1)); if (l && b) updatedChunks_.insert(cpos + Vec2i(-1, 1)); if (r && b) updatedChunks_.insert(cpos + Vec2i(1, 1)); }; auto markChunksModified = [&](ChunkPos cpos, Vec2i rpos, float light) { markChunks(cpos, rpos, light * attenuate(std::max(rpos.x - 5, 0)) >= LIGHT_CUTOFF, light * attenuate(std::max(CHUNK_WIDTH - rpos.x - 5, 0)) >= LIGHT_CUTOFF, light * attenuate(std::max(rpos.y - 5, 0)) >= LIGHT_CUTOFF, light * attenuate(std::max(CHUNK_HEIGHT - rpos.y - 5, 0)) >= LIGHT_CUTOFF); }; auto markChunksModifiedRange = [&](ChunkPos cpos, Vec2i rpos, int range) { markChunks(cpos, rpos, rpos.x <= range, CHUNK_WIDTH - rpos.x <= range, rpos.y <= range, CHUNK_WIDTH - rpos.y <= range); }; 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.i]))); markAdjacentChunksModified(evt.pos); return; } else if (evt.tag == Event::Tag::CHUNK_REMOVED) { chunks_.erase(evt.pos); markAdjacentChunksModified(evt.pos); return; } ChunkPos cpos = lightChunkPos(evt.pos); LightChunk *ch = getChunk(cpos); if (!ch) return; Vec2i rpos = lightRelPos(evt.pos); switch (evt.tag) { case Event::Tag::BLOCK_ADDED: ch->blocks.set(rpos.y * CHUNK_WIDTH + rpos.x, true); ch->blocksLine[rpos.x] += 1; markChunksModifiedRange(cpos, rpos, LIGHT_CUTOFF_DIST); break; case Event::Tag::BLOCK_REMOVED: ch->blocks.set(rpos.y * CHUNK_WIDTH + rpos.x, false); ch->blocksLine[rpos.x] -= 1; markChunksModifiedRange(cpos, rpos, LIGHT_CUTOFF_DIST); break; case Event::Tag::LIGHT_ADDED: ch->lightSources[rpos] += evt.f; markChunksModified(cpos, rpos, ch->lightSources[rpos]); break; case Event::Tag::LIGHT_REMOVED: markChunksModified(cpos, rpos, ch->lightSources[rpos]); ch->lightSources[rpos] -= evt.f; if (ch->lightSources[rpos] < LIGHT_CUTOFF) { ch->lightSources.erase(rpos); } break; // These were handled earlier case Event::Tag::CHUNK_ADDED: case Event::Tag::CHUNK_REMOVED: break; } } float LightServer::recalcTile( LightChunk &chunk, ChunkPos cpos, Vec2i rpos, TilePos base, std::vector> &lights) { TilePos pos = rpos + base; 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; TilePos currtile = TilePos(floor(currpos.x), floor(currpos.y)); auto proceed = [&]() { TilePos t; while ((t = TilePos(floor(currpos.x), floor(currpos.y))) == currtile) { currpos += step; } currtile = t; }; bool hit = false; Vec2i target = TilePos(floor(to.x), floor(to.y)); while (currtile != target && (currpos - from).squareLength() <= diff.squareLength()) { if (tileIsSolid(currtile)) { hit = true; break; } proceed(); } return hit; }; auto diffusedRaycast = [&](Vec2 from, Vec2 to, Vec2 norm) { if (raycast(from, to)) { return 0.0f; } float dot = (to - from).norm().dot(norm); if (dot > 1) dot = 1; else if (dot < 0) dot = 0; return dot; }; bool isSolid = tileIsSolid(pos); bool culled = tileIsSolid(pos + Vec2i(-1, 0)) && tileIsSolid(pos + Vec2i(1, 0)) && tileIsSolid(pos + Vec2i(0, -1)) && tileIsSolid(pos + Vec2i(0, 1)); float acc = 0; for (auto &[lightpos, level]: lights) { if (lightpos == pos) { acc += level; continue; } if (culled) { continue; } int squareDist = (lightpos - pos).squareLength(); if (squareDist > LIGHT_CUTOFF_DIST * LIGHT_CUTOFF_DIST) { continue; } float light = level * attenuateSquared(squareDist); if (light < LIGHT_CUTOFF) { continue; } if (!isSolid) { bool hit = raycast( Vec2(pos.x + 0.5, pos.y + 0.5), Vec2(lightpos.x + 0.5, lightpos.y + 0.5)); if (!hit) { acc += light; } continue; } float frac = 0; float f; if ((f = diffusedRaycast( Vec2(pos.x + 0.5, pos.y - 0.1), Vec2(lightpos.x + 0.5, lightpos.y + 0.5), Vec2(0, -1))) > frac) { frac = f; } if ((f = diffusedRaycast( Vec2(pos.x + 0.5, pos.y + 1.1), Vec2(lightpos.x + 0.5, lightpos.y + 0.5), Vec2(0, 1))) > frac) { frac = f; } if ((f = diffusedRaycast( Vec2(pos.x - 0.1, pos.y + 0.5), Vec2(lightpos.x + 0.5, lightpos.y + 0.5), Vec2(-1, 0))) > frac) { frac = f; } if ((f = diffusedRaycast( Vec2(pos.x + 1.1, pos.y + 0.5), Vec2(lightpos.x + 0.5, lightpos.y + 0.5), Vec2(1, 0))) > frac) { frac = f; } acc += light * frac; } return acc; } void LightServer::processChunkSun(LightChunk &chunk, ChunkPos cpos) { LightChunk *tc = getChunk(cpos + Vec2i(0, -1)); int base = cpos.y * CHUNK_HEIGHT; std::bitset line; for (int ry = 0; ry < CHUNK_HEIGHT; ++ry) { int y = base + ry; float light; if (y <= 20) { light = 1; } else { light = attenuate(y - 20); if (light < LIGHT_CUTOFF) { light = 0; } } for (int rx = 0; rx < CHUNK_WIDTH; ++rx) { bool lit = light > 0 && tc && tc->blocksLine[rx] == 0 && !line[rx]; if (lit) { chunk.lightBuffer()[ry * CHUNK_WIDTH + rx] = light; if (chunk.blocks[ry * CHUNK_WIDTH + rx]) { line[rx] = true; } } else { chunk.lightBuffer()[ry * CHUNK_WIDTH + rx] = 0; } } } } void LightServer::processChunkLights(LightChunk &chunk, ChunkPos cpos) { TilePos base = cpos * Vec2i(CHUNK_WIDTH, CHUNK_HEIGHT); std::vector> lights; for (auto &[pos, level]: chunk.lightSources) { lights.emplace_back(Vec2i(pos) + base, level); } auto addLightFromChunk = [&](LightChunk *chunk, int dx, int dy) { if (chunk == nullptr) { return; } TilePos b = base + Vec2i(dx * CHUNK_WIDTH, dy * CHUNK_HEIGHT); for (auto &[pos, level]: chunk->lightSources) { lights.emplace_back(TilePos(pos) + b, level); } }; for (int y = -1; y <= 1; ++y) { for (int x = -1; x <= 1; ++x) { if (y == 0 && x == 0) continue; addLightFromChunk(getChunk(cpos + Vec2i(x, y)), x, y); } } chunk.bounces.clear(); for (int y = 0; y < CHUNK_HEIGHT; ++y) { for (int x = 0; x < CHUNK_WIDTH; ++x) { float light = recalcTile(chunk, cpos, Vec2i(x, y), base, lights); chunk.lightBuffer()[y * CHUNK_WIDTH + x] += light; if (light > 0 && chunk.blocks[y * CHUNK_WIDTH + x]) { chunk.bounces.emplace_back(base + Vec2i(x, y), light * 0.1); } } } } void LightServer::processChunkBounces(LightChunk &chunk, ChunkPos cpos) { TilePos base = cpos * Vec2i(CHUNK_WIDTH, CHUNK_HEIGHT); std::vector> lights; for (auto &light: chunk.bounces) { lights.emplace_back(light); } auto addLightFromChunk = [&](LightChunk *chunk) { if (chunk == nullptr) { return; } for (auto &light: chunk->bounces) { lights.emplace_back(light); } }; for (int y = -1; y <= 1; ++y) { for (int x = -1; x <= 1; ++x) { if (y == 0 && x == 0) continue; addLightFromChunk(getChunk(cpos + Vec2i(x, y))); } } for (int y = 0; y < CHUNK_HEIGHT; ++y) { for (int x = 0; x < CHUNK_WIDTH; ++x) { float light = recalcTile(chunk, cpos, Vec2i(x, y), base, lights); float sum = chunk.lightBuffer()[y * CHUNK_WIDTH + x] + light; chunk.lightBuffer()[y * CHUNK_WIDTH + x] = sum; } } } void LightServer::processChunkSmoothing(LightChunk &chunk, ChunkPos cpos) { LightChunk *tc = getChunk(cpos + Vec2i(0, -1)); LightChunk *bc = getChunk(cpos + Vec2i(0, 1)); LightChunk *lc = getChunk(cpos + Vec2i(-1, 0)); LightChunk *rc = getChunk(cpos + Vec2i(1, 0)); auto getLight = [&](LightChunk &chunk, int x, int y) { return chunk.lightBuffer()[y * CHUNK_WIDTH + x]; }; auto calc = [&](int x1, int x2, int y1, int y2, auto tf, auto bf, auto lf, auto rf) { float *dest = chunk.lightBuffers + CHUNK_WIDTH * CHUNK_HEIGHT * ((chunk.buffer + 1) % 2); for (int y = y1; y < y2; ++y) { for (int x = x1; x < x2; ++x) { float t = tf(x, y); float b = bf(x, y); float l = lf(x, y); float r = rf(x, y); float light = chunk.lightBuffer()[y * CHUNK_WIDTH + x]; int count = 1; if (t > light) { light += t; count += 1; } if (b > light) { light += b; count += 1; } if (l > light) { light += l; count += 1; } if (r > light) { light += r; count += 1; } light /= count; dest[y * CHUNK_WIDTH + x] = light; } } }; calc(1, CHUNK_WIDTH - 1, 1, CHUNK_HEIGHT - 1, [&](int x, int y) { return getLight(chunk, x, y - 1); }, [&](int x, int y) { return getLight(chunk, x, y + 1); }, [&](int x, int y) { return getLight(chunk, x - 1, y); }, [&](int x, int y) { return getLight(chunk, x + 1, y); }); if (tc) { calc(1, CHUNK_WIDTH - 1, 0, 1, [&](int x, int y) { return tc->lightBuffer()[(CHUNK_HEIGHT - 1) * CHUNK_WIDTH + x]; }, [&](int x, int y) { return getLight(chunk, x, y + 1); }, [&](int x, int y) { return getLight(chunk, x - 1, y); }, [&](int x, int y) { return getLight(chunk, x + 1, y); }); } if (bc) { calc(1, CHUNK_WIDTH - 1, CHUNK_HEIGHT - 1, CHUNK_HEIGHT, [&](int x, int y) { return getLight(chunk, x, y - 1); }, [&](int x, int y) { return bc->lightBuffer()[x]; }, [&](int x, int y) { return getLight(chunk, x - 1, y); }, [&](int x, int y) { return getLight(chunk, x + 1, y); }); } if (lc) { calc(0, 1, 1, CHUNK_HEIGHT - 1, [&](int x, int y) { return getLight(chunk, x, y - 1); }, [&](int x, int y) { return getLight(chunk, x, y + 1); }, [&](int x, int y) { return lc->lightBuffer()[y * CHUNK_WIDTH + CHUNK_WIDTH - 1]; }, [&](int x, int y) { return getLight(chunk, x + 1, y); }); } if (rc) { calc(CHUNK_WIDTH - 1, CHUNK_WIDTH, 1, CHUNK_HEIGHT - 1, [&](int x, int y) { return getLight(chunk, x, y - 1); }, [&](int x, int y) { return getLight(chunk, x, y + 1); }, [&](int x, int y) { return getLight(chunk, x - 1, y); }, [&](int x, int y) { return rc->lightBuffer()[y * CHUNK_WIDTH]; }); } if (tc && lc) { calc(0, 1, 0, 1, [&](int x, int y) { return tc->lightBuffer()[(CHUNK_HEIGHT - 1) * CHUNK_WIDTH + x]; }, [&](int x, int y) { return getLight(chunk, x, y + 1); }, [&](int x, int y) { return lc->lightBuffer()[y * CHUNK_WIDTH + CHUNK_WIDTH - 1]; }, [&](int x, int y) { return getLight(chunk, x + 1, y); }); } if (tc && rc) { calc(CHUNK_WIDTH - 1, CHUNK_WIDTH, 0, 1, [&](int x, int y) { return tc->lightBuffer()[(CHUNK_HEIGHT - 1) * CHUNK_WIDTH + x]; }, [&](int x, int y) { return getLight(chunk, x, y + 1); }, [&](int x, int y) { return getLight(chunk, x - 1, y); }, [&](int x, int y) { return rc->lightBuffer()[y * CHUNK_WIDTH]; }); } if (bc && lc) { calc(0, 1, CHUNK_HEIGHT - 1, CHUNK_HEIGHT, [&](int x, int y) { return getLight(chunk, x, y - 1); }, [&](int x, int y) { return bc->lightBuffer()[x]; }, [&](int x, int y) { return lc->lightBuffer()[y * CHUNK_WIDTH + CHUNK_WIDTH - 1]; }, [&](int x, int y) { return getLight(chunk, x + 1, y); }); } if (bc && rc) { calc(CHUNK_WIDTH - 1, CHUNK_WIDTH, CHUNK_HEIGHT - 1, CHUNK_HEIGHT, [&](int x, int y) { return getLight(chunk, x, y - 1); }, [&](int x, int y) { return bc->lightBuffer()[x]; }, [&](int x, int y) { return getLight(chunk, x - 1, y); }, [&](int x, int y) { return rc->lightBuffer()[y * CHUNK_WIDTH]; }); } } void LightServer::finalizeChunk(LightChunk &chunk) { for (int y = 0; y < CHUNK_HEIGHT; ++y) { for (int x = 0; x < CHUNK_WIDTH; ++x) { chunk.lightLevels[y * CHUNK_WIDTH + x] = linToSRGB(chunk.lightBuffer()[y * CHUNK_HEIGHT + x]); } } } void LightServer::run() { std::unique_lock lock(mut_, std::defer_lock); while (running_) { lock.lock(); cond_.wait(lock, [&] { return buffers_[buffer_].size() > 0 || !running_; }); std::vector &buf = buffers_[buffer_]; std::vector &newChunks = newChunkBuffers_[buffer_]; buffer_ = (buffer_ + 1) % 2; lock.unlock(); updatedChunks_.clear(); for (auto &evt: buf) { processEvent(evt, newChunks); } buf.clear(); newChunks.clear(); for (auto &pos: updatedChunks_) { auto ch = chunks_.find(pos); if (ch != chunks_.end()) { processChunkSun(ch->second, ChunkPos(pos.first, pos.second)); } } for (auto &pos: updatedChunks_) { auto ch = chunks_.find(pos); if (ch != chunks_.end()) { processChunkLights(ch->second, ChunkPos(pos.first, pos.second)); } } for (auto &pos: updatedChunks_) { auto ch = chunks_.find(pos); if (ch != chunks_.end()) { processChunkBounces(ch->second, ChunkPos(pos.first, pos.second)); } } for (int i = 0; i < 4; ++i) { for (auto &pos: updatedChunks_) { auto ch = chunks_.find(pos); if (ch != chunks_.end()) { processChunkSmoothing(ch->second, ChunkPos(pos.first, pos.second)); ch->second.buffer = (ch->second.buffer + 1) % 2; } } } for (auto &pos: updatedChunks_) { auto ch = chunks_.find(pos); if (ch != chunks_.end()) { finalizeChunk(ch->second); } } for (auto &pos: updatedChunks_) { auto ch = chunks_.find(pos); if (ch != chunks_.end()) { cb_.onLightChunkUpdated(ch->second, pos); } } } } }