OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / src / dc_terrain_generator_procnodes_bounded.h

112 lines · c ref:
#ifndef DC_TERRAIN_GENERATOR_PROCNODES_BOUNDED_H
#define DC_TERRAIN_GENERATOR_PROCNODES_BOUNDED_H

#include "dc_terrain_generator.h"

#include <godot_cpp/variant/string.hpp>
#include <string>

namespace godot {

// ---------------------------------------------------------------------------
// DCTerrainGeneratorProcNodesBounded — a proc-nodes graph, at its own size
// ---------------------------------------------------------------------------
//
// WHY THIS IS NOT DCTerrainGeneratorProcNodes.
//
// That one is the WORLD generator: it maps world metres into a UV-like node
// space through terrain_height, adds its own mountain / relief / detail noise
// layers, clips the density to [floor_y, ceiling_y], and takes its vertical
// scale from a discovered parameter whose node must be titled "Erosion Noise".
// All of that is right for a planet and wrong for an asset. A graph bridged
// from Blender has no Erosion Noise node, so its vertical scale is pinned at
// the 5.0 default whatever the map does, and a mountain 25.8 node units tall
// over a 40-unit footprint cannot fit a band of 5 -- a ratio no choice of
// terrain_height changes, because both axes scale together.
//
// So this generator does not scale anything. DCGenerator::evaluate takes world
// coordinates, the graph declares its bounds in world units, and the field is
// meshed at the size Blender says it is. The emitted wrapper is one line.
//
// BOUNDED, NOT TILING. get_bounds() is answered from the graph's own Group
// Output interface -- "Bounds Min" / "Bounds Max", read by name and converted
// out of Blender's frame by ProcNodeGraph::get_density_bounds. A graph that
// declares no bounds falls back to the base's "unbounded", which is the honest
// answer rather than a guessed box.
//
// ⚠ GPU ONLY, deliberately, and it says so rather than inventing a CPU field.
// evaluate() returns air. The CPU path would have to call back into the graph
// from dc-terrain's worker threads, and neither the reflection call nor
// ProcNodeGraph's cached execution plan has been shown to be safe there. A
// generator that quietly returns a DIFFERENT field on the CPU than it does on
// the GPU is the exact failure the emitted-density gate exists to catch, so
// this returns nothing and is visible about it -- the same stance
// DCGeneratorGeiss takes while its volumes are unloaded.
class DCTerrainGeneratorProcNodesBounded : public DCTerrainGenerator {
	GDCLASS(DCTerrainGeneratorProcNodesBounded, DCTerrainGenerator)

	// Held as a generic Resource: dc-terrain has NO compile-time dependency on
	// proc-nodes and must not gain one. Everything goes through call().
	Ref<Resource> graph_;
	std::string cached_glsl_;
	String last_compile_error_ = "never compiled (no graph has been assigned)";

	bool has_bounds_ = false;
	float bmin_[3] = { 0, 0, 0 };
	float bmax_[3] = { 0, 0, 0 };
	float authored_voxel_size_ = 0.0f;

	class Bridge : public DCGenerator {
		DCTerrainGeneratorProcNodesBounded *owner_;

	public:
		Bridge(DCTerrainGeneratorProcNodesBounded *o) : owner_(o) {}

		float evaluate(float, float, float, float) override {
			// Air. See the GPU-ONLY note above: an invented CPU field that
			// disagrees with the shader is worse than no field.
			return -1.0f;
		}
		bool has_gpu_shader() const override { return !owner_->cached_glsl_.empty(); }
		const char *gpu_shader_source() const override {
			return owner_->cached_glsl_.empty() ? nullptr : owner_->cached_glsl_.c_str();
		}
		bool get_bounds(float p_min[3], float p_max[3]) const override {
			if (!owner_->has_bounds_) {
				return false;
			}
			for (int i = 0; i < 3; i++) {
				p_min[i] = owner_->bmin_[i];
				p_max[i] = owner_->bmax_[i];
			}
			return true;
		}
	};
	Bridge bridge_{ this };

	void _recompile();

protected:
	static void _bind_methods();

public:
	DCGenerator *get_native_generator() override { return &bridge_; }

	void set_graph(const Ref<Resource> &p_graph);
	Ref<Resource> get_graph() const { return graph_; }

	// Bound so a headless gate can assert on them: "visible" has to mean
	// answerable, not printed.
	String get_last_compile_error() const { return last_compile_error_; }
	bool has_compiled_shader() const { return !cached_glsl_.empty(); }
	String get_compiled_glsl() const { return String(cached_glsl_.c_str()); }
	bool has_bounds() const { return has_bounds_; }
	Vector3 get_bounds_min() const { return Vector3(bmin_[0], bmin_[1], bmin_[2]); }
	Vector3 get_bounds_max() const { return Vector3(bmax_[0], bmax_[1], bmax_[2]); }
	float get_authored_voxel_size() const { return authored_voxel_size_; }
};

} // namespace godot

#endif // DC_TERRAIN_GENERATOR_PROCNODES_BOUNDED_H