chunk generation
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
116
include/WorldGraph/WorldGraphChunk.h
Normal file
116
include/WorldGraph/WorldGraphChunk.h
Normal 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
|
||||
@@ -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).
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user