OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / graph / proc_node_graph.h

247 lines · c ref:
#ifndef PROC_NODES_PROC_NODE_GRAPH_H
#define PROC_NODES_PROC_NODE_GRAPH_H

#include "proc_node.h"
#include "../core/proc_geometry.h"
#include <godot_cpp/variant/packed_string_array.hpp>
#include <godot_cpp/classes/resource.hpp>

namespace godot {

struct ExecutionPlan;

struct ProcConnection {
	int from_node = -1;
	int from_socket = -1;
	int to_node = -1;
	int to_socket = -1;
};

struct ProcZoneInfo {
	int zone_id = -1;
	enum ZoneType { ZONE_REPEAT, ZONE_FOREACH, ZONE_SIMULATION };
	ZoneType zone_type = ZONE_REPEAT;
	int input_node = -1;   // Index of ZoneInput node in graph
	int output_node = -1;  // Index of ZoneOutput node in graph
	Vector<int> body_nodes; // Indices of nodes inside zone (topological order)
	int parent_zone = -1;  // For nesting (-1 = root level)
};

class ProcNodeGraph : public Resource {
	GDCLASS(ProcNodeGraph, Resource);

protected:
	static void _bind_methods();

	bool _set(const StringName &p_name, const Variant &p_property);
	bool _get(const StringName &p_name, Variant &r_property) const;
	void _get_property_list(List<PropertyInfo> *p_list) const;

	Vector<Ref<ProcNode>> nodes;
	Vector<ProcConnection> connections;

	// WHICH FRAME THIS GRAPH'S ARITHMETIC IS IN.
	//
	// A graph is a program over numbers; almost nothing in it knows what "up"
	// means. Math, Vector Math, Combine XYZ and every texture are pure
	// arithmetic and produce whatever numbers they are given. Only two things
	// carry a frame: the mesh PRIMITIVES, which have to lay their vertices out
	// somewhere, and the Godot-only terrain family, which treats .y as height.
	//
	// So a bridged graph is handled the way an exporter handles a mesh: run it
	// UNCHANGED, so it computes exactly Blender's numbers, and convert once at
	// the end. The alternative -- translating vector values as they cross --
	// cannot work, because a vector assembled by Combine XYZ has no value to
	// translate and its three scalars are individually meaningless.
	//
	// GODOT is the default, so every graph authored in the Godot editor, and
	// every .tres already on disk, behaves exactly as before.
	int source_frame = 0; // 0 GODOT, 1 BLENDER

public:
	// Node management
	int add_node(const Ref<ProcNode> &p_node);
	void remove_node(int p_idx);
	Ref<ProcNode> get_node(int p_idx) const;

	void set_source_frame(int p_frame) { source_frame = p_frame; }
	int get_source_frame() const { return source_frame; }
	bool is_blender_frame() const { return source_frame == 1; }
	int get_node_count() const { return nodes.size(); }

	// Connection management
	void connect_sockets(int p_from_node, int p_from_socket, int p_to_node, int p_to_socket);
	void disconnect_sockets(int p_from_node, int p_from_socket, int p_to_node, int p_to_socket);
	void clear_connections_for_node(int p_node_idx);

	// Find what's connected to a specific input socket
	// Returns -1 if nothing connected
	int get_input_source_node(int p_node_idx, int p_socket_idx) const;
	int get_input_source_socket(int p_node_idx, int p_socket_idx) const;

	// Get all connections
	const Vector<ProcConnection> &get_connections() const { return connections; }
	// Evaluate the graph and read one output socket. For tests and debugging:
	// it does not need a geometry output node, so a single value node can be
	// exercised on its own.
	Variant evaluate_socket(int p_node_idx, int p_socket_idx) const;

	// Evaluate phacelle_noise from src/shared/erosion_filter.inc directly.
	//
	// This exists so the Blender port can be checked against THE REAL FILTER
	// rather than against a Python transcription of it. Three bugs this session
	// were faithful ports of a wrong reference, and a transcription used as the
	// reference can only agree with itself.
	//
	// Returns (interpolated.x, interpolated.y, side_dir.x, side_dir.y).
	static Vector4 debug_phacelle_noise(Vector2 p, Vector2 dir, float freq,
			float offset, float normalization);

	// erosion_filter, same reasoning. Returns
	// [delta.x, delta.y, delta.z, magnitude, ridge].
	static PackedFloat32Array debug_erosion_filter(Vector2 p, Vector3 height_and_slope,
			float fade_target, float strength, float gully_weight, float detail,
			Vector4 rounding, Vector4 onset, Vector2 assumed_slope,
			float scale, int octaves, float lacunarity, float gain,
			float cell_scale, float normalization, float max_octaves);

	// erosion_node_eval_full -- the wrapper ErosionNoiseNode is built on, and
	// the last layer of the filter the Blender port had no reference for.
	// Everything under it (erosion_hash, the noise, phacelle_noise,
	// erosion_filter) is already checked against the two probes above; this
	// closes the stack.
	//
	// Nineteen parameters because that is what the function takes. Packing them
	// into an array would make the probe shorter and the call site unreadable,
	// and the whole point of these statics is that the test names the same
	// arguments the shipping code does.
	//
	// Returns (eroded * height_scale, ridge).
	static Vector2 debug_erosion_node_eval_full(Vector2 p, Vector3 gn,
			float height_scale, float erosion_strength, float cell_size,
			int octaves, float gain, float peak_height_divisor,
			float gully_band_scale, float input_height_amplitude,
			float octave_lacunarity, float gully_depth,
			float fine_gully_slope_restriction, float gully_normalize_threshold,
			Vector2 height_offset_pair, Vector4 rounding_set, Vector4 onset_set,
			Vector2 assumed_slope_pair, float max_octaves);

	int get_connection_count() const { return connections.size(); }
	PackedInt32Array get_connections_flat() const;
	// Multi-input support: how many links reach one input socket, and each in
	// the order they were made.
	int get_input_link_count(int p_node_idx, int p_socket_idx) const;
	bool get_input_link_at(int p_node_idx, int p_socket_idx, int p_k,
			int &r_from_node, int &r_from_socket) const;

	// Find output node (first node with no outputs connected to anything downstream)
	int find_output_node() const;

	// Compile all GPU-capable nodes to a standalone GLSL function
	// Returns GLSL code with: float <function_name>(vec3 pos) { ... }
	// For use by external systems (dc-terrain density evaluation, etc.)
	// When p_clamp_octaves is true, octave counts use min(N, pn_max_octaves)
	// so the caller can enforce Nyquist limits per voxel resolution.
	// p_skip_types names node TYPES to leave out of the emitted function --
	// get_node_type(), e.g. "ErosionNoise". A layer that is skipped contributes
	// nothing AND drags nothing in: with no erosion node the noise library is
	// not emitted either, because nothing left mentions noise. That is what
	// makes an in-editor "erosion on/off" a real toggle rather than a strength
	// of zero, which costs the same octave loop either way.
	// THE CPU SIDE OF proc_density, and the reason it exists: dc-terrain answered
	// every CPU density query -- collision, sample_density, the mesher's grid
	// fill -- from a hand-written replica that recognised two node TITLES, so
	// any other graph rendered one thing and collided with another.
	//
	// The result is chosen by EXACTLY the rules the GLSL emitter uses: an
	// explicit Group Output float if the graph has one, otherwise the
	// displacement of each point's y, which is what `pos.y - query_y` means in
	// the shader. Two conventions would be two answers.
	//
	// BATCH IS THE PRIMARY FORM. Measured over a 35,937-point chunk fill, the
	// executor's own overhead is 0.1 ms -- about 3 ns a sample -- while the
	// erosion filter those points run is 206 ms, which the replica pays too. The
	// machinery is not the expensive part; the algorithm is.
	PackedFloat32Array evaluate_density_batch(const PackedVector3Array &p_positions);

	// One point, for collision and the other scalar queries. Holds its geometry
	// and its plan resident, because re-converting an input mesh per call is
	// what made the generic host path cost 15.8 us.
	float evaluate_density_at(const Vector3 &p_pos);

	String compile_to_glsl_function(const String &p_function_name = "proc_density",
			bool p_clamp_octaves = false,
			const PackedStringArray &p_skip_types = PackedStringArray()) const;

	// WHICH VALUE THE GRAPH'S DENSITY IS -- the one rule, in one place.
	//
	// An explicit Group Output with a CONNECTED FLOAT input is the graph saying
	// what it produces; anything else means "the displacement of pos.y". This
	// was written TWICE -- here for the CPU evaluator and again as a loop in
	// gpu_compiler.cpp for the emitter -- with a comment on one of them saying
	// it followed "the same rules gpu_compiler.cpp uses", which asserts
	// agreement rather than enforcing it. A mutation to one left the other
	// answering the old way, which is how the duplication was found
	// (hyper-games #65 NEXT 5).
	bool find_density_output(int &r_node, int &r_socket) const;

	// The graph index of the node the authoring tool called p_source_id, or -1.
	// An id is NOT an index: the importer skips nodes it cannot map, so every
	// index after a skipped one is shifted.
	int find_node_by_source_id(int p_source_id) const;

	// THE GRAPH'S OWN EXTENT, declared beside its density.
	//
	// A Blender group interface takes any number of named, typed outputs, so a
	// graph says how big it is the same way it says what its density is: four
	// sockets on the Group Output, read BY NAME -- "Bounds Min", "Bounds Max",
	// "Voxel Size". Nothing in the bridge needed changing for this; the Group
	// Output node already crosses carrying its whole interface.
	//
	// Returns an empty Dictionary when the graph declares no bounds, which is
	// the honest answer for an unbounded field and is what
	// DCGenerator::get_bounds already defaults to.
	//
	// ⚠ THE BOUNDS ARE IN THE GRAPH'S FRAME. A bridged graph is authored in
	// Blender's, so the values are Blender coordinates and a consumer wants
	// world. M = (x, y, z) -> (-x, z, y) is applied here, and because M NEGATES
	// x a converted pair is no longer sorted -- min/max are recomputed
	// componentwise afterwards. Skipping that yields an inverted box, which
	// reads downstream as "the terrain is missing" rather than as a frame error.
	Dictionary get_density_bounds() const;

	// THE BATCH PATH'S TEXT, FOR THE GATE.
	//
	// compile_batch's output is otherwise reachable only through a GPU
	// dispatch, so the one path where `pc.paramN` is CORRECT had no instrument
	// that could read it -- which is how Set Position's packer came to read
	// socket 1 and call it the offset vector, for as long as socket 1 has been
	// Selection. This returns the same String GraphExecutor hands to
	// create_pipeline. It compiles nothing and touches no RenderingDevice.
	String compile_batch_glsl(const PackedInt32Array &p_node_indices) const;

	// Access
	const Vector<Ref<ProcNode>> &get_nodes() const { return nodes; }

	ProcNodeGraph() = default;
	~ProcNodeGraph();

private:
	// Scratch for the density entry points.
	//
	// ⚠ THE PLAN, not the geometry, is what a scalar query cannot afford to
	// rebuild. Measured: compiling it per call put a single query at 18.84 us --
	// WORSE than the 15.8 us of the generic host path this was meant to beat,
	// and the mesh conversion I had blamed was not the cost at all.
	Ref<ProcGeometry> _density_scratch;
	// By POINTER, behind a forward declaration: including graph_executor.h here
	// is a cycle -- it needs ProcNodeGraph -- and the error it produces names
	// ProcZoneInfo rather than the cycle.
	ExecutionPlan *_density_plan = nullptr;
	bool _density_plan_dirty = true;
};

} // namespace godot

#endif // PROC_NODES_PROC_NODE_GRAPH_H