#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