feat(core): add sky and block light propagation
This commit is contained in:
1 parent
3ea0180527
commit
772f112903
2 files changed
+159
No files matched your search
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "World.h"
|
||||
#include "Math.h"
|
||||
|
||||
namespace vf {
|
||||
|
||||
constexpr int maxLight = 15;
|
||||
|
||||
// Baked light for a rectangular world region. Two channels: `sun` (skylight,
|
||||
// seeded from the open top of the box and travelling straight down without
|
||||
// attenuation) and `block` (emitter light from Glowstone). Both are BFS
|
||||
// flood-filled, losing one level per step into a transparent neighbour.
|
||||
struct LightField {
|
||||
Vec3i origin; // world-space minimum corner
|
||||
Vec3i dims; // extent in blocks
|
||||
std::vector<uint8_t> sun;
|
||||
std::vector<uint8_t> block;
|
||||
|
||||
int index(int lx, int ly, int lz) const {
|
||||
return (ly * dims.z + lz) * dims.x + lx;
|
||||
}
|
||||
|
||||
bool localInBox(int lx, int ly, int lz) const {
|
||||
return lx >= 0 && lx < dims.x && ly >= 0 && ly < dims.y && lz >= 0 && lz < dims.z;
|
||||
}
|
||||
|
||||
// World-space sampling. Skylight above the box top reads as open sky (full),
|
||||
// everything else outside the box reads dark.
|
||||
uint8_t sunAtWorld(int gx, int gy, int gz) const {
|
||||
int lx = gx - origin.x, ly = gy - origin.y, lz = gz - origin.z;
|
||||
if (ly >= dims.y && lx >= 0 && lx < dims.x && lz >= 0 && lz < dims.z)
|
||||
return static_cast<uint8_t>(maxLight);
|
||||
if (!localInBox(lx, ly, lz)) return 0;
|
||||
return sun[index(lx, ly, lz)];
|
||||
}
|
||||
|
||||
uint8_t blockAtWorld(int gx, int gy, int gz) const {
|
||||
int lx = gx - origin.x, ly = gy - origin.y, lz = gz - origin.z;
|
||||
if (!localInBox(lx, ly, lz)) return 0;
|
||||
return block[index(lx, ly, lz)];
|
||||
}
|
||||
|
||||
// Combined perceived brightness 0..1: brighter of scaled skylight and block
|
||||
// light. `dayFactor` in 0..1 dims the sun for night without touching torches.
|
||||
float combinedAtWorld(int gx, int gy, int gz, float dayFactor = 1.0f) const {
|
||||
float s = static_cast<float>(sunAtWorld(gx, gy, gz)) * dayFactor;
|
||||
float b = static_cast<float>(blockAtWorld(gx, gy, gz));
|
||||
float best = s > b ? s : b;
|
||||
return best / static_cast<float>(maxLight);
|
||||
}
|
||||
};
|
||||
|
||||
// Flood-fills skylight and block light across a world region. The box top is
|
||||
// treated as open sky. Opaque blocks stop propagation; Air, Water and Leaves
|
||||
// carry light onward.
|
||||
LightField computeLight(const World& world, const Vec3i& origin, const Vec3i& dims);
|
||||
|
||||
} // namespace vf
|
||||
@@ -0,0 +1,98 @@
|
||||
#include "vf/Lighting.h"
|
||||
|
||||
#include <deque>
|
||||
|
||||
namespace vf {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Node {
|
||||
int lx, ly, lz;
|
||||
uint8_t level;
|
||||
};
|
||||
|
||||
const int neighbourOffset[6][3] = {{1, 0, 0}, {-1, 0, 0}, {0, 1, 0},
|
||||
{0, -1, 0}, {0, 0, 1}, {0, 0, -1}};
|
||||
|
||||
} // namespace
|
||||
|
||||
LightField computeLight(const World& world, const Vec3i& origin, const Vec3i& dims) {
|
||||
LightField field;
|
||||
field.origin = origin;
|
||||
field.dims = dims;
|
||||
std::size_t cells = static_cast<std::size_t>(dims.x) * dims.y * dims.z;
|
||||
field.sun.assign(cells, 0);
|
||||
field.block.assign(cells, 0);
|
||||
|
||||
auto occludes = [&](int lx, int ly, int lz) {
|
||||
return occludesLight(world.blockAt(origin.x + lx, origin.y + ly, origin.z + lz));
|
||||
};
|
||||
|
||||
std::deque<Node> queue;
|
||||
|
||||
for (int lx = 0; lx < dims.x; ++lx) {
|
||||
for (int lz = 0; lz < dims.z; ++lz) {
|
||||
for (int ly = dims.y - 1; ly >= 0; --ly) {
|
||||
if (occludes(lx, ly, lz)) break;
|
||||
int i = field.index(lx, ly, lz);
|
||||
field.sun[i] = static_cast<uint8_t>(maxLight);
|
||||
queue.push_back({lx, ly, lz, static_cast<uint8_t>(maxLight)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (!queue.empty()) {
|
||||
Node node = queue.front();
|
||||
queue.pop_front();
|
||||
uint8_t current = field.sun[field.index(node.lx, node.ly, node.lz)];
|
||||
if (current != node.level) continue;
|
||||
|
||||
for (auto& off : neighbourOffset) {
|
||||
int nx = node.lx + off[0], ny = node.ly + off[1], nz = node.lz + off[2];
|
||||
if (!field.localInBox(nx, ny, nz) || occludes(nx, ny, nz)) continue;
|
||||
bool straightDown = off[1] == -1 && current == maxLight;
|
||||
uint8_t target = straightDown ? static_cast<uint8_t>(maxLight)
|
||||
: static_cast<uint8_t>(current - 1);
|
||||
if (target == 0) continue;
|
||||
int ni = field.index(nx, ny, nz);
|
||||
if (field.sun[ni] >= target) continue;
|
||||
field.sun[ni] = target;
|
||||
queue.push_back({nx, ny, nz, target});
|
||||
}
|
||||
}
|
||||
|
||||
queue.clear();
|
||||
for (int lx = 0; lx < dims.x; ++lx) {
|
||||
for (int ly = 0; ly < dims.y; ++ly) {
|
||||
for (int lz = 0; lz < dims.z; ++lz) {
|
||||
int emission = blockLightEmission(
|
||||
world.blockAt(origin.x + lx, origin.y + ly, origin.z + lz));
|
||||
if (emission <= 0) continue;
|
||||
int i = field.index(lx, ly, lz);
|
||||
field.block[i] = static_cast<uint8_t>(emission);
|
||||
queue.push_back({lx, ly, lz, static_cast<uint8_t>(emission)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (!queue.empty()) {
|
||||
Node node = queue.front();
|
||||
queue.pop_front();
|
||||
uint8_t current = field.block[field.index(node.lx, node.ly, node.lz)];
|
||||
if (current != node.level || current <= 1) continue;
|
||||
|
||||
for (auto& off : neighbourOffset) {
|
||||
int nx = node.lx + off[0], ny = node.ly + off[1], nz = node.lz + off[2];
|
||||
if (!field.localInBox(nx, ny, nz) || occludes(nx, ny, nz)) continue;
|
||||
uint8_t target = static_cast<uint8_t>(current - 1);
|
||||
int ni = field.index(nx, ny, nz);
|
||||
if (field.block[ni] >= target) continue;
|
||||
field.block[ni] = target;
|
||||
queue.push_back({nx, ny, nz, target});
|
||||
}
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
} // namespace vf
|
||||
Reference in new issue
Block a user