diff --git a/core/include/vf/Lighting.h b/core/include/vf/Lighting.h new file mode 100644 index 0000000..f306d87 --- /dev/null +++ b/core/include/vf/Lighting.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#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 sun; + std::vector 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(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(sunAtWorld(gx, gy, gz)) * dayFactor; + float b = static_cast(blockAtWorld(gx, gy, gz)); + float best = s > b ? s : b; + return best / static_cast(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 diff --git a/core/src/Lighting.cpp b/core/src/Lighting.cpp new file mode 100644 index 0000000..7c958f5 --- /dev/null +++ b/core/src/Lighting.cpp @@ -0,0 +1,98 @@ +#include "vf/Lighting.h" + +#include + +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(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 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(maxLight); + queue.push_back({lx, ly, lz, static_cast(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(maxLight) + : static_cast(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(emission); + queue.push_back({lx, ly, lz, static_cast(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(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