#include "math_nodes.h"
#include "../shared/blender_colorband.h"
#include "../shared/blender_curvemapping.h"
#include "../shared/blender_math_ops.h"
#include "../shared/blender_ramp_blend.h"
#include "../gpu/blender_ramp_blend_glsl.gen.h"
#include "../graph/graph_executor.h"
#include <godot_cpp/variant/utility_functions.hpp>
#include <cmath>
namespace godot {
// --- MathNode ---
void MathNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_operation", "operation"), &MathNode::set_operation);
ClassDB::bind_method(D_METHOD("get_operation"), &MathNode::get_operation);
ADD_PROPERTY(PropertyInfo(Variant::INT, "operation"), "set_operation", "get_operation");
}
MathNode::MathNode() {
// Blender's three inputs. MULTIPLY_ADD, COMPARE, SMOOTH_MIN, SMOOTH_MAX and
// WRAP all read the third one, and this node had two -- so a link to it was
// dropped and those five operations quietly used the socket default.
input_defs.push_back({ "Value", SocketType::Float, 0.5f });
input_defs.push_back({ "Value_001", SocketType::Float, 0.5f });
input_defs.push_back({ "Value_002", SocketType::Float, 0.5f });
output_defs.push_back({ "Value", SocketType::Float, Variant() });
}
void MathNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
SocketValue a = p_ctx.read_input(p_node_idx, 0);
SocketValue b = p_ctx.read_input(p_node_idx, 1);
SocketValue c = p_ctx.read_input(p_node_idx, 2);
if (!a.is_field && !b.is_field && !c.is_field) {
p_ctx.write_float(p_node_idx, 0,
bmath::math_op(operation, a.to_float(), b.to_float(), c.to_float()));
return;
}
int n = MAX(a.field_size, MAX(b.field_size, c.field_size));
if (n == 0) {
n = 1;
}
AttrDomain domain = a.is_field ? a.domain : (b.is_field ? b.domain : c.domain);
PackedFloat32Array result;
result.resize(n);
for (int i = 0; i < n; i++) {
result.ptrw()[i] = bmath::math_op(operation, a.float_at(i), b.float_at(i), c.float_at(i));
}
p_ctx.write_float_field(p_node_idx, 0, result, domain);
}
// BLENDER'S FLOAT MATH, IN GLSL, AGAINST THE SAME NUMBERING THE CPU USES.
//
// bmath::math_op in src/shared/blender_math_ops.h is the reference, and it is
// the code that SHIPS -- the Blender parity suite gates it at 71/71 over all 41
// operations. This is a second expression of it, which is exactly the shape the
// erosion filter got wrong once ("a transcription used as the reference can only
// ever agree with itself"), so it is not trusted: demo/glsl_op_gate_dump.gd
// pairs every operation's CPU answer with this emitted text and the standalone
// -lvulkan gate runs it on a real device. A transcription CHECKED against the
// shipping code over every branch is a different thing from one used as the
// reference.
//
// The safe_* family is why a degenerate argument gives a number rather than a
// NaN, and each picks a specific value: divide by zero is 0, log of a
// non-positive is 0, sqrt clamps at 0, and a negative base with a FRACTIONAL
// exponent is 0 while an integral one is not.
static const char *MATH_GLSL_PRELUDE = R"GLSL(
// GUARDED, because two nodes need these and a prelude is emitted per node.
// Vector Math's helpers are built on these, so its prelude carries a copy;
// deduplicating by string then either drops them or defines them twice
// depending on which node came first. GLSL HAS A PREPROCESSOR, so the
// duplicate is simply dropped and no emitter has to reason about it.
#ifndef PN_MATH_HELPERS
#define PN_MATH_HELPERS
float pn_safe_divide(float a, float b) { return (b != 0.0) ? a / b : 0.0; }
// ⛔ NOT mod(): GLSL's mod is FLOORED (a - b*floor(a/b)) and C's fmodf is
// TRUNCATED. Measured on an RTX 2070: mod(0.5, -1.0) is -0.5 where fmodf gives
// 0.5, and mod(-0.75, 2.0) is 1.25 where fmodf gives -0.75. MATH_FLOORED_MODULO
// is a separate operation precisely because these differ.
float pn_safe_mod(float a, float b) { return (b != 0.0) ? a - b * trunc(a / b) : 0.0; }
float pn_safe_floored_mod(float a, float b) { return (b != 0.0) ? a - floor(a / b) * b : 0.0; }
float pn_safe_log(float a, float base) {
if (a <= 0.0 || base <= 0.0) { return 0.0; }
return pn_safe_divide(log(a), log(base));
}
float pn_safe_sqrt(float a) { return sqrt(max(a, 0.0)); }
float pn_safe_inv_sqrt(float a) { return (a > 0.0) ? 1.0 / sqrt(a) : 0.0; }
float pn_safe_asin(float a) { return asin(clamp(a, -1.0, 1.0)); }
float pn_safe_acos(float a) { return acos(clamp(a, -1.0, 1.0)); }
float pn_safe_pow(float base, float expo) {
// A negative base with a fractional exponent is 0, an integral one is not.
// ⛔ AND GLSL's pow() IS UNDEFINED FOR A NEGATIVE BASE -- measured, it
// returns NaN for pow(-0.75, 2.0) where C's powf gives 0.5625. The integral
// case has to be done by hand: magnitude from the absolute value, sign from
// whether the exponent is odd.
if (base < 0.0) {
if (expo != float(int(expo))) { return 0.0; }
float m = pow(abs(base), expo);
return (mod(abs(expo), 2.0) == 1.0) ? -m : m;
}
return pow(base, expo);
}
float pn_wrap(float value, float maxv, float minv) {
float range = maxv - minv;
return (range != 0.0) ? value - (range * floor((value - minv) / range)) : minv;
}
float pn_pingpong(float value, float scale) {
if (scale == 0.0) { return 0.0; }
float f = (value - scale) / (scale * 2.0);
return abs((f - floor(f)) * scale * 2.0 - scale);
}
float pn_smoothmin(float a, float b, float c) {
if (c != 0.0) {
float h = max(c - abs(a - b), 0.0) / c;
return min(a, b) - h * h * h * c * (1.0 / 6.0);
}
return min(a, b);
}
float pn_compatible_sign(float f) { return (f > 0.0) ? 1.0 : ((f < 0.0) ? -1.0 : 0.0); }
#endif
)GLSL";
String MathNode::compile_glsl_prelude(int p_node_idx) const {
// Always. GLSL has no nested functions, so helpers belong at file scope,
// and GpuCompiler drops a prelude it has already emitted -- keying that on
// the node's INDEX instead meant a graph whose Math node was not node 0
// emitted no helpers at all and could not compile.
return String(MATH_GLSL_PRELUDE);
}
String MathNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
const String a = p_ctx.in(p_node_idx, 0);
const String b = p_ctx.in(p_node_idx, 1);
const String c = p_ctx.in(p_node_idx, 2);
String e;
switch (operation) {
case bmath::MATH_ADD: e = "(" + a + " + " + b + ")"; break;
case bmath::MATH_SUBTRACT: e = "(" + a + " - " + b + ")"; break;
case bmath::MATH_MULTIPLY: e = "(" + a + " * " + b + ")"; break;
case bmath::MATH_DIVIDE: e = "pn_safe_divide(" + a + ", " + b + ")"; break;
case bmath::MATH_SINE: e = "sin(" + a + ")"; break;
case bmath::MATH_COSINE: e = "cos(" + a + ")"; break;
case bmath::MATH_TANGENT: e = "tan(" + a + ")"; break;
case bmath::MATH_ARCSINE: e = "pn_safe_asin(" + a + ")"; break;
case bmath::MATH_ARCCOSINE: e = "pn_safe_acos(" + a + ")"; break;
case bmath::MATH_ARCTANGENT: e = "atan(" + a + ")"; break;
case bmath::MATH_POWER: e = "pn_safe_pow(" + a + ", " + b + ")"; break;
case bmath::MATH_LOGARITHM: e = "pn_safe_log(" + a + ", " + b + ")"; break;
case bmath::MATH_MINIMUM: e = "min(" + a + ", " + b + ")"; break;
case bmath::MATH_MAXIMUM: e = "max(" + a + ", " + b + ")"; break;
case bmath::MATH_ROUND: e = "floor(" + a + " + 0.5)"; break;
case bmath::MATH_LESS_THAN: e = "((" + a + " < " + b + ") ? 1.0 : 0.0)"; break;
case bmath::MATH_GREATER_THAN: e = "((" + a + " > " + b + ") ? 1.0 : 0.0)"; break;
case bmath::MATH_MODULO: e = "pn_safe_mod(" + a + ", " + b + ")"; break;
case bmath::MATH_ABSOLUTE: e = "abs(" + a + ")"; break;
case bmath::MATH_ARCTAN2: e = "atan(" + a + ", " + b + ")"; break;
case bmath::MATH_FLOOR: e = "floor(" + a + ")"; break;
case bmath::MATH_CEIL: e = "ceil(" + a + ")"; break;
case bmath::MATH_FRACTION: e = "(" + a + " - floor(" + a + "))"; break;
case bmath::MATH_SQRT: e = "pn_safe_sqrt(" + a + ")"; break;
case bmath::MATH_INV_SQRT: e = "pn_safe_inv_sqrt(" + a + ")"; break;
case bmath::MATH_SIGN: e = "pn_compatible_sign(" + a + ")"; break;
case bmath::MATH_EXPONENT: e = "exp(" + a + ")"; break;
case bmath::MATH_RADIANS: e = "radians(" + a + ")"; break;
case bmath::MATH_DEGREES: e = "degrees(" + a + ")"; break;
case bmath::MATH_SINH: e = "sinh(" + a + ")"; break;
case bmath::MATH_COSH: e = "cosh(" + a + ")"; break;
case bmath::MATH_TANH: e = "tanh(" + a + ")"; break;
case bmath::MATH_TRUNC: e = "((" + a + " >= 0.0) ? floor(" + a + ") : ceil(" + a + "))"; break;
case bmath::MATH_SNAP: e = "(floor(pn_safe_divide(" + a + ", " + b + ")) * " + b + ")"; break;
case bmath::MATH_WRAP: e = "pn_wrap(" + a + ", " + b + ", " + c + ")"; break;
case bmath::MATH_COMPARE:
// FLT_EPSILON, not 0: the CPU floor is MAX(c, FLT_EPSILON), so a
// tolerance of 0 still matches exactly-equal values.
e = "(((" + a + " == " + b + ") || (abs(" + a + " - " + b + ") <= max(" + c +
", 1.1920929e-7))) ? 1.0 : 0.0)";
break;
case bmath::MATH_MULTIPLY_ADD: e = "((" + a + " * " + b + ") + " + c + ")"; break;
case bmath::MATH_PINGPONG: e = "pn_pingpong(" + a + ", " + b + ")"; break;
case bmath::MATH_SMOOTH_MIN: e = "pn_smoothmin(" + a + ", " + b + ", " + c + ")"; break;
case bmath::MATH_SMOOTH_MAX: e = "(-pn_smoothmin(-(" + a + "), -(" + b + "), " + c + "))"; break;
case bmath::MATH_FLOORED_MODULO: e = "pn_safe_floored_mod(" + a + ", " + b + ")"; break;
default: e = "0.0"; break;
}
return p_ctx.declare(p_node_idx, 0) + " = " + e + ";";
}
void VectorMathNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_operation", "operation"), &VectorMathNode::set_operation);
ClassDB::bind_method(D_METHOD("get_operation"), &VectorMathNode::get_operation);
ADD_PROPERTY(PropertyInfo(Variant::INT, "operation"), "set_operation", "get_operation");
}
VectorMathNode::VectorMathNode() {
// BLENDER'S LAYOUT EXACTLY, and here the index is the only thing that can
// identify a socket: Blender names all three vector inputs "Vector", so
// resolution by name is ambiguous and falls through to the index.
input_defs.push_back({ "Vector", SocketType::Vector, Vector3() });
input_defs.push_back({ "Vector_001", SocketType::Vector, Vector3() });
input_defs.push_back({ "Vector_002", SocketType::Vector, Vector3() });
input_defs.push_back({ "Scale", SocketType::Float, 1.0f });
output_defs.push_back({ "Vector", SocketType::Vector, Variant() });
output_defs.push_back({ "Value", SocketType::Float, Variant() });
}
void VectorMathNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
SocketValue a = p_ctx.read_input(p_node_idx, 0);
SocketValue b = p_ctx.read_input(p_node_idx, 1);
SocketValue c = p_ctx.read_input(p_node_idx, 2);
SocketValue sv = p_ctx.read_input(p_node_idx, 3);
bool is_field = a.is_field || b.is_field || c.is_field || sv.is_field;
if (!is_field) {
float fr;
Vector3 vr = bmath::vector_math_op(operation, a.to_vector(), b.to_vector(),
c.to_vector(), sv.to_float(), fr);
p_ctx.write_vector(p_node_idx, 0, vr);
p_ctx.write_float(p_node_idx, 1, fr);
return;
}
int n = MAX(MAX(a.field_size, b.field_size), MAX(c.field_size, sv.field_size));
if (n == 0) {
n = 1;
}
AttrDomain domain = a.is_field ? a.domain
: (b.is_field ? b.domain : (c.is_field ? c.domain : sv.domain));
PackedVector3Array vec_arr;
PackedFloat32Array float_arr;
vec_arr.resize(n);
float_arr.resize(n);
for (int i = 0; i < n; i++) {
float fr;
vec_arr.ptrw()[i] = bmath::vector_math_op(operation, a.vec_at(i), b.vec_at(i), c.vec_at(i),
sv.float_at(i), fr);
float_arr.ptrw()[i] = fr;
}
p_ctx.write_vec_field(p_node_idx, 0, vec_arr, domain);
p_ctx.write_float_field(p_node_idx, 1, float_arr, domain);
}
// --- CombineXYZNode ---
void CombineXYZNode::_bind_methods() {
}
CombineXYZNode::CombineXYZNode() {
input_defs.push_back({ "X", SocketType::Float, 0.0f });
input_defs.push_back({ "Y", SocketType::Float, 0.0f });
input_defs.push_back({ "Z", SocketType::Float, 0.0f });
output_defs.push_back({ "Vector", SocketType::Vector, Variant() });
}
void CombineXYZNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
SocketValue xv = p_ctx.read_input(p_node_idx, 0);
SocketValue yv = p_ctx.read_input(p_node_idx, 1);
SocketValue zv = p_ctx.read_input(p_node_idx, 2);
if (xv.is_field || yv.is_field || zv.is_field) {
int n = MAX(MAX(xv.field_size, yv.field_size), zv.field_size);
if (n == 0) n = 1;
PackedVector3Array result;
result.resize(n);
for (int i = 0; i < n; i++) {
result.ptrw()[i] = Vector3(xv.float_at(i), yv.float_at(i), zv.float_at(i));
}
p_ctx.write_vec_field(p_node_idx, 0, result);
} else {
p_ctx.write_vector(p_node_idx, 0, Vector3(xv.to_float(), yv.to_float(), zv.to_float()));
}
}
String CombineXYZNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
// A STATEMENT, not a fragment. This returned "vec3(x, y, z)" -- undefined
// identifiers, no assignment, no semicolon -- so any graph using it produced
// a shader that could not compile. The inputs come from the graph now.
return p_ctx.declare(p_node_idx, 0) + " = vec3(" + p_ctx.in(p_node_idx, 0) + ", " +
p_ctx.in(p_node_idx, 1) + ", " + p_ctx.in(p_node_idx, 2) + ");";
}
// --- SeparateXYZNode ---
void SeparateXYZNode::_bind_methods() {
}
SeparateXYZNode::SeparateXYZNode() {
input_defs.push_back({ "Vector", SocketType::Vector, Vector3() });
output_defs.push_back({ "X", SocketType::Float, Variant() });
output_defs.push_back({ "Y", SocketType::Float, Variant() });
output_defs.push_back({ "Z", SocketType::Float, Variant() });
}
void SeparateXYZNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
SocketValue v = p_ctx.read_input(p_node_idx, 0);
if (v.is_field) {
int n = v.field_size;
PackedFloat32Array x_arr, y_arr, z_arr;
x_arr.resize(n); y_arr.resize(n); z_arr.resize(n);
for (int i = 0; i < n; i++) {
Vector3 vi = v.vec_at(i);
x_arr.ptrw()[i] = vi.x;
y_arr.ptrw()[i] = vi.y;
z_arr.ptrw()[i] = vi.z;
}
p_ctx.write_float_field(p_node_idx, 0, x_arr);
p_ctx.write_float_field(p_node_idx, 1, y_arr);
p_ctx.write_float_field(p_node_idx, 2, z_arr);
} else {
Vector3 vi = v.to_vector();
p_ctx.write_float(p_node_idx, 0, vi.x);
p_ctx.write_float(p_node_idx, 1, vi.y);
p_ctx.write_float(p_node_idx, 2, vi.z);
}
}
String SeparateXYZNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
// Was "v.x; v.y; v.z" -- three expression statements over an identifier
// nothing declared. Three real outputs now, each its own variable.
const String v = p_ctx.in(p_node_idx, 0);
return p_ctx.declare(p_node_idx, 0) + " = (" + v + ").x;\n\t" +
p_ctx.declare(p_node_idx, 1) + " = (" + v + ").y;\n\t" +
p_ctx.declare(p_node_idx, 2) + " = (" + v + ").z;";
}
// --- BooleanMathNode ---
void BooleanMathNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_operation", "operation"), &BooleanMathNode::set_operation);
ClassDB::bind_method(D_METHOD("get_operation"), &BooleanMathNode::get_operation);
ADD_PROPERTY(PropertyInfo(Variant::INT, "operation"), "set_operation", "get_operation");
}
BooleanMathNode::BooleanMathNode() {
input_defs.push_back({ "A", SocketType::Bool, false });
input_defs.push_back({ "B", SocketType::Bool, false });
output_defs.push_back({ "Result", SocketType::Bool, Variant() });
}
void BooleanMathNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
bool a = p_ctx.read_bool(p_node_idx, 0);
bool b = p_ctx.read_bool(p_node_idx, 1);
bool result = false;
switch (operation) {
case BooleanMathOp::And: result = a && b; break;
case BooleanMathOp::Or: result = a || b; break;
case BooleanMathOp::Not: result = !a; break;
case BooleanMathOp::Xor: result = a != b; break;
case BooleanMathOp::Nand: result = !(a && b); break;
case BooleanMathOp::Nor: result = !(a || b); break;
case BooleanMathOp::Xnor: result = a == b; break;
}
p_ctx.write_bool(p_node_idx, 0, result);
}
String BooleanMathNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
// Was an expression fragment. NOT and the negated forms read only A, exactly
// as the CPU switch does.
const String a = p_ctx.in(p_node_idx, 0);
const String b = p_ctx.in(p_node_idx, 1);
String expr;
switch (operation) {
case BooleanMathOp::And: expr = "(" + a + " && " + b + ")"; break;
case BooleanMathOp::Or: expr = "(" + a + " || " + b + ")"; break;
case BooleanMathOp::Not: expr = "(!" + a + ")"; break;
case BooleanMathOp::Xor: expr = "(" + a + " != " + b + ")"; break;
case BooleanMathOp::Nand: expr = "(!(" + a + " && " + b + "))"; break;
case BooleanMathOp::Nor: expr = "(!(" + a + " || " + b + "))"; break;
case BooleanMathOp::Xnor: expr = "(" + a + " == " + b + ")"; break;
default: expr = "false"; break;
}
return p_ctx.declare(p_node_idx, 0) + " = " + expr + ";";
}
// --- CompareNode ---
void CompareNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_operation", "operation"), &CompareNode::set_operation);
ClassDB::bind_method(D_METHOD("get_operation"), &CompareNode::get_operation);
ADD_PROPERTY(PropertyInfo(Variant::INT, "operation"), "set_operation", "get_operation");
}
CompareNode::CompareNode() {
input_defs.push_back({ "A", SocketType::Float, 0.0f });
input_defs.push_back({ "B", SocketType::Float, 0.0f });
output_defs.push_back({ "Result", SocketType::Bool, Variant() });
}
static bool _compare_op(CompareOp op, float a, float b) {
switch (op) {
case CompareOp::Less: return a < b;
case CompareOp::LessEqual: return a <= b;
case CompareOp::Greater: return a > b;
case CompareOp::GreaterEqual: return a >= b;
case CompareOp::Equal: return fabsf(a - b) < 1e-6f;
case CompareOp::NotEqual: return fabsf(a - b) >= 1e-6f;
default: return false;
}
}
void CompareNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
SocketValue a_val = p_ctx.read_input(p_node_idx, 0);
SocketValue b_val = p_ctx.read_input(p_node_idx, 1);
if (!a_val.is_field && !b_val.is_field) {
// Scalar path
p_ctx.write_bool(p_node_idx, 0, _compare_op(operation, a_val.to_float(), b_val.to_float()));
return;
}
// Field path: element-wise, output int field (1=true, 0=false)
int n = MAX(a_val.field_size, b_val.field_size);
if (n == 0) n = 1;
PackedInt32Array result;
result.resize(n);
int32_t *out = result.ptrw();
for (int i = 0; i < n; i++) {
out[i] = _compare_op(operation, a_val.float_at(i), b_val.float_at(i)) ? 1 : 0;
}
p_ctx.write_int_field(p_node_idx, 0, result);
}
String CompareNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
// Was an expression fragment. EQUAL AND NOT-EQUAL ARE NOT == AND != : the
// CPU compares with a 1e-6 tolerance (_compare_op above), and a shader using
// == on floats would disagree with it on almost every value that matters.
const String a = p_ctx.in(p_node_idx, 0);
const String b = p_ctx.in(p_node_idx, 1);
String expr;
switch (operation) {
case CompareOp::Less: expr = "(" + a + " < " + b + ")"; break;
case CompareOp::LessEqual: expr = "(" + a + " <= " + b + ")"; break;
case CompareOp::Greater: expr = "(" + a + " > " + b + ")"; break;
case CompareOp::GreaterEqual: expr = "(" + a + " >= " + b + ")"; break;
case CompareOp::Equal: expr = "(abs(" + a + " - " + b + ") < 1e-6)"; break;
case CompareOp::NotEqual: expr = "(abs(" + a + " - " + b + ") >= 1e-6)"; break;
default: expr = "false"; break;
}
return p_ctx.declare(p_node_idx, 0) + " = " + expr + ";";
}
// --- ClampNode ---
void ClampNode::_bind_methods() {
}
ClampNode::ClampNode() {
input_defs.push_back({ "Value", SocketType::Float, 0.0f });
input_defs.push_back({ "Min", SocketType::Float, 0.0f });
input_defs.push_back({ "Max", SocketType::Float, 1.0f });
output_defs.push_back({ "Result", SocketType::Float, Variant() });
}
void ClampNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
SocketValue val = p_ctx.read_input(p_node_idx, 0);
float min_val = p_ctx.read_float(p_node_idx, 1);
float max_val = p_ctx.read_float(p_node_idx, 2);
if (val.is_field) {
int n = val.field_size;
PackedFloat32Array result;
result.resize(n);
for (int i = 0; i < n; i++) {
result.ptrw()[i] = CLAMP(val.float_at(i), min_val, max_val);
}
p_ctx.write_float_field(p_node_idx, 0, result);
} else {
p_ctx.write_float(p_node_idx, 0, CLAMP(val.to_float(), min_val, max_val));
}
}
String ClampNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
// Was "clamp(value, min_val, max_val)". CLAMP() on the CPU side is Godot's,
// which is min(max(x, lo), hi) -- the same order GLSL's clamp uses, so the
// two agree when lo > hi as well.
return p_ctx.declare(p_node_idx, 0) + " = clamp(" + p_ctx.in(p_node_idx, 0) + ", " +
p_ctx.in(p_node_idx, 1) + ", " + p_ctx.in(p_node_idx, 2) + ");";
}
// --- MapRangeNode ---
// BLENDER'S VECTOR MATH, IN GLSL. bmath::vector_math_op is the reference and
// the standalone -lvulkan gate compares every operation against it on a real
// device -- which is how MATH's pow() and mod() were caught, and those two
// traps are here too, componentwise.
//
// Three operations return a SCALAR (dot, distance, length) and Blender puts
// them on the node's second output. Both outputs are declared every time: GLSL
// has no way to leave one undefined, and a consumer reading the unused one gets
// the zero the CPU writes into r_value rather than whatever was on the stack.
static const char *VECMATH_GLSL_PRELUDE = R"GLSL(
#ifndef PN_VECMATH_HELPERS
#define PN_VECMATH_HELPERS
vec3 pn_vsafe_divide(vec3 a, vec3 b) {
return vec3(pn_safe_divide(a.x, b.x), pn_safe_divide(a.y, b.y), pn_safe_divide(a.z, b.z));
}
vec3 pn_vnormalize(vec3 v) {
// A zero vector normalizes to ZERO, not NaN -- Blender's normalize does the
// same, and GLSL's does not.
float l = length(v);
return (l > 0.0) ? v / l : vec3(0.0);
}
vec3 pn_vproject(vec3 p, vec3 v) {
if (v.x == 0.0 && v.y == 0.0 && v.z == 0.0) { return vec3(0.0); }
return v * (dot(p, v) / dot(v, v));
}
vec3 pn_vreflect(vec3 incident, vec3 normal) {
return incident - normal * (2.0 * dot(normal, incident));
}
vec3 pn_vrefract(vec3 incident, vec3 normal, float eta) {
float dot_ni = dot(normal, incident);
float k = 1.0 - eta * eta * (1.0 - dot_ni * dot_ni);
if (k < 0.0) { return vec3(0.0); }
return incident * eta - normal * (eta * dot_ni + sqrt(k));
}
vec3 pn_vfaceforward(vec3 vector, vec3 incident, vec3 reference) {
return (dot(reference, incident) < 0.0) ? vector : -vector;
}
vec3 pn_vsign(vec3 a) {
return vec3(pn_compatible_sign(a.x), pn_compatible_sign(a.y), pn_compatible_sign(a.z));
}
vec3 pn_vsafe_mod(vec3 a, vec3 b) {
return vec3(pn_safe_mod(a.x, b.x), pn_safe_mod(a.y, b.y), pn_safe_mod(a.z, b.z));
}
vec3 pn_vsafe_pow(vec3 a, vec3 b) {
return vec3(pn_safe_pow(a.x, b.x), pn_safe_pow(a.y, b.y), pn_safe_pow(a.z, b.z));
}
vec3 pn_vwrap(vec3 a, vec3 b, vec3 c) {
return vec3(pn_wrap(a.x, b.x, c.x), pn_wrap(a.y, b.y, c.y), pn_wrap(a.z, b.z, c.z));
}
vec3 pn_vsnap(vec3 a, vec3 b) {
return vec3(floor(pn_safe_divide(a.x, b.x)) * b.x,
floor(pn_safe_divide(a.y, b.y)) * b.y,
floor(pn_safe_divide(a.z, b.z)) * b.z);
}
#endif
)GLSL";
String VectorMathNode::compile_glsl_prelude(int p_node_idx) const {
// Depends on MathNode's helpers (pn_safe_divide, pn_safe_mod, pn_safe_pow,
// pn_wrap, pn_compatible_sign), so a graph with a Vector Math node and no
// Math node needs both. Emitting both here would duplicate them when a Math
// node IS present, so the float helpers are emitted unconditionally and the
// vector ones follow.
// The float helpers first: this node's own use pn_safe_divide and friends,
// and the emitter drops whichever copy arrives second.
return String(MATH_GLSL_PRELUDE) + String(VECMATH_GLSL_PRELUDE);
}
String VectorMathNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
const String a = p_ctx.in(p_node_idx, 0);
const String b = p_ctx.in(p_node_idx, 1);
const String c = p_ctx.in(p_node_idx, 2);
const String s = p_ctx.in(p_node_idx, 3);
String vec = "vec3(0.0)";
String val = "0.0";
switch (operation) {
case bmath::VEC_ADD: vec = "(" + a + " + " + b + ")"; break;
case bmath::VEC_SUBTRACT: vec = "(" + a + " - " + b + ")"; break;
case bmath::VEC_MULTIPLY: vec = "(" + a + " * " + b + ")"; break;
case bmath::VEC_DIVIDE: vec = "pn_vsafe_divide(" + a + ", " + b + ")"; break;
case bmath::VEC_CROSS_PRODUCT: vec = "cross(" + a + ", " + b + ")"; break;
case bmath::VEC_PROJECT: vec = "pn_vproject(" + a + ", " + b + ")"; break;
case bmath::VEC_REFLECT: vec = "pn_vreflect(" + a + ", pn_vnormalize(" + b + "))"; break;
case bmath::VEC_DOT_PRODUCT: val = "dot(" + a + ", " + b + ")"; break;
case bmath::VEC_DISTANCE: val = "distance(" + a + ", " + b + ")"; break;
case bmath::VEC_LENGTH: val = "length(" + a + ")"; break;
case bmath::VEC_SCALE: vec = "(" + a + " * " + s + ")"; break;
case bmath::VEC_NORMALIZE: vec = "pn_vnormalize(" + a + ")"; break;
case bmath::VEC_SNAP: vec = "pn_vsnap(" + a + ", " + b + ")"; break;
case bmath::VEC_FLOOR: vec = "floor(" + a + ")"; break;
case bmath::VEC_CEIL: vec = "ceil(" + a + ")"; break;
case bmath::VEC_MODULO: vec = "pn_vsafe_mod(" + a + ", " + b + ")"; break;
case bmath::VEC_FRACTION: vec = "(" + a + " - floor(" + a + "))"; break;
case bmath::VEC_ABSOLUTE: vec = "abs(" + a + ")"; break;
case bmath::VEC_MINIMUM: vec = "min(" + a + ", " + b + ")"; break;
case bmath::VEC_MAXIMUM: vec = "max(" + a + ", " + b + ")"; break;
case bmath::VEC_WRAP: vec = "pn_vwrap(" + a + ", " + b + ", " + c + ")"; break;
case bmath::VEC_SINE: vec = "sin(" + a + ")"; break;
case bmath::VEC_COSINE: vec = "cos(" + a + ")"; break;
case bmath::VEC_TANGENT: vec = "tan(" + a + ")"; break;
case bmath::VEC_REFRACT:
vec = "pn_vrefract(" + a + ", pn_vnormalize(" + b + "), " + s + ")"; break;
case bmath::VEC_FACEFORWARD:
vec = "pn_vfaceforward(" + a + ", " + b + ", " + c + ")"; break;
case bmath::VEC_MULTIPLY_ADD: vec = "((" + a + " * " + b + ") + " + c + ")"; break;
case bmath::VEC_POWER: vec = "pn_vsafe_pow(" + a + ", " + b + ")"; break;
case bmath::VEC_SIGN: vec = "pn_vsign(" + a + ")"; break;
case bmath::VEC_ROUND: vec = "floor(" + a + " + vec3(0.5))"; break;
default: break;
}
return p_ctx.declare(p_node_idx, 0) + " = " + vec + ";\n\t" +
p_ctx.declare(p_node_idx, 1) + " = " + val + ";";
}
void MapRangeNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_data_type", "t"), &MapRangeNode::set_data_type);
ClassDB::bind_method(D_METHOD("get_data_type"), &MapRangeNode::get_data_type);
ADD_PROPERTY(PropertyInfo(Variant::INT, "data_type"), "set_data_type", "get_data_type");
ClassDB::bind_method(D_METHOD("set_interpolation_type", "t"), &MapRangeNode::set_interpolation_type);
ClassDB::bind_method(D_METHOD("get_interpolation_type"), &MapRangeNode::get_interpolation_type);
ADD_PROPERTY(PropertyInfo(Variant::INT, "interpolation_type"), "set_interpolation_type", "get_interpolation_type");
ClassDB::bind_method(D_METHOD("set_clamp", "c"), &MapRangeNode::set_clamp);
ClassDB::bind_method(D_METHOD("get_clamp"), &MapRangeNode::get_clamp);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "clamp"), "set_clamp", "get_clamp");
}
MapRangeNode::MapRangeNode() {
// Blender's twelve inputs: the float set, then the vector set. Both are
// always present on the node; `data_type` chooses which is read.
input_defs.push_back({ "Value", SocketType::Float, 0.0f });
input_defs.push_back({ "From Min", SocketType::Float, 0.0f });
input_defs.push_back({ "From Max", SocketType::Float, 1.0f });
input_defs.push_back({ "To Min", SocketType::Float, 0.0f });
input_defs.push_back({ "To Max", SocketType::Float, 1.0f });
input_defs.push_back({ "Steps", SocketType::Float, 4.0f });
input_defs.push_back({ "Vector", SocketType::Vector, Vector3() });
input_defs.push_back({ "From_Min_FLOAT3", SocketType::Vector, Vector3(0, 0, 0) });
input_defs.push_back({ "From_Max_FLOAT3", SocketType::Vector, Vector3(1, 1, 1) });
input_defs.push_back({ "To_Min_FLOAT3", SocketType::Vector, Vector3(0, 0, 0) });
input_defs.push_back({ "To_Max_FLOAT3", SocketType::Vector, Vector3(1, 1, 1) });
input_defs.push_back({ "Steps_FLOAT3", SocketType::Vector, Vector3(4, 4, 4) });
output_defs.push_back({ "Result", SocketType::Float, Variant() });
output_defs.push_back({ "Vector", SocketType::Vector, Variant() });
}
// node_shader_map_range.cc. safe_divide returns 0 rather than infinity, which is
// what makes from_min == from_max give the start of the output range and
// steps == 0 give it too.
static inline float _mr_safe_divide(float a, float b) { return b != 0.0f ? a / b : 0.0f; }
// NOT a plain clamp: an INVERTED range is handled by swapping the bounds, so a
// node mapping to (1, 0) clamps between 0 and 1 rather than collapsing.
static inline float _mr_clamp_range(float value, float lo, float hi) {
return (lo > hi) ? CLAMP(value, hi, lo) : CLAMP(value, lo, hi);
}
static float _mr_eval(int interp, bool do_clamp, float value, float from_min, float from_max,
float to_min, float to_max, float steps) {
float factor = _mr_safe_divide(value - from_min, from_max - from_min);
switch (interp) {
case 1: { // STEPPED
factor = _mr_safe_divide(floorf(factor * (steps + 1.0f)), steps);
float result = to_min + factor * (to_max - to_min);
return do_clamp ? _mr_clamp_range(result, to_min, to_max) : result;
}
case 2: // SMOOTHSTEP -- clamps the FACTOR, and ignores the clamp flag,
// which is why Blender hides the checkbox for it.
factor = CLAMP(factor, 0.0f, 1.0f);
factor = (3.0f - 2.0f * factor) * (factor * factor);
return to_min + factor * (to_max - to_min);
case 3: // SMOOTHERSTEP
factor = CLAMP(factor, 0.0f, 1.0f);
factor = factor * factor * factor * (factor * (factor * 6.0f - 15.0f) + 10.0f);
return to_min + factor * (to_max - to_min);
default: { // LINEAR
float result = to_min + factor * (to_max - to_min);
return do_clamp ? _mr_clamp_range(result, to_min, to_max) : result;
}
}
}
void MapRangeNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
const int base = (data_type == 1) ? 6 : 0;
SocketValue in[6];
bool is_field = false;
int n = 1;
AttrDomain domain = AttrDomain::Point;
for (int i = 0; i < 6; i++) {
in[i] = p_ctx.read_input(p_node_idx, base + i);
if (in[i].is_field) {
if (!is_field) {
domain = in[i].domain;
}
is_field = true;
n = MAX(n, in[i].field_size);
}
}
if (data_type == 1) {
auto at = [&](int e) -> Vector3 {
Vector3 v = in[0].vec_at(e), fmin = in[1].vec_at(e), fmax = in[2].vec_at(e);
Vector3 tmin = in[3].vec_at(e), tmax = in[4].vec_at(e), st = in[5].vec_at(e);
return Vector3(
_mr_eval(interpolation_type, clamp_result, v.x, fmin.x, fmax.x, tmin.x, tmax.x, st.x),
_mr_eval(interpolation_type, clamp_result, v.y, fmin.y, fmax.y, tmin.y, tmax.y, st.y),
_mr_eval(interpolation_type, clamp_result, v.z, fmin.z, fmax.z, tmin.z, tmax.z, st.z));
};
if (!is_field) {
p_ctx.write_vector(p_node_idx, 1, at(0));
return;
}
PackedVector3Array out;
out.resize(n);
for (int e = 0; e < n; e++) {
out.ptrw()[e] = at(e);
}
p_ctx.write_vec_field(p_node_idx, 1, out, domain);
return;
}
auto at = [&](int e) -> float {
return _mr_eval(interpolation_type, clamp_result, in[0].float_at(e), in[1].float_at(e),
in[2].float_at(e), in[3].float_at(e), in[4].float_at(e), in[5].float_at(e));
};
if (!is_field) {
p_ctx.write_float(p_node_idx, 0, at(0));
return;
}
PackedFloat32Array out;
out.resize(n);
for (int e = 0; e < n; e++) {
out.ptrw()[e] = at(e);
}
p_ctx.write_float_field(p_node_idx, 0, out, domain);
}
// --- MixNode ---
// node_shader_map_range.cc, in GLSL. Two behaviours here are not what "map a
// range" suggests and both are in the CPU beside this:
//
// * the clamp handles an INVERTED range by swapping the bounds, so a node
// mapping to (1, 0) clamps between 0 and 1 rather than collapsing;
// * SMOOTHSTEP and SMOOTHERSTEP clamp the FACTOR and ignore the clamp flag
// entirely, which is why Blender hides the checkbox for them.
//
// safe_divide returning 0 rather than infinity is what makes from_min ==
// from_max give the start of the output range, and steps == 0 give it too.
static const char *MAPRANGE_GLSL_PRELUDE = R"GLSL(
#ifndef PN_MAPRANGE_HELPERS
#define PN_MAPRANGE_HELPERS
float pn_mr_safe_divide(float a, float b) { return (b != 0.0) ? a / b : 0.0; }
float pn_mr_clamp_range(float v, float lo, float hi) {
return (lo > hi) ? clamp(v, hi, lo) : clamp(v, lo, hi);
}
float pn_map_range(int interp, bool do_clamp, float value, float from_min, float from_max,
float to_min, float to_max, float steps) {
float factor = pn_mr_safe_divide(value - from_min, from_max - from_min);
if (interp == 1) {
factor = pn_mr_safe_divide(floor(factor * (steps + 1.0)), steps);
float r = to_min + factor * (to_max - to_min);
return do_clamp ? pn_mr_clamp_range(r, to_min, to_max) : r;
}
if (interp == 2) {
factor = clamp(factor, 0.0, 1.0);
factor = (3.0 - 2.0 * factor) * (factor * factor);
return to_min + factor * (to_max - to_min);
}
if (interp == 3) {
factor = clamp(factor, 0.0, 1.0);
factor = factor * factor * factor * (factor * (factor * 6.0 - 15.0) + 10.0);
return to_min + factor * (to_max - to_min);
}
float r = to_min + factor * (to_max - to_min);
return do_clamp ? pn_mr_clamp_range(r, to_min, to_max) : r;
}
#endif
)GLSL";
String MapRangeNode::compile_glsl_prelude(int p_node_idx) const {
return String(MAPRANGE_GLSL_PRELUDE);
}
String MapRangeNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
// SIX SOCKETS, AND WHICH SIX DEPENDS ON data_type: the vector variant reads
// 6..11, which is the same shape the CPU's `base` handles. Both outputs are
// declared because GLSL cannot leave one undefined.
const int base = (data_type == 1) ? 6 : 0;
String in[6];
for (int i = 0; i < 6; i++) {
in[i] = p_ctx.in(p_node_idx, base + i);
}
const String interp = itos(interpolation_type);
const String docl = clamp_result ? "true" : "false";
if (data_type == 1) {
String parts[3];
const char *comp[3] = { ".x", ".y", ".z" };
for (int k = 0; k < 3; k++) {
parts[k] = "pn_map_range(" + interp + ", " + docl + ", " + in[0] + comp[k] + ", " +
in[1] + comp[k] + ", " + in[2] + comp[k] + ", " + in[3] + comp[k] + ", " +
in[4] + comp[k] + ", " + in[5] + comp[k] + ")";
}
return p_ctx.declare(p_node_idx, 0) + " = 0.0;\n\t" +
p_ctx.declare(p_node_idx, 1) + " = vec3(" + parts[0] + ", " + parts[1] + ", " +
parts[2] + ");";
}
return p_ctx.declare(p_node_idx, 0) + " = pn_map_range(" + interp + ", " + docl + ", " +
in[0] + ", " + in[1] + ", " + in[2] + ", " + in[3] + ", " + in[4] + ", " + in[5] +
");\n\t" + p_ctx.declare(p_node_idx, 1) + " = vec3(0.0);";
}
void MixNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_data_type", "t"), &MixNode::set_data_type);
ClassDB::bind_method(D_METHOD("get_data_type"), &MixNode::get_data_type);
ADD_PROPERTY(PropertyInfo(Variant::INT, "data_type"), "set_data_type", "get_data_type");
ClassDB::bind_method(D_METHOD("set_blend_type", "t"), &MixNode::set_blend_type);
ClassDB::bind_method(D_METHOD("get_blend_type"), &MixNode::get_blend_type);
ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_type"), "set_blend_type", "get_blend_type");
ClassDB::bind_method(D_METHOD("set_clamp_factor", "c"), &MixNode::set_clamp_factor);
ClassDB::bind_method(D_METHOD("get_clamp_factor"), &MixNode::get_clamp_factor);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "clamp_factor"), "set_clamp_factor", "get_clamp_factor");
ClassDB::bind_method(D_METHOD("set_clamp_result", "c"), &MixNode::set_clamp_result);
ClassDB::bind_method(D_METHOD("get_clamp_result"), &MixNode::get_clamp_result);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "clamp_result"), "set_clamp_result", "get_clamp_result");
ClassDB::bind_method(D_METHOD("set_factor_mode", "m"), &MixNode::set_factor_mode);
ClassDB::bind_method(D_METHOD("get_factor_mode"), &MixNode::get_factor_mode);
ADD_PROPERTY(PropertyInfo(Variant::INT, "factor_mode"), "set_factor_mode", "get_factor_mode");
}
MixNode::MixNode() {
// Blender's ten inputs and four outputs, in Blender's order. Every data
// type has its own pair and they all exist on the node at once;
// `data_type` chooses which pair is read and which output is written.
input_defs.push_back({ "Factor_Float", SocketType::Float, 0.5f });
input_defs.push_back({ "Factor_Vector", SocketType::Vector, Vector3(0.5f, 0.5f, 0.5f) });
input_defs.push_back({ "A_Float", SocketType::Float, 0.0f });
input_defs.push_back({ "B_Float", SocketType::Float, 0.0f });
input_defs.push_back({ "A_Vector", SocketType::Vector, Vector3() });
input_defs.push_back({ "B_Vector", SocketType::Vector, Vector3() });
input_defs.push_back({ "A_Color", SocketType::Color, Color(0.5f, 0.5f, 0.5f, 1.0f) });
input_defs.push_back({ "B_Color", SocketType::Color, Color(0.5f, 0.5f, 0.5f, 1.0f) });
input_defs.push_back({ "A_Rotation", SocketType::Rotation, Quaternion() });
input_defs.push_back({ "B_Rotation", SocketType::Rotation, Quaternion() });
output_defs.push_back({ "Result_Float", SocketType::Float, Variant() });
output_defs.push_back({ "Result_Vector", SocketType::Vector, Variant() });
output_defs.push_back({ "Result_Color", SocketType::Color, Variant() });
output_defs.push_back({ "Result_Rotation", SocketType::Rotation, Variant() });
}
static const char *MIX_QUAT_GLSL_PRELUDE = R"GLSL(
#ifndef PN_QUAT_SLERP
#define PN_QUAT_SLERP
// A PORT OF godot's Quaternion::slerp, not a shared text.
//
// Every other .inc in src/shared/ exists so one algorithm is compiled twice.
// This one cannot be: the CPU side calls godot-cpp's Quaternion::slerp, which
// is godot's C++ and not ours to dual-compile. So this IS a second
// implementation, and the emitted-op gate is what holds it to the first --
// which is the same instrument, and the same argument, that the erosion
// filter's history says a transcription needs.
//
// Read off godot-cpp/src/variant/quaternion.cpp:115-153, including
// CMP_EPSILON = 0.00001 and the sign flip when the two point away from each
// other. ⚠ godot computes scale0 through a DOUBLE literal -- `Math::sin((1.0 -
// p_weight) * omega)` -- and scale1 in float. That asymmetry cannot be
// reproduced here and is why Rotation is compared within tolerance rather
// than exactly.
vec4 pn_quat_slerp(vec4 from, vec4 to, float t) {
float cosom = dot(from, to);
vec4 to1;
if (cosom < 0.0) {
cosom = -cosom;
to1 = -to;
} else {
to1 = to;
}
float scale0;
float scale1;
if ((1.0 - cosom) > 0.00001) {
float omega = acos(cosom);
float sinom = sin(omega);
scale0 = sin((1.0 - t) * omega) / sinom;
scale1 = sin(t * omega) / sinom;
} else {
scale0 = 1.0 - t;
scale1 = t;
}
return from * scale0 + to1 * scale1;
}
// godot's Quaternion::normalized() is `*this / length()`, with no guard.
vec4 pn_quat_normalize(vec4 q) { return q / length(q); }
#endif
)GLSL";
String MixNode::compile_glsl_prelude(int p_node_idx) const {
if (data_type == 2) {
return String(BLENDER_RAMP_BLEND_GLSL);
}
if (data_type == 14) {
return String(MIX_QUAT_GLSL_PRELUDE);
}
return String();
}
String MixNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
// The socket LAYOUT depends on data_type, exactly as execute() reads it:
// Float takes 2/3, Vector 4/5, and a NON-UNIFORM vector factor reads socket
// 1 rather than 0. Getting that wrong reads someone else's socket and looks
// like a wrong blend rather than a wrong address.
const bool is_vector = (data_type == 1);
const bool is_color = (data_type == 2);
const bool is_rotation = (data_type == 14);
const bool vector_factor = is_vector && factor_mode != 0;
// Same ladder execute() uses: Rotation 8/9, Colour 6/7, Vector 4/5,
// Float 2/3. Getting it wrong reads someone else's socket and looks like a
// wrong blend rather than a wrong address.
const int a_sock = is_rotation ? 8 : (is_color ? 6 : (is_vector ? 4 : 2));
String t = p_ctx.in(p_node_idx, vector_factor ? 1 : 0);
const String a = p_ctx.in(p_node_idx, a_sock);
const String b = p_ctx.in(p_node_idx, a_sock + 1);
if (clamp_factor) {
t = (vector_factor ? String("clamp(") + t + ", vec3(0.0), vec3(1.0))"
: String("clamp(") + t + ", 0.0, 1.0)");
}
// Declared in socket order so a consumer of any output gets a defined
// variable; the ones this configuration does not compute are zero, which is
// what the CPU leaves them as too.
String out;
const String f_decl = p_ctx.declare(p_node_idx, 0);
const String v_decl = p_ctx.declare(p_node_idx, 1);
if (is_color) {
// ramp_blend's nineteen modes. clamp_result clamps ALL FOUR channels,
// alpha included, which is what the CPU does.
String e = String("br_ramp_blend(") + itos(blend_type) + String(", ") + a +
String(", ") + t + String(", ") + b + String(")");
if (clamp_result) {
e = String("clamp(") + e + String(", vec4(0.0), vec4(1.0))");
}
out = f_decl + String(" = 0.0;\n\t") + v_decl + String(" = vec3(0.0);\n\t");
out += p_ctx.declare(p_node_idx, 2) + String(" = ") + e + String(";\n\t");
out += p_ctx.declare(p_node_idx, 3) + String(" = vec4(0.0);");
return out;
}
if (is_rotation) {
// Both operands are NORMALISED first, exactly as execute() does --
// a.normalized().slerp(b.normalized(), t). clamp_result does not apply
// to a rotation on either side.
out = f_decl + String(" = 0.0;\n\t") + v_decl + String(" = vec3(0.0);\n\t");
out += p_ctx.declare(p_node_idx, 2) + String(" = vec4(0.0);\n\t");
out += p_ctx.declare(p_node_idx, 3) + String(" = pn_quat_slerp(pn_quat_normalize(") +
a + String("), pn_quat_normalize(") + b + String("), ") + t + String(");");
return out;
}
if (is_vector) {
// a * (1 - t) + b * t componentwise -- the same algebra as mix() and a
// different rounding, which is what the CPU does and therefore what the
// gate compares against.
const String tv = vector_factor ? t : (String("vec3(") + t + ")");
out = f_decl + String(" = 0.0;\n\t") + v_decl + " = (" + a + " * (vec3(1.0) - " + tv +
") + " + b + " * " + tv + ");";
} else {
out = f_decl + String(" = (") + a + " * (1.0 - " + t + ") + " + b + " * " + t +
");\n\t" + v_decl + " = vec3(0.0);";
}
// The colour and rotation outputs exist on the node whatever the data type,
// and a graph may read them; they are zero here because this configuration
// does not produce them.
out += "\n\t" + p_ctx.declare(p_node_idx, 2) + " = vec4(0.0);";
out += "\n\t" + p_ctx.declare(p_node_idx, 3) + " = vec4(0.0);";
return out;
}
void MixNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
// RNA's data_type numbering: FLOAT 0, VECTOR 1, RGBA 2, ROTATION 14.
const bool is_vector = (data_type == 1);
const bool is_color = (data_type == 2);
const bool is_rotation = (data_type == 14);
const bool vector_factor = is_vector && factor_mode != 0;
SocketValue fac = p_ctx.read_input(p_node_idx, vector_factor ? 1 : 0);
int a_sock = is_rotation ? 8 : (is_color ? 6 : (is_vector ? 4 : 2));
SocketValue a = p_ctx.read_input(p_node_idx, a_sock);
SocketValue b = p_ctx.read_input(p_node_idx, a_sock + 1);
bool is_field = fac.is_field || a.is_field || b.is_field;
int n = MAX(fac.field_size, MAX(a.field_size, b.field_size));
if (n == 0) {
n = 1;
}
AttrDomain domain = fac.is_field ? fac.domain : (a.is_field ? a.domain : b.domain);
const int out_sock = is_rotation ? 3 : (is_color ? 2 : (is_vector ? 1 : 0));
// A NON-UNIFORM vector factor is not `interpolate` with a scalar: Blender
// writes it out as a * (1 - t) + b * t per component, which is the same
// algebra and a different rounding.
auto vec_at = [&](int e) -> Vector3 {
Vector3 av = a.vec_at(e), bv = b.vec_at(e);
if (vector_factor) {
Vector3 t = fac.vec_at(e);
if (clamp_factor) {
t = Vector3(CLAMP(t.x, 0.0f, 1.0f), CLAMP(t.y, 0.0f, 1.0f), CLAMP(t.z, 0.0f, 1.0f));
}
return Vector3(av.x * (1.0f - t.x) + bv.x * t.x, av.y * (1.0f - t.y) + bv.y * t.y,
av.z * (1.0f - t.z) + bv.z * t.z);
}
float t = fac.float_at(e);
if (clamp_factor) {
t = CLAMP(t, 0.0f, 1.0f);
}
return av * (1.0f - t) + bv * t;
};
auto float_at = [&](int e) -> float {
float t = fac.float_at(e);
if (clamp_factor) {
t = CLAMP(t, 0.0f, 1.0f);
}
return a.float_at(e) * (1.0f - t) + b.float_at(e) * t;
};
auto color_at = [&](int e) -> Color {
float t = fac.float_at(e);
if (clamp_factor) {
t = CLAMP(t, 0.0f, 1.0f);
}
Color r = bramp::ramp_blend(blend_type, a.color_at(e), t, b.color_at(e));
if (clamp_result) {
r = Color(CLAMP(r.r, 0.0f, 1.0f), CLAMP(r.g, 0.0f, 1.0f), CLAMP(r.b, 0.0f, 1.0f),
CLAMP(r.a, 0.0f, 1.0f));
}
return r;
};
auto rot_at = [&](int e) -> Quaternion {
float t = fac.float_at(e);
if (clamp_factor) {
t = CLAMP(t, 0.0f, 1.0f);
}
return a.quat_at(e).normalized().slerp(b.quat_at(e).normalized(), t);
};
if (!is_field) {
if (is_color) {
p_ctx.write_color(p_node_idx, out_sock, color_at(0));
} else if (is_vector) {
p_ctx.write_vector(p_node_idx, out_sock, vec_at(0));
} else if (is_rotation) {
p_ctx.write_rotation(p_node_idx, out_sock, rot_at(0));
} else {
p_ctx.write_float(p_node_idx, out_sock, float_at(0));
}
return;
}
if (is_color) {
PackedColorArray out;
out.resize(n);
for (int e = 0; e < n; e++) {
out.ptrw()[e] = color_at(e);
}
p_ctx.write_color_field(p_node_idx, out_sock, out, domain);
} else if (is_vector) {
PackedVector3Array out;
out.resize(n);
for (int e = 0; e < n; e++) {
out.ptrw()[e] = vec_at(e);
}
p_ctx.write_vec_field(p_node_idx, out_sock, out, domain);
} else if (is_rotation) {
PackedVector4Array out;
out.resize(n);
for (int e = 0; e < n; e++) {
Quaternion q = rot_at(e);
out.ptrw()[e] = Vector4(q.x, q.y, q.z, q.w);
}
p_ctx.write_rot_field(p_node_idx, out_sock, out, domain);
} else {
PackedFloat32Array out;
out.resize(n);
for (int e = 0; e < n; e++) {
out.ptrw()[e] = float_at(e);
}
p_ctx.write_float_field(p_node_idx, out_sock, out, domain);
}
}
// --- ColorRampNode ---
void ColorRampNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_ramp_stops", "stops"), &ColorRampNode::set_ramp_stops);
ClassDB::bind_method(D_METHOD("get_ramp_stops"), &ColorRampNode::get_ramp_stops);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT32_ARRAY, "ramp_stops"),
"set_ramp_stops", "get_ramp_stops");
ClassDB::bind_method(D_METHOD("set_interpolation", "i"), &ColorRampNode::set_interpolation);
ClassDB::bind_method(D_METHOD("get_interpolation"), &ColorRampNode::get_interpolation);
ADD_PROPERTY(PropertyInfo(Variant::INT, "interpolation"),
"set_interpolation", "get_interpolation");
ClassDB::bind_method(D_METHOD("set_color_mode", "m"), &ColorRampNode::set_color_mode);
ClassDB::bind_method(D_METHOD("get_color_mode"), &ColorRampNode::get_color_mode);
ADD_PROPERTY(PropertyInfo(Variant::INT, "color_mode"), "set_color_mode", "get_color_mode");
ClassDB::bind_method(D_METHOD("set_hue_interpolation", "h"),
&ColorRampNode::set_hue_interpolation);
ClassDB::bind_method(D_METHOD("get_hue_interpolation"),
&ColorRampNode::get_hue_interpolation);
ADD_PROPERTY(PropertyInfo(Variant::INT, "hue_interpolation"),
"set_hue_interpolation", "get_hue_interpolation");
}
ColorRampNode::ColorRampNode() {
// Blender calls this socket "Factor" and gives it the identifier "Fac".
// blender-bridge resolves sockets BY NAME, so "Fac" matched nothing and fell
// back to the index. And its default is 0.5, not 0 -- which is what an
// unconnected Factor evaluates to in a bridged graph.
input_defs.push_back({ "Factor", SocketType::Float, 0.5f });
output_defs.push_back({ "Color", SocketType::Color, Color(1, 1, 1, 1) });
output_defs.push_back({ "Alpha", SocketType::Float, Variant() });
// A FRESH BLENDER COLOR RAMP, so a node created here and one created there
// start from the same gradient: two stops, black at 0 and white at 1, RGB,
// LINEAR, hue NEAR. An empty ramp would evaluate to nothing at all, which
// is the failure this node used to have in a different costume.
ramp_stops = PackedFloat32Array();
ramp_stops.push_back(0.0f);
ramp_stops.push_back(0.0f);
ramp_stops.push_back(0.0f);
ramp_stops.push_back(0.0f);
ramp_stops.push_back(1.0f);
ramp_stops.push_back(1.0f);
ramp_stops.push_back(1.0f);
ramp_stops.push_back(1.0f);
ramp_stops.push_back(1.0f);
ramp_stops.push_back(1.0f);
}
void ColorRampNode::set_ramp_stops(const PackedFloat32Array &p_stops) {
// Validate HERE and not in execute(): execute runs per evaluation and a
// warning in it would either spam or be swallowed, while a set happens once
// and is where the bad data actually arrives.
const int n = p_stops.size();
if (n % 5 != 0) {
UtilityFunctions::push_warning(
"ColorRampNode: ramp_stops holds ", n,
" floats, which is not a whole number of 5-float stops "
"(position, r, g, b, a). The trailing ", n % 5, " will be ignored.");
}
if (n / 5 > BCOBA_MAX_STOPS) {
UtilityFunctions::push_warning(
"ColorRampNode: ramp_stops holds ", n / 5,
" stops; Blender's own limit is ", BCOBA_MAX_STOPS,
" (MAXCOLORBAND) and the rest will be ignored.");
}
ramp_stops = p_stops;
}
void ColorRampNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
bcoba::ColorBand coba;
coba.ipotype = interpolation;
coba.ipotype_hue = hue_interpolation;
coba.color_mode = color_mode;
const int stop_count = MIN(ramp_stops.size() / 5, BCOBA_MAX_STOPS);
const float *sp = ramp_stops.ptr();
for (int s = 0; s < stop_count; s++) {
bcoba::Stop &st = coba.data[s];
st.pos = sp[s * 5 + 0];
st.r = sp[s * 5 + 1];
st.g = sp[s * 5 + 2];
st.b = sp[s * 5 + 3];
st.a = sp[s * 5 + 4];
}
coba.tot = stop_count;
// THE BAND IS BUILT ONCE, outside the per-element loop. Blender's node is a
// FUNCTION NODE, so it is evaluated per element on the consumer's domain --
// `read_float` took element 0, which gave every vertex the first one's
// colour. A constant, which looks like a result. The rotation family failed
// the same way before #62 and it read as "the voronoi contributes nothing".
SocketValue fac = p_ctx.read_input(p_node_idx, 0);
// An EMPTY band is the one case Blender declines to evaluate. It leaves the
// caller's buffer alone, so opaque black is ours -- and it is only reachable
// by clearing the property, since the constructor seeds Blender's default.
auto eval = [&](int e) -> Color {
float out[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
bcoba::colorband_evaluate(coba, fac.float_at(e), out);
return Color(out[0], out[1], out[2], out[3]);
};
if (!fac.is_field) {
// Alpha is the ramp's interpolated alpha, which is what Blender's
// ColorBandFunction writes: `alphas[i] = color.a`.
const Color c = eval(0);
p_ctx.write_color(p_node_idx, 0, c);
p_ctx.write_float(p_node_idx, 1, c.a);
return;
}
int n = fac.field_size;
if (n == 0) {
n = 1;
}
PackedColorArray cols;
PackedFloat32Array alphas;
cols.resize(n);
alphas.resize(n);
for (int e = 0; e < n; e++) {
const Color c = eval(e);
cols.ptrw()[e] = c;
alphas.ptrw()[e] = c.a;
}
p_ctx.write_color_field(p_node_idx, 0, cols, fac.domain);
p_ctx.write_float_field(p_node_idx, 1, alphas, fac.domain);
}
// --- The curve decoder shared by Float Curve and RGB Curves ---
//
// Not in shared/, because shared/ holds ports of Blender's own code and this is
// ours: the flat three-array encoding the bridge and the .tres use.
static void _fill_curve_mapping(bcuma::CurveMapping &r_cumap,
const PackedFloat32Array &p_points, const PackedInt32Array &p_handles,
const PackedInt32Array &p_counts, int p_extend, bool p_use_clip,
const PackedFloat32Array &p_clip) {
r_cumap.flag = (p_use_clip ? bcuma::CUMA_DO_CLIP : 0) |
(p_extend & bcuma::CUMA_EXTEND_EXTRAPOLATE);
if (p_clip.size() >= 4) {
r_cumap.clip_xmin = p_clip[0];
r_cumap.clip_ymin = p_clip[1];
r_cumap.clip_xmax = p_clip[2];
r_cumap.clip_ymax = p_clip[3];
}
const int ncurves = MIN(p_counts.size(), BCUMA_MAX_CURVES);
r_cumap.totcurve = MAX(ncurves, 1);
int at = 0; // running point index across the concatenated curves
for (int c = 0; c < ncurves; c++) {
bcuma::CurveMap &cm = r_cumap.cm[c];
const int count = p_counts[c];
int kept = 0;
for (int i = 0; i < count; i++) {
const int pi = at + i;
if (pi * 2 + 1 >= p_points.size() || kept >= BCUMA_MAX_POINTS) {
break;
}
cm.points[kept].x = p_points[pi * 2 + 0];
cm.points[kept].y = p_points[pi * 2 + 1];
// The bridge sends Blender's handle_type, which is the POINT FLAG
// (AUTO 0, VECTOR 2, AUTO_CLAMPED 4), not the HD_* the bezier code
// wants (HD_AUTO 1, HD_VECT 2, HD_AUTO_ANIM 4). Two of the three
// numbers collide, so passing it through unconverted looks right
// for VECTOR and turns AUTO into HD_FREE -- which calchandle skips
// entirely, leaving both handles on the knot.
cm.points[kept].handle = bcuma::handle_from_point_flag(
pi < p_handles.size() ? p_handles[pi] : 0);
kept++;
}
cm.totpoint = kept;
cm.built = false;
at += count;
}
bcuma::curvemapping_init(r_cumap);
}
// --- FloatCurveNode ---
void FloatCurveNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_curve_points", "p"), &FloatCurveNode::set_curve_points);
ClassDB::bind_method(D_METHOD("get_curve_points"), &FloatCurveNode::get_curve_points);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT32_ARRAY, "curve_points"),
"set_curve_points", "get_curve_points");
ClassDB::bind_method(D_METHOD("set_curve_handles", "p"), &FloatCurveNode::set_curve_handles);
ClassDB::bind_method(D_METHOD("get_curve_handles"), &FloatCurveNode::get_curve_handles);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_INT32_ARRAY, "curve_handles"),
"set_curve_handles", "get_curve_handles");
ClassDB::bind_method(D_METHOD("set_curve_counts", "p"), &FloatCurveNode::set_curve_counts);
ClassDB::bind_method(D_METHOD("get_curve_counts"), &FloatCurveNode::get_curve_counts);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_INT32_ARRAY, "curve_counts"),
"set_curve_counts", "get_curve_counts");
ClassDB::bind_method(D_METHOD("set_extend", "e"), &FloatCurveNode::set_extend);
ClassDB::bind_method(D_METHOD("get_extend"), &FloatCurveNode::get_extend);
ADD_PROPERTY(PropertyInfo(Variant::INT, "extend"), "set_extend", "get_extend");
ClassDB::bind_method(D_METHOD("set_use_clip", "c"), &FloatCurveNode::set_use_clip);
ClassDB::bind_method(D_METHOD("get_use_clip"), &FloatCurveNode::get_use_clip);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_clip"), "set_use_clip", "get_use_clip");
ClassDB::bind_method(D_METHOD("set_clip_rect", "r"), &FloatCurveNode::set_clip_rect);
ClassDB::bind_method(D_METHOD("get_clip_rect"), &FloatCurveNode::get_clip_rect);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT32_ARRAY, "clip_rect"),
"set_clip_rect", "get_clip_rect");
}
FloatCurveNode::FloatCurveNode() {
// Blender's Factor comes FIRST and blends the curve's result against the
// input. Ours had only Value, so a graph wiring the factor drove the value.
// Value's default is 1.0 in Blender, not 0 -- measured, not read off the
// declaration.
input_defs.push_back({ "Factor", SocketType::Float, 1.0f });
input_defs.push_back({ "Value", SocketType::Float, 1.0f });
output_defs.push_back({ "Value", SocketType::Float, Variant() });
// A FRESH BLENDER FLOAT CURVE: the identity from (0,0) to (1,1), auto
// handles, extrapolating, clipped to the unit square.
curve_points.push_back(0.0f);
curve_points.push_back(0.0f);
curve_points.push_back(1.0f);
curve_points.push_back(1.0f);
curve_handles.push_back(0);
curve_handles.push_back(0);
curve_counts.push_back(2);
clip_rect.push_back(0.0f);
clip_rect.push_back(0.0f);
clip_rect.push_back(1.0f);
clip_rect.push_back(1.0f);
}
void FloatCurveNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
bcuma::CurveMapping cumap;
_fill_curve_mapping(cumap, curve_points, curve_handles, curve_counts,
extend, use_clip, clip_rect);
// THE TABLE IS BUILT ONCE, outside the per-element loop -- 257 entries per
// curve, so building it per vertex would be ruinous as well as wrong.
SocketValue fs = p_ctx.read_input(p_node_idx, 0);
SocketValue vs = p_ctx.read_input(p_node_idx, 1);
auto eval = [&](int e) -> float {
const float value = vs.float_at(e);
const float factor = fs.float_at(e);
float out = bcuma::curvemapping_evaluateF(cumap, 0, value);
// Blender blends only when fac differs from 1, and toward the INPUT.
if (factor != 1.0f) {
out = (1.0f - factor) * value + factor * out;
}
return out;
};
// EITHER input can be the field. Factor being one is not exotic: masking a
// curve's effect per vertex is the ordinary reason to wire it.
if (!fs.is_field && !vs.is_field) {
p_ctx.write_float(p_node_idx, 0, eval(0));
return;
}
int n = MAX(fs.field_size, vs.field_size);
if (n == 0) {
n = 1;
}
const AttrDomain domain = fs.is_field ? fs.domain : vs.domain;
PackedFloat32Array out;
out.resize(n);
for (int e = 0; e < n; e++) {
out.ptrw()[e] = eval(e);
}
p_ctx.write_float_field(p_node_idx, 0, out, domain);
}
// --- RGBCurvesNode ---
void RGBCurvesNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_curve_points", "p"), &RGBCurvesNode::set_curve_points);
ClassDB::bind_method(D_METHOD("get_curve_points"), &RGBCurvesNode::get_curve_points);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT32_ARRAY, "curve_points"),
"set_curve_points", "get_curve_points");
ClassDB::bind_method(D_METHOD("set_curve_handles", "p"), &RGBCurvesNode::set_curve_handles);
ClassDB::bind_method(D_METHOD("get_curve_handles"), &RGBCurvesNode::get_curve_handles);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_INT32_ARRAY, "curve_handles"),
"set_curve_handles", "get_curve_handles");
ClassDB::bind_method(D_METHOD("set_curve_counts", "p"), &RGBCurvesNode::set_curve_counts);
ClassDB::bind_method(D_METHOD("get_curve_counts"), &RGBCurvesNode::get_curve_counts);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_INT32_ARRAY, "curve_counts"),
"set_curve_counts", "get_curve_counts");
ClassDB::bind_method(D_METHOD("set_extend", "e"), &RGBCurvesNode::set_extend);
ClassDB::bind_method(D_METHOD("get_extend"), &RGBCurvesNode::get_extend);
ADD_PROPERTY(PropertyInfo(Variant::INT, "extend"), "set_extend", "get_extend");
ClassDB::bind_method(D_METHOD("set_use_clip", "c"), &RGBCurvesNode::set_use_clip);
ClassDB::bind_method(D_METHOD("get_use_clip"), &RGBCurvesNode::get_use_clip);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_clip"), "set_use_clip", "get_use_clip");
ClassDB::bind_method(D_METHOD("set_clip_rect", "r"), &RGBCurvesNode::set_clip_rect);
ClassDB::bind_method(D_METHOD("get_clip_rect"), &RGBCurvesNode::get_clip_rect);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT32_ARRAY, "clip_rect"),
"set_clip_rect", "get_clip_rect");
}
RGBCurvesNode::RGBCurvesNode() {
// Blender NAMES this socket "Factor" and gives it the identifier "Fac";
// blender-bridge resolves by NAME, so "Fac" matched nothing.
input_defs.push_back({ "Factor", SocketType::Float, 1.0f });
input_defs.push_back({ "Color", SocketType::Color, Color(1, 1, 1, 1) });
output_defs.push_back({ "Color", SocketType::Color, Variant() });
// FOUR identity curves, as a fresh Blender node has: R, G, B, then the
// combined one.
for (int c = 0; c < 4; c++) {
curve_points.push_back(0.0f);
curve_points.push_back(0.0f);
curve_points.push_back(1.0f);
curve_points.push_back(1.0f);
curve_handles.push_back(0);
curve_handles.push_back(0);
curve_counts.push_back(2);
}
clip_rect.push_back(0.0f);
clip_rect.push_back(0.0f);
clip_rect.push_back(1.0f);
clip_rect.push_back(1.0f);
}
void RGBCurvesNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
bcuma::CurveMapping cumap;
_fill_curve_mapping(cumap, curve_points, curve_handles, curve_counts,
extend, use_clip, clip_rect);
// Four tables, built ONCE outside the per-element loop.
SocketValue fs = p_ctx.read_input(p_node_idx, 0);
SocketValue cs = p_ctx.read_input(p_node_idx, 1);
auto eval = [&](int e) -> Color {
const Color in = cs.color_at(e);
const float src[3] = { in.r, in.g, in.b };
float out[3];
// The COMBINED curve runs first, per channel, and its result goes
// through that channel's own curve. This path is UNCLIPPED -- Float
// Curve clamps to clipr and this does not, from the same flag.
bcuma::curvemapping_evaluateRGBF(cumap, out, src);
// Blender's interp_v3_v3v3(out, in, out, fac): only when fac differs
// from 1, and only the RGB.
const float factor = fs.float_at(e);
if (factor != 1.0f) {
for (int c = 0; c < 3; c++) {
out[c] = src[c] + factor * (out[c] - src[c]);
}
}
// ALPHA IS FORCED TO 1, unconditionally and after the blend. Not
// carried from the input and not blended -- `col_out[i].a = 1.0f`.
return Color(out[0], out[1], out[2], 1.0f);
};
if (!fs.is_field && !cs.is_field) {
p_ctx.write_color(p_node_idx, 0, eval(0));
return;
}
int n = MAX(fs.field_size, cs.field_size);
if (n == 0) {
n = 1;
}
const AttrDomain domain = fs.is_field ? fs.domain : cs.domain;
PackedColorArray out;
out.resize(n);
for (int e = 0; e < n; e++) {
out.ptrw()[e] = eval(e);
}
p_ctx.write_color_field(p_node_idx, 0, out, domain);
}
} // namespace godot