chunk generation

This commit is contained in:
Connor
2026-02-22 11:57:39 +09:00
parent 9b0c9a87fa
commit 7b80eda561
12 changed files with 1533 additions and 417 deletions

View File

@@ -6,6 +6,7 @@
#include <optional>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace WorldGraph {
@@ -74,6 +75,10 @@ public:
/// dependencies) is connected.
bool IsValid(NodeID outputNode) const;
/// Returns all node IDs reachable from (and including) \p outputNode.
/// Useful for inspecting query-node offsets to compute padding requirements.
std::vector<NodeID> GetDependencies(NodeID outputNode) const;
private:
friend class GraphSerializer;

View File

@@ -0,0 +1,116 @@
#pragma once
#include "WorldGraph/WorldGraph.h"
#include <cstdint>
#include <vector>
namespace WorldGraph {
// ─────────────────────────────── TileGrid ────────────────────────────────────
/// A 2D tile-ID array covering a contiguous world region.
///
/// Storage is row-major: data[ly * width + lx]
/// where lx = worldX - originX, ly = worldY - originY.
///
/// worldY increases "upward" (positive = sky, negative = underground).
struct TileGrid {
std::vector<int32_t> data;
int32_t originX { 0 };
int32_t originY { 0 };
int32_t width { 0 };
int32_t height { 0 };
TileGrid() = default;
TileGrid(int32_t ox, int32_t oy, int32_t w, int32_t h)
: data(static_cast<size_t>(w) * h, 0)
, originX(ox), originY(oy), width(w), height(h) {}
bool Contains(int32_t worldX, int32_t worldY) const noexcept;
/// Returns the tile ID at (worldX, worldY), or 0 if out of bounds.
int32_t Get(int32_t worldX, int32_t worldY) const noexcept;
/// Sets the tile at (worldX, worldY). No-op if out of bounds.
void Set(int32_t worldX, int32_t worldY, int32_t id) noexcept;
/// Build an EvalContext for cell (wx, wy) backed by this grid as prev-pass data.
EvalContext MakeEvalContext(int32_t wx, int32_t wy, uint64_t seed) const noexcept;
};
// ─────────────────────────────── PaddingBounds ───────────────────────────────
/// Extra tiles a pass needs beyond its output region from the previous pass,
/// in each compass direction.
struct PaddingBounds {
int32_t negX { 0 }; ///< Extra tiles needed toward X.
int32_t posX { 0 }; ///< Extra tiles needed toward +X.
int32_t negY { 0 }; ///< Extra tiles needed toward Y.
int32_t posY { 0 }; ///< Extra tiles needed toward +Y.
/// Expand to accommodate a single relative offset (dx, dy).
void Include(int32_t dx, int32_t dy) noexcept;
/// Expand to be at least as large as another PaddingBounds.
void Include(const PaddingBounds& o) noexcept;
int32_t TotalX() const noexcept { return negX + posX; }
int32_t TotalY() const noexcept { return negY + posY; }
bool IsZero() const noexcept { return negX == 0 && posX == 0 && negY == 0 && posY == 0; }
};
/// Walk the subgraph rooted at \p outputNode and return the maximum query offsets
/// found in any QueryTile / QueryRange / QueryDistance node.
///
/// This tells you how much border the PREVIOUS pass must generate so that every
/// query within this pass can be satisfied.
PaddingBounds ComputeRequiredPadding(const Graph& graph, Graph::NodeID outputNode);
// ─────────────────────────────── Generation API ──────────────────────────────
/// One pass in a multi-pass world-generation pipeline.
struct GenerationPass {
const Graph& graph;
Graph::NodeID outputNode;
};
/// Generate a tile grid covering [originX .. originX+width-1] × [originY .. originY+height-1].
///
/// For each cell the graph is evaluated with a context that provides:
/// - The cell's world position.
/// - The full contents of \p prevPassData as the previous-pass tile source
/// (nullptr for the first pass).
///
/// Semantics of the return value:
/// - Non-zero → set that tile to the returned ID.
/// - Zero → "no change": if prevPassData is available the cell keeps its
/// previous-pass value; otherwise it stays 0 (AIR).
TileGrid GenerateRegion(
const Graph& graph,
Graph::NodeID outputNode,
int32_t originX, int32_t originY,
int32_t width, int32_t height,
const TileGrid* prevPassData,
uint64_t seed);
/// Run multiple passes over a chunk, automatically deriving the padded regions
/// each pass needs.
///
/// Algorithm
/// ─────────
/// 1. passes[last] generates exactly [chunkOriginX, chunkOriginY, W × H].
/// 2. For every earlier pass i, ComputeRequiredPadding(passes[i+1]) determines
/// how much larger passes[i]'s output must be so passes[i+1] can query it.
/// This expansion accumulates from last to first.
/// 3. Passes execute in forward order; each feeds its output to the next.
///
/// The returned TileGrid covers the final chunk region only.
TileGrid GenerateChunk(
const std::vector<GenerationPass>& passes,
int32_t chunkOriginX, int32_t chunkOriginY,
int32_t chunkWidth, int32_t chunkHeight,
uint64_t seed);
} // namespace WorldGraph

View File

@@ -3,7 +3,9 @@
#include "WorldGraph/WorldGraphTypes.h"
#include "config.h"
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <string>
#include <vector>
@@ -209,6 +211,131 @@ public:
}
};
// ─────────────────────────────── More math nodes ─────────────────────────────
/// sin(a) in radians (Float)
class SinNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Sin"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
return Value::MakeFloat(std::sin(in[0].AsFloat()));
}
};
/// cos(a) in radians (Float)
class CosNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float }; }
std::string GetName() const override { return "Cos"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 1);
return Value::MakeFloat(std::cos(in[0].AsFloat()));
}
};
/// fmod(a, b) — returns 0 when b == 0 (Float)
class ModuloNode : public Node {
public:
Type GetOutputType() const override { return Type::Float; }
std::vector<Type> GetInputTypes() const override { return { Type::Float, Type::Float }; }
std::string GetName() const override { return "Modulo"; }
Value Evaluate(const EvalContext&, const std::vector<Value>& in) const override {
DEV_ASSERT(in.size() == 2);
float b = in[1].AsFloat();
if (b == 0.0f) return Value::MakeFloat(0.0f);
return Value::MakeFloat(std::fmod(in[0].AsFloat(), b));
}
};
// ─────────────────────────────── Tile query nodes ────────────────────────────
//
// Query nodes read from the PREVIOUS generation pass via EvalContext::GetPrevTile().
// Their offsets are baked into the node so the chunk generator can compute the
// required border padding BEFORE evaluation begins.
//
// Returns 0 (AIR / no data) for any out-of-bounds access.
/// Returns true when the previous-pass tile at (worldX+offsetX, worldY+offsetY)
/// equals expectedID.
class QueryTileNode : public Node {
public:
int32_t offsetX { 0 };
int32_t offsetY { 0 };
int32_t expectedID { 0 };
QueryTileNode() = default;
QueryTileNode(int32_t ox, int32_t oy, int32_t id)
: offsetX(ox), offsetY(oy), expectedID(id) {}
Type GetOutputType() const override { return Type::Bool; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "QueryTile"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
return Value::MakeBool(
ctx.GetPrevTile(ctx.worldX + offsetX, ctx.worldY + offsetY) == expectedID);
}
};
/// Counts tiles matching tileID within the rectangle
/// [worldX+minX .. worldX+maxX] × [worldY+minY .. worldY+maxY] (inclusive).
class QueryRangeNode : public Node {
public:
int32_t minX { 0 };
int32_t minY { 1 };
int32_t maxX { 0 };
int32_t maxY { 4 };
int32_t tileID { 0 };
QueryRangeNode() = default;
QueryRangeNode(int32_t mnX, int32_t mnY, int32_t mxX, int32_t mxY, int32_t id)
: minX(mnX), minY(mnY), maxX(mxX), maxY(mxY), tileID(id) {}
Type GetOutputType() const override { return Type::Int; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "QueryRange"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
int32_t count = 0;
for (int32_t dy = minY; dy <= maxY; ++dy)
for (int32_t dx = minX; dx <= maxX; ++dx)
if (ctx.GetPrevTile(ctx.worldX + dx, ctx.worldY + dy) == tileID)
++count;
return Value::MakeInt(count);
}
};
/// Returns the Chebyshev distance (max(|dx|,|dy|)) to the nearest tile of
/// tileID within maxDistance tiles. Returns maxDistance + 1 if none found.
/// The current cell (distance 0) is never considered a match.
class QueryDistanceNode : public Node {
public:
int32_t tileID { 0 };
int32_t maxDistance { 4 };
QueryDistanceNode() = default;
QueryDistanceNode(int32_t id, int32_t maxDist)
: tileID(id), maxDistance(maxDist) {}
Type GetOutputType() const override { return Type::Int; }
std::vector<Type> GetInputTypes() const override { return {}; }
std::string GetName() const override { return "QueryDistance"; }
Value Evaluate(const EvalContext& ctx, const std::vector<Value>&) const override {
int32_t best = maxDistance + 1;
for (int32_t dy = -maxDistance; dy <= maxDistance; ++dy) {
for (int32_t dx = -maxDistance; dx <= maxDistance; ++dx) {
int32_t d = (std::abs(dx) > std::abs(dy)) ? std::abs(dx) : std::abs(dy);
if (d == 0 || d >= best) continue;
if (ctx.GetPrevTile(ctx.worldX + dx, ctx.worldY + dy) == tileID)
best = d;
}
}
return Value::MakeInt(best);
}
};
// ─────────────────────────────── Source / constant nodes ─────────────────────
/// Outputs a fixed floating-point constant (no inputs).

View File

@@ -34,7 +34,21 @@ struct EvalContext {
int32_t worldX { 0 }; ///< World-space tile column.
int32_t worldY { 0 }; ///< World-space tile row.
uint64_t seed { 0 }; ///< World seed for deterministic noise.
// TODO: previous-pass tile accessor for tile-query nodes.
// ── Previous-pass tile data (optional) ────────────────────────────────
// Set by the chunk generator before calling Evaluate().
// Row-major: prevTiles[ly * prevWidth + lx], lx = worldX - prevOriginX.
const int32_t* prevTiles { nullptr };
int32_t prevOriginX { 0 };
int32_t prevOriginY { 0 };
int32_t prevWidth { 0 };
int32_t prevHeight { 0 };
/// Query the previous pass at an absolute world position.
/// Returns 0 (AIR / empty) when no previous pass or position is out of bounds.
inline int32_t GetPrevTile(int32_t x, int32_t y) const noexcept;
inline bool HasPrevPass() const noexcept { return prevTiles != nullptr; }
};
inline float Value::AsFloat() const noexcept {
@@ -64,6 +78,14 @@ inline bool Value::AsBool() const noexcept {
return false;
}
inline int32_t EvalContext::GetPrevTile(int32_t x, int32_t y) const noexcept {
if (!prevTiles) return 0;
int32_t lx = x - prevOriginX;
int32_t ly = y - prevOriginY;
if (lx < 0 || lx >= prevWidth || ly < 0 || ly >= prevHeight) return 0;
return prevTiles[ly * prevWidth + lx];
}
inline bool Value::operator==(const Value& o) const noexcept {
if (type != o.type) return false;
switch (type) {