Merge pull request #659 from eliotbyte/feature/block-placement

Feature/block placement
This commit is contained in:
MihailRis 2025-11-12 19:43:34 +03:00 • committed by GitHub
commit 626a08b9d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 748 additions and 428 deletions

View file

@ -427,6 +427,22 @@ Where:
- point_a, point_b - vec3, vec3 positions of the start and end of the tunnel. - point_a, point_b - vec3, vec3 positions of the start and end of the tunnel.
- radius - radius of the tunnel in blocks - radius - radius of the tunnel in blocks
Single block:
```lua
{":block", block_id, position, [rotation], [priority]}
```
Where:
- block_id: numeric runtime id of the block to place.
- position: vec3 world position in blocks, relative to the current chunk start.
- rotation: 0–3, rotation around the Y axis. Default: 0. For extended blocks (size > 1), all segments use this rotation.
- priority: integer order. Higher values are placed later and overwrite lower‑priority placements.
Notes:
- `:block` automatically expands extended blocks into all their segments and replaces any voxels occupying those cells.
- Placement is chunk‑border safe: the engine distributes the placement to all affected chunk prototypes based on the block’s size/AABB.
- Use `:block` for single blocks; use `:line` for tunnels or continuous lines.
### Small structures placement ### Small structures placement
```lua ```lua

View file

@ -430,6 +430,22 @@ end
- точка_а, точка_б - vec3, vec3 позиции начала и конца тоннеля. - точка_а, точка_б - vec3, vec3 позиции начала и конца тоннеля.
- радиус - радиус тоннеля в блоках - радиус - радиус тоннеля в блоках
Одиночный блок:
```lua
{":block", id_блока, позиция, [поворот], [приоритет]}
```
Где:
- id_блока: числовой runtime‑id блока, который нужно поставить.
- позиция: vec3 позиция в блоках относительно начала текущего чанка.
- поворот: 0–3, поворот вокруг оси Y. По умолчанию: 0. Для расширенных блоков (размер > 1) этот поворот применяется ко всем сегментам.
- приоритет: целое число. Бóльший приоритет ставится позже и перезаписывает более низкий.
Примечания:
- `:block` автоматически раскладывает расширенные блоки на сегменты и заменяет любые блоки в занимаемых ячейках.
- Размещение корректно работает на границах чанков: движок сам разносит плейсмент по затрагиваемым прототипам на основе размера/AABB блока.
- `:block` используйте для точечных блоков; `:line` — для туннелей/линий.
### Расстановка малых структур ### Расстановка малых структур

View file

@ -12,6 +12,62 @@
const glm::vec3 BlocksRenderer::SUN_VECTOR(0.528265, 0.833149, -0.163704); const glm::vec3 BlocksRenderer::SUN_VECTOR(0.528265, 0.833149, -0.163704);
const float DIRECTIONAL_LIGHT_FACTOR = 0.3f; const float DIRECTIONAL_LIGHT_FACTOR = 0.3f;
namespace {
static constexpr float K_CHUNK_CENTER_BIAS = 0.5f;
static constexpr float K_AO_NORMAL_PUSH = 0.75f;
static constexpr float K_FACE_OFFSET_EPS = 1e-3f;
static inline void expand_aabb_point(AABB& aabb, bool& init, const glm::vec3& p) {
if (!init) { aabb.a = aabb.b = p; init = true; } else { aabb.addPoint(p); }
}
static inline float apply_directional_factor(float d) {
return (1.0f - DIRECTIONAL_LIGHT_FACTOR) + d * DIRECTIONAL_LIGHT_FACTOR;
}
static inline void expand_aabb_4(
AABB& aabb, bool& init,
const glm::vec3& p0, const glm::vec3& p1,
const glm::vec3& p2, const glm::vec3& p3
) {
expand_aabb_point(aabb, init, p0);
expand_aabb_point(aabb, init, p1);
expand_aabb_point(aabb, init, p2);
expand_aabb_point(aabb, init, p3);
}
static inline void expand_aabb_4_if_needed(
AABB& aabb, bool& init, bool densePass,
const glm::vec3& p0, const glm::vec3& p1,
const glm::vec3& p2, const glm::vec3& p3
) {
if (!densePass) {
expand_aabb_4(aabb, init, p0, p1, p2, p3);
}
}
static inline void compute_face_points(
const glm::vec3& coord,
const glm::vec3& X, const glm::vec3& Y, const glm::vec3& Z,
float bias,
glm::vec3& p0, glm::vec3& p1, glm::vec3& p2, glm::vec3& p3
) {
float s = bias;
p0 = coord + (-X - Y + Z) * s;
p1 = coord + ( X - Y + Z) * s;
p2 = coord + ( X + Y + Z) * s;
p3 = coord + (-X + Y + Z) * s;
}
static inline void fill_texfaces(
const ContentGfxCache& cache,
blockid_t id, uint8_t variantId, bool densePass,
UVRegion (&out)[6]
) {
for (int f = 0; f < 6; ++f) out[f] = cache.getRegion(id, variantId, f, densePass);
}
}
BlocksRenderer::BlocksRenderer( BlocksRenderer::BlocksRenderer(
size_t capacity, size_t capacity,
const Content& content, const Content& content,
@ -91,23 +147,30 @@ void BlocksRenderer::face(
auto X = axisX * w; auto X = axisX * w;
auto Y = axisY * h; auto Y = axisY * h;
auto Z = axisZ * d; auto Z = axisZ * d;
float s = 0.5f; glm::vec3 p0, p1, p2, p3;
vertex(coord + (-X - Y + Z) * s, region.u1, region.v1, lights[0] * tint, axisZ, 0); compute_face_points(coord, X, Y, Z, K_CHUNK_CENTER_BIAS, p0, p1, p2, p3);
vertex(coord + ( X - Y + Z) * s, region.u2, region.v1, lights[1] * tint, axisZ, 0); vertex(p0, region.u1, region.v1, lights[0] * tint, axisZ, 0);
vertex(coord + ( X + Y + Z) * s, region.u2, region.v2, lights[2] * tint, axisZ, 0); vertex(p1, region.u2, region.v1, lights[1] * tint, axisZ, 0);
vertex(coord + (-X + Y + Z) * s, region.u1, region.v2, lights[3] * tint, axisZ, 0); vertex(p2, region.u2, region.v2, lights[2] * tint, axisZ, 0);
vertex(p3, region.u1, region.v2, lights[3] * tint, axisZ, 0);
index(0, 1, 3, 1, 2, 3); index(0, 1, 3, 1, 2, 3);
// Expand local opaque AABB while vertices are still in chunk-local space
expand_aabb_4_if_needed(localAabb, localAabbInit, densePass, p0, p1, p2, p3);
} }
void BlocksRenderer::vertexAO( void BlocksRenderer::vertexAO(
const glm::vec3& coord, const glm::vec3& coord,
float u, float v, float u, float v,
const glm::vec4& tint, const glm::vec4& tint,
float normalHalfLen,
const glm::vec3& axisX, const glm::vec3& axisX,
const glm::vec3& axisY, const glm::vec3& axisY,
const glm::vec3& axisZ const glm::vec3& axisZ
) { ) {
auto pos = coord+axisZ*0.5f+(axisX+axisY)*0.5f; // Sample AO in world-voxel grid with a slightly longer reach along the normal
// to avoid sampling the same chunk voxel when faces reside over neighbor chunk.
auto pos = coord + axisZ * normalHalfLen + (axisX + axisY) * 0.5f;
auto light = pickSoftLight( auto light = pickSoftLight(
glm::ivec3(std::round(pos.x), std::round(pos.y), std::round(pos.z)), glm::ivec3(std::round(pos.x), std::round(pos.y), std::round(pos.z)),
axisX, axisX,
@ -129,27 +192,33 @@ void BlocksRenderer::faceAO(
return; return;
} }
float s = 0.5f;
if (lights) { if (lights) {
float d = glm::dot(glm::normalize(Z), SUN_VECTOR); const auto nZ = glm::normalize(Z);
d = (1.0f - DIRECTIONAL_LIGHT_FACTOR) + d * DIRECTIONAL_LIGHT_FACTOR; float d = apply_directional_factor(glm::dot(nZ, SUN_VECTOR));
auto axisX = glm::normalize(X); auto axisX = glm::normalize(X);
auto axisY = glm::normalize(Y); auto axisY = glm::normalize(Y);
auto axisZ = glm::normalize(Z); auto axisZ = nZ;
glm::vec4 tint(d); glm::vec4 tint(d);
vertexAO(coord + (-X - Y + Z) * s, region.u1, region.v1, tint, axisX, axisY, axisZ); const float nh = K_AO_NORMAL_PUSH; // push AO sample a bit farther along normal
vertexAO(coord + ( X - Y + Z) * s, region.u2, region.v1, tint, axisX, axisY, axisZ); glm::vec3 p0, p1, p2, p3;
vertexAO(coord + ( X + Y + Z) * s, region.u2, region.v2, tint, axisX, axisY, axisZ); compute_face_points(coord, X, Y, Z, K_CHUNK_CENTER_BIAS, p0, p1, p2, p3);
vertexAO(coord + (-X + Y + Z) * s, region.u1, region.v2, tint, axisX, axisY, axisZ); vertexAO(p0, region.u1, region.v1, tint, nh, axisX, axisY, axisZ);
vertexAO(p1, region.u2, region.v1, tint, nh, axisX, axisY, axisZ);
vertexAO(p2, region.u2, region.v2, tint, nh, axisX, axisY, axisZ);
vertexAO(p3, region.u1, region.v2, tint, nh, axisX, axisY, axisZ);
expand_aabb_4_if_needed(localAabb, localAabbInit, densePass, p0, p1, p2, p3);
} else { } else {
auto axisZ = glm::normalize(Z); auto axisZ = glm::normalize(Z);
glm::vec4 tint(1.0f); glm::vec4 tint(1.0f);
vertex(coord + (-X - Y + Z) * s, region.u1, region.v1, tint, axisZ, 1); glm::vec3 p0, p1, p2, p3;
vertex(coord + ( X - Y + Z) * s, region.u2, region.v1, tint, axisZ, 1); compute_face_points(coord, X, Y, Z, K_CHUNK_CENTER_BIAS, p0, p1, p2, p3);
vertex(coord + ( X + Y + Z) * s, region.u2, region.v2, tint, axisZ, 1); vertex(p0, region.u1, region.v1, tint, axisZ, 1);
vertex(coord + (-X + Y + Z) * s, region.u1, region.v2, tint, axisZ, 1); vertex(p1, region.u2, region.v1, tint, axisZ, 1);
vertex(p2, region.u2, region.v2, tint, axisZ, 1);
vertex(p3, region.u1, region.v2, tint, axisZ, 1);
expand_aabb_4_if_needed(localAabb, localAabbInit, densePass, p0, p1, p2, p3);
} }
index(0, 1, 2, 0, 2, 3); index(0, 1, 2, 0, 2, 3);
} }
@ -168,16 +237,19 @@ void BlocksRenderer::face(
return; return;
} }
float s = 0.5f; const auto nZ = glm::normalize(Z);
if (lights) { if (lights) {
float d = glm::dot(glm::normalize(Z), SUN_VECTOR); float d = apply_directional_factor(glm::dot(nZ, SUN_VECTOR));
d = (1.0f - DIRECTIONAL_LIGHT_FACTOR) + d * DIRECTIONAL_LIGHT_FACTOR;
tint *= d; tint *= d;
} }
vertex(coord + (-X - Y + Z) * s, region.u1, region.v1, tint, Z, lights ? 0 : 1); const auto nZ2 = lights ? nZ : Z;
vertex(coord + ( X - Y + Z) * s, region.u2, region.v1, tint, Z, lights ? 0 : 1); glm::vec3 p0, p1, p2, p3;
vertex(coord + ( X + Y + Z) * s, region.u2, region.v2, tint, Z, lights ? 0 : 1); compute_face_points(coord, X, Y, Z, K_CHUNK_CENTER_BIAS, p0, p1, p2, p3);
vertex(coord + (-X + Y + Z) * s, region.u1, region.v2, tint, Z, lights ? 0 : 1); vertex(p0, region.u1, region.v1, tint, nZ2, lights ? 0 : 1);
vertex(p1, region.u2, region.v1, tint, nZ2, lights ? 0 : 1);
vertex(p2, region.u2, region.v2, tint, nZ2, lights ? 0 : 1);
vertex(p3, region.u1, region.v2, tint, nZ2, lights ? 0 : 1);
expand_aabb_4_if_needed(localAabb, localAabbInit, densePass, p0, p1, p2, p3);
index(0, 1, 2, 0, 2, 3); index(0, 1, 2, 0, 2, 3);
} }
@ -333,8 +405,7 @@ void BlocksRenderer::blockCustomModel(
continue; continue;
} }
float d = glm::dot(n, SUN_VECTOR); float d = apply_directional_factor(glm::dot(n, SUN_VECTOR));
d = (1.0f - DIRECTIONAL_LIGHT_FACTOR) + d * DIRECTIONAL_LIGHT_FACTOR;
glm::vec3 t = glm::cross(r, n); glm::vec3 t = glm::cross(r, n);
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
@ -347,14 +418,18 @@ void BlocksRenderer::blockCustomModel(
vcoord.z * Z + r * 0.5f + t * 0.5f + n * 0.5f; vcoord.z * Z + r * 0.5f + t * 0.5f + n * 0.5f;
aoColor = pickSoftLight(p.x, p.y, p.z, glm::ivec3(r), glm::ivec3(t)); aoColor = pickSoftLight(p.x, p.y, p.z, glm::ivec3(r), glm::ivec3(t));
} }
auto pLocal = coord + vcoord.x * X + vcoord.y * Y + vcoord.z * Z;
this->vertex( this->vertex(
coord + vcoord.x * X + vcoord.y * Y + vcoord.z * Z, pLocal,
vertex.uv.x, vertex.uv.x,
vertex.uv.y, vertex.uv.y,
mesh.shading ? (glm::vec4(d, d, d, d) * aoColor) : glm::vec4(1, 1, 1, d), mesh.shading ? (glm::vec4(d, d, d, d) * aoColor) : glm::vec4(1, 1, 1, d),
n, n,
mesh.shading ? 0.0f : 1.0 mesh.shading ? 0.0f : 1.0
); );
if (!densePass) {
expand_aabb_point(localAabb, localAabbInit, pLocal);
}
indexBuffer[indexCount++] = vertexOffset++; indexBuffer[indexCount++] = vertexOffset++;
} }
} }
@ -502,14 +577,8 @@ void BlocksRenderer::render(
if (def.translucent) { if (def.translucent) {
continue; continue;
} }
const UVRegion texfaces[6] { UVRegion texfaces[6];
cache.getRegion(id, variantId, 0, densePass), fill_texfaces(cache, id, variantId, densePass, texfaces);
cache.getRegion(id, variantId, 1, densePass),
cache.getRegion(id, variantId, 2, densePass),
cache.getRegion(id, variantId, 3, densePass),
cache.getRegion(id, variantId, 4, densePass),
cache.getRegion(id, variantId, 5, densePass)
};
int x = i % CHUNK_W; int x = i % CHUNK_W;
int y = i / (CHUNK_D * CHUNK_W); int y = i / (CHUNK_D * CHUNK_W);
int z = (i / CHUNK_D) % CHUNK_W; int z = (i / CHUNK_D) % CHUNK_W;
@ -581,14 +650,8 @@ SortingMeshData BlocksRenderer::renderTranslucent(
if (!def.translucent) { if (!def.translucent) {
continue; continue;
} }
const UVRegion texfaces[6] { UVRegion texfaces[6];
cache.getRegion(id, variantId, 0, densePass), fill_texfaces(cache, id, variantId, densePass, texfaces);
cache.getRegion(id, variantId, 1, densePass),
cache.getRegion(id, variantId, 2, densePass),
cache.getRegion(id, variantId, 3, densePass),
cache.getRegion(id, variantId, 4, densePass),
cache.getRegion(id, variantId, 5, densePass)
};
int x = i % CHUNK_W; int x = i % CHUNK_W;
int y = i / (CHUNK_D * CHUNK_W); int y = i / (CHUNK_D * CHUNK_W);
int z = (i / CHUNK_D) % CHUNK_W; int z = (i / CHUNK_D) % CHUNK_W;
@ -654,6 +717,9 @@ SortingMeshData BlocksRenderer::renderTranslucent(
aabb.addPoint(vertex.position); aabb.addPoint(vertex.position);
} }
// also widen overall local AABB for translucent geometry
expand_aabb_point(localAabb, localAabbInit, vertex.position);
vertex.position.x += chunk->x * CHUNK_W + 0.5f; vertex.position.x += chunk->x * CHUNK_W + 0.5f;
vertex.position.y += 0.5f; vertex.position.y += 0.5f;
vertex.position.z += chunk->z * CHUNK_D + 0.5f; vertex.position.z += chunk->z * CHUNK_D + 0.5f;
@ -689,6 +755,9 @@ SortingMeshData BlocksRenderer::renderTranslucent(
void BlocksRenderer::build(const Chunk* chunk, const Chunks* chunks) { void BlocksRenderer::build(const Chunk* chunk, const Chunks* chunks) {
this->chunk = chunk; this->chunk = chunk;
// reset local AABB accumulation
localAabbInit = false;
localAabb = AABB{glm::vec3(0.0f), glm::vec3(0.0f)};
voxelsBuffer->setPosition( voxelsBuffer->setPosition(
chunk->x * CHUNK_W - voxelBufferPadding, 0, chunk->x * CHUNK_W - voxelBufferPadding, 0,
chunk->z * CHUNK_D - voxelBufferPadding); chunk->z * CHUNK_D - voxelBufferPadding);
@ -766,7 +835,8 @@ ChunkMeshData BlocksRenderer::createMesh() {
ChunkVertex::ATTRIBUTES, sizeof(ChunkVertex::ATTRIBUTES) / sizeof(VertexAttribute) ChunkVertex::ATTRIBUTES, sizeof(ChunkVertex::ATTRIBUTES) / sizeof(VertexAttribute)
) )
), ),
std::move(sortingMesh) std::move(sortingMesh),
localAabbInit ? localAabb : AABB{glm::vec3(0.0f), glm::vec3(0.0f)}
}; };
} }

View file

@ -11,6 +11,7 @@
#include "maths/util.hpp" #include "maths/util.hpp"
#include "commons.hpp" #include "commons.hpp"
#include "settings.hpp" #include "settings.hpp"
#include "maths/aabb.hpp"
template<typename VertexStructure> class Mesh; template<typename VertexStructure> class Mesh;
class Content; class Content;
@ -32,7 +33,8 @@ class BlocksRenderer {
size_t indexCount; size_t indexCount;
size_t denseIndexCount; size_t denseIndexCount;
size_t capacity; size_t capacity;
int voxelBufferPadding = 2; // Increased to better cover cross-chunk sampling for extended blocks (e.g., 3x3x3)
int voxelBufferPadding = 4;
bool overflow = false; bool overflow = false;
bool cancelled = false; bool cancelled = false;
bool densePass = false; bool densePass = false;
@ -47,6 +49,9 @@ class BlocksRenderer {
util::PseudoRandom randomizer; util::PseudoRandom randomizer;
SortingMeshData sortingMesh; SortingMeshData sortingMesh;
// Accumulated local-space AABB over opaque build pass
AABB localAabb {glm::vec3(0.0f), glm::vec3(0.0f)};
bool localAabbInit = false;
void vertex( void vertex(
const glm::vec3& coord, const glm::vec3& coord,
@ -61,6 +66,7 @@ class BlocksRenderer {
void vertexAO( void vertexAO(
const glm::vec3& coord, float u, float v, const glm::vec3& coord, float u, float v,
const glm::vec4& brightness, const glm::vec4& brightness,
float normalHalfLen,
const glm::vec3& axisX, const glm::vec3& axisX,
const glm::vec3& axisY, const glm::vec3& axisY,
const glm::vec3& axisZ const glm::vec3& axisZ
@ -170,6 +176,7 @@ public:
ChunkMesh render(const Chunk* chunk, const Chunks* chunks); ChunkMesh render(const Chunk* chunk, const Chunks* chunks);
ChunkMeshData createMesh(); ChunkMeshData createMesh();
VoxelsVolume* getVoxelsBuffer() const; VoxelsVolume* getVoxelsBuffer() const;
inline const AABB& getLocalAabb() const { return localAabb; }
size_t getMemoryConsumption() const; size_t getMemoryConsumption() const;

View file

@ -1,375 +1,435 @@
#include "ChunksRenderer.hpp" #include "ChunksRenderer.hpp"
#include "BlocksRenderer.hpp" #include "BlocksRenderer.hpp"
#include "debug/Logger.hpp" #include "debug/Logger.hpp"
#include "assets/Assets.hpp" #include "assets/Assets.hpp"
#include "graphics/core/Mesh.hpp" #include "graphics/core/Mesh.hpp"
#include "graphics/core/Shader.hpp" #include "graphics/core/Shader.hpp"
#include "graphics/core/Texture.hpp" #include "graphics/core/Texture.hpp"
#include "graphics/core/Atlas.hpp" #include "graphics/core/Atlas.hpp"
#include "voxels/Chunk.hpp" #include "voxels/Chunk.hpp"
#include "voxels/Chunks.hpp" #include "voxels/Chunks.hpp"
#include "world/Level.hpp" #include "world/Level.hpp"
#include "window/Camera.hpp" #include "window/Camera.hpp"
#include "maths/FrustumCulling.hpp" #include "maths/FrustumCulling.hpp"
#include "util/listutil.hpp" #include "util/listutil.hpp"
#include "settings.hpp" #include "settings.hpp"
#include <algorithm>
static debug::Logger logger("chunks-render");
static debug::Logger logger("chunks-render");
size_t ChunksRenderer::visibleChunks = 0;
size_t ChunksRenderer::visibleChunks = 0;
class RendererWorker : public util::Worker<std::shared_ptr<Chunk>, RendererResult> {
const Chunks& chunks; namespace {
BlocksRenderer renderer; struct CullingBounds { glm::vec3 min; glm::vec3 max; };
public: static constexpr float K_CHUNK_CENTER_BIAS = 0.5f;
RendererWorker( // Minimal thickness to avoid culling flicker for geometry that forms 2D sheets
const Level& level, static constexpr float K_AABB_MIN_EXTENT = 1e-2f;
const Chunks& chunks,
const ContentGfxCache& cache, static inline bool has_volume(const AABB& aabb) {
const EngineSettings& settings auto s = aabb.size();
) return s.x > 0.0f || s.y > 0.0f || s.z > 0.0f;
: chunks(chunks), }
renderer(
settings.graphics.denseRender.get() static inline CullingBounds compute_chunk_culling_bounds(
? settings.graphics.chunkMaxVerticesDense.get() const Chunk& chunk,
: settings.graphics.chunkMaxVertices.get(), const std::unordered_map<glm::ivec2, ChunkMesh>& meshes
level.content, ) {
cache, glm::vec3 min(chunk.x * CHUNK_W, chunk.bottom, chunk.z * CHUNK_D);
settings glm::vec3 max(
) { chunk.x * CHUNK_W + CHUNK_W,
} chunk.top,
chunk.z * CHUNK_D + CHUNK_D
RendererResult operator()(const std::shared_ptr<Chunk>& chunk) override { );
renderer.build(chunk.get(), &chunks); auto it = meshes.find({chunk.x, chunk.z});
if (renderer.isCancelled()) { if (it != meshes.end()) {
return RendererResult { const auto& aabb = it->second.localAabb;
glm::ivec2(chunk->x, chunk->z), true, ChunkMeshData {}}; if (has_volume(aabb)) {
} // Convert to world coords (same 0.5 bias as draw model matrix)
auto meshData = renderer.createMesh(); min = glm::vec3(chunk.x * CHUNK_W + aabb.min().x + K_CHUNK_CENTER_BIAS,
return RendererResult { aabb.min().y + K_CHUNK_CENTER_BIAS,
glm::ivec2(chunk->x, chunk->z), false, std::move(meshData)}; chunk.z * CHUNK_D + aabb.min().z + K_CHUNK_CENTER_BIAS);
} max = glm::vec3(chunk.x * CHUNK_W + aabb.max().x + K_CHUNK_CENTER_BIAS,
}; aabb.max().y + K_CHUNK_CENTER_BIAS,
chunk.z * CHUNK_D + aabb.max().z + K_CHUNK_CENTER_BIAS);
ChunksRenderer::ChunksRenderer(
const Level* level, // Clamp vertically to chunk vertical span to keep bounds tight and valid
const Chunks& chunks, min.y = (std::max)(static_cast<float>(chunk.bottom), min.y);
const Assets& assets, max.y = (std::min)(static_cast<float>(chunk.top), max.y);
const Frustum& frustum,
const ContentGfxCache& cache, // Ensure non-degenerate extents to avoid view-dependent flicker
const EngineSettings& settings glm::vec3 size = max - min;
) auto inflate_axis = [&](int axis) {
: chunks(chunks), float c = (min[axis] + max[axis]) * 0.5f;
assets(assets), min[axis] = c - K_AABB_MIN_EXTENT * 0.5f;
frustum(frustum), max[axis] = c + K_AABB_MIN_EXTENT * 0.5f;
settings(settings), };
threadPool( if (size.x < K_AABB_MIN_EXTENT) inflate_axis(0);
"chunks-render-pool", if (size.y < K_AABB_MIN_EXTENT) inflate_axis(1);
[&]() { if (size.z < K_AABB_MIN_EXTENT) inflate_axis(2);
return std::make_shared<RendererWorker>( }
*level, chunks, cache, settings }
); return {min, max};
}, }
[&](RendererResult& result) { }
if (!result.cancelled) {
auto meshData = std::move(result.meshData); class RendererWorker : public util::Worker<std::shared_ptr<Chunk>, RendererResult> {
meshes[result.key] = ChunkMesh { const Chunks& chunks;
std::make_unique<Mesh<ChunkVertex>>(meshData.mesh), BlocksRenderer renderer;
std::move(meshData.sortingMesh)}; public:
} RendererWorker(
inwork.erase(result.key); const Level& level,
}, const Chunks& chunks,
settings.graphics.chunkMaxRenderers.get() const ContentGfxCache& cache,
) { const EngineSettings& settings
threadPool.setStopOnFail(false); )
renderer = std::make_unique<BlocksRenderer>( : chunks(chunks),
settings.graphics.chunkMaxVertices.get(), renderer(
level->content, cache, settings settings.graphics.denseRender.get()
); ? settings.graphics.chunkMaxVerticesDense.get()
logger.info() << "created " << threadPool.getWorkersCount() << " workers"; : settings.graphics.chunkMaxVertices.get(),
logger.info() << "memory consumption is " level.content,
<< renderer->getMemoryConsumption() * threadPool.getWorkersCount() cache,
<< " B"; settings
} ) {
}
ChunksRenderer::~ChunksRenderer() = default;
RendererResult operator()(const std::shared_ptr<Chunk>& chunk) override {
const Mesh<ChunkVertex>* ChunksRenderer::render( renderer.build(chunk.get(), &chunks);
const std::shared_ptr<Chunk>& chunk, bool important if (renderer.isCancelled()) {
) { return RendererResult {
chunk->flags.modified = false; glm::ivec2(chunk->x, chunk->z), true, ChunkMeshData {}};
if (important) { }
auto mesh = renderer->render(chunk.get(), &chunks); auto meshData = renderer.createMesh();
meshes[glm::ivec2(chunk->x, chunk->z)] = ChunkMesh { return RendererResult {
std::move(mesh.mesh), std::move(mesh.sortingMeshData) glm::ivec2(chunk->x, chunk->z), false, std::move(meshData)};
}; }
return meshes[glm::ivec2(chunk->x, chunk->z)].mesh.get(); };
}
glm::ivec2 key(chunk->x, chunk->z); ChunksRenderer::ChunksRenderer(
if (inwork.find(key) != inwork.end()) { const Level* level,
return nullptr; const Chunks& chunks,
} const Assets& assets,
inwork[key] = true; const Frustum& frustum,
threadPool.enqueueJob(chunk); const ContentGfxCache& cache,
return nullptr; const EngineSettings& settings
} )
: chunks(chunks),
void ChunksRenderer::unload(const Chunk* chunk) { assets(assets),
auto found = meshes.find(glm::ivec2(chunk->x, chunk->z)); frustum(frustum),
if (found != meshes.end()) { settings(settings),
meshes.erase(found); threadPool(
} "chunks-render-pool",
} [&]() {
return std::make_shared<RendererWorker>(
void ChunksRenderer::clear() { *level, chunks, cache, settings
meshes.clear(); );
inwork.clear(); },
threadPool.clearQueue(); [&](RendererResult& result) {
} if (!result.cancelled) {
auto meshData = std::move(result.meshData);
const Mesh<ChunkVertex>* ChunksRenderer::getOrRender( meshes[result.key] = ChunkMesh {
const std::shared_ptr<Chunk>& chunk, bool important std::make_unique<Mesh<ChunkVertex>>(meshData.mesh),
) { std::move(meshData.sortingMesh)};
auto found = meshes.find(glm::ivec2(chunk->x, chunk->z)); meshes[result.key].localAabb = meshData.localAabb;
if (found == meshes.end()) { }
return render(chunk, important); inwork.erase(result.key);
} },
if (chunk->flags.modified && chunk->flags.lighted) { settings.graphics.chunkMaxRenderers.get()
render(chunk, important); ) {
} threadPool.setStopOnFail(false);
return found->second.mesh.get(); renderer = std::make_unique<BlocksRenderer>(
} settings.graphics.chunkMaxVertices.get(),
level->content, cache, settings
void ChunksRenderer::update() { );
threadPool.update(); logger.info() << "created " << threadPool.getWorkersCount() << " workers";
} logger.info() << "memory consumption is "
<< renderer->getMemoryConsumption() * threadPool.getWorkersCount()
const Mesh<ChunkVertex>* ChunksRenderer::retrieveChunk( << " B";
size_t index, const Camera& camera, bool culling }
) {
auto chunk = chunks.getChunks()[index]; ChunksRenderer::~ChunksRenderer() = default;
if (chunk == nullptr) {
return nullptr; const Mesh<ChunkVertex>* ChunksRenderer::render(
} const std::shared_ptr<Chunk>& chunk, bool important
if (!chunk->flags.lighted) { ) {
const auto& found = meshes.find({chunk->x, chunk->z}); chunk->flags.modified = false;
if (found == meshes.end()) { if (important) {
return nullptr; auto mesh = renderer->render(chunk.get(), &chunks);
} else { meshes[glm::ivec2(chunk->x, chunk->z)] = ChunkMesh {
return found->second.mesh.get(); std::move(mesh.mesh), std::move(mesh.sortingMeshData)
} };
} // propagate local aabb from immediate path too
float distance = glm::distance( meshes[glm::ivec2(chunk->x, chunk->z)].localAabb = renderer->getLocalAabb();
camera.position, return meshes[glm::ivec2(chunk->x, chunk->z)].mesh.get();
glm::vec3( }
(chunk->x + 0.5f) * CHUNK_W, glm::ivec2 key(chunk->x, chunk->z);
camera.position.y, if (inwork.find(key) != inwork.end()) {
(chunk->z + 0.5f) * CHUNK_D return nullptr;
) }
); inwork[key] = true;
auto mesh = getOrRender(chunk, distance < CHUNK_W * 1.5f); threadPool.enqueueJob(chunk);
if (mesh == nullptr) { return nullptr;
return nullptr; }
}
if (chunk->flags.dirtyHeights) { void ChunksRenderer::unload(const Chunk* chunk) {
chunk->updateHeights(); auto found = meshes.find(glm::ivec2(chunk->x, chunk->z));
} if (found != meshes.end()) {
if (culling) { meshes.erase(found);
glm::vec3 min(chunk->x * CHUNK_W, chunk->bottom, chunk->z * CHUNK_D); }
glm::vec3 max( }
chunk->x * CHUNK_W + CHUNK_W,
chunk->top, void ChunksRenderer::clear() {
chunk->z * CHUNK_D + CHUNK_D meshes.clear();
); inwork.clear();
threadPool.clearQueue();
if (!frustum.isBoxVisible(min, max)) return nullptr; }
}
return mesh; const Mesh<ChunkVertex>* ChunksRenderer::getOrRender(
} const std::shared_ptr<Chunk>& chunk, bool important
) {
void ChunksRenderer::drawShadowsPass( auto found = meshes.find(glm::ivec2(chunk->x, chunk->z));
const Camera& camera, Shader& shader, const Camera& playerCamera if (found == meshes.end()) {
) { return render(chunk, important);
Frustum frustum; }
frustum.update(camera.getProjView()); if (chunk->flags.modified && chunk->flags.lighted) {
render(chunk, important);
const auto& atlas = assets.require<Atlas>("blocks"); }
return found->second.mesh.get();
atlas.getTexture()->bind(); }
auto denseDistance = settings.graphics.denseRenderDistance.get(); void ChunksRenderer::update() {
auto denseDistance2 = denseDistance * denseDistance; threadPool.update();
}
for (const auto& chunk : chunks.getChunks()) {
if (chunk == nullptr) { const Mesh<ChunkVertex>* ChunksRenderer::retrieveChunk(
continue; size_t index, const Camera& camera, bool culling
} ) {
glm::ivec2 pos {chunk->x, chunk->z}; auto chunk = chunks.getChunks()[index];
const auto& found = meshes.find({chunk->x, chunk->z}); if (chunk == nullptr) {
if (found == meshes.end()) { return nullptr;
continue; }
} if (!chunk->flags.lighted) {
const auto& found = meshes.find({chunk->x, chunk->z});
glm::vec3 coord( if (found == meshes.end()) {
pos.x * CHUNK_W + 0.5f, 0.5f, pos.y * CHUNK_D + 0.5f return nullptr;
); } else {
return found->second.mesh.get();
glm::vec3 min(chunk->x * CHUNK_W, chunk->bottom, chunk->z * CHUNK_D); }
glm::vec3 max( }
chunk->x * CHUNK_W + CHUNK_W, float distance = glm::distance(
chunk->top, camera.position,
chunk->z * CHUNK_D + CHUNK_D glm::vec3(
); (chunk->x + 0.5f) * CHUNK_W,
camera.position.y,
if (!frustum.isBoxVisible(min, max)) { (chunk->z + 0.5f) * CHUNK_D
continue; )
} );
glm::mat4 model = glm::translate(glm::mat4(1.0f), coord); auto mesh = getOrRender(chunk, distance < CHUNK_W * 1.5f);
shader.uniformMatrix("u_model", model); if (mesh == nullptr) {
found->second.mesh->draw(GL_TRIANGLES, return nullptr;
glm::distance2(playerCamera.position * glm::vec3(1, 0, 1), }
(min + max) * 0.5f * glm::vec3(1, 0, 1)) < denseDistance2); if (chunk->flags.dirtyHeights) {
} chunk->updateHeights();
} }
if (culling) {
void ChunksRenderer::drawChunks( const auto bounds = compute_chunk_culling_bounds(*chunk, meshes);
const Camera& camera, Shader& shader if (!frustum.isBoxVisible(bounds.min, bounds.max)) return nullptr;
) { }
const auto& atlas = assets.require<Atlas>("blocks"); return mesh;
}
atlas.getTexture()->bind();
void ChunksRenderer::drawShadowsPass(
// [warning] this whole method is not thread-safe for chunks const Camera& camera, Shader& shader, const Camera& playerCamera
) {
int chunksWidth = chunks.getWidth(); Frustum frustum;
int chunksOffsetX = chunks.getOffsetX(); frustum.update(camera.getProjView());
int chunksOffsetY = chunks.getOffsetY();
const auto& atlas = assets.require<Atlas>("blocks");
if (indices.size() != chunks.getVolume()) {
indices.clear(); atlas.getTexture()->bind();
for (int i = 0; i < chunks.getVolume(); i++) {
indices.push_back(ChunksSortEntry {i, 0}); auto denseDistance = settings.graphics.denseRenderDistance.get();
} auto denseDistance2 = denseDistance * denseDistance;
}
float px = camera.position.x / static_cast<float>(CHUNK_W) - 0.5f; for (const auto& chunk : chunks.getChunks()) {
float pz = camera.position.z / static_cast<float>(CHUNK_D) - 0.5f; if (chunk == nullptr) {
for (auto& index : indices) { continue;
float x = index.index % chunksWidth + chunksOffsetX - px; }
float z = index.index / chunksWidth + chunksOffsetY - pz; glm::ivec2 pos {chunk->x, chunk->z};
index.d = (x * x + z * z) * 1024; const auto& found = meshes.find({chunk->x, chunk->z});
} if (found == meshes.end()) {
util::insertion_sort(indices.begin(), indices.end()); continue;
}
bool culling = settings.graphics.frustumCulling.get();
glm::vec3 coord(
visibleChunks = 0; pos.x * CHUNK_W + K_CHUNK_CENTER_BIAS, K_CHUNK_CENTER_BIAS, pos.y * CHUNK_D + K_CHUNK_CENTER_BIAS
shader.uniform1i("u_alphaClip", true); );
auto denseDistance = settings.graphics.denseRenderDistance.get(); const auto bounds = compute_chunk_culling_bounds(*chunk, meshes);
auto denseDistance2 = denseDistance * denseDistance; if (!frustum.isBoxVisible(bounds.min, bounds.max)) {
continue;
// TODO: minimize draw calls number }
for (int i = indices.size()-1; i >= 0; i--) { glm::mat4 model = glm::translate(glm::mat4(1.0f), coord);
auto& chunk = chunks.getChunks()[indices[i].index]; shader.uniformMatrix("u_model", model);
auto mesh = retrieveChunk(indices[i].index, camera, culling); found->second.mesh->draw(GL_TRIANGLES,
glm::distance2(playerCamera.position * glm::vec3(1, 0, 1),
if (mesh) { (bounds.min + bounds.max) * 0.5f * glm::vec3(1, 0, 1)) < denseDistance2);
glm::vec3 coord( }
chunk->x * CHUNK_W + 0.5f, 0.5f, chunk->z * CHUNK_D + 0.5f }
);
glm::mat4 model = glm::translate(glm::mat4(1.0f), coord); void ChunksRenderer::drawChunks(
shader.uniformMatrix("u_model", model); const Camera& camera, Shader& shader
mesh->draw(GL_TRIANGLES, glm::distance2(camera.position * glm::vec3(1, 0, 1), ) {
(coord + glm::vec3(CHUNK_W * 0.5f, 0.0f, CHUNK_D * 0.5f))) < denseDistance2); const auto& atlas = assets.require<Atlas>("blocks");
visibleChunks++;
} atlas.getTexture()->bind();
}
} // [warning] this whole method is not thread-safe for chunks
static inline void write_sorting_mesh_entries( int chunksWidth = chunks.getWidth();
ChunkVertex* buffer, const std::vector<SortingMeshEntry>& chunkEntries int chunksOffsetX = chunks.getOffsetX();
) { int chunksOffsetY = chunks.getOffsetY();
for (const auto& entry : chunkEntries) {
const auto& vertexData = entry.vertexData; if (indices.size() != chunks.getVolume()) {
std::memcpy( indices.clear();
buffer, for (int i = 0; i < chunks.getVolume(); i++) {
vertexData.data(), indices.push_back(ChunksSortEntry {i, 0});
vertexData.size() * sizeof(ChunkVertex) }
); }
buffer += vertexData.size(); float px = camera.position.x / static_cast<float>(CHUNK_W) - 0.5f;
} float pz = camera.position.z / static_cast<float>(CHUNK_D) - 0.5f;
} for (auto& index : indices) {
float x = index.index % chunksWidth + chunksOffsetX - px;
void ChunksRenderer::drawSortedMeshes(const Camera& camera, Shader& shader) { float z = index.index / chunksWidth + chunksOffsetY - pz;
const int sortInterval = TRANSLUCENT_BLOCKS_SORT_INTERVAL; index.d = (x * x + z * z) * 1024;
static int frameid = 0; }
frameid++; util::insertion_sort(indices.begin(), indices.end());
bool culling = settings.graphics.frustumCulling.get(); bool culling = settings.graphics.frustumCulling.get();
const auto& chunks = this->chunks.getChunks();
const auto& cameraPos = camera.position; visibleChunks = 0;
const auto& atlas = assets.require<Atlas>("blocks"); shader.uniform1i("u_alphaClip", true);
shader.use(); auto denseDistance = settings.graphics.denseRenderDistance.get();
atlas.getTexture()->bind(); auto denseDistance2 = denseDistance * denseDistance;
shader.uniformMatrix("u_model", glm::mat4(1.0f));
shader.uniform1i("u_alphaClip", false); // TODO: minimize draw calls number
for (int i = indices.size()-1; i >= 0; i--) {
for (const auto& index : indices) { auto& chunk = chunks.getChunks()[indices[i].index];
const auto& chunk = chunks[index.index]; auto mesh = retrieveChunk(indices[i].index, camera, culling);
if (chunk == nullptr || !chunk->flags.lighted) {
continue; if (mesh) {
} glm::vec3 coord(
const auto& found = meshes.find(glm::ivec2(chunk->x, chunk->z)); chunk->x * CHUNK_W + K_CHUNK_CENTER_BIAS, K_CHUNK_CENTER_BIAS, chunk->z * CHUNK_D + K_CHUNK_CENTER_BIAS
if (found == meshes.end() || found->second.sortingMeshData.entries.empty()) { );
continue; glm::mat4 model = glm::translate(glm::mat4(1.0f), coord);
} shader.uniformMatrix("u_model", model);
mesh->draw(GL_TRIANGLES, glm::distance2(camera.position * glm::vec3(1, 0, 1),
if (culling) { (coord + glm::vec3(CHUNK_W * 0.5f, 0.0f, CHUNK_D * 0.5f))) < denseDistance2);
glm::vec3 min(chunk->x * CHUNK_W, chunk->bottom, chunk->z * CHUNK_D); visibleChunks++;
glm::vec3 max( }
chunk->x * CHUNK_W + CHUNK_W, }
chunk->top, }
chunk->z * CHUNK_D + CHUNK_D
); static inline void write_sorting_mesh_entries(
ChunkVertex* buffer, const std::vector<SortingMeshEntry>& chunkEntries
if (!frustum.isBoxVisible(min, max)) continue; ) {
} for (const auto& entry : chunkEntries) {
const auto& vertexData = entry.vertexData;
auto& chunkEntries = found->second.sortingMeshData.entries; std::memcpy(
buffer,
if (chunkEntries.size() == 1) { vertexData.data(),
auto& entry = chunkEntries.at(0); vertexData.size() * sizeof(ChunkVertex)
if (found->second.sortedMesh == nullptr) { );
found->second.sortedMesh = std::make_unique<Mesh<ChunkVertex>>( buffer += vertexData.size();
entry.vertexData.data(), entry.vertexData.size() }
); }
}
found->second.sortedMesh->draw(); void ChunksRenderer::drawSortedMeshes(const Camera& camera, Shader& shader) {
continue; const int sortInterval = TRANSLUCENT_BLOCKS_SORT_INTERVAL;
} static int frameid = 0;
for (auto& entry : chunkEntries) { frameid++;
entry.distance = static_cast<long long>(
glm::distance2(entry.position, cameraPos) const bool culling = settings.graphics.frustumCulling.get();
); const auto& chunks = this->chunks.getChunks();
} const auto& cameraPos = camera.position;
if (found->second.sortedMesh == nullptr || const auto& atlas = assets.require<Atlas>("blocks");
(frameid + chunk->x) % sortInterval == 0) {
std::sort(chunkEntries.begin(), chunkEntries.end()); shader.use();
size_t size = 0; atlas.getTexture()->bind();
for (const auto& entry : chunkEntries) { shader.uniformMatrix("u_model", glm::mat4(1.0f));
size += entry.vertexData.size(); shader.uniform1i("u_alphaClip", false);
}
struct VisibleChunkTrans {
static util::Buffer<ChunkVertex> buffer; glm::ivec2 key;
if (buffer.size() < size) { const std::shared_ptr<Chunk>* chunkPtr;
buffer = util::Buffer<ChunkVertex>(size); long long nearestDist2;
} };
write_sorting_mesh_entries(buffer.data(), chunkEntries); std::vector<VisibleChunkTrans> order;
found->second.sortedMesh = std::make_unique<Mesh<ChunkVertex>>( order.reserve(indices.size());
buffer.data(), size
); // Build per-chunk nearest distance for translucent entries
} for (const auto& index : indices) {
found->second.sortedMesh->draw(); const auto& chunk = chunks[index.index];
} if (chunk == nullptr || !chunk->flags.lighted) {
} continue;
}
const auto found = meshes.find(glm::ivec2(chunk->x, chunk->z));
if (found == meshes.end()) {
continue;
}
const auto& entries = found->second.sortingMeshData.entries;
if (entries.empty()) {
continue;
}
if (culling) {
const auto bounds = compute_chunk_culling_bounds(*chunk, meshes);
if (!frustum.isBoxVisible(bounds.min, bounds.max)) continue;
}
long long nearest = LLONG_MAX;
for (const auto& e : entries) {
long long d2 = static_cast<long long>(glm::distance2(e.position, cameraPos));
if (d2 < nearest) nearest = d2;
}
order.push_back(VisibleChunkTrans{glm::ivec2(chunk->x, chunk->z), &chunks[index.index], nearest});
}
// Sort chunks by nearest translucent distance back-to-front (far to near)
std::sort(order.begin(), order.end(), [](const VisibleChunkTrans& a, const VisibleChunkTrans& b) {
return a.nearestDist2 > b.nearestDist2;
});
// Draw per-chunk sorted mesh (keeps GPU buffers and avoids per-frame repack)
for (const auto& item : order) {
const auto& chunk = *item.chunkPtr;
const auto found = meshes.find(item.key);
if (found == meshes.end()) continue;
auto& chunkEntries = found->second.sortingMeshData.entries;
if (chunkEntries.empty()) continue;
// Keep per-chunk internal order up-to-date occasionally
if (found->second.sortedMesh == nullptr || (frameid + chunk->x) % sortInterval == 0) {
for (auto& entry : chunkEntries) {
entry.distance = static_cast<long long>(
glm::distance2(entry.position, cameraPos)
);
}
std::sort(chunkEntries.begin(), chunkEntries.end());
size_t size = 0;
for (const auto& entry : chunkEntries) {
size += entry.vertexData.size();
}
static util::Buffer<ChunkVertex> buffer;
if (buffer.size() < size) {
buffer = util::Buffer<ChunkVertex>(size);
}
write_sorting_mesh_entries(buffer.data(), chunkEntries);
found->second.sortedMesh = std::make_unique<Mesh<ChunkVertex>>(
buffer.data(), size
);
}
found->second.sortedMesh->draw();
}
}

View file

@ -8,6 +8,7 @@
#include "graphics/core/MeshData.hpp" #include "graphics/core/MeshData.hpp"
#include "util/Buffer.hpp" #include "util/Buffer.hpp"
#include "maths/aabb.hpp"
/// @brief Chunk mesh vertex format /// @brief Chunk mesh vertex format
struct ChunkVertex { struct ChunkVertex {
@ -44,10 +45,12 @@ struct SortingMeshData {
struct ChunkMeshData { struct ChunkMeshData {
MeshData<ChunkVertex> mesh; MeshData<ChunkVertex> mesh;
SortingMeshData sortingMesh; SortingMeshData sortingMesh;
AABB localAabb; // mesh-space (chunk-local) bounds for precise culling
}; };
struct ChunkMesh { struct ChunkMesh {
std::unique_ptr<Mesh<ChunkVertex>> mesh; std::unique_ptr<Mesh<ChunkVertex>> mesh;
SortingMeshData sortingMeshData; SortingMeshData sortingMeshData;
std::unique_ptr<Mesh<ChunkVertex> > sortedMesh = nullptr; std::unique_ptr<Mesh<ChunkVertex> > sortedMesh = nullptr;
AABB localAabb; // mesh-space (chunk-local) bounds for precise culling
}; };

View file

@ -152,6 +152,32 @@ public:
placements.emplace_back(priority, LinePlacement {block, a, b, radius}); placements.emplace_back(priority, LinePlacement {block, a, b, radius});
} }
void perform_block(lua::State* L, std::vector<Placement>& placements) {
rawgeti(L, 2);
blockid_t block = touinteger(L, -1);
pop(L);
rawgeti(L, 3);
glm::ivec3 pos = tovec3(L, -1);
pop(L);
uint8_t rotation = 0;
if (objlen(L, -1) >= 4) {
rawgeti(L, 4);
rotation = tointeger(L, -1) & 0b11;
pop(L);
}
int priority = 0;
if (objlen(L, -1) >= 5) {
rawgeti(L, 5);
priority = tointeger(L, -1);
pop(L);
}
placements.emplace_back(priority, BlockPlacement {block, pos, rotation});
}
void perform_placement(lua::State* L, std::vector<Placement>& placements) { void perform_placement(lua::State* L, std::vector<Placement>& placements) {
rawgeti(L, 1); rawgeti(L, 1);
int structIndex = 0; int structIndex = 0;
@ -162,6 +188,11 @@ public:
perform_line(L, placements); perform_line(L, placements);
return; return;
} else if (!std::strcmp(name, ":block")) {
pop(L);
perform_block(L, placements);
return;
} }
const auto& found = def.structuresIndices.find(name); const auto& found = def.structuresIndices.find(name);
if (found != def.structuresIndices.end()) { if (found != def.structuresIndices.end()) {

View file

@ -27,12 +27,23 @@ struct LinePlacement {
} }
}; };
struct BlockPlacement {
blockid_t block;
glm::ivec3 position;
uint8_t rotation;
bool mirror;
BlockPlacement(blockid_t block, glm::ivec3 position, uint8_t rotation, bool mirror=false)
: block(block), position(std::move(position)), rotation(rotation), mirror(mirror) {
}
};
struct Placement { struct Placement {
int priority; int priority;
std::variant<StructurePlacement, LinePlacement> placement; std::variant<StructurePlacement, LinePlacement, BlockPlacement> placement;
Placement( Placement(
int priority, int priority,
std::variant<StructurePlacement, LinePlacement> placement std::variant<StructurePlacement, LinePlacement, BlockPlacement> placement
) : priority(priority), placement(std::move(placement)) {} ) : priority(priority), placement(std::move(placement)) {}
}; };

View file

@ -204,6 +204,47 @@ void WorldGenerator::placeLine(const LinePlacement& line, int priority) {
} }
} }
void WorldGenerator::placeBlock(const BlockPlacement& block, int priority) {
// Compute world-space AABB of the extended block to distribute to prototypes
const auto& indices = content.getIndices()->blocks;
const auto& def = indices.require(block.block);
const auto& rot = def.rotations.variants[block.rotation & 0b11];
glm::ivec3 minp = block.position;
glm::ivec3 maxp = block.position;
const auto size = def.size;
for (int sy = 0; sy < size.y; sy++) {
for (int sz = 0; sz < size.z; sz++) {
for (int sx = 0; sx < size.x; sx++) {
glm::ivec3 p = block.position;
p += rot.axes[0] * sx;
p += rot.axes[1] * sy;
p += rot.axes[2] * sz;
minp = glm::min(minp, p);
maxp = glm::max(maxp, p);
}
}
}
// inclusive-exclusive for max; expand by 1 to compute chunk coverage
maxp += glm::ivec3(1, 1, 1);
AABB aabb(minp, maxp);
int cxa = floordiv<CHUNK_W>(aabb.a.x);
int cza = floordiv<CHUNK_D>(aabb.a.z);
int cxb = floordiv<CHUNK_W>(aabb.b.x);
int czb = floordiv<CHUNK_D>(aabb.b.z);
for (int cz = cza; cz <= czb; cz++) {
for (int cx = cxa; cx <= cxb; cx++) {
const auto& found = prototypes.find({cx, cz});
if (found != prototypes.end()) {
// position becomes relative to prototype chunk
glm::ivec3 rel = block.position - glm::ivec3(cx * CHUNK_W, 0, cz * CHUNK_D);
bool owner = (cx == floordiv<CHUNK_W>(block.position.x)) && (cz == floordiv<CHUNK_D>(block.position.z));
found->second->placements.emplace_back(priority, BlockPlacement{block.block, rel, block.rotation, !owner});
}
}
}
}
void WorldGenerator::placeStructures( void WorldGenerator::placeStructures(
const std::vector<Placement>& placements, const std::vector<Placement>& placements,
ChunkPrototype& prototype, ChunkPrototype& prototype,
@ -217,9 +258,11 @@ void WorldGenerator::placeStructures(
continue; continue;
} }
placeStructure(*sp, placement.priority, chunkX, chunkZ); placeStructure(*sp, placement.priority, chunkX, chunkZ);
} else if (auto lp = std::get_if<LinePlacement>(&placement.placement)) {
placeLine(*lp, placement.priority);
} else { } else {
const auto& line = std::get<LinePlacement>(placement.placement); const auto& bp = std::get<BlockPlacement>(placement.placement);
placeLine(line, placement.priority); placeBlock(bp, placement.priority);
} }
} }
} }
@ -482,9 +525,10 @@ void WorldGenerator::generatePlacements(
for (const auto& placement : placements) { for (const auto& placement : placements) {
if (auto structure = std::get_if<StructurePlacement>(&placement.placement)) { if (auto structure = std::get_if<StructurePlacement>(&placement.placement)) {
generateStructure(prototype, *structure, voxels, chunkX, chunkZ); generateStructure(prototype, *structure, voxels, chunkX, chunkZ);
} else { } else if (auto line = std::get_if<LinePlacement>(&placement.placement)) {
const auto& line = std::get<LinePlacement>(placement.placement); generateLine(prototype, *line, voxels, chunkX, chunkZ);
generateLine(prototype, line, voxels, chunkX, chunkZ); } else if (auto block = std::get_if<BlockPlacement>(&placement.placement)) {
generateBlock(prototype, *block, voxels, chunkX, chunkZ);
} }
} }
} }
@ -591,6 +635,61 @@ void WorldGenerator::generateLine(
} }
} }
void WorldGenerator::generateBlock(
const ChunkPrototype& prototype,
const BlockPlacement& placement,
voxel* voxels,
int chunkX, int chunkZ
) {
const auto& indices = content.getIndices()->blocks;
const auto& def = indices.require(placement.block);
glm::ivec3 origin = placement.position; // relative; may be outside
int rotIndex = 0;
if (def.rotatable && def.rotations.variantsCount) {
rotIndex = placement.rotation % def.rotations.variantsCount;
}
// write origin only for owner chunk (mirror==false) and if inside bounds
if (!placement.mirror &&
origin.x >= 0 && origin.x < CHUNK_W &&
origin.y >= 0 && origin.y < CHUNK_H &&
origin.z >= 0 && origin.z < CHUNK_D) {
auto& vox = voxels[vox_index(origin.x, origin.y, origin.z)];
vox.id = placement.block;
vox.state = {};
vox.state.rotation = rotIndex;
}
// expand extended blocks
if (def.rt.extended) {
const auto& rot = def.rotations.variants[rotIndex];
const auto size = def.size;
for (int sy = 0; sy < size.y; sy++) {
for (int sz = 0; sz < size.z; sz++) {
for (int sx = 0; sx < size.x; sx++) {
if ((sx | sy | sz) == 0) continue;
glm::ivec3 pos = origin;
pos += rot.axes[0] * sx;
pos += rot.axes[1] * sy;
pos += rot.axes[2] * sz;
if (pos.x < 0 || pos.x >= CHUNK_W ||
pos.y < 0 || pos.y >= CHUNK_H ||
pos.z < 0 || pos.z >= CHUNK_D) {
continue;
}
struct voxel seg;
seg.id = placement.block;
seg.state = {};
seg.state.rotation = rotIndex;
seg.state.segment = ((sx > 0) | ((sy > 0) << 1) | ((sz > 0) << 2));
voxels[vox_index(pos.x, pos.y, pos.z)] = seg;
}
}
}
}
}
WorldGenDebugInfo WorldGenerator::createDebugInfo() const { WorldGenDebugInfo WorldGenerator::createDebugInfo() const {
const auto& area = surroundMap.getArea(); const auto& area = surroundMap.getArea();
const auto& levels = area.getBuffer(); const auto& levels = area.getBuffer();

View file

@ -79,6 +79,7 @@ class WorldGenerator {
); );
void placeLine(const LinePlacement& line, int priority); void placeLine(const LinePlacement& line, int priority);
void placeBlock(const BlockPlacement& block, int priority);
void generatePlacements( void generatePlacements(
const ChunkPrototype& prototype, voxel* voxels, int x, int z const ChunkPrototype& prototype, voxel* voxels, int x, int z
@ -89,6 +90,12 @@ class WorldGenerator {
voxel* voxels, voxel* voxels,
int x, int z int x, int z
); );
void generateBlock(
const ChunkPrototype& prototype,
const BlockPlacement& placement,
voxel* voxels,
int x, int z
);
void generateStructure( void generateStructure(
const ChunkPrototype& prototype, const ChunkPrototype& prototype,
const StructurePlacement& placement, const StructurePlacement& placement,