// SPDX-License-Identifier: Apache-2.0 // // Cycles node semantics: what Blender's Cycles renderer computes for the Math, Map Range, Clamp, Mix and // RGB to BW nodes and for a Color Ramp lookup, ported function by function from the Cycles kernel // (intern/cycles of the Blender v5.2.2 tag; SPDX-FileCopyrightText: 2011-2022 Blender Foundation, Apache-2.0): // kernel/svm/{math_util,map_range,clamp,mix,color_util,ramp,convert}.h, util/math_base.h, util/color.h and // util/math_float3.h. Each function's comment names its Cycles source. Modified by GameCult: Cycles C++ // becomes C#, a function per node operation, and the node's socket defaults and clamp options are left to // the caller. This file is Apache-2.0 (section 4: it was changed); the rest of CultMath is MIT, except the // MPL-2.0 Phacelle files (see THIRD-PARTY-NOTICES.md). shaders/CultMath.Cycles.hlsl mirrors it bit for bit. // // Operands are finite floats. NaN and infinite operands are outside the oracle's reach and the mirror's. // CyclesOracleTests holds every function to the results of Blender 5.2.2's own CPU Cycles. namespace CultMath; public static partial class math { // FLT_EPSILON, the least epsilon a Compare node honours. private const float CYCLES_FLT_EPSILON = 1.1920929e-07f; /// The scene-linear red luminance coefficient Cycles' RGB to BW node reads from Blender 5.2.2's default OCIO config. public const float CYCLES_RGB_TO_Y_R = 0.2126729f; /// The scene-linear green luminance coefficient Cycles' RGB to BW node reads from Blender 5.2.2's default OCIO config. public const float CYCLES_RGB_TO_Y_G = 0.7151522f; /// The scene-linear blue luminance coefficient Cycles' RGB to BW node reads from Blender 5.2.2's default OCIO config. public const float CYCLES_RGB_TO_Y_B = 0.0721750f; // ---- Math node (kernel/svm/math_util.h svm_math; util/math_base.h) ---- /// Math node Divide (safe_divide): a / b, or 0 when b is 0. public static float cycles_divide(float a, float b) => b != 0.0f ? a / b : 0.0f; /// /// Math node Power (safe_powf with compatible_powf): 0 for a negative base with a fractional exponent, /// 1 for exponent 0, 0 for base 0. A negative base takes an integral exponent's parity. An exponent past /// the int range counts as fractional for a negative base, where Cycles' int round trip cannot hold it. /// public static float cycles_power(float a, float b) { var whole = b >= 0.0f ? floor(b) : ceil(b); if (a < 0.0f && (b != whole || b >= 2147483648.0f || b < -2147483648.0f)) return 0.0f; if (b == 0.0f) return 1.0f; if (a == 0.0f) return 0.0f; if (a < 0.0f) { var magnitude = pow(-a, b); return b - 2.0f * floor(0.5f * b) != 0.0f ? -magnitude : magnitude; } return pow(a, b); } /// Math node Logarithm (safe_logf): log(a) / log(b), or 0 when a or b is not positive or log(b) is 0. public static float cycles_logarithm(float a, float b) { if (a <= 0.0f || b <= 0.0f) return 0.0f; var denominator = log(b); return denominator != 0.0f ? log(a) / denominator : 0.0f; } /// Math node Square Root (safe_sqrtf): the square root of max(a, 0). public static float cycles_sqrt(float a) => sqrt(max(a, 0.0f)); /// Math node Inverse Square Root (inversesqrtf): 1 / sqrt(a), or 0 when a is not positive. public static float cycles_inverse_sqrt(float a) => a > 0.0f ? 1.0f / sqrt(a) : 0.0f; /// /// Math node Truncated Modulo (safe_modulo), the remainder with the dividend's sign, or 0 when b is 0. /// Cycles computes fmodf. GLSL has no fmod and HLSL's is this quotient form on a GPU, so this is the form /// every CultMath target shares: it is not fmodf bit for bit, and CyclesOracleTests states how far it is. /// public static float cycles_modulo(float a, float b) { var quotient = a / b; return b != 0.0f ? a - b * (quotient >= 0.0f ? floor(quotient) : ceil(quotient)) : 0.0f; } /// Math node Floored Modulo (safe_floored_modulo): a - floor(a / b) b, or 0 when b is 0. public static float cycles_floored_modulo(float a, float b) => b != 0.0f ? a - floor(a / b) * b : 0.0f; /// Math node Wrap (wrapf; note the argument order): value wrapped into [min_value, max_value), or min_value for an empty range. public static float cycles_wrap(float value, float max_value, float min_value) { var range = max_value - min_value; return range != 0.0f ? value - (range * floor((value - min_value) / range)) : min_value; } /// Math node Snap: floor(safe_divide(a, b)) b. public static float cycles_snap(float a, float b) => floor(cycles_divide(a, b)) * b; /// Math node Ping-Pong (pingpongf): a triangle wave of a with period 2 b, or 0 when b is 0. public static float cycles_pingpong(float a, float b) { var scaled = (a - b) / (b * 2.0f); return b != 0.0f ? abs((scaled - floor(scaled)) * b * 2.0f - b) : 0.0f; } /// Math node Smooth Minimum (smoothminf): the polynomial smooth minimum of width k, or min(a, b) when k is 0. public static float cycles_smooth_min(float a, float b, float k) { var h = max(k - abs(a - b), 0.0f) / k; return k != 0.0f ? min(a, b) - h * h * h * k * 0.16666667f : min(a, b); } /// Math node Smooth Maximum: -smoothminf(-a, -b, k). public static float cycles_smooth_max(float a, float b, float k) => -cycles_smooth_min(-a, -b, k); /// Math node Compare: 1 when a equals b or they differ by at most max(epsilon, FLT_EPSILON), else 0. public static float cycles_compare(float a, float b, float epsilon) => (a == b || abs(a - b) <= max(epsilon, CYCLES_FLT_EPSILON)) ? 1.0f : 0.0f; /// Math node Round: floor(a + 0.5). public static float cycles_round(float a) => floor(a + 0.5f); /// Math node Truncate: floor for a >= 0, ceil otherwise. public static float cycles_trunc(float a) => a >= 0.0f ? floor(a) : ceil(a); /// Math node Fraction: a - floor(a). public static float cycles_fract(float a) => a - floor(a); /// Math node Sign (compatible_signf): 0 for 0, -1 below, 1 above. public static float cycles_sign(float a) => a == 0.0f ? 0.0f : (a < 0.0f ? -1.0f : 1.0f); // ---- Map Range node (kernel/svm/map_range.h svm_node_map_range) ---- // A Map Range node's Clamp option is a Clamp node of type Range on its result (MapRangeNode::expand): // compose cycles_clamp_range over these. /// Map Range, Linear: to_min + (value - from_min) / (from_max - from_min) (to_max - to_min), or 0 when from_max equals from_min. public static float cycles_map_range_linear(float value, float from_min, float from_max, float to_min, float to_max) { var factor = (value - from_min) / (from_max - from_min); return from_max != from_min ? to_min + factor * (to_max - to_min) : 0.0f; } /// Map Range, Stepped: the linear factor quantized to `steps` steps (0 when steps is not positive), or 0 when from_max equals from_min. public static float cycles_map_range_stepped(float value, float from_min, float from_max, float to_min, float to_max, float steps) { var factor = (value - from_min) / (from_max - from_min); factor = steps > 0.0f ? floor(factor * (steps + 1.0f)) / steps : 0.0f; return from_max != from_min ? to_min + factor * (to_max - to_min) : 0.0f; } /// /// Map Range, Smooth Step: the value goes through smoothstep(from_min, from_max, .), flipped when from_min /// is above from_max (util/math_base.h smoothstep), or 0 when from_max equals from_min. /// public static float cycles_map_range_smoothstep(float value, float from_min, float from_max, float to_min, float to_max) { var low = min(from_min, from_max); var high = max(from_min, from_max); var t = (value - low) / (high - low); var eased = value < low ? 0.0f : (value >= high ? 1.0f : (3.0f - 2.0f * t) * (t * t)); var factor = from_min > from_max ? 1.0f - eased : eased; return from_max != from_min ? to_min + factor * (to_max - to_min) : 0.0f; } /// Map Range, Smoother Step (map_range.h smootherstep), flipped when from_min is above from_max, or 0 when from_max equals from_min. public static float cycles_map_range_smootherstep(float value, float from_min, float from_max, float to_min, float to_max) { var low = min(from_min, from_max); var high = max(from_min, from_max); var x = clamp(cycles_divide(value - low, high - low), 0.0f, 1.0f); var eased = x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); var factor = from_min > from_max ? 1.0f - eased : eased; return from_max != from_min ? to_min + factor * (to_max - to_min) : 0.0f; } // ---- Clamp node (kernel/svm/clamp.h svm_node_clamp) ---- /// Clamp node, Min Max: clamp(value, min_value, max_value). public static float cycles_clamp_minmax(float value, float min_value, float max_value) => clamp(value, min_value, max_value); /// Clamp node, Range: as Min Max, with the bounds swapped when min_value is above max_value. public static float cycles_clamp_range(float value, float min_value, float max_value) => min_value > max_value ? clamp(value, max_value, min_value) : clamp(value, min_value, max_value); // ---- Mix node (kernel/svm/mix.h, color_util.h) ---- // Factor clamping (svm_node_mix_color's saturatef) and result clamping (saturate) are the caller's. /// Mix node, Float (svm_node_mix_float): a (1 - t) + b t. public static float cycles_mix_float(float t, float a, float b) => a * (1.0f - t) + b * t; /// Mix node, Vector, uniform factor (svm_node_mix_vector): a (1 - t) + b t per component. public static float3 cycles_mix_vector(float t, float3 a, float3 b) => a * (new float3(1.0f, 1.0f, 1.0f) - t) + b * t; /// Mix node, Mix blend mode (svm_mix_blend). public static float3 cycles_mix_blend(float t, float3 col1, float3 col2) => col1 + t * (col2 - col1); /// Mix node, Add blend mode (svm_mix_add). public static float3 cycles_mix_add(float t, float3 col1, float3 col2) => col1 + t * ((col1 + col2) - col1); /// Mix node, Multiply blend mode (svm_mix_mul). public static float3 cycles_mix_mul(float t, float3 col1, float3 col2) => col1 + t * (col1 * col2 - col1); /// Mix node, Screen blend mode (svm_mix_screen). public static float3 cycles_mix_screen(float t, float3 col1, float3 col2) { var tm = 1.0f - t; var one = new float3(1.0f, 1.0f, 1.0f); return one - (new float3(tm, tm, tm) + t * (one - col2)) * (one - col1); } /// Mix node, Overlay blend mode (svm_mix_overlay). public static float3 cycles_mix_overlay(float t, float3 col1, float3 col2) { var tm = 1.0f - t; return new float3( col1.x < 0.5f ? col1.x * (tm + 2.0f * t * col2.x) : 1.0f - (tm + 2.0f * t * (1.0f - col2.x)) * (1.0f - col1.x), col1.y < 0.5f ? col1.y * (tm + 2.0f * t * col2.y) : 1.0f - (tm + 2.0f * t * (1.0f - col2.y)) * (1.0f - col1.y), col1.z < 0.5f ? col1.z * (tm + 2.0f * t * col2.z) : 1.0f - (tm + 2.0f * t * (1.0f - col2.z)) * (1.0f - col1.z)); } /// Mix node, Subtract blend mode (svm_mix_sub). public static float3 cycles_mix_sub(float t, float3 col1, float3 col2) => col1 + t * ((col1 - col2) - col1); /// Mix node, Divide blend mode (svm_mix_div): a channel whose col2 is 0 keeps col1. public static float3 cycles_mix_div(float t, float3 col1, float3 col2) { var tm = 1.0f - t; return new float3( col2.x != 0.0f ? tm * col1.x + t * col1.x / col2.x : col1.x, col2.y != 0.0f ? tm * col1.y + t * col1.y / col2.y : col1.y, col2.z != 0.0f ? tm * col1.z + t * col1.z / col2.z : col1.z); } /// Mix node, Difference blend mode (svm_mix_diff). public static float3 cycles_mix_diff(float t, float3 col1, float3 col2) => col1 + t * (abs(col1 - col2) - col1); /// Mix node, Exclusion blend mode (svm_mix_exclusion). public static float3 cycles_mix_exclusion(float t, float3 col1, float3 col2) => max(col1 + t * ((col1 + col2 - 2.0f * col1 * col2) - col1), new float3(0.0f, 0.0f, 0.0f)); /// Mix node, Darken blend mode (svm_mix_dark). public static float3 cycles_mix_dark(float t, float3 col1, float3 col2) => col1 + t * (min(col1, col2) - col1); /// Mix node, Lighten blend mode (svm_mix_light). public static float3 cycles_mix_light(float t, float3 col1, float3 col2) => col1 + t * (max(col1, col2) - col1); /// Mix node, Dodge blend mode (svm_mix_dodge). public static float3 cycles_mix_dodge(float t, float3 col1, float3 col2) { var divisor = new float3(1.0f, 1.0f, 1.0f) - t * col2; return new float3( col1.x != 0.0f ? (divisor.x <= 0.0f ? 1.0f : min(col1.x / divisor.x, 1.0f)) : col1.x, col1.y != 0.0f ? (divisor.y <= 0.0f ? 1.0f : min(col1.y / divisor.y, 1.0f)) : col1.y, col1.z != 0.0f ? (divisor.z <= 0.0f ? 1.0f : min(col1.z / divisor.z, 1.0f)) : col1.z); } /// Mix node, Burn blend mode (svm_mix_burn). public static float3 cycles_mix_burn(float t, float3 col1, float3 col2) { var tm = 1.0f - t; var divisor = tm + t * col2; var burnt = new float3(1.0f, 1.0f, 1.0f) - (new float3(1.0f, 1.0f, 1.0f) - col1) / divisor; return new float3( divisor.x <= 0.0f ? 0.0f : clamp(burnt.x, 0.0f, 1.0f), divisor.y <= 0.0f ? 0.0f : clamp(burnt.y, 0.0f, 1.0f), divisor.z <= 0.0f ? 0.0f : clamp(burnt.z, 0.0f, 1.0f)); } /// RGB to HSV as Cycles computes it (util/color.h rgb_to_hsv): hue in [0, 1), saturation, value. public static float3 cycles_rgb_to_hsv(float3 rgb) { var cmax = max(rgb.x, max(rgb.y, rgb.z)); var cmin = min(rgb.x, min(rgb.y, rgb.z)); var cdelta = cmax - cmin; var s = cmax != 0.0f ? cdelta / cmax : 0.0f; var c = (new float3(cmax, cmax, cmax) - rgb) / cdelta; var h = rgb.x == cmax ? c.z - c.y : (rgb.y == cmax ? 2.0f + c.x - c.z : 4.0f + c.y - c.x); h = h / 6.0f; h = h < 0.0f ? h + 1.0f : h; return new float3(s != 0.0f ? h : 0.0f, s, cmax); } /// HSV to RGB as Cycles computes it (util/color.h hsv_to_rgb). public static float3 cycles_hsv_to_rgb(float3 hsv) { var h = hsv.x == 1.0f ? 0.0f : hsv.x; h = h * 6.0f; var i = floor(h); var f = h - i; var p = hsv.z * (1.0f - hsv.y); var q = hsv.z * (1.0f - (hsv.y * f)); var t = hsv.z * (1.0f - (hsv.y * (1.0f - f))); var rgb = i == 0.0f ? new float3(hsv.z, t, p) : (i == 1.0f ? new float3(q, hsv.z, p) : (i == 2.0f ? new float3(p, hsv.z, t) : (i == 3.0f ? new float3(p, q, hsv.z) : (i == 4.0f ? new float3(t, p, hsv.z) : new float3(hsv.z, p, q))))); return hsv.y != 0.0f ? rgb : new float3(hsv.z, hsv.z, hsv.z); } /// Mix node, Hue blend mode (svm_mix_hue). public static float3 cycles_mix_hue(float t, float3 col1, float3 col2) { var hsv2 = cycles_rgb_to_hsv(col2); var hsv = cycles_rgb_to_hsv(col1); var shifted = cycles_hsv_to_rgb(new float3(hsv2.x, hsv.y, hsv.z)); return hsv2.y != 0.0f ? col1 + t * (shifted - col1) : col1; } /// Mix node, Saturation blend mode (svm_mix_sat). public static float3 cycles_mix_sat(float t, float3 col1, float3 col2) { var tm = 1.0f - t; var hsv = cycles_rgb_to_hsv(col1); var hsv2 = cycles_rgb_to_hsv(col2); return hsv.y != 0.0f ? cycles_hsv_to_rgb(new float3(hsv.x, tm * hsv.y + t * hsv2.y, hsv.z)) : col1; } /// Mix node, Value blend mode (svm_mix_val). public static float3 cycles_mix_val(float t, float3 col1, float3 col2) { var tm = 1.0f - t; var hsv = cycles_rgb_to_hsv(col1); var hsv2 = cycles_rgb_to_hsv(col2); return cycles_hsv_to_rgb(new float3(hsv.x, hsv.y, tm * hsv.z + t * hsv2.z)); } /// Mix node, Color blend mode (svm_mix_color). public static float3 cycles_mix_color(float t, float3 col1, float3 col2) { var hsv2 = cycles_rgb_to_hsv(col2); var hsv = cycles_rgb_to_hsv(col1); var shifted = cycles_hsv_to_rgb(new float3(hsv2.x, hsv2.y, hsv.z)); return hsv2.y != 0.0f ? col1 + t * (shifted - col1) : col1; } /// Mix node, Soft Light blend mode (svm_mix_soft). public static float3 cycles_mix_soft(float t, float3 col1, float3 col2) { var tm = 1.0f - t; var one = new float3(1.0f, 1.0f, 1.0f); var scr = one - (one - col2) * (one - col1); return tm * col1 + t * ((one - col1) * col2 * col1 + col1 * scr); } /// Mix node, Linear Light blend mode (svm_mix_linear). public static float3 cycles_mix_linear(float t, float3 col1, float3 col2) => col1 + t * (2.0f * col2 + new float3(-1.0f, -1.0f, -1.0f)); // ---- RGB to BW node (kernel/svm/convert.h NODE_CONVERT_CF) ---- /// RGB to BW node: the scene-linear luminance of a colour. public static float cycles_rgb_to_bw(float3 color) => color.x * CYCLES_RGB_TO_Y_R + color.y * CYCLES_RGB_TO_Y_G + color.z * CYCLES_RGB_TO_Y_B; // ---- Color Ramp lookup (kernel/svm/ramp.h rgb_ramp_lookup, without extrapolation) ---- /// /// Where a Color Ramp of `size` table entries is read at t: (index, weight), the lower entry and the /// weight of the next one. The colour is table[index] when weight is 0 and /// (1 - weight) table[index] + weight table[index + 1] otherwise. The ramp table itself is the caller's. /// public static float2 cycles_ramp_index(float t, int size) { var f = (t > 0.0f ? (t < 1.0f ? t : 1.0f) : 0.0f) * (float)(size - 1); var index = clamp((int)f, 0, size - 1); var lower = (float)index; return new float2(lower, f - lower); } }