OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / core / proc_volume.cpp

197 lines · cpp ref:
#include "proc_volume.h"
#include "../gpu/proc_simplex_glsl.gen.h"
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/classes/shader_material.hpp>
#include <godot_cpp/classes/shader.hpp>

namespace godot {

void ProcVolume::_bind_methods() {
	ClassDB::bind_method(D_METHOD("build_fog_shader_code"), &ProcVolume::build_fog_shader_code);
	ClassDB::bind_method(D_METHOD("make_fog_material"), &ProcVolume::make_fog_material);
	ClassDB::bind_method(D_METHOD("get_glsl_source"), &ProcVolume::get_glsl_source);
	ClassDB::bind_method(D_METHOD("set_glsl_source", "source"), &ProcVolume::set_glsl_source);
	ClassDB::bind_method(D_METHOD("get_density_grid"), &ProcVolume::get_density_grid);
	ClassDB::bind_method(D_METHOD("set_density_grid", "grid"), &ProcVolume::set_density_grid);
	ClassDB::bind_method(D_METHOD("get_grid_origin"), &ProcVolume::get_grid_origin);
	ClassDB::bind_method(D_METHOD("set_grid_origin", "origin"), &ProcVolume::set_grid_origin);
	ClassDB::bind_method(D_METHOD("get_grid_size"), &ProcVolume::get_grid_size);
	ClassDB::bind_method(D_METHOD("set_grid_size", "size"), &ProcVolume::set_grid_size);
	ClassDB::bind_method(D_METHOD("get_grid_resolution"), &ProcVolume::get_grid_resolution);
	ClassDB::bind_method(D_METHOD("set_grid_resolution", "resolution"), &ProcVolume::set_grid_resolution);
	ClassDB::bind_method(D_METHOD("has_glsl"), &ProcVolume::has_glsl);
	ClassDB::bind_method(D_METHOD("has_grid"), &ProcVolume::has_grid);
	ClassDB::bind_method(D_METHOD("sample_density", "world_pos"), &ProcVolume::sample_density);

	ADD_PROPERTY(PropertyInfo(Variant::STRING, "glsl_source"), "set_glsl_source", "get_glsl_source");
	ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT32_ARRAY, "density_grid"), "set_density_grid", "get_density_grid");
	ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "grid_origin"), "set_grid_origin", "get_grid_origin");
	ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "grid_size"), "set_grid_size", "get_grid_size");
	ADD_PROPERTY(PropertyInfo(Variant::VECTOR3I, "grid_resolution"), "set_grid_resolution", "get_grid_resolution");
}

float ProcVolume::sample_density(const Vector3 &p_world_pos) const {
	if (density_grid.size() == 0 || grid_resolution.x <= 0 || grid_resolution.y <= 0 || grid_resolution.z <= 0) {
		return 0.0f;
	}

	// Map world position to grid coordinates [0, resolution)
	Vector3 local = (p_world_pos - grid_origin) / grid_size;
	float fx = local.x * grid_resolution.x - 0.5f;
	float fy = local.y * grid_resolution.y - 0.5f;
	float fz = local.z * grid_resolution.z - 0.5f;

	int x0 = (int)Math::floor(fx);
	int y0 = (int)Math::floor(fy);
	int z0 = (int)Math::floor(fz);
	float dx = fx - x0;
	float dy = fy - y0;
	float dz = fz - z0;

	int rx = grid_resolution.x;
	int ry = grid_resolution.y;
	int rz = grid_resolution.z;
	const float *grid = density_grid.ptr();
	int grid_size_total = rx * ry * rz;

	auto sample = [&](int ix, int iy, int iz) -> float {
		ix = CLAMP(ix, 0, rx - 1);
		iy = CLAMP(iy, 0, ry - 1);
		iz = CLAMP(iz, 0, rz - 1);
		int idx = iz * rx * ry + iy * rx + ix;
		return (idx < grid_size_total) ? grid[idx] : 0.0f;
	};

	// Trilinear interpolation
	float c000 = sample(x0, y0, z0);
	float c100 = sample(x0 + 1, y0, z0);
	float c010 = sample(x0, y0 + 1, z0);
	float c110 = sample(x0 + 1, y0 + 1, z0);
	float c001 = sample(x0, y0, z0 + 1);
	float c101 = sample(x0 + 1, y0, z0 + 1);
	float c011 = sample(x0, y0 + 1, z0 + 1);
	float c111 = sample(x0 + 1, y0 + 1, z0 + 1);

	float c00 = c000 * (1.0f - dx) + c100 * dx;
	float c10 = c010 * (1.0f - dx) + c110 * dx;
	float c01 = c001 * (1.0f - dx) + c101 * dx;
	float c11 = c011 * (1.0f - dx) + c111 * dx;

	float c0 = c00 * (1.0f - dy) + c10 * dy;
	float c1 = c01 * (1.0f - dy) + c11 * dy;

	return c0 * (1.0f - dz) + c1 * dz;
}

Ref<ProcVolume> ProcVolume::duplicate_volume() const {
	Ref<ProcVolume> dup;
	dup.instantiate();
	dup->glsl_source = glsl_source;
	dup->density_grid = density_grid;
	dup->grid_origin = grid_origin;
	dup->grid_size = grid_size;
	dup->grid_resolution = grid_resolution;
	return dup;
}


// ── Fog material ────────────────────────────────────────────────────────────

String ProcVolume::build_fog_shader_code() const {
	// The graph's function verbatim, then a fog() that drives DENSITY from it.
	//
	// WORLD_POSITION is the froxel's world-space point; the volume is evaluated
	// in its OWN space, so the FogVolume's placement moves the cloud without
	// recompiling anything.
	//
	// The density field is an SDF -- positive inside -- and fog wants an
	// OPACITY in 0..1. `edge_softness` is how many world units the transition
	// spans: 0 gives a hard-edged blob, large values a soft cloud. Multiplying
	// rather than stepping keeps it a continuous field, which is what the
	// froxel integrator wants.
	String src;
	src += "shader_type fog;\n\n";
	src += "// Emitted by proc-nodes: ProcNodeGraph::compile_to_glsl_function.\n";
	src += "// Do not edit here -- edit the graph.\n";
	// The simplex library, for the noise uniforms below. It is #ifndef-guarded,
	// so a graph that already emitted it (any Simplex Noise node) includes it
	// once -- Godot's shader preprocessor honours the guard, measured.
	src += String(PROC_SIMPLEX_GLSL);
	src += "\n";
	src += glsl_source;
	src += "\n";
	src += "uniform vec3 volume_origin = vec3(0.0);\n";
	src += "uniform float iso_level = 0.0;\n";
	src += "uniform float edge_softness : hint_range(0.001, 100.0) = 4.0;\n";
	src += "uniform float density_scale : hint_range(0.0, 32.0) = 1.0;\n";
	src += "uniform vec4 volume_albedo : source_color = vec4(1.0, 1.0, 1.0, 1.0);\n";
	src += "uniform vec3 volume_emission = vec3(0.0);\n";
	// ── HEIGHT GRADIENT ─────────────────────────────────────────────────────
	// Density falls to zero with ALTITUDE, so a cloud thins out at its top
	// instead of ending on the SDF's hard shell. Both ends are uniforms: a
	// cloud's shape and where it fades are different decisions, and baking the
	// fade into the graph would mean recompiling to move it.
	//
	// WORLD y, not volume-local: "high up" is a property of the world, and a
	// cloud moved upward should fade MORE, not carry its gradient with it.
	src += "uniform float height_fade_start = 1e20;\n";
	src += "uniform float height_fade_range : hint_range(0.001, 4096.0) = 200.0;\n";
	// ── NOISE VARIABILITY ───────────────────────────────────────────────────
	// Modulates the density the SDF gives, so the volume is a formation rather
	// than a smooth solid. Separate from any noise inside the graph: this one is
	// tunable on the material without recompiling anything, and it can drift
	// with TIME.
	//
	// ⚠ ps_fbm_noise_3d IS proc_simplex.inc -- the same text the CPU shim and
	// the emitted GLSL compile. A second noise implementation for the fog path
	// is precisely the divergence this repo spent months removing.
	src += "uniform float noise_scale : hint_range(0.0, 1.0) = 0.02;\n";
	src += "uniform float noise_strength : hint_range(0.0, 1.0) = 0.0;\n";
	src += "uniform int noise_octaves : hint_range(1, 8) = 3;\n";
	src += "uniform float noise_drift = 0.0;\n";
	src += "uniform float noise_roughness : hint_range(0.0, 1.0) = 0.5;\n";
	src += "uniform float noise_lacunarity : hint_range(1.0, 4.0) = 2.0;\n\n";
	src += "void fog() {\n";
	src += "\tvec3 vp = WORLD_POSITION - volume_origin;\n";
	src += "\tfloat d = proc_density(vp);\n";
	src += "\tfloat dens = clamp((d - iso_level) / edge_softness, 0.0, 1.0);\n";
	src += "\n\t// Altitude fade. The default start is 1e20, so a material that\n";
	src += "\t// sets neither uniform behaves exactly as it did before this existed.\n";
	src += "\tfloat hf = (WORLD_POSITION.y - height_fade_start) / max(height_fade_range, 0.001);\n";
	src += "\tdens *= 1.0 - clamp(hf, 0.0, 1.0);\n";
	src += "\n\t// Noise. strength 0 is a no-op, so this is opt-in too.\n";
	src += "\tif (noise_strength > 0.0) {\n";
	src += "\t\tvec3 np = vp * noise_scale + vec3(noise_drift * TIME, 0.0, 0.0);\n";
	src += "\t\tfloat n = ps_fbm_noise_3d(np.x, np.y, np.z, noise_octaves,\n";
	src += "\t\t\t\tnoise_roughness, noise_lacunarity) * 0.5 + 0.5;\n";
	src += "\t\tdens *= mix(1.0, n, noise_strength);\n";
	src += "\t}\n";
	src += "\n\tDENSITY = dens * density_scale;\n";
	src += "\tALBEDO = volume_albedo.rgb;\n";
	src += "\tEMISSION = volume_emission;\n";
	src += "}\n";
	return src;
}

Ref<Material> ProcVolume::make_fog_material() const {
	if (glsl_source.is_empty()) {
		// An empty volume would compile to a shader with no proc_density and
		// fail at the call site, which reads as a shader bug rather than as an
		// empty volume. Say which it is.
		UtilityFunctions::push_error(
				"proc-nodes: ProcVolume has no GLSL, so there is no fog material to "
				"make. A volume graph must end in a Volume to Mesh node (or carry a "
				"Group Output float) for the terminal to compile one.");
		return Ref<Material>();
	}
	Ref<Shader> sh;
	sh.instantiate();
	sh->set_code(build_fog_shader_code());
	Ref<ShaderMaterial> mat;
	mat.instantiate();
	mat->set_shader(sh);
	return mat;
}

} // namespace godot