// Blender's rotation and matrix node family.
//
// These need SocketType::Rotation and SocketType::Matrix, which is why they did
// not exist before: a rotation spelled as three euler floats does not round-trip
// (euler loses the winding a quaternion keeps) and a 4x4 has no vector spelling.
//
// FunctionNodeMatrixSVD is deliberately absent. A 4x4 singular value
// decomposition is a real numerical routine, not a one-liner, and a wrong one
// is worse than a missing one.
#include "rotation_nodes.h"
#include "../gpu/glsl_emit_context.h"
#include "../graph/graph_executor.h"
#include <godot_cpp/classes/global_constants.hpp>
#include <godot_cpp/variant/basis.hpp>
namespace godot {
// EULER ORDER. Blender's euler is "XYZ" and Godot's matching order is named
// ZYX, because the two name the same thing from opposite ends: Blender by the
// order the rotations are APPLIED (X first), Godot by the order the matrices are
// MULTIPLIED (Rz * Ry * Rx). Same rotation, opposite spelling.
//
// This was got wrong twice. First by leaving godot-cpp on its defaults, which
// are YXZ -- Quaternion::from_euler's own comment says "R = Y(a1).X(a2).Z(a3)".
// Then by "fixing" it to EULER_ORDER_XYZ, which reads correct, passed a test
// that only checked the spelling, and was still a different rotation.
//
// Settled by measurement rather than by reading the names: all six orders were
// evaluated against Blender's own node output for euler (0.3, 0.5, 0.7), and
// ZYX matched to 1e-6 while XYZ was off by 0.36.
#define PN_EULER_ORDER EulerOrder::EULER_ORDER_ZYX
static inline Quaternion pn_quat_from_euler(const Vector3 &p_euler) {
return Basis::from_euler(p_euler, PN_EULER_ORDER).get_rotation_quaternion();
}
static inline Vector3 pn_euler_from_quat(const Quaternion &p_q) {
return Basis(p_q.normalized()).get_euler(PN_EULER_ORDER);
}
// ---------------------------------------------------------------------------
// THE FIELD LIFT
//
// Blender's rotation and matrix nodes are FIELD FUNCTIONS: hand any of them a
// per-element input and every output is per-element too. All eighteen here read
// one value and wrote one value, and there was not a single `is_field` in this
// file -- so a field put through any of them COLLAPSED. Everything downstream
// was then evaluated at one sample and returned a constant, with no null node
// and no warning to say so.
//
// The bridged mountain's cell space is exactly that shape,
// Position -> Rotate Vector -> Voronoi, and this is what made its Voronoi a
// constant over all 39366 vertices -- which reads as "the voronoi contributes
// nothing" rather than as a broken node.
//
// The lift is uniform because the operations are: read every input once, take
// the element count from whichever inputs are fields, and evaluate the same
// expression per element. A node with no field input writes a scalar exactly as
// before, so nothing that worked changes.
// ---------------------------------------------------------------------------
struct PnIn {
Vector<SocketValue> v;
bool field = false;
int n = 1;
AttrDomain domain = AttrDomain::Point;
PnIn(ProcEvalContext &p_ctx, int p_node, int p_count) {
for (int i = 0; i < p_count; i++) {
SocketValue s = p_ctx.read_input(p_node, i);
if (s.is_field) {
if (!field) {
domain = s.domain;
}
field = true;
n = MAX(n, s.field_size);
}
v.push_back(s);
}
}
float f(int s, int e) const { return v[s].float_at(e); }
Vector3 vec(int s, int e) const { return v[s].vec_at(e); }
Quaternion quat(int s, int e) const { return v[s].quat_at(e); }
Projection mat(int s, int e) const { return v[s].mat_at(e); }
};
template <typename F>
static void _emit_f(ProcEvalContext &c, int nd, int sk, const PnIn &in, F fn) {
if (!in.field) {
c.write_float(nd, sk, fn(0));
return;
}
PackedFloat32Array a;
a.resize(in.n);
for (int e = 0; e < in.n; e++) {
a.ptrw()[e] = fn(e);
}
c.write_float_field(nd, sk, a, in.domain);
}
template <typename F>
static void _emit_vec(ProcEvalContext &c, int nd, int sk, const PnIn &in, F fn) {
if (!in.field) {
c.write_vector(nd, sk, fn(0));
return;
}
PackedVector3Array a;
a.resize(in.n);
for (int e = 0; e < in.n; e++) {
a.ptrw()[e] = fn(e);
}
c.write_vec_field(nd, sk, a, in.domain);
}
template <typename F>
static void _emit_rot(ProcEvalContext &c, int nd, int sk, const PnIn &in, F fn) {
if (!in.field) {
c.write_rotation(nd, sk, fn(0));
return;
}
PackedVector4Array a;
a.resize(in.n);
for (int e = 0; e < in.n; e++) {
Quaternion q = fn(e);
a.ptrw()[e] = Vector4(q.x, q.y, q.z, q.w);
}
c.write_rot_field(nd, sk, a, in.domain);
}
template <typename F>
static void _emit_mat(ProcEvalContext &c, int nd, int sk, const PnIn &in, F fn) {
if (!in.field) {
c.write_matrix(nd, sk, fn(0));
return;
}
// 16 floats per element, column-major, matching SocketValue::mat_at.
PackedFloat32Array a;
a.resize(in.n * 16);
for (int e = 0; e < in.n; e++) {
Projection m = fn(e);
for (int col = 0; col < 4; col++) {
for (int row = 0; row < 4; row++) {
a.ptrw()[e * 16 + col * 4 + row] = m.columns[col][row];
}
}
}
c.write_mat_field(nd, sk, a, in.domain);
}
void AxisAngleToRotationNode::_bind_methods() {}
AxisAngleToRotationNode::AxisAngleToRotationNode() {
input_defs.push_back({ "Axis", SocketType::Vector, Vector3(0, 0, 1) });
input_defs.push_back({ "Angle", SocketType::Float, 0.0f });
output_defs.push_back({ "Rotation", SocketType::Rotation, Variant() });
}
void AxisAngleToRotationNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 2);
_emit_rot(p_ctx, p_node_idx, 0, in, [&](int e) {
Vector3 axis = in.vec(0, e);
if (axis.length_squared() < 1e-12f) axis = Vector3(0, 0, 1);
return Quaternion(axis.normalized(), in.f(1, e));
});
}
void EulerToRotationNode::_bind_methods() {}
EulerToRotationNode::EulerToRotationNode() {
input_defs.push_back({ "Euler", SocketType::Vector, Vector3() });
output_defs.push_back({ "Rotation", SocketType::Rotation, Variant() });
}
void EulerToRotationNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 1);
_emit_rot(p_ctx, p_node_idx, 0, in, [&](int e) { return pn_quat_from_euler(in.vec(0, e)); });
}
void QuaternionToRotationNode::_bind_methods() {}
QuaternionToRotationNode::QuaternionToRotationNode() {
input_defs.push_back({ "W", SocketType::Float, 1.0f });
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({ "Rotation", SocketType::Rotation, Variant() });
}
void QuaternionToRotationNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 4);
_emit_rot(p_ctx, p_node_idx, 0, in, [&](int e) {
Quaternion q(in.f(1, e), in.f(2, e), in.f(3, e), in.f(0, e));
if (q.length_squared() < 1e-12f) q = Quaternion();
return q.normalized();
});
}
void RotationToAxisAngleNode::_bind_methods() {}
RotationToAxisAngleNode::RotationToAxisAngleNode() {
input_defs.push_back({ "Rotation", SocketType::Rotation, Variant() });
output_defs.push_back({ "Axis", SocketType::Vector, Variant() });
output_defs.push_back({ "Angle", SocketType::Float, Variant() });
}
void RotationToAxisAngleNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 1);
_emit_vec(p_ctx, p_node_idx, 0, in, [&](int e) { return in.quat(0, e).normalized().get_axis(); });
_emit_f(p_ctx, p_node_idx, 1, in, [&](int e) { return in.quat(0, e).normalized().get_angle(); });
}
void RotationToEulerNode::_bind_methods() {}
RotationToEulerNode::RotationToEulerNode() {
input_defs.push_back({ "Rotation", SocketType::Rotation, Variant() });
output_defs.push_back({ "Euler", SocketType::Vector, Variant() });
}
void RotationToEulerNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 1);
_emit_vec(p_ctx, p_node_idx, 0, in, [&](int e) { return pn_euler_from_quat(in.quat(0, e)); });
}
void RotationToQuaternionNode::_bind_methods() {}
RotationToQuaternionNode::RotationToQuaternionNode() {
input_defs.push_back({ "Rotation", SocketType::Rotation, Variant() });
output_defs.push_back({ "W", SocketType::Float, Variant() });
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 RotationToQuaternionNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 1);
_emit_f(p_ctx, p_node_idx, 0, in, [&](int e) { return in.quat(0, e).w; });
_emit_f(p_ctx, p_node_idx, 1, in, [&](int e) { return in.quat(0, e).x; });
_emit_f(p_ctx, p_node_idx, 2, in, [&](int e) { return in.quat(0, e).y; });
_emit_f(p_ctx, p_node_idx, 3, in, [&](int e) { return in.quat(0, e).z; });
}
void InvertRotationNode::_bind_methods() {}
InvertRotationNode::InvertRotationNode() {
input_defs.push_back({ "Rotation", SocketType::Rotation, Variant() });
output_defs.push_back({ "Rotation", SocketType::Rotation, Variant() });
}
void InvertRotationNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 1);
_emit_rot(p_ctx, p_node_idx, 0, in, [&](int e) { return in.quat(0, e).normalized().inverse(); });
}
void RotateRotationNode::_bind_methods() {}
RotateRotationNode::RotateRotationNode() {
input_defs.push_back({ "Rotation", SocketType::Rotation, Variant() });
input_defs.push_back({ "Rotate By", SocketType::Rotation, Variant() });
output_defs.push_back({ "Rotation", SocketType::Rotation, Variant() });
}
void RotateRotationNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 2);
_emit_rot(p_ctx, p_node_idx, 0, in, [&](int e) {
return (in.quat(1, e).normalized() * in.quat(0, e).normalized()).normalized();
});
}
void RotateVectorNode::_bind_methods() {}
RotateVectorNode::RotateVectorNode() {
input_defs.push_back({ "Vector", SocketType::Vector, Vector3() });
input_defs.push_back({ "Rotation", SocketType::Rotation, Variant() });
output_defs.push_back({ "Vector", SocketType::Vector, Variant() });
}
void RotateVectorNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 2);
_emit_vec(p_ctx, p_node_idx, 0, in, [&](int e) {
return in.quat(1, e).normalized().xform(in.vec(0, e));
});
}
void InputRotationNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_rotation", "q"), &InputRotationNode::set_rotation);
ClassDB::bind_method(D_METHOD("get_rotation"), &InputRotationNode::get_rotation);
ADD_PROPERTY(PropertyInfo(Variant::QUATERNION, "rotation"), "set_rotation", "get_rotation");
}
InputRotationNode::InputRotationNode() {
output_defs.push_back({ "Rotation", SocketType::Rotation, Variant() });
}
void InputRotationNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
p_ctx.write_rotation(p_node_idx, 0, rotation);
}
void RotateEulerNode::_bind_methods() {}
RotateEulerNode::RotateEulerNode() {
input_defs.push_back({ "Rotation", SocketType::Vector, Vector3() });
input_defs.push_back({ "Rotate By", SocketType::Vector, Vector3() });
output_defs.push_back({ "Rotation", SocketType::Vector, Variant() });
}
void RotateEulerNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 2);
_emit_vec(p_ctx, p_node_idx, 0, in, [&](int e) {
Quaternion a = pn_quat_from_euler(in.vec(0, e));
Quaternion b = pn_quat_from_euler(in.vec(1, e));
return pn_euler_from_quat((b * a).normalized());
});
}
void VectorRotateNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_rotation_type", "t"), &VectorRotateNode::set_rotation_type);
ClassDB::bind_method(D_METHOD("get_rotation_type"), &VectorRotateNode::get_rotation_type);
ADD_PROPERTY(PropertyInfo(Variant::INT, "rotation_type"), "set_rotation_type", "get_rotation_type");
ClassDB::bind_method(D_METHOD("set_invert", "i"), &VectorRotateNode::set_invert);
ClassDB::bind_method(D_METHOD("get_invert"), &VectorRotateNode::get_invert);
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "invert"), "set_invert", "get_invert");
}
VectorRotateNode::VectorRotateNode() {
input_defs.push_back({ "Vector", SocketType::Vector, Vector3() });
input_defs.push_back({ "Center", SocketType::Vector, Vector3() });
input_defs.push_back({ "Axis", SocketType::Vector, Vector3(0, 0, 1) });
input_defs.push_back({ "Angle", SocketType::Float, 0.0f });
// Blender's fifth socket, only read by EULER_XYZ.
input_defs.push_back({ "Rotation", SocketType::Vector, Vector3() });
output_defs.push_back({ "Vector", SocketType::Vector, Variant() });
}
void VectorRotateNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 5);
_emit_vec(p_ctx, p_node_idx, 0, in, [&](int e) {
Vector3 v = in.vec(0, e);
Vector3 c = in.vec(1, e);
// EULER_XYZ builds a matrix from the Rotation socket and INVERTS the
// matrix rather than negating the angles -- for a composed rotation
// those are the same thing only because the inverse of a rotation
// matrix is its transpose, which is what invert_m3 gives here.
if (rotation_type == 4) {
Basis b = Basis::from_euler(in.vec(4, e), PN_EULER_ORDER);
if (invert) {
b = b.inverse();
}
return c + b.xform(v - c);
}
Vector3 axis;
switch (rotation_type) {
case 1: axis = Vector3(1, 0, 0); break;
case 2: axis = Vector3(0, 1, 0); break;
case 3: axis = Vector3(0, 0, 1); break;
default: axis = in.vec(2, e); break;
}
float angle = in.f(3, e);
if (invert) {
angle = -angle;
}
if (axis.length_squared() < 1e-12f) {
return v;
}
return c + Quaternion(axis.normalized(), angle).xform(v - c);
});
}
void CombineMatrixNode::_bind_methods() {}
CombineMatrixNode::CombineMatrixNode() {
input_defs.push_back({ "Column 1 Row 1", SocketType::Float, 1.0f });
input_defs.push_back({ "Column 1 Row 2", SocketType::Float, 0.0f });
input_defs.push_back({ "Column 1 Row 3", SocketType::Float, 0.0f });
input_defs.push_back({ "Column 1 Row 4", SocketType::Float, 0.0f });
input_defs.push_back({ "Column 2 Row 1", SocketType::Float, 0.0f });
input_defs.push_back({ "Column 2 Row 2", SocketType::Float, 1.0f });
input_defs.push_back({ "Column 2 Row 3", SocketType::Float, 0.0f });
input_defs.push_back({ "Column 2 Row 4", SocketType::Float, 0.0f });
input_defs.push_back({ "Column 3 Row 1", SocketType::Float, 0.0f });
input_defs.push_back({ "Column 3 Row 2", SocketType::Float, 0.0f });
input_defs.push_back({ "Column 3 Row 3", SocketType::Float, 1.0f });
input_defs.push_back({ "Column 3 Row 4", SocketType::Float, 0.0f });
input_defs.push_back({ "Column 4 Row 1", SocketType::Float, 0.0f });
input_defs.push_back({ "Column 4 Row 2", SocketType::Float, 0.0f });
input_defs.push_back({ "Column 4 Row 3", SocketType::Float, 0.0f });
input_defs.push_back({ "Column 4 Row 4", SocketType::Float, 1.0f });
output_defs.push_back({ "Matrix", SocketType::Matrix, Variant() });
}
void CombineMatrixNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 16);
_emit_mat(p_ctx, p_node_idx, 0, in, [&](int e) {
Projection p;
for (int c = 0; c < 4; c++) {
for (int r = 0; r < 4; r++) {
p.columns[c][r] = in.f(c * 4 + r, e);
}
}
return p;
});
}
void SeparateMatrixNode::_bind_methods() {}
SeparateMatrixNode::SeparateMatrixNode() {
input_defs.push_back({ "Matrix", SocketType::Matrix, Variant() });
output_defs.push_back({ "Column 1 Row 1", SocketType::Float, Variant() });
output_defs.push_back({ "Column 1 Row 2", SocketType::Float, Variant() });
output_defs.push_back({ "Column 1 Row 3", SocketType::Float, Variant() });
output_defs.push_back({ "Column 1 Row 4", SocketType::Float, Variant() });
output_defs.push_back({ "Column 2 Row 1", SocketType::Float, Variant() });
output_defs.push_back({ "Column 2 Row 2", SocketType::Float, Variant() });
output_defs.push_back({ "Column 2 Row 3", SocketType::Float, Variant() });
output_defs.push_back({ "Column 2 Row 4", SocketType::Float, Variant() });
output_defs.push_back({ "Column 3 Row 1", SocketType::Float, Variant() });
output_defs.push_back({ "Column 3 Row 2", SocketType::Float, Variant() });
output_defs.push_back({ "Column 3 Row 3", SocketType::Float, Variant() });
output_defs.push_back({ "Column 3 Row 4", SocketType::Float, Variant() });
output_defs.push_back({ "Column 4 Row 1", SocketType::Float, Variant() });
output_defs.push_back({ "Column 4 Row 2", SocketType::Float, Variant() });
output_defs.push_back({ "Column 4 Row 3", SocketType::Float, Variant() });
output_defs.push_back({ "Column 4 Row 4", SocketType::Float, Variant() });
}
void SeparateMatrixNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 1);
for (int c = 0; c < 4; c++) {
for (int r = 0; r < 4; r++) {
_emit_f(p_ctx, p_node_idx, c * 4 + r, in,
[&](int e) { return in.mat(0, e).columns[c][r]; });
}
}
}
void InvertMatrixNode::_bind_methods() {}
InvertMatrixNode::InvertMatrixNode() {
input_defs.push_back({ "Matrix", SocketType::Matrix, Variant() });
output_defs.push_back({ "Matrix", SocketType::Matrix, Variant() });
output_defs.push_back({ "Invertible", SocketType::Bool, Variant() });
}
void InvertMatrixNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 1);
_emit_mat(p_ctx, p_node_idx, 0, in, [&](int e) {
Projection p = in.mat(0, e);
return Math::abs(p.determinant()) > 1e-12 ? p.inverse() : Projection();
});
// Invertible is a BOOL socket; a bool field is carried as an int field.
if (!in.field) {
p_ctx.write_bool(p_node_idx, 1, Math::abs(in.mat(0, 0).determinant()) > 1e-12);
} else {
PackedInt32Array a;
a.resize(in.n);
for (int e = 0; e < in.n; e++) {
a.ptrw()[e] = Math::abs(in.mat(0, e).determinant()) > 1e-12 ? 1 : 0;
}
p_ctx.write_int_field(p_node_idx, 1, a, in.domain);
}
}
void MatrixDeterminantNode::_bind_methods() {}
MatrixDeterminantNode::MatrixDeterminantNode() {
input_defs.push_back({ "Matrix", SocketType::Matrix, Variant() });
output_defs.push_back({ "Determinant", SocketType::Float, Variant() });
}
void MatrixDeterminantNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 1);
_emit_f(p_ctx, p_node_idx, 0, in, [&](int e) { return (float)in.mat(0, e).determinant(); });
}
void MatrixMultiplyNode::_bind_methods() {}
MatrixMultiplyNode::MatrixMultiplyNode() {
input_defs.push_back({ "Matrix", SocketType::Matrix, Variant() });
input_defs.push_back({ "Matrix", SocketType::Matrix, Variant() });
output_defs.push_back({ "Matrix", SocketType::Matrix, Variant() });
}
void MatrixMultiplyNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 2);
_emit_mat(p_ctx, p_node_idx, 0, in,
[&](int e) { return in.mat(0, e) * in.mat(1, e); });
}
void TransposeMatrixNode::_bind_methods() {}
TransposeMatrixNode::TransposeMatrixNode() {
input_defs.push_back({ "Matrix", SocketType::Matrix, Variant() });
output_defs.push_back({ "Matrix", SocketType::Matrix, Variant() });
}
void TransposeMatrixNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
PnIn in(p_ctx, p_node_idx, 1);
_emit_mat(p_ctx, p_node_idx, 0, in, [&](int e) {
Projection p = in.mat(0, e);
Projection t;
for (int c = 0; c < 4; c++) {
for (int r = 0; r < 4; r++) {
t.columns[c][r] = p.columns[r][c];
}
}
return t;
});
}
// --- GLSL for the two rotation nodes the density cone needs ------------------
//
// PN_EULER_ORDER is ZYX, so the basis is Rz * Ry * Rx and the quaternion is
// qz * qy * qx -- matrix product order, which is what "ZYX" names here. The
// composition is written out rather than fused so the order is readable at the
// point it matters.
static const char *PN_QUAT_GLSL = R"(
#ifndef PN_QUAT_ROT_HELPERS
#define PN_QUAT_ROT_HELPERS
vec4 pn_quat_axis(vec3 axis, float ang) {
float h = ang * 0.5;
return vec4(axis * sin(h), cos(h));
}
vec4 pn_quat_mul(vec4 a, vec4 b) {
return vec4(a.w * b.xyz + b.w * a.xyz + cross(a.xyz, b.xyz),
a.w * b.w - dot(a.xyz, b.xyz));
}
vec4 pn_quat_from_euler_zyx(vec3 e) {
vec4 qz = pn_quat_axis(vec3(0.0, 0.0, 1.0), e.z);
vec4 qy = pn_quat_axis(vec3(0.0, 1.0, 0.0), e.y);
vec4 qx = pn_quat_axis(vec3(1.0, 0.0, 0.0), e.x);
return pn_quat_mul(pn_quat_mul(qz, qy), qx);
}
vec3 pn_quat_xform(vec4 q, vec3 v) {
q = normalize(q);
vec3 t = 2.0 * cross(q.xyz, v);
return v + q.w * t + cross(q.xyz, t);
}
#endif
)";
String EulerToRotationNode::compile_glsl_prelude(int) const {
return String(PN_QUAT_GLSL);
}
String EulerToRotationNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
return p_ctx.declare(p_node_idx, 0) + String(" = pn_quat_from_euler_zyx(") +
p_ctx.in(p_node_idx, 0) + String(");");
}
String RotateVectorNode::compile_glsl_prelude(int) const {
return String(PN_QUAT_GLSL);
}
String RotateVectorNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
// execute() normalizes the quaternion before xform; pn_quat_xform does the
// same, so an unnormalized Rotation arriving from either side behaves alike.
return p_ctx.declare(p_node_idx, 0) + String(" = pn_quat_xform(") +
p_ctx.in(p_node_idx, 1) + String(", ") + p_ctx.in(p_node_idx, 0) + String(");");
}
} // namespace godot