Shader Nodes: support Integer Math node in EEVEE and Cycles

This adds the Integer Math node to shader nodes. It's the same node that
can already be used in Geometry Nodes and the Compositor.

While creating the regression test, I noticed that the integer math
implementation was a bit broken:
* The integer modulo (`%`) operator on the GPU is undefined for negative
  divisors (the added regression test showed that the behavior is not
  consistent across platforms).
* Use integer power function instead of using float implementation with
  casts.

The MaterialX implementation is omitted for now. I didn't look into it
in detail yet but for the Boolean Math node (#162798) it seemed like
some more work is needed there to support this type.

Pull Request: https://projects.blender.org/blender/blender/pulls/163805
This commit is contained in:
Jacques Lucke 2026-09-14 10:36:19 +02:00
parent fe998a1cd9
commit ba82476ade
19 changed files with 389 additions and 7 deletions

View file

@ -549,6 +549,11 @@ static ShaderNode *add_node(Scene *scene,
boolean_math_node->set_math_type((NodeBooleanMathType)b_node.custom1);
node = boolean_math_node;
}
else if (b_node.is_type("FunctionNodeIntegerMath"_ustr)) {
IntegerMathNode *integer_math_node = graph->create_node<IntegerMathNode>();
integer_math_node->set_math_type((NodeIntegerMathType)b_node.custom1);
node = integer_math_node;
}
else if (b_node.is_type("ShaderNodeVectorMath"_ustr)) {
VectorMathNode *vector_math_node = graph->create_node<VectorMathNode>();
vector_math_node->set_math_type((NodeVectorMathType)b_node.custom1);

View file

@ -85,6 +85,7 @@ set(SRC_KERNEL_SVM_HEADERS
svm/mapping_util.h
svm/math.h
svm/math_util.h
svm/integer_math.h
svm/mix.h
svm/node_types.h
svm/node_types_template.h

View file

@ -58,6 +58,7 @@ set(SRC_OSL
node_map_range.osl
node_mapping.osl
node_math.osl
node_integer_math.osl
node_metallic_bsdf.osl
node_mix.osl
node_mix_closure.osl

View file

@ -0,0 +1,96 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include "stdcycles.h"
int integer_math_divide_floor(int a, int b)
{
int d = a / b;
int r = a % b;
return (r != 0) ? (d - int((a < 0) != (b < 0))) : d;
}
int integer_math_gcd(int a_in, int b_in)
{
int a = abs(a_in);
int b = abs(b_in);
while (b != 0) {
int t = b;
b = a % b;
a = t;
}
return a;
}
int integer_math_power(int base_in, int exponent_in)
{
int base = base_in;
int exponent = exponent_in;
if (exponent < 0) {
if (base == 1 || base == -1) {
return (base < 0 && (exponent & 1) != 0) ? -1 : 1;
}
return 0;
}
int result = 1;
while (exponent != 0) {
if ((exponent & 1) != 0) {
result *= base;
}
exponent = exponent >> 1;
base = base * base;
}
return result;
}
shader node_integer_math(
string math_type = "add", int Value1 = 0, int Value2 = 0, int Value3 = 0, output int Value = 0)
{
if (math_type == "add")
Value = Value1 + Value2;
else if (math_type == "subtract")
Value = Value1 - Value2;
else if (math_type == "multiply")
Value = Value1 * Value2;
else if (math_type == "divide")
Value = (Value2 != 0) ? (Value1 / Value2) : 0;
else if (math_type == "multiply_add")
Value = Value1 * Value2 + Value3;
else if (math_type == "power")
Value = integer_math_power(Value1, Value2);
else if (math_type == "floored_modulo")
Value = (Value2 != 0) ? (((Value1 % Value2) + Value2) % Value2) : 0;
else if (math_type == "absolute")
Value = abs(Value1);
else if (math_type == "minimum")
Value = min(Value1, Value2);
else if (math_type == "maximum")
Value = max(Value1, Value2);
else if (math_type == "gcd")
Value = integer_math_gcd(Value1, Value2);
else if (math_type == "lcm") {
int gcd = integer_math_gcd(Value1, Value2);
Value = (gcd != 0) ? abs(Value1 / gcd * Value2) : 0;
}
else if (math_type == "negate")
Value = -Value1;
else if (math_type == "sign")
Value = int(0 < Value1) - int(Value1 < 0);
else if (math_type == "divide_floor")
Value = (Value2 != 0) ? integer_math_divide_floor(Value1, Value2) : 0;
else if (math_type == "divide_ceil")
Value = (Value2 != 0) ? -integer_math_divide_floor(Value1, -Value2) : 0;
else if (math_type == "divide_round") {
int abs_b = abs(Value2);
int sign_b = int(0 < Value2) - int(Value2 < 0);
if (Value1 >= 0)
Value = ((abs_b != 0) ? ((2 * Value1 + abs_b) / (2 * abs_b)) : 0) * sign_b;
else
Value = -((abs_b != 0) ? ((2 * -Value1 + abs_b) / (2 * abs_b)) : 0) * sign_b;
}
else if (math_type == "modulo")
Value = (Value2 != 0) ? (Value1 % Value2) : 0;
else
Value = 0;
}

View file

@ -0,0 +1,118 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "kernel/svm/node_types.h"
#include "kernel/svm/util.h"
CCL_NAMESPACE_BEGIN
ccl_device int svm_integer_math_divide_floor(const int a, const int b)
{
const int d = a / b;
const int r = a % b;
return (r != 0) ? (d - int((a < 0) != (b < 0))) : d;
}
ccl_device int svm_integer_math_gcd(int a, int b)
{
a = abs(a);
b = abs(b);
while (b != 0) {
const int t = b;
b = a % b;
a = t;
}
return a;
}
ccl_device int svm_integer_math_power(int base, int exponent)
{
if (exponent < 0) {
if (base == 1 || base == -1) {
return (base < 0 && (exponent & 1) != 0) ? -1 : 1;
}
return 0;
}
int result = 1;
while (exponent != 0) {
if ((exponent & 1) != 0) {
result *= base;
}
exponent >>= 1;
base *= base;
}
return result;
}
ccl_device int svm_integer_math(const NodeIntegerMathType type,
const int a,
const int b,
const int c)
{
switch (type) {
case NODE_INTEGER_MATH_ADD:
return a + b;
case NODE_INTEGER_MATH_SUBTRACT:
return a - b;
case NODE_INTEGER_MATH_MULTIPLY:
return a * b;
case NODE_INTEGER_MATH_DIVIDE:
return (b != 0) ? (a / b) : 0;
case NODE_INTEGER_MATH_MULTIPLY_ADD:
return a * b + c;
case NODE_INTEGER_MATH_POWER:
return svm_integer_math_power(a, b);
case NODE_INTEGER_MATH_FLOORED_MODULO:
return (b != 0) ? (((a % b) + b) % b) : 0;
case NODE_INTEGER_MATH_ABSOLUTE:
return abs(a);
case NODE_INTEGER_MATH_MINIMUM:
return min(a, b);
case NODE_INTEGER_MATH_MAXIMUM:
return max(a, b);
case NODE_INTEGER_MATH_GCD:
return svm_integer_math_gcd(a, b);
case NODE_INTEGER_MATH_LCM: {
const int gcd = svm_integer_math_gcd(a, b);
return (gcd != 0) ? abs(a / gcd * b) : 0;
}
case NODE_INTEGER_MATH_NEGATE:
return -a;
case NODE_INTEGER_MATH_SIGN:
return int(0 < a) - int(a < 0);
case NODE_INTEGER_MATH_DIVIDE_FLOOR:
return (b != 0) ? svm_integer_math_divide_floor(a, b) : 0;
case NODE_INTEGER_MATH_DIVIDE_CEIL:
return (b != 0) ? -svm_integer_math_divide_floor(a, -b) : 0;
case NODE_INTEGER_MATH_DIVIDE_ROUND: {
const int abs_b = abs(b);
const int sign_b = int(0 < b) - int(b < 0);
if (a >= 0) {
return ((abs_b != 0) ? ((2 * a + abs_b) / (2 * abs_b)) : 0) * sign_b;
}
return -((abs_b != 0) ? ((2 * -a + abs_b) / (2 * abs_b)) : 0) * sign_b;
}
case NODE_INTEGER_MATH_MODULO:
return (b != 0) ? (a % b) : 0;
}
return 0;
}
ccl_device_noinline void svm_node_integer_math(
ccl_private float *ccl_restrict stack, const ccl_global SVMNodeIntegerMath &ccl_restrict node)
{
const int a = stack_load(stack, node.value1);
const int b = stack_load(stack, node.value2);
const int c = stack_load(stack, node.value3);
const int result = svm_integer_math(node.math_type, a, b, c);
if (stack_valid(node.result_offset)) {
stack_store_int(stack, node.result_offset, result);
}
}
CCL_NAMESPACE_END

View file

@ -71,6 +71,18 @@ struct SVMNodeBooleanMath {
static_assert(alignof(SVMNodeBooleanMath) <= alignof(uint));
static_assert(sizeof(SVMNodeBooleanMath) % sizeof(uint) == 0);
/* NODE_INTEGER_MATH */
struct SVMNodeIntegerMath {
NodeIntegerMathType math_type;
SVMInputInt value1;
SVMInputInt value2;
SVMInputInt value3;
SVMStackOffset result_offset;
uint8_t _pad[3];
};
static_assert(alignof(SVMNodeIntegerMath) <= alignof(uint));
static_assert(sizeof(SVMNodeIntegerMath) % sizeof(uint) == 0);
/* NODE_CLAMP */
struct SVMNodeClamp {
NodeClampType clamp_type;

View file

@ -50,6 +50,7 @@ SHADER_NODE_TYPE(NODE_VOLUME_COEFFICIENTS)
SHADER_NODE_TYPE(NODE_PRINCIPLED_VOLUME)
SHADER_NODE_TYPE(NODE_MATH)
SHADER_NODE_TYPE(NODE_BOOLEAN_MATH)
SHADER_NODE_TYPE(NODE_INTEGER_MATH)
SHADER_NODE_TYPE_DERIVATIVE(NODE_VECTOR_MATH)
SHADER_NODE_TYPE(NODE_RGB_RAMP)
SHADER_NODE_TYPE(NODE_GAMMA)

View file

@ -54,6 +54,7 @@
#include "kernel/svm/hsv.h"
#include "kernel/svm/ies.h"
#include "kernel/svm/image.h"
#include "kernel/svm/integer_math.h"
#include "kernel/svm/invert.h"
#include "kernel/svm/light_path.h"
#include "kernel/svm/magic.h"
@ -369,6 +370,9 @@ ccl_device void svm_eval_nodes(KernelGlobals kg,
SVM_CASE(NODE_BOOLEAN_MATH)
svm_node_boolean_math(stack, svm_node_get<SVMNodeBooleanMath>(kg, &offset));
break;
SVM_CASE(NODE_INTEGER_MATH)
svm_node_integer_math(stack, svm_node_get<SVMNodeIntegerMath>(kg, &offset));
break;
SVM_CASE(NODE_VECTOR_MATH)
svm_node_vector_math<float3>(stack, svm_node_get<SVMNodeVectorMath>(kg, &offset));
break;

View file

@ -233,6 +233,27 @@ enum NodeBooleanMathType : uint {
NODE_BOOLEAN_MATH_NIMPLY,
};
enum NodeIntegerMathType : uint {
NODE_INTEGER_MATH_ADD,
NODE_INTEGER_MATH_SUBTRACT,
NODE_INTEGER_MATH_MULTIPLY,
NODE_INTEGER_MATH_DIVIDE,
NODE_INTEGER_MATH_MULTIPLY_ADD,
NODE_INTEGER_MATH_POWER,
NODE_INTEGER_MATH_FLOORED_MODULO,
NODE_INTEGER_MATH_ABSOLUTE,
NODE_INTEGER_MATH_MINIMUM,
NODE_INTEGER_MATH_MAXIMUM,
NODE_INTEGER_MATH_GCD,
NODE_INTEGER_MATH_LCM,
NODE_INTEGER_MATH_NEGATE,
NODE_INTEGER_MATH_SIGN,
NODE_INTEGER_MATH_DIVIDE_FLOOR,
NODE_INTEGER_MATH_DIVIDE_CEIL,
NODE_INTEGER_MATH_DIVIDE_ROUND,
NODE_INTEGER_MATH_MODULO,
};
enum NodeVectorMathType : uint {
NODE_VECTOR_MATH_ADD,
NODE_VECTOR_MATH_SUBTRACT,

View file

@ -33,6 +33,7 @@
#include "kernel/closure/bsdf_microfacet.h"
#include "kernel/svm/boolean_math.h"
#include "kernel/svm/color_util.h"
#include "kernel/svm/integer_math.h"
#include "kernel/svm/mapping_util.h"
#include "kernel/svm/math_util.h"
#include "kernel/svm/ramp_util.h"
@ -7092,6 +7093,70 @@ void BooleanMathNode::compile(OSLCompiler &compiler)
compiler.add(this, "node_boolean_math");
}
/* Integer Math */
NODE_DEFINE(IntegerMathNode)
{
NodeType *type = NodeType::add("integer_math", create, NodeType::SHADER);
static NodeEnum type_enum;
type_enum.insert("add", NODE_INTEGER_MATH_ADD);
type_enum.insert("subtract", NODE_INTEGER_MATH_SUBTRACT);
type_enum.insert("multiply", NODE_INTEGER_MATH_MULTIPLY);
type_enum.insert("divide", NODE_INTEGER_MATH_DIVIDE);
type_enum.insert("multiply_add", NODE_INTEGER_MATH_MULTIPLY_ADD);
type_enum.insert("power", NODE_INTEGER_MATH_POWER);
type_enum.insert("floored_modulo", NODE_INTEGER_MATH_FLOORED_MODULO);
type_enum.insert("absolute", NODE_INTEGER_MATH_ABSOLUTE);
type_enum.insert("minimum", NODE_INTEGER_MATH_MINIMUM);
type_enum.insert("maximum", NODE_INTEGER_MATH_MAXIMUM);
type_enum.insert("gcd", NODE_INTEGER_MATH_GCD);
type_enum.insert("lcm", NODE_INTEGER_MATH_LCM);
type_enum.insert("negate", NODE_INTEGER_MATH_NEGATE);
type_enum.insert("sign", NODE_INTEGER_MATH_SIGN);
type_enum.insert("divide_floor", NODE_INTEGER_MATH_DIVIDE_FLOOR);
type_enum.insert("divide_ceil", NODE_INTEGER_MATH_DIVIDE_CEIL);
type_enum.insert("divide_round", NODE_INTEGER_MATH_DIVIDE_ROUND);
type_enum.insert("modulo", NODE_INTEGER_MATH_MODULO);
SOCKET_ENUM(math_type, "Type", type_enum, NODE_INTEGER_MATH_ADD);
SOCKET_IN_INT(value1, "Value1", 0);
SOCKET_IN_INT(value2, "Value2", 0);
SOCKET_IN_INT(value3, "Value3", 0);
SOCKET_OUT_INT(value, "Value");
return type;
}
IntegerMathNode::IntegerMathNode() : ShaderNode(get_node_type()) {}
void IntegerMathNode::constant_fold(const ConstantFolder &folder)
{
if (folder.all_inputs_constant()) {
folder.make_constant(svm_integer_math(math_type, value1, value2, value3));
}
}
void IntegerMathNode::compile(SVMCompiler &compiler)
{
compiler.add_node(this,
NODE_INTEGER_MATH,
SVMNodeIntegerMath{
.math_type = math_type,
.value1 = compiler.input_int("Value1"),
.value2 = compiler.input_int("Value2"),
.value3 = compiler.input_int("Value3"),
.result_offset = compiler.output("Value"),
});
}
void IntegerMathNode::compile(OSLCompiler &compiler)
{
compiler.parameter(this, "math_type");
compiler.add(this, "node_integer_math");
}
/* VectorMath */
NODE_DEFINE(VectorMathNode)

View file

@ -1559,6 +1559,17 @@ class BooleanMathNode : public ShaderNode {
NODE_SOCKET_API(NodeBooleanMathType, math_type)
};
class IntegerMathNode : public ShaderNode {
public:
SHADER_NODE_CLASS(IntegerMathNode)
void constant_fold(const ConstantFolder &folder) override;
NODE_SOCKET_API(int, value1)
NODE_SOCKET_API(int, value2)
NODE_SOCKET_API(int, value3)
NODE_SOCKET_API(NodeIntegerMathType, math_type)
};
class NormalNode : public ShaderNode {
public:
SHADER_NODE_CLASS(NormalNode)

View file

@ -452,6 +452,12 @@ class NODE_MT_shader_node_math_base(node_add_menu.NodeMenu):
self.node_operator(layout, "ShaderNodeClamp")
self.node_operator(layout, "ShaderNodeFloatCurve")
self.node_operator(layout, "ShaderNodeMapRange")
self.node_operator_with_searchable_enum(
context,
layout,
"FunctionNodeIntegerMath",
"operation",
defaults_callback=node_add_menu.set_int_math_node_default_props)
self.node_operator_with_searchable_enum(
context,
layout,

View file

@ -555,7 +555,17 @@ static void register_common_functions_impl()
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"int ** int", [](int a, int b) { return math::pow(a, b); }, exec_fast);
"int ** int",
[](int base, int exponent) {
if (exponent < 0) {
if (base == 1 || base == -1) {
return (base < 0 && (exponent & 1) != 0) ? -1 : 1;
}
return 0;
}
return pow_i(base, exponent);
},
exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI3_SO<int, int, int, int>(

View file

@ -9,8 +9,7 @@
int divide_floor(int a, int b)
{
int d = a / b;
int r = a % b;
return (r != 0) ? (d - int((a < 0) != (b < 0))) : d;
return (a != d * b) ? (d - int((a < 0) != (b < 0))) : d;
}
int euclid_gcd(int a, int b)
@ -55,16 +54,35 @@ void integer_math_multiply_add(int a, int b, int c, int &result)
result = a * b + c;
}
int integer_power(int base, int exponent)
{
if (exponent < 0) {
if (base == 1 || base == -1) {
return (base < 0 && (exponent & 1) != 0) ? -1 : 1;
}
return 0;
}
int result = 1;
while (exponent != 0) {
if ((exponent & 1) != 0) {
result *= base;
}
exponent >>= 1;
base *= base;
}
return result;
}
[[node]]
void integer_math_power(int base, int exponent, int /*c*/, int &result)
{
result = int(pow(float(base), float(exponent)));
result = integer_power(base, exponent);
}
[[node]]
void integer_math_floored_modulo(int a, int b, int /*c*/, int &result)
{
result = (b != 0) ? (((a % b) + b) % b) : 0;
result = (b != 0) ? (a - divide_floor(a, b) * b) : 0;
}
[[node]]
@ -138,5 +156,6 @@ void integer_math_divide_round(int a, int b, int /*c*/, int &result)
[[node]]
void integer_math_modulo(int a, int b, int /*c*/, int &result)
{
result = (b != 0) ? (a % b) : 0;
/* Can't use `%` as it is undefined for negative b on the GPU.*/
result = (b != 0) ? a - (a / b) * b : 0;
}

View file

@ -345,7 +345,7 @@ static void node_register()
{
static bke::bNodeType ntype;
fn_cmp_node_type_base(&ntype, "FunctionNodeIntegerMath"_ustr, FN_NODE_INTEGER_MATH);
common_node_type_base(&ntype, "FunctionNodeIntegerMath"_ustr, FN_NODE_INTEGER_MATH);
ntype.ui_name = "Integer Math";
ntype.ui_description = "Perform various math operations on the given integer inputs";
ntype.enum_name_legacy = "INTEGER_MATH";

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0cfc2bb10378e7ca9907e504d5f72d606a0740cbd134b36f6ba943c4aa09f33e
size 13588

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1018a56d697b1eff3ec26e56f7fcfcb74478cda9bb0e35ff21622c925dc005f7
size 4724

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0611d15d780dc6f429d534de5c02d53222108cf80f28e8f32878c6f0bca79174
size 1274148

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:71cdc833ad0d243e05666bb3b50d81a2165a674690907e3494ab75d7eb4b4cff
size 1589