OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / nodes / noise_nodes.h

482 lines · c ref:
#ifndef PROC_NODES_NOISE_NODES_H
#define PROC_NODES_NOISE_NODES_H

#include "../graph/proc_node.h"
#include "erosion_wrapper_glsl.h"   // HeightBlockMips, the shared sampler

#include <map>
#include <vector>

namespace godot {

// Simplex Noise — Ashima polynomial permutation (matches GPU + Blender GeoNodes group)
class NoiseTextureNode : public ProcNode {
	GDCLASS(NoiseTextureNode, ProcNode);

protected:
	static void _bind_methods();

	float scale = 5.0f;
	int detail = 4;
	float roughness = 0.5f;
	float lacunarity = 2.0f;

public:
	String compile_glsl_prelude(int p_node_idx) const override;
	StringName get_node_type() const override { return "SimplexNoise"; }
	String get_node_title() const override { return "Simplex Noise"; }
	String get_node_category() const override { return "Texture"; }
	bool supports_gpu() const override { return true; }

	void set_scale(float p_scale) { scale = p_scale; }
	float get_scale() const { return scale; }
	void set_detail(int p_detail) { detail = p_detail; }
	int get_detail() const { return detail; }
	void set_roughness(float p_rough) { roughness = p_rough; }
	float get_roughness() const { return roughness; }
	void set_lacunarity(float p_lac) { lacunarity = p_lac; }
	float get_lacunarity() const { return lacunarity; }

	void execute(ProcEvalContext &p_ctx, int p_node_idx) override;
	String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;

	NoiseTextureNode();
};

// Noise Texture — Blender's exact Perlin noise (Jenkins hash + fade + trilinear)
// Mirrors Blender's ShaderNodeTexNoise: same sockets, same defaults, same names.
// Includes all 5 fractal variants via noise_type property.
class PerlinNoiseNode : public ProcNode {
	GDCLASS(PerlinNoiseNode, ProcNode);

protected:
	static void _bind_methods();

	int noise_dimensions = 3; // 1-4
	int noise_type = 1;       // 0=Multifractal, 1=fBM, 2=Hybrid, 3=Ridged, 4=Hetero
	bool normalize = true;

public:
	StringName get_node_type() const override { return "NoiseTexture"; }
	String get_node_title() const override { return "Noise Texture"; }
	String get_node_category() const override { return "Texture"; }
	// ON since 2026-09-05. It used to emit the OLD arithmetic -- a different
	// lattice, no per-dimension scale constant, no distortion -- from a
	// hand-written GLSL Perlin that nothing compared against this node's C++.
	// There is no second implementation now: compile_glsl calls into
	// blender_perlin.inc, the same text execute() runs through the shim.
	bool supports_gpu() const override { return true; }
	String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;
	String compile_glsl_prelude(int p_node_idx) const override;

	void set_noise_dimensions(int p_dim) { noise_dimensions = CLAMP(p_dim, 1, 4); }
	int get_noise_dimensions() const { return noise_dimensions; }
	void set_noise_type(int p_type) { noise_type = CLAMP(p_type, 0, 4); }
	int get_noise_type() const { return noise_type; }
	void set_normalize(bool p_norm) { normalize = p_norm; }
	bool get_normalize() const { return normalize; }

	void execute(ProcEvalContext &p_ctx, int p_node_idx) override;

	PerlinNoiseNode();
};

// Displace — displaces vertices along normals using Perlin fBM noise.
// Faithful mirror of Blender's ProcNodes_Displace group (ShaderNodeTexNoise fBM).
// Sockets match Blender exactly: Geometry, Strength, Scale, Detail, Roughness, Lacunarity.
// For other noise variants (Ridged, Hybrid, etc.), compose PerlinNoiseNode → SetPosition.
class DisplaceNode : public ProcNode {
	GDCLASS(DisplaceNode, ProcNode);

protected:
	static void _bind_methods();

public:
	StringName get_node_type() const override { return "Displace"; }
	String get_node_title() const override { return "Displace"; }
	String get_node_category() const override { return "Geometry"; }
	bool supports_gpu() const override { return true; }
	String compile_glsl_prelude(int p_node_idx) const override;

	// Strength, Scale, Detail, Roughness, Lacunarity -- five floats in
	// param0..param4. See ProcNode::get_push_constant_sockets().
	PackedInt32Array get_push_constant_sockets() const override {
		PackedInt32Array m;
		m.push_back(1); m.push_back(2); m.push_back(3); m.push_back(4); m.push_back(5);
		return m;
	}

	void execute(ProcEvalContext &p_ctx, int p_node_idx) override;
	String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;

	DisplaceNode();
};

// Voronoi/Worley noise — distance to nearest cell center + cell color
// Voronoi Texture — Blender's, ported in src/shared/blender_voronoi.h.
//
// What was here was a hand-rolled 3x3x3 F1 with a hash of its own: two input
// sockets against Blender's nine, two outputs against five, 3D only, one
// feature, one metric, no fractal octaves and no randomness. It produced
// plausible cells, which is the worst way for a bridge to be wrong -- a graph
// crosses, evaluates, and is a different function from the one the author was
// looking at in Blender.
class VoronoiTextureNode : public ProcNode {
	GDCLASS(VoronoiTextureNode, ProcNode);

	int voronoi_dimensions = 3; // 1..4
	int feature = 0;            // SHD_VORONOI_*: F1, F2, SMOOTH_F1, DISTANCE_TO_EDGE, N_SPHERE_RADIUS
	int metric = 0;             // EUCLIDEAN, MANHATTAN, CHEBYCHEV, MINKOWSKI
	bool normalize = false;

protected:
	static void _bind_methods();

public:
	StringName get_node_type() const override { return "VoronoiTexture"; }
	String get_node_title() const override { return "Voronoi Texture"; }
	String get_node_category() const override { return "Texture"; }
	// ON since 2026-09-05. The GLSL used to be a THIRD arithmetic -- a
	// hand-rolled 3x3x3 F1 with a hash of its own. It now calls into
	// blender_voronoi.inc, the same text execute() runs through the shim.
	bool supports_gpu() const override { return true; }
	String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;
	String compile_glsl_prelude(int p_node_idx) const override;

	void set_voronoi_dimensions(int p_d) { voronoi_dimensions = p_d; }
	int get_voronoi_dimensions() const { return voronoi_dimensions; }
	void set_feature(int p_f) { feature = p_f; }
	int get_feature() const { return feature; }
	void set_metric(int p_m) { metric = p_m; }
	int get_metric() const { return metric; }
	void set_normalize(bool p_n) { normalize = p_n; }
	bool get_normalize() const { return normalize; }

	void execute(ProcEvalContext &p_ctx, int p_node_idx) override;

	VoronoiTextureNode();
};

// Wave Texture — sine/saw/triangle wave patterns
// Wave Texture — Blender's, which is bands or rings in four directions each,
// three profiles, and a Perlin fBM distortion pass. Ours was two sockets, one
// output, and a wave along (x + y + z) that matches none of Blender's
// directions.
class WaveTextureNode : public ProcNode {
	GDCLASS(WaveTextureNode, ProcNode);

protected:
	static void _bind_methods();

	int wave_type = 0;       // 0 BANDS, 1 RINGS
	int bands_direction = 0; // X, Y, Z, DIAGONAL
	int rings_direction = 0; // X, Y, Z, SPHERICAL
	int wave_profile = 0;    // SIN, SAW, TRI

public:
	StringName get_node_type() const override { return "WaveTexture"; }
	String get_node_title() const override { return "Wave Texture"; }
	String get_node_category() const override { return "Texture"; }
	// ON since 2026-09-05. The GLSL was the old (x + y + z) wave -- one
	// direction, no rings, no profile, no distortion. It now mirrors
	// execute(), with the distortion octaves coming from blender_perlin.inc.
	bool supports_gpu() const override { return true; }
	String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;
	String compile_glsl_prelude(int p_node_idx) const override;

	void set_wave_type(int p_t) { wave_type = p_t; }
	int get_wave_type() const { return wave_type; }
	void set_bands_direction(int p_d) { bands_direction = p_d; }
	int get_bands_direction() const { return bands_direction; }
	void set_rings_direction(int p_d) { rings_direction = p_d; }
	int get_rings_direction() const { return rings_direction; }
	void set_wave_profile(int p_p) { wave_profile = p_p; }
	int get_wave_profile() const { return wave_profile; }

	void execute(ProcEvalContext &p_ctx, int p_node_idx) override;

	WaveTextureNode();
};

// White Noise — uniform random noise per position
// White Noise — Blender's, which is `hash_float_to_float` and
// `hash_float_to_float3` and nothing else. That makes it the exact oracle for
// the hash every other texture stands on, which is why it is ported first.
//
// Ours was `fract(sin(dot(...)))` with one input socket and one output: the
// right shape, unrelated numbers.
class WhiteNoiseNode : public ProcNode {
	GDCLASS(WhiteNoiseNode, ProcNode);

	int noise_dimensions = 3; // 1..4, matching Blender's noise_dimensions

protected:
	static void _bind_methods();

public:
	StringName get_node_type() const override { return "WhiteNoise"; }
	String get_node_title() const override { return "White Noise"; }
	String get_node_category() const override { return "Texture"; }
	// ON since 2026-09-05, and this is the first of the five texture nodes to
	// come back, because it is the one with nothing above the hash. There is
	// no second implementation to drift from: compile_glsl emits calls into
	// blender_hash.inc, which is the same text execute() runs through the C++
	// shim. What used to be here -- "the GLSL is NOT this function" -- was
	// true of a hand-written copy, and the way to stop saying it was to
	// remove the copy rather than to write a better one.
	bool supports_gpu() const override { return true; }
	String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;
	String compile_glsl_prelude(int p_node_idx) const override;

	void set_noise_dimensions(int p_d) { noise_dimensions = p_d; }
	int get_noise_dimensions() const { return noise_dimensions; }

	void execute(ProcEvalContext &p_ctx, int p_node_idx) override;

	WhiteNoiseNode();
};

// Gradient Texture — linear/radial/spherical gradient
class GradientTextureNode : public ProcNode {
	GDCLASS(GradientTextureNode, ProcNode);

protected:
	static void _bind_methods();

	int gradient_type = 0; // 0=LINEAR, 1=RADIAL, 2=SPHERICAL

public:
	StringName get_node_type() const override { return "GradientTexture"; }
	String get_node_title() const override { return "Gradient Texture"; }
	String get_node_category() const override { return "Texture"; }
	bool supports_gpu() const override { return true; }

	void set_gradient_type(int p_type) { gradient_type = p_type; }
	int get_gradient_type() const { return gradient_type; }

	void execute(ProcEvalContext &p_ctx, int p_node_idx) override;
	String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;

	GradientTextureNode();
};

// GradientNoise — iq's gradient noise with analytical derivatives.
// Returns height + dh/dx + dh/dz per vertex. Outputs float field (height)
// and vec3 field (height, dh/dx, dh/dz). Essential for erosion pipeline.
class GradientNoiseNode : public ProcNode {
	GDCLASS(GradientNoiseNode, ProcNode);

protected:
	static void _bind_methods();

public:
	StringName get_node_type() const override { return "GradientNoise"; }
	String get_node_title() const override { return "Gradient Noise"; }
	String get_node_category() const override { return "Noise"; }
	bool supports_gpu() const override { return true; }
	String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;

	void execute(ProcEvalContext &p_ctx, int p_node_idx) override;

	GradientNoiseNode();
};

// FractalTerrain — complete eroded terrain generator in one node.
// Exact translation of runevision's Shadertoy: base FractalNoise + Erosion.
// Takes a flat MeshGrid, outputs fully displaced + eroded terrain.
// All-in-one for convenience. Parameters match Shadertoy defaults.
class FractalTerrainNode : public ProcNode {
	GDCLASS(FractalTerrainNode, ProcNode);

protected:
	static void _bind_methods();

public:
	StringName get_node_type() const override { return "FractalTerrain"; }
	String get_node_title() const override { return "Fractal Terrain"; }
	String get_node_category() const override { return "Noise"; }

	void execute(ProcEvalContext &p_ctx, int p_node_idx) override;

	FractalTerrainNode();
};

// Erosion Noise — directional erosion gullies with analytical derivatives.
// clayjohn (2018) → Fewes (2023) → runevision (2025). MIT licensed.
// Cosine-wave stripes along terrain slope with multi-octave branching.
// Each octave modifies the slope via analytical derivatives, causing
// natural gully branching without recomputing slope from geometry.
//
// Key: cos(waveInput) for height = peaks get no erosion (cos=1 at zero slope).
// slopePower sharpens peaks. cellScale controls gully length.
// ---------------------------------------------------------------------------
// HeightfieldNode — terrain from a baked heightmap tile.
//
// Emits gn_height / gn_slope, the SAME two names GradientNoiseNode emits, so it
// drops straight into ErosionNoise's Height/Slope sockets (7/8) with nothing
// else changed. Swap the node, get a different terrain: that is the whole point
// of a composed density graph, and it is why this is a node rather than another
// flag on the generator.
//
// The tile is the node's own argument. dc-terrain never learns what a
// heightfield is -- it transports the floats as opaque custom data and meshes
// whatever density body it is handed.
//
// ONE TILE FOR NOW. The sampler already addresses in world metres against an
// origin, so a grid of tiles is a change to hblk_texel's addressing (pick the
// tile, then the texel) and nothing above it. Confirm one first.
class HeightfieldNode : public ProcNode {
	GDCLASS(HeightfieldNode, ProcNode);

	String tile_path_;
	int    n_ = 0;
	float  mpp_ = 30.0f, ox_ = 0.0f, oz_ = 0.0f;
	PackedFloat32Array data_;
	HeightBlockMips    mips_;

	// ---- THE TILE LIST -------------------------------------------------------
	//
	// The 900 km world is 119x119 = 14,161 tiles and 841 are generated, so the
	// set is INCOMPLETE and it GROWS. A stitched block cannot express that: it
	// fixes membership at bake time and there is no solid 119x119 to stitch.
	//
	// So tiles are held as a LIST keyed by world position, absent cells read as
	// a flat STUB rather than as an error, and add_tile() works at runtime.
	// Those three are one requirement, not three: you cannot ship an incomplete
	// world without a stub, and a stub is only useful if it can later be
	// replaced without a reload.
	//
	// ⚠ ADDITIVE AND INERT. data_/mips_/get_gpu_data() are untouched and still
	// describe the single tile_path_ tile, so every existing map emits exactly
	// the GLSL it emitted before. Nothing consumes this list yet; the GPU-side
	// addressing change ("pick the tile, then the texel") is its own step.
	struct HTile {
		int   n = 0;              // stored side INCLUDING the 1 px apron
		float mpp = 30.0f, ox = 0.0f, oz = 0.0f;
		std::vector<float> h;     // n*n, row = z, column = x
	};
	std::map<int64_t, HTile> tiles_;
	float stub_height_ = 0.0f;
	// The world the table is sized to. map_dc_diffusion.gd: root span =
	// 28 * 2^(OCTREE_DEPTH-1) * VOXEL_SIZE, OCTREE_DEPTH 16. A PROPERTY, not a
	// constant, because sizing the table to the tiles you hold is what makes a
	// new tile recompile the shader.
	float world_span_m_ = 28.0f * 32768.0f;
	HeightGridPack grid_;       // rebuilt whenever tiles_ changes
	void _rebuild_grid();
	void _grid_upsert(int p_tx, int p_ty);

	// ⚠ AXIS SWAP, and it is stitch_block.py's convention, not a choice here:
	// "tx indexes the FIRST array axis -> WORLD Z; ty indexes the SECOND ->
	// WORLD X". Getting this backwards mirrors the world and looks plausible.
	static int64_t _key(int tx, int ty) {
		return ((int64_t)tx << 32) ^ (uint32_t)ty;
	}
	bool _load_hblk(const String &p_path, HTile &out) const;

protected:
	static void _bind_methods();

public:
	HeightfieldNode();

	StringName get_node_type() const override { return "Heightfield"; }
	String get_node_title() const override { return "Heightfield"; }
	String get_node_category() const override { return "Terrain"; }
	bool supports_gpu() const override { return true; }
	String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;
	String compile_glsl_prelude(int p_node_idx) const override;
	void execute(ProcEvalContext &p_ctx, int p_node_idx) override;

	void set_tile_path(const String &p_path);
	String get_tile_path() const { return tile_path_; }

	// ---- tile list (see the note above) --------------------------------------
	/// Load one per-tile .hblk into the grid. Safe to call at RUNTIME.
	bool add_tile(const String &p_path);
	/// Height for absent cells. A flat stub, not an error.
	void  set_stub_height(float p_h) { stub_height_ = p_h; }
	float get_stub_height() const { return stub_height_; }
	int   tile_count() const { return (int)tiles_.size(); }
	bool  has_tile(int p_tx, int p_ty) const { return tiles_.count(_key(p_tx, p_ty)) > 0; }
	/// World-metre sample across the whole grid; stub where no tile is loaded.
	float sample_height(float p_x, float p_z) const;
	void  clear_tiles() { tiles_.clear(); _rebuild_grid(); }
	void  set_world_span(float p_m) { world_span_m_ = p_m; _rebuild_grid(); }
	float get_world_span() const { return world_span_m_; }
	bool  has_grid() const { return grid_.ok && grid_.tiles > 0; }
	/// The packed layout's scalars, for the engine's CPU twin. Keys match
	/// HeightGridDesc's fields.
	Dictionary get_grid_info() const;

	// The opaque floats dc-terrain uploads. THE GRID WINS when tiles are loaded:
	// data_/mips_ still describe the single tile_path_ tile, so a graph that has
	// neither keeps emitting exactly what it emitted before.
	PackedFloat32Array get_gpu_data() const;
	int get_tile_size() const { return n_; }
	float get_tile_mpp() const { return mpp_; }
	Vector2 get_tile_origin() const { return Vector2(ox_, oz_); }
};

class ErosionNoiseNode : public ProcNode {
	GDCLASS(ErosionNoiseNode, ProcNode);

protected:
	static void _bind_methods();

public:
	StringName get_node_type() const override { return "ErosionNoise"; }
	String get_node_title() const override { return "Erosion Noise"; }
	String get_node_category() const override { return "Noise"; }
	bool supports_gpu() const override { return true; }
	String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;

	void execute(ProcEvalContext &p_ctx, int p_node_idx) override;

	ErosionNoiseNode();
};

// AltitudeColor — colors vertices by height using the Shadertoy material recipe.
// cliff(grey) → dirt(brown) → grass(green) → snow(white) → sand → water(blue).
// Takes geometry, reads Y height, outputs geometry with vertex colors.
class AltitudeColorNode : public ProcNode {
	GDCLASS(AltitudeColorNode, ProcNode);

protected:
	static void _bind_methods();

public:
	StringName get_node_type() const override { return "AltitudeColor"; }
	String get_node_title() const override { return "Altitude Color"; }
	String get_node_category() const override { return "Utility"; }
	bool supports_gpu() const override { return true; }
	String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;

	void execute(ProcEvalContext &p_ctx, int p_node_idx) override;

	AltitudeColorNode();
};

// SlopeGradient — computes terrain slope direction from vertex positions.
// Takes geometry, outputs per-vertex 2D slope direction (as vec3 with Y=0).
// Uses finite differences from neighboring vertices.
class SlopeGradientNode : public ProcNode {
	GDCLASS(SlopeGradientNode, ProcNode);

protected:
	static void _bind_methods();

public:
	StringName get_node_type() const override { return "SlopeGradient"; }
	String get_node_title() const override { return "Slope Gradient"; }
	String get_node_category() const override { return "Geometry"; }

	void execute(ProcEvalContext &p_ctx, int p_node_idx) override;

	SlopeGradientNode();
};

} // namespace godot

#endif // PROC_NODES_NOISE_NODES_H