OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes commits / dcaf7d0

glsl: Wave, and Mix's colour and rotation -- the list is closed
The last three items off #64's GLSL debt. Every node that claimed GPU support now has arithmetic behind it, and every configuration of Mix emits. op parity 722 -> 770/770 (48 Wave cases) noise parity 4992 -> 5296/5296 (304 ramp_blend cases) golden 12/12 -> 14/14 blender 29/29 mix, 298/298 colour ramp, 26/26 crossing ramp .inc vs the old header 32800/32800 bit-exact WAVE mirrors execute(): both wave types, every band and ring direction, all three profiles, and Blender's precision nudge on unit coordinates, which shifts every result slightly and is not optional. Distortion is perlin fBM with lacunarity FIXED at 2 and normalize on -- the node has no socket for either, so they are literals. ⚠ SAW and TRI take fract(nn / 2pi), and the CPU does that division in DOUBLE because Math_PI is a double, where the shader does it in float. A last-bit difference in the quotient becomes a WHOLE-UNIT difference in fract() for any nn landing on a period boundary. All three profiles x both types x four directions are gated for exactly that reason; on these inputs it does not bite, which is measured and not assumed. MIX COLOUR is blender_ramp_blend.inc, the fifth dual-compiled source: nineteen distinct modes, four of them through HSV, several not a lerp of anything. Transformed from the header, not retyped; the out-parameter conversions return a vec3 now and Blender's `r_col`-is-both-input-and-result signature survives as a vec4 in and a vec4 out, indexed, which GLSL allows on a vector and the shim's operator[] provides. 32800/32800 bit-exact including the grey and black corners where rgb_to_hsv's 1e-20 bias rather than a branch is what keeps the hue defined. MIX ROTATION is a PORT, and it is the one place in this work that is not a shared text. The CPU calls godot-cpp's Quaternion::slerp, which is godot's C++ and not ours to dual-compile, so there really are two implementations and the emitted-op gate is what holds them together. Read off godot-cpp/src/variant/quaternion.cpp:115-153 -- CMP_EPSILON, the sign flip, and the linear fallback when the quaternions are nearly equal. ⚠ godot computes scale0 through a double literal and scale1 in float; that asymmetry cannot be reproduced in GLSL, which is why rotation is held to a tolerance rather than to the bit. THE GOLDEN GATE'S CASE G CHANGED MEANING, deliberately. It asserted that colour Mix REFUSED, which was the honest state while ramp_blend had no GLSL -- emitting a plain lerp would have turned all nineteen modes into MIX silently. Now both halves exist, so what is worth gating is not "does it refuse" but "does it emit the right thing": the emitted text must name br_ramp_blend with the configured mode and pn_quat_slerp, and -- after the max_distance bug in the Voronoi node -- must be shown to read sockets 6 and 7 rather than some other pair, since a value taken from the wrong place looks like a wrong blend and not a wrong address. STILL NOT DONE, and it is not a cleanup: get_noise_library_glsl() is still load-bearing for DisplaceNode and one other node, each paired with a FILE-LOCAL CPU copy of perlin/simplex in noise_nodes.cpp. Retiring those means re-pointing both at the .inc, which changes the emitted density shader for every shipping graph that uses Displace and therefore needs the terrain correlation re-measured. It deserves its own change, not a late line in this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dcaf7d07f6e0797707e91c71cfb4bc540ddf0d01
by plankatron <fleeky@gmail.com> · 2026-09-06T00:41:19.000Z

Changed files (11) +686 -264

SConstruct +6 -0
……@@ -72,6 +72,12 @@ glsl_inc_gen = [
7272 "src/shared/blender_voronoi.inc",
7373 "python3 tools/gen_glsl_inc.py $SOURCE $TARGET BLENDER_VORONOI_GLSL",
7474 ),
75 # The colour blend modes, fifth .inc. Depends on nothing else.
76 env.Command(
77 "src/gpu/blender_ramp_blend_glsl.gen.h",
78 "src/shared/blender_ramp_blend.inc",
79 "python3 tools/gen_glsl_inc.py $SOURCE $TARGET BLENDER_RAMP_BLEND_GLSL",
80 ),
7581]
7682
7783sources = (
demo/glsl_golden_gate.gd +45 -6
……@@ -198,17 +198,56 @@ func _init() -> void:
198198 _check("geometry chain still displaces", et.find("return pos.y - query_y;") != -1,
199199 "returns the displacement")
200200
201 # G. supports_gpu() CAN DEPEND ON CONFIGURATION. Mix emits float and vector
202 # blends; colour needs ramp_blend's nineteen modes and rotation a slerp,
203 # so those configurations must refuse rather than quietly emit a lerp --
204 # which would turn every colour blend into MIX.
201 # G. EVERY Mix CONFIGURATION EMITS NOW, and each emits its OWN blend.
202 # This case used to assert that colour REFUSED, which was the honest
203 # state while ramp_blend had no GLSL: emitting a plain lerp there would
204 # have turned all nineteen colour modes into MIX silently. Both halves
205 # landed 2026-09-05, so what is worth gating is no longer "does it
206 # refuse" but "does it emit the RIGHT thing" -- a regression to a lerp
207 # would still compile and still look like a blend.
205208 var mg := _erosion_graph()
206209 var mixn := MixNode.new()
207210 mixn.set("data_type", 2) # RGBA
211 mixn.set("blend_type", 12) # HUE, which is nothing like a lerp
208212 mg.add_node(mixn)
209213 var mt: String = mg.compile_to_glsl_function("proc_density", true, empty)
210 _check("colour Mix refuses", mt.is_empty(),
211 "emitted %d chars" % mt.length() if not mt.is_empty() else "emission refused")
214 _check("colour Mix emits ramp_blend", not mt.is_empty()
215 and mt.find("br_ramp_blend(12,") != -1,
216 "emitted %d chars" % mt.length() if not mt.is_empty() else "REFUSED")
217
218 # And it must address the COLOUR sockets, 6 and 7. The Voronoi work
219 # turned up exactly this class of bug -- a value taken from the wrong
220 # place looks like a wrong blend, not a wrong address -- so wire two
221 # distinguishable colours in and check they arrive as the operands, in
222 # that order.
223 var mgc := _erosion_graph()
224 var mixc := MixNode.new()
225 mixc.set("data_type", 2)
226 mixc.set("blend_type", 0)
227 var mixc_idx := mgc.add_node(mixc)
228 var ca := ColorNode.new()
229 ca.set("value", Color(0.125, 0.25, 0.375, 1.0))
230 var ca_idx := mgc.add_node(ca)
231 var cb := ColorNode.new()
232 cb.set("value", Color(0.875, 0.75, 0.625, 1.0))
233 var cb_idx := mgc.add_node(cb)
234 mgc.connect_sockets(ca_idx, 0, mixc_idx, 6)
235 mgc.connect_sockets(cb_idx, 0, mixc_idx, 7)
236 var mct: String = mgc.compile_to_glsl_function("proc_density", true, empty)
237 var a_var := "n%d_s0" % ca_idx
238 var b_var := "n%d_s0" % cb_idx
239 _check("colour Mix reads sockets 6 and 7",
240 mct.find("br_ramp_blend(0, " + a_var) != -1 and mct.find(", " + b_var + ")") != -1,
241 "A and B arrive as the operands, in order")
242
243 var mgr := _erosion_graph()
244 var mixr := MixNode.new()
245 mixr.set("data_type", 14) # rotation
246 mgr.add_node(mixr)
247 var mrt: String = mgr.compile_to_glsl_function("proc_density", true, empty)
248 _check("rotation Mix emits slerp", not mrt.is_empty()
249 and mrt.find("pn_quat_slerp(") != -1,
250 "emitted %d chars" % mrt.length() if not mrt.is_empty() else "REFUSED")
212251
213252 var mg2 := _erosion_graph()
214253 var mixn2 := MixNode.new()
demo/glsl_op_gate_dump.gd +51 -0
……@@ -399,6 +399,57 @@ func _init() -> void:
399399 PackedStringArray()),
400400 })
401401
402 # --- Wave Texture -----------------------------------------------------
403 # Cheap enough to cover properly here: the only heavy part is the
404 # distortion, which is one perlin fBM and only when Distortion != 0.
405 #
406 # All three PROFILES, because SAW and TRI take fract() of nn/2pi and the
407 # CPU does that division in DOUBLE (Math_PI is a double) where the shader
408 # does it in float. A last-bit difference in the quotient becomes a
409 # whole-unit difference in fract() for any nn landing on a period
410 # boundary, so this is measured rather than assumed. Both wave types and
411 # every band/ring direction, since each is a different arm.
412 for wt in [0, 1]:
413 for wdir in [0, 1, 2, 3]:
414 for prof in [0, 1, 2]:
415 for dist_on in [0, 1]:
416 var g9 := ProcNodeGraph.new()
417 var wv := WaveTextureNode.new()
418 wv.set("wave_type", wt)
419 if wt == 0:
420 wv.set("bands_direction", wdir)
421 else:
422 wv.set("rings_direction", wdir)
423 wv.set("wave_profile", prof)
424 var wv_idx := g9.add_node(wv)
425 var wvec := VectorNode.new()
426 wvec.set("value", Vector3(0.75, -1.25, 2.5))
427 g9.connect_sockets(g9.add_node(wvec), 0, wv_idx, 0)
428 var wsc := ValueNode.new()
429 wsc.set("value", 3.0)
430 g9.connect_sockets(g9.add_node(wsc), 0, wv_idx, 1)
431 var wds := ValueNode.new()
432 wds.set("value", 1.5 if dist_on == 1 else 0.0)
433 g9.connect_sockets(g9.add_node(wds), 0, wv_idx, 2)
434 var wdt := ValueNode.new()
435 wdt.set("value", 2.5)
436 g9.connect_sockets(g9.add_node(wdt), 0, wv_idx, 3)
437 var wph := ValueNode.new()
438 wph.set("value", 0.35)
439 g9.connect_sockets(g9.add_node(wph), 0, wv_idx, 6)
440 var go9 := GroupOutputNode.new()
441 go9.set_interface([{"name": "Density", "type": 0}])
442 var go9_idx := g9.add_node(go9)
443 # output 1 is Fac; output 0 is Colour (Blender's order)
444 g9.connect_sockets(wv_idx, 1, go9_idx, 0)
445 cases.append({
446 "node": "Wave.t%d.d%d.p%d" % [wt, wdir, prof], "op": prof,
447 "inputs": [float(wt), float(wdir), float(dist_on)],
448 "cpu": float(g9.evaluate_socket(wv_idx, 1)),
449 "glsl": g9.compile_to_glsl_function("proc_density", false,
450 PackedStringArray()),
451 })
452
402453 # A DIRECTORY, not JSON: stage 2 is a standalone C++ program with no JSON
403454 # parser, and adding one to read our own test data would be more code than
404455 # the gate. One manifest line per case, one .glsl file per case.
src/nodes/math_nodes.cpp +86 -1
……@@ -4,6 +4,7 @@
44#include "../shared/blender_curvemapping.h"
55#include "../shared/blender_math_ops.h"
66#include "../shared/blender_ramp_blend.h"
7#include "../gpu/blender_ramp_blend_glsl.gen.h"
78#include "../graph/graph_executor.h"
89#include <godot_cpp/variant/utility_functions.hpp>
910#include <cmath>
……@@ -833,14 +834,75 @@ MixNode::MixNode() {
833834 output_defs.push_back({ "Result_Rotation", SocketType::Rotation, Variant() });
834835}
835836
837static const char *MIX_QUAT_GLSL_PRELUDE = R"GLSL(
838#ifndef PN_QUAT_SLERP
839#define PN_QUAT_SLERP
840// A PORT OF godot's Quaternion::slerp, not a shared text.
841//
842// Every other .inc in src/shared/ exists so one algorithm is compiled twice.
843// This one cannot be: the CPU side calls godot-cpp's Quaternion::slerp, which
844// is godot's C++ and not ours to dual-compile. So this IS a second
845// implementation, and the emitted-op gate is what holds it to the first --
846// which is the same instrument, and the same argument, that the erosion
847// filter's history says a transcription needs.
848//
849// Read off godot-cpp/src/variant/quaternion.cpp:115-153, including
850// CMP_EPSILON = 0.00001 and the sign flip when the two point away from each
851// other. ⚠ godot computes scale0 through a DOUBLE literal -- `Math::sin((1.0 -
852// p_weight) * omega)` -- and scale1 in float. That asymmetry cannot be
853// reproduced here and is why Rotation is compared within tolerance rather
854// than exactly.
855vec4 pn_quat_slerp(vec4 from, vec4 to, float t) {
856 float cosom = dot(from, to);
857 vec4 to1;
858 if (cosom < 0.0) {
859 cosom = -cosom;
860 to1 = -to;
861 } else {
862 to1 = to;
863 }
864 float scale0;
865 float scale1;
866 if ((1.0 - cosom) > 0.00001) {
867 float omega = acos(cosom);
868 float sinom = sin(omega);
869 scale0 = sin((1.0 - t) * omega) / sinom;
870 scale1 = sin(t * omega) / sinom;
871 } else {
872 scale0 = 1.0 - t;
873 scale1 = t;
874 }
875 return from * scale0 + to1 * scale1;
876}
877
878// godot's Quaternion::normalized() is `*this / length()`, with no guard.
879vec4 pn_quat_normalize(vec4 q) { return q / length(q); }
880#endif
881)GLSL";
882
883String MixNode::compile_glsl_prelude(int p_node_idx) const {
884 if (data_type == 2) {
885 return String(BLENDER_RAMP_BLEND_GLSL);
886 }
887 if (data_type == 14) {
888 return String(MIX_QUAT_GLSL_PRELUDE);
889 }
890 return String();
891}
892
836893String MixNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
837894 // The socket LAYOUT depends on data_type, exactly as execute() reads it:
838895 // Float takes 2/3, Vector 4/5, and a NON-UNIFORM vector factor reads socket
839896 // 1 rather than 0. Getting that wrong reads someone else's socket and looks
840897 // like a wrong blend rather than a wrong address.
841898 const bool is_vector = (data_type == 1);
899 const bool is_color = (data_type == 2);
900 const bool is_rotation = (data_type == 14);
842901 const bool vector_factor = is_vector && factor_mode != 0;
843 const int a_sock = is_vector ? 4 : 2;
902 // Same ladder execute() uses: Rotation 8/9, Colour 6/7, Vector 4/5,
903 // Float 2/3. Getting it wrong reads someone else's socket and looks like a
904 // wrong blend rather than a wrong address.
905 const int a_sock = is_rotation ? 8 : (is_color ? 6 : (is_vector ? 4 : 2));
844906
845907 String t = p_ctx.in(p_node_idx, vector_factor ? 1 : 0);
846908 const String a = p_ctx.in(p_node_idx, a_sock);
……@@ -857,6 +919,29 @@ String MixNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const
857919 String out;
858920 const String f_decl = p_ctx.declare(p_node_idx, 0);
859921 const String v_decl = p_ctx.declare(p_node_idx, 1);
922 if (is_color) {
923 // ramp_blend's nineteen modes. clamp_result clamps ALL FOUR channels,
924 // alpha included, which is what the CPU does.
925 String e = String("br_ramp_blend(") + itos(blend_type) + String(", ") + a +
926 String(", ") + t + String(", ") + b + String(")");
927 if (clamp_result) {
928 e = String("clamp(") + e + String(", vec4(0.0), vec4(1.0))");
929 }
930 out = f_decl + String(" = 0.0;\n\t") + v_decl + String(" = vec3(0.0);\n\t");
931 out += p_ctx.declare(p_node_idx, 2) + String(" = ") + e + String(";\n\t");
932 out += p_ctx.declare(p_node_idx, 3) + String(" = vec4(0.0);");
933 return out;
934 }
935 if (is_rotation) {
936 // Both operands are NORMALISED first, exactly as execute() does --
937 // a.normalized().slerp(b.normalized(), t). clamp_result does not apply
938 // to a rotation on either side.
939 out = f_decl + String(" = 0.0;\n\t") + v_decl + String(" = vec3(0.0);\n\t");
940 out += p_ctx.declare(p_node_idx, 2) + String(" = vec4(0.0);\n\t");
941 out += p_ctx.declare(p_node_idx, 3) + String(" = pn_quat_slerp(pn_quat_normalize(") +
942 a + String("), pn_quat_normalize(") + b + String("), ") + t + String(");");
943 return out;
944 }
860945 if (is_vector) {
861946 // a * (1 - t) + b * t componentwise -- the same algebra as mix() and a
862947 // different rounding, which is what the CPU does and therefore what the
src/nodes/math_nodes.h +8 -8
……@@ -275,15 +275,15 @@ public:
275275 StringName get_node_type() const override { return "Mix"; }
276276 String get_node_title() const override { return "Mix"; }
277277 String get_node_category() const override { return "Math"; }
278 // The GLSL is the old single-lerp form.
279 // PER CONFIGURATION, not per node type. Float and Vector are plain
280 // interpolation and are emitted; Colour needs ramp_blend's nineteen modes
281 // with the HSV and HSL conversions under them, and Rotation needs a
282 // quaternion slerp. Answering false for those makes the emitter REFUSE and
283 // name the node, which is the honest state -- claiming support and emitting
284 // a plain lerp would quietly turn every colour blend into MIX.
285 bool supports_gpu() const override { return data_type == 0 || data_type == 1; }
278 // ALL FOUR CONFIGURATIONS since 2026-09-05. It used to answer per
279 // configuration, true for Float and Vector only, because Colour needs
280 // ramp_blend's nineteen modes with the HSV and HSL conversions under them
281 // and Rotation needs a quaternion slerp. Both exist now:
282 // blender_ramp_blend.inc for the first, and a port of godot's own
283 // Quaternion::slerp for the second.
284 bool supports_gpu() const override { return true; }
286285 String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;
286 String compile_glsl_prelude(int p_node_idx) const override;
287287
288288 void set_data_type(int p_t) { data_type = p_t; }
289289 int get_data_type() const { return data_type; }
src/nodes/noise_nodes.cpp +84 -0
……@@ -1068,6 +1068,90 @@ WaveTextureNode::WaveTextureNode() {
10681068 output_defs.push_back({ "Fac", SocketType::Float, Variant() });
10691069}
10701070
1071String WaveTextureNode::compile_glsl_prelude(int p_node_idx) const {
1072 // The hash and perlin, because Distortion is perlin fBM.
1073 return String(BLENDER_HASH_GLSL) + String(BLENDER_PERLIN_GLSL);
1074}
1075
1076String WaveTextureNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
1077 // Mirrors execute(). wave_type, bands_direction, rings_direction and
1078 // wave_profile are node PROPERTIES, so the branches are chosen here and
1079 // what reaches the shader is one arm of each.
1080 const String pre = String("wv") + itos(p_node_idx);
1081 const String P = pre + String("_p");
1082 const String N = pre + String("_n");
1083 const String V = pre + String("_v");
1084
1085 // Blender's own nudge against precision issues on unit coordinates. It
1086 // shifts every result slightly, so it is not optional.
1087 String s = String("vec3 ") + P + String(" = ((") + p_ctx.in(p_node_idx, 0) +
1088 String(") * (") + p_ctx.in(p_node_idx, 1) +
1089 String(") + vec3(0.000001)) * 0.999999;\n\t");
1090
1091 String nexpr;
1092 if (wave_type == 0) { // BANDS
1093 switch (bands_direction) {
1094 case 1: nexpr = P + String(".y * 20.0"); break;
1095 case 2: nexpr = P + String(".z * 20.0"); break;
1096 case 3: nexpr = String("(") + P + String(".x + ") + P + String(".y + ") + P +
1097 String(".z) * 10.0"); break;
1098 default: nexpr = P + String(".x * 20.0"); break;
1099 }
1100 } else { // RINGS -- a masked copy of p, then its length
1101 String mask;
1102 switch (rings_direction) {
1103 case 0: mask = "vec3(0.0, 1.0, 1.0)"; break;
1104 case 1: mask = "vec3(1.0, 0.0, 1.0)"; break;
1105 case 2: mask = "vec3(1.0, 1.0, 0.0)"; break;
1106 default: mask = ""; break; // SPHERICAL: the whole vector
1107 }
1108 nexpr = String("length(") + P + (mask.is_empty() ? String("") : String(" * ") + mask) +
1109 String(") * 20.0");
1110 }
1111 s += String("float ") + N + String(" = ") + nexpr + String(";\n\t");
1112 s += N + String(" += ") + p_ctx.in(p_node_idx, 6) + String(";\n\t");
1113
1114 // Distortion. Lacunarity is FIXED at 2 and normalize is on -- the node has
1115 // no socket for either, so they are literals rather than inputs.
1116 const String D = pre + String("_d");
1117 s += String("float ") + D + String(" = ") + p_ctx.in(p_node_idx, 2) + String(";\n\t");
1118 s += String("if (") + D + String(" != 0.0) { ") + N + String(" += ") + D +
1119 String(" * (bp_perlin_fbm_3(") + P + String(" * (") + p_ctx.in(p_node_idx, 4) +
1120 String("), ") + p_ctx.in(p_node_idx, 3) + String(", ") + p_ctx.in(p_node_idx, 5) +
1121 String(", 2.0, true) * 2.0 - 1.0); }\n\t");
1122
1123 // ⚠ TAU AND PI AS FLOAT LITERALS. The CPU writes `nn / (Math_PI * 2.0f)`,
1124 // and Math_PI is a DOUBLE, so that division happens in double and is
1125 // narrowed once. Here both sides are float. The difference is in the last
1126 // bits -- but SAW and TRI take fract() of the result, which turns a last-bit
1127 // difference into a whole-unit one for any nn that lands exactly on a
1128 // period boundary. Gated rather than assumed.
1129 String vexpr;
1130 switch (wave_profile) {
1131 case 1: // SAW
1132 vexpr = String("fract(") + N + String(" / 6.28318530717958647692)");
1133 break;
1134 case 2: { // TRI
1135 const String t = pre + String("_t");
1136 s += String("float ") + t + String(" = ") + N +
1137 String(" / 6.28318530717958647692;\n\t");
1138 vexpr = String("abs(") + t + String(" - floor(") + t + String(" + 0.5)) * 2.0");
1139 break;
1140 }
1141 default: // SIN
1142 vexpr = String("0.5 + 0.5 * sin(") + N +
1143 String(" - 3.14159265358979323846 * 0.5)");
1144 break;
1145 }
1146 s += String("float ") + V + String(" = ") + vexpr + String(";\n\t");
1147
1148 // Colour FIRST, then Fac -- Blender's output order.
1149 s += p_ctx.declare(p_node_idx, 0) + String(" = vec4(") + V + String(", ") + V +
1150 String(", ") + V + String(", 1.0);\n\t");
1151 s += p_ctx.declare(p_node_idx, 1) + String(" = ") + V + String(";");
1152 return s;
1153}
1154
10711155void WaveTextureNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
10721156 SocketValue in[7];
10731157 bool is_field = false;
src/nodes/noise_nodes.h +6 -2
……@@ -167,8 +167,12 @@ public:
167167 StringName get_node_type() const override { return "WaveTexture"; }
168168 String get_node_title() const override { return "Wave Texture"; }
169169 String get_node_category() const override { return "Texture"; }
170 // The GLSL is the old (x + y + z) wave.
171 bool supports_gpu() const override { return false; }
170 // ON since 2026-09-05. The GLSL was the old (x + y + z) wave -- one
171 // direction, no rings, no profile, no distortion. It now mirrors
172 // execute(), with the distortion octaves coming from blender_perlin.inc.
173 bool supports_gpu() const override { return true; }
174 String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;
175 String compile_glsl_prelude(int p_node_idx) const override;
172176
173177 void set_wave_type(int p_t) { wave_type = p_t; }
174178 int get_wave_type() const { return wave_type; }
src/shared/blender_noise_cpp_shim.h +10 -0
……@@ -43,8 +43,13 @@ namespace bnoisegl {
4343
4444typedef uint32_t uint;
4545
46// GLSL indexes a vector with [] and my structs are not arrays, so each one
47// carries the accessor. blender_ramp_blend.inc keeps Blender's own signature
48// -- `r_col` is both the A input and the result -- and that body indexes.
4649struct vec2 {
4750 float x, y;
51 float &operator[](int i) { return i == 0 ? x : y; }
52 const float &operator[](int i) const { return i == 0 ? x : y; }
4853 vec2() : x(0.0f), y(0.0f) {}
4954 explicit vec2(float s) : x(s), y(s) {}
5055 vec2(float a, float b) : x(a), y(b) {}
……@@ -52,6 +57,8 @@ struct vec2 {
5257
5358struct vec3 {
5459 float x, y, z;
60 float &operator[](int i) { return i == 0 ? x : (i == 1 ? y : z); }
61 const float &operator[](int i) const { return i == 0 ? x : (i == 1 ? y : z); }
5562 vec3() : x(0.0f), y(0.0f), z(0.0f) {}
5663 explicit vec3(float s) : x(s), y(s), z(s) {}
5764 vec3(float a, float b, float c) : x(a), y(b), z(c) {}
……@@ -59,6 +66,8 @@ struct vec3 {
5966
6067struct vec4 {
6168 float x, y, z, w;
69 float &operator[](int i) { return i == 0 ? x : (i == 1 ? y : (i == 2 ? z : w)); }
70 const float &operator[](int i) const { return i == 0 ? x : (i == 1 ? y : (i == 2 ? z : w)); }
6271 vec4() : x(0.0f), y(0.0f), z(0.0f), w(0.0f) {}
6372 explicit vec4(float s) : x(s), y(s), z(s), w(s) {}
6473 vec4(float a, float b, float c, float d) : x(a), y(b), z(c), w(d) {}
……@@ -189,6 +198,7 @@ inline float exp(float x) { return std::exp(x); }
189198#include "blender_hash.inc"
190199#include "blender_perlin.inc"
191200#include "blender_voronoi.inc"
201#include "blender_ramp_blend.inc"
192202
193203} // namespace bnoisegl
194204} // namespace godot
src/shared/blender_ramp_blend.h +29 -243
……@@ -1,30 +1,25 @@
11#ifndef PROC_NODES_BLENDER_RAMP_BLEND_H
22#define PROC_NODES_BLENDER_RAMP_BLEND_H
33
4// BLENDER'S COLOUR BLEND MODES, PORTED FROM BLENDER'S SOURCE.
4// BLENDER'S COLOUR BLEND MODES — NOW AN ADAPTER, NOT AN IMPLEMENTATION.
55//
6// `ramp_blend` from `source/blender/blenkernel/intern/material.cc` and the HSV
7// pair from `source/blender/blenlib/intern/math_color.cc`, Blender source
8// commit 61a36e08, BLENDER_VERSION 502. The enum is MA_RAMP_* from
9// DNA_material_types.h.
6// The arithmetic is in src/shared/blender_ramp_blend.inc, compiled as GLSL and
7// as C++ from one text, as the hash, perlin, voronoi and erosion filter are.
8// Every name callers used is still here -- including the reference
9// out-parameter signatures, which the .inc could not keep (GLSL has no
10// references) and which this header restores over vec3 returns.
1011//
1112// This is what the Mix node does for COLOUR data, and it is nineteen distinct
12// functions, several of which are not a lerp of anything. Ours implemented one
13// of them (a plain lerp on a float) and had no blend_type at all, so every
14// colour Mix crossing the bridge came out as MIX regardless of what it said in
15// Blender.
13// functions, several of which are not a lerp of anything. proc-nodes
14// implemented one of them and had no blend_type at all, so every colour Mix
15// crossing the bridge came out as MIX regardless of what Blender said.
1616//
1717// The alpha channel is deliberately left alone by every mode except BLEND --
18// that asymmetry is Blender's, not an oversight here.
19//
20// rgb_to_hsv is ported rather than using Godot's Color::get_h/s/v: Blender's
21// adds 1e-20 to the divisors instead of branching on zero, so a grey or black
22// input gives a defined hue and saturation rather than a NaN or an early out,
23// and the four HSV modes are built on that behaviour.
18// that asymmetry is Blender's, not an oversight.
2419
25#include <godot_cpp/variant/color.hpp>
20#include "blender_noise_cpp_shim.h"
2621
27#include <cmath>
22#include <godot_cpp/variant/color.hpp>
2823
2924namespace godot {
3025namespace bramp {
……@@ -36,247 +31,38 @@ enum {
3631 RAMP_COLOR = 15, RAMP_SOFT = 16, RAMP_LINEAR = 17, RAMP_EXCLUSION = 18,
3732};
3833
34// The conversions kept their out-parameter shape for callers; the .inc returns
35// a vec3 because GLSL has no references.
3936static inline void rgb_to_hsv(float r, float g, float b, float &h, float &s, float &v) {
40 float k = 0.0f;
41 if (g < b) {
42 float t = g;
43 g = b;
44 b = t;
45 k = -1.0f;
46 }
47 float min_gb = b;
48 if (r < g) {
49 float t = r;
50 r = g;
51 g = t;
52 k = -2.0f / 6.0f - k;
53 min_gb = MIN(g, b);
54 }
55 float chroma = r - min_gb;
56 h = fabsf(k + (g - b) / (6.0f * chroma + 1e-20f));
57 s = chroma / (r + 1e-20f);
58 v = r;
37 bnoisegl::vec3 o = bnoisegl::br_rgb_to_hsv(r, g, b);
38 h = o.x; s = o.y; v = o.z;
5939}
60
6140static inline void hsv_to_rgb(float h, float s, float v, float &r, float &g, float &b) {
62 float nr = fabsf(h * 6.0f - 3.0f) - 1.0f;
63 float ng = 2.0f - fabsf(h * 6.0f - 2.0f);
64 float nb = 2.0f - fabsf(h * 6.0f - 4.0f);
65 nr = CLAMP(nr, 0.0f, 1.0f);
66 nb = CLAMP(nb, 0.0f, 1.0f);
67 ng = CLAMP(ng, 0.0f, 1.0f);
68 r = ((nr - 1.0f) * s + 1.0f) * v;
69 g = ((ng - 1.0f) * s + 1.0f) * v;
70 b = ((nb - 1.0f) * s + 1.0f) * v;
41 bnoisegl::vec3 o = bnoisegl::br_hsv_to_rgb(h, s, v);
42 r = o.x; g = o.y; b = o.z;
7143}
72
73// The HSL pair, same file. Not used by any blend mode -- it is here because
74// Color Ramp's HSL colour mode needs it and this is where the math_color.cc
75// ports live. Unlike rgb_to_hsv it DOES branch on the achromatic case rather
76// than biasing the divisor, so grey gives h = s = 0 exactly.
7744static inline void rgb_to_hsl(float r, float g, float b, float &h, float &s, float &l) {
78 const float cmax = MAX(r, MAX(g, b));
79 const float cmin = MIN(r, MIN(g, b));
80 l = MIN(1.0f, (cmax + cmin) / 2.0f);
81
82 if (cmax == cmin) {
83 h = s = 0.0f; // achromatic
84 } else {
85 const float d = cmax - cmin;
86 s = l > 0.5f ? d / (2.0f - cmax - cmin) : d / (cmax + cmin);
87 if (cmax == r) {
88 h = (g - b) / d + (g < b ? 6.0f : 0.0f);
89 } else if (cmax == g) {
90 h = (b - r) / d + 2.0f;
91 } else {
92 h = (r - g) / d + 4.0f;
93 }
94 }
95 h /= 6.0f;
45 bnoisegl::vec3 o = bnoisegl::br_rgb_to_hsl(r, g, b);
46 h = o.x; s = o.y; l = o.z;
9647}
97
9848static inline void hsl_to_rgb(float h, float s, float l, float &r, float &g, float &b) {
99 float nr = fabsf(h * 6.0f - 3.0f) - 1.0f;
100 float ng = 2.0f - fabsf(h * 6.0f - 2.0f);
101 float nb = 2.0f - fabsf(h * 6.0f - 4.0f);
102 nr = CLAMP(nr, 0.0f, 1.0f);
103 nb = CLAMP(nb, 0.0f, 1.0f);
104 ng = CLAMP(ng, 0.0f, 1.0f);
105
106 const float chroma = (1.0f - fabsf(2.0f * l - 1.0f)) * s;
107
108 r = (nr - 0.5f) * chroma + l;
109 g = (ng - 0.5f) * chroma + l;
110 b = (nb - 0.5f) * chroma + l;
49 bnoisegl::vec3 o = bnoisegl::br_hsl_to_rgb(h, s, l);
50 r = o.x; g = o.y; b = o.z;
11151}
11252
11353// r_col is BOTH the A input and the result, exactly as Blender's signature has
11454// it; col is B.
11555static inline void ramp_blend(int type, float r_col[4], const float fac, const float col[4]) {
116 float tmp;
117 const float facm = 1.0f - fac;
118
119 switch (type) {
120 case RAMP_BLEND:
121 for (int i = 0; i < 4; i++) {
122 r_col[i] = facm * r_col[i] + fac * col[i];
123 }
124 break;
125 case RAMP_ADD:
126 for (int i = 0; i < 3; i++) {
127 r_col[i] += fac * col[i];
128 }
129 break;
130 case RAMP_MULT:
131 for (int i = 0; i < 3; i++) {
132 r_col[i] *= (facm + fac * col[i]);
133 }
134 break;
135 case RAMP_SCREEN:
136 for (int i = 0; i < 3; i++) {
137 r_col[i] = 1.0f - (facm + fac * (1.0f - col[i])) * (1.0f - r_col[i]);
138 }
139 break;
140 case RAMP_OVERLAY:
141 for (int i = 0; i < 3; i++) {
142 if (r_col[i] < 0.5f) {
143 r_col[i] *= (facm + 2.0f * fac * col[i]);
144 } else {
145 r_col[i] = 1.0f - (facm + 2.0f * fac * (1.0f - col[i])) * (1.0f - r_col[i]);
146 }
147 }
148 break;
149 case RAMP_SUB:
150 for (int i = 0; i < 3; i++) {
151 r_col[i] -= fac * col[i];
152 }
153 break;
154 case RAMP_DIV:
155 for (int i = 0; i < 3; i++) {
156 if (col[i] != 0.0f) {
157 r_col[i] = facm * r_col[i] + fac * r_col[i] / col[i];
158 }
159 }
160 break;
161 case RAMP_DIFF:
162 for (int i = 0; i < 3; i++) {
163 r_col[i] = facm * r_col[i] + fac * fabsf(r_col[i] - col[i]);
164 }
165 break;
166 case RAMP_EXCLUSION:
167 for (int i = 0; i < 3; i++) {
168 r_col[i] = MAX(facm * r_col[i] +
169 fac * (r_col[i] + col[i] - 2.0f * r_col[i] * col[i]),
170 0.0f);
171 }
172 break;
173 case RAMP_DARK:
174 for (int i = 0; i < 3; i++) {
175 r_col[i] = MIN(r_col[i], col[i]) * fac + r_col[i] * facm;
176 }
177 break;
178 case RAMP_LIGHT:
179 for (int i = 0; i < 3; i++) {
180 r_col[i] = MAX(r_col[i], col[i]) * fac + r_col[i] * facm;
181 }
182 break;
183 case RAMP_DODGE:
184 for (int i = 0; i < 3; i++) {
185 if (r_col[i] != 0.0f) {
186 tmp = 1.0f - fac * col[i];
187 if (tmp <= 0.0f) {
188 r_col[i] = 1.0f;
189 } else if ((tmp = r_col[i] / tmp) > 1.0f) {
190 r_col[i] = 1.0f;
191 } else {
192 r_col[i] = tmp;
193 }
194 }
195 }
196 break;
197 case RAMP_BURN:
198 for (int i = 0; i < 3; i++) {
199 tmp = facm + fac * col[i];
200 if (tmp <= 0.0f) {
201 r_col[i] = 0.0f;
202 } else if ((tmp = (1.0f - (1.0f - r_col[i]) / tmp)) < 0.0f) {
203 r_col[i] = 0.0f;
204 } else if (tmp > 1.0f) {
205 r_col[i] = 1.0f;
206 } else {
207 r_col[i] = tmp;
208 }
209 }
210 break;
211 case RAMP_HUE: {
212 float rH, rS, rV, colH, colS, colV, tr, tg, tb;
213 rgb_to_hsv(col[0], col[1], col[2], colH, colS, colV);
214 if (colS != 0.0f) {
215 rgb_to_hsv(r_col[0], r_col[1], r_col[2], rH, rS, rV);
216 hsv_to_rgb(colH, rS, rV, tr, tg, tb);
217 r_col[0] = facm * r_col[0] + fac * tr;
218 r_col[1] = facm * r_col[1] + fac * tg;
219 r_col[2] = facm * r_col[2] + fac * tb;
220 }
221 break;
222 }
223 case RAMP_SAT: {
224 float rH, rS, rV, colH, colS, colV;
225 rgb_to_hsv(r_col[0], r_col[1], r_col[2], rH, rS, rV);
226 if (rS != 0.0f) {
227 rgb_to_hsv(col[0], col[1], col[2], colH, colS, colV);
228 hsv_to_rgb(rH, facm * rS + fac * colS, rV, r_col[0], r_col[1], r_col[2]);
229 }
230 break;
231 }
232 case RAMP_VAL: {
233 float rH, rS, rV, colH, colS, colV;
234 rgb_to_hsv(r_col[0], r_col[1], r_col[2], rH, rS, rV);
235 rgb_to_hsv(col[0], col[1], col[2], colH, colS, colV);
236 hsv_to_rgb(rH, rS, facm * rV + fac * colV, r_col[0], r_col[1], r_col[2]);
237 break;
238 }
239 case RAMP_COLOR: {
240 float rH, rS, rV, colH, colS, colV, tr, tg, tb;
241 rgb_to_hsv(col[0], col[1], col[2], colH, colS, colV);
242 if (colS != 0.0f) {
243 rgb_to_hsv(r_col[0], r_col[1], r_col[2], rH, rS, rV);
244 hsv_to_rgb(colH, colS, rV, tr, tg, tb);
245 r_col[0] = facm * r_col[0] + fac * tr;
246 r_col[1] = facm * r_col[1] + fac * tg;
247 r_col[2] = facm * r_col[2] + fac * tb;
248 }
249 break;
250 }
251 case RAMP_SOFT: {
252 // The non-fac Screen mix first, then blended in.
253 float sc[3];
254 for (int i = 0; i < 3; i++) {
255 sc[i] = 1.0f - (1.0f - col[i]) * (1.0f - r_col[i]);
256 }
257 for (int i = 0; i < 3; i++) {
258 r_col[i] = facm * r_col[i] +
259 fac * (((1.0f - r_col[i]) * col[i] * r_col[i]) + (r_col[i] * sc[i]));
260 }
261 break;
262 }
263 case RAMP_LINEAR:
264 for (int i = 0; i < 3; i++) {
265 if (col[i] > 0.5f) {
266 r_col[i] = r_col[i] + fac * (2.0f * (col[i] - 0.5f));
267 } else {
268 r_col[i] = r_col[i] + fac * (2.0f * col[i] - 1.0f);
269 }
270 }
271 break;
272 }
56 bnoisegl::vec4 o = bnoisegl::br_ramp_blend(type,
57 bnoisegl::vec4(r_col[0], r_col[1], r_col[2], r_col[3]), fac,
58 bnoisegl::vec4(col[0], col[1], col[2], col[3]));
59 r_col[0] = o.x; r_col[1] = o.y; r_col[2] = o.z; r_col[3] = o.w;
27360}
27461
27562static inline Color ramp_blend(int type, const Color &a, float fac, const Color &b) {
276 float r[4] = { a.r, a.g, a.b, a.a };
277 const float c[4] = { b.r, b.g, b.b, b.a };
278 ramp_blend(type, r, fac, c);
279 return Color(r[0], r[1], r[2], r[3]);
63 bnoisegl::vec4 o = bnoisegl::br_ramp_blend(type, bnoisegl::vec4(a.r, a.g, a.b, a.a), fac,
64 bnoisegl::vec4(b.r, b.g, b.b, b.a));
65 return Color(o.x, o.y, o.z, o.w);
28066}
28167
28268} // namespace bramp
src/shared/blender_ramp_blend.inc +313 -0
……@@ -0,0 +1,313 @@
1// ============================================================================
2// BLENDER'S COLOUR BLEND MODES -- ONE TEXT, COMPILED AS BOTH GLSL AND C++.
3//
4// `ramp_blend` from `source/blender/blenkernel/intern/material.cc` and the HSV
5// and HSL pairs from `source/blender/blenlib/intern/math_color.cc`, Blender
6// source commit 61a36e08, BLENDER_VERSION 502. The enum is MA_RAMP_* from
7// DNA_material_types.h.
8//
9// This is what the Mix node does for COLOUR data, and it is nineteen distinct
10// functions, several of which are not a lerp of anything. proc-nodes
11// implemented one of them (a plain lerp) and had no blend_type at all, so
12// every colour Mix crossing the bridge came out as MIX regardless of what it
13// said in Blender.
14//
15// The alpha channel is deliberately left alone by every mode except BLEND --
16// that asymmetry is Blender's, not an oversight here.
17//
18// rgb_to_hsv is ported rather than using Godot's Color::get_h/s/v: Blender's
19// adds 1e-20 to the divisors instead of branching on zero, so a grey or black
20// input gives a defined hue and saturation rather than a NaN or an early out,
21// and the four HSV modes are built on that behaviour.
22//
23// ---------------------------------------------------------------------------
24// THE SUBSET
25//
26// NO REFERENCE OUT-PARAMETERS. The four conversions wrote three floats
27// through `float &`; they return a vec3 now, and their call sites unpack it.
28//
29// NO ARRAY IN/OUT PARAMETER. Blender's signature has `r_col` as both the A
30// input and the result; it is a vec4 in and a vec4 out here. The body still
31// INDEXES it, which GLSL allows on a vector and which the C++ shim's
32// operator[] provides.
33// ============================================================================
34
35#ifndef PN_BLENDER_RAMP_BLEND
36#define PN_BLENDER_RAMP_BLEND
37
38#ifndef BN_FN
39#define BN_FN
40#endif
41
42const int BR_BLEND = 0;
43const int BR_ADD = 1;
44const int BR_MULT = 2;
45const int BR_SUB = 3;
46const int BR_SCREEN = 4;
47const int BR_DIV = 5;
48const int BR_DIFF = 6;
49const int BR_DARK = 7;
50const int BR_LIGHT = 8;
51const int BR_OVERLAY = 9;
52const int BR_DODGE = 10;
53const int BR_BURN = 11;
54const int BR_HUE = 12;
55const int BR_SAT = 13;
56const int BR_VAL = 14;
57const int BR_COLOR = 15;
58const int BR_SOFT = 16;
59const int BR_LINEAR = 17;
60const int BR_EXCLUSION = 18;
61
62
63BN_FN vec3 br_rgb_to_hsv(float r, float g, float b) {
64 float h, s, v;
65
66 float k = 0.0f;
67 if (g < b) {
68 float t = g;
69 g = b;
70 b = t;
71 k = -1.0f;
72 }
73 float min_gb = b;
74 if (r < g) {
75 float t = r;
76 r = g;
77 g = t;
78 k = -2.0f / 6.0f - k;
79 min_gb = min(g, b);
80 }
81 float chroma = r - min_gb;
82 h = abs(k + (g - b) / (6.0f * chroma + 1e-20f));
83 s = chroma / (r + 1e-20f);
84 v = r;
85 return vec3(h, s, v);
86}
87
88BN_FN vec3 br_hsv_to_rgb(float h, float s, float v) {
89 float r, g, b;
90
91 float nr = abs(h * 6.0f - 3.0f) - 1.0f;
92 float ng = 2.0f - abs(h * 6.0f - 2.0f);
93 float nb = 2.0f - abs(h * 6.0f - 4.0f);
94 nr = clamp(nr, 0.0f, 1.0f);
95 nb = clamp(nb, 0.0f, 1.0f);
96 ng = clamp(ng, 0.0f, 1.0f);
97 r = ((nr - 1.0f) * s + 1.0f) * v;
98 g = ((ng - 1.0f) * s + 1.0f) * v;
99 b = ((nb - 1.0f) * s + 1.0f) * v;
100 return vec3(r, g, b);
101}
102
103// The HSL pair, same file. Not used by any blend mode -- it is here because
104// Color Ramp's HSL colour mode needs it and this is where the math_color.cc
105// ports live. Unlike rgb_to_hsv it DOES branch on the achromatic case rather
106// than biasing the divisor, so grey gives h = s = 0 exactly.
107BN_FN vec3 br_rgb_to_hsl(float r, float g, float b) {
108 float h, s, l;
109
110 float cmax = max(r, max(g, b));
111 float cmin = min(r, min(g, b));
112 l = min(1.0f, (cmax + cmin) / 2.0f);
113
114 if (cmax == cmin) {
115 h = s = 0.0f; // achromatic
116 } else {
117 float d = cmax - cmin;
118 s = l > 0.5f ? d / (2.0f - cmax - cmin) : d / (cmax + cmin);
119 if (cmax == r) {
120 h = (g - b) / d + (g < b ? 6.0f : 0.0f);
121 } else if (cmax == g) {
122 h = (b - r) / d + 2.0f;
123 } else {
124 h = (r - g) / d + 4.0f;
125 }
126 }
127 h /= 6.0f;
128 return vec3(h, s, l);
129}
130
131BN_FN vec3 br_hsl_to_rgb(float h, float s, float l) {
132 float r, g, b;
133
134 float nr = abs(h * 6.0f - 3.0f) - 1.0f;
135 float ng = 2.0f - abs(h * 6.0f - 2.0f);
136 float nb = 2.0f - abs(h * 6.0f - 4.0f);
137 nr = clamp(nr, 0.0f, 1.0f);
138 nb = clamp(nb, 0.0f, 1.0f);
139 ng = clamp(ng, 0.0f, 1.0f);
140
141 float chroma = (1.0f - abs(2.0f * l - 1.0f)) * s;
142
143 r = (nr - 0.5f) * chroma + l;
144 g = (ng - 0.5f) * chroma + l;
145 b = (nb - 0.5f) * chroma + l;
146 return vec3(r, g, b);
147}
148
149// r_col is BOTH the A input and the result, exactly as Blender's signature has
150// it; col is B.
151BN_FN vec4 br_ramp_blend(int type, vec4 r_col, float fac, vec4 col) {
152 float tmp;
153 float facm = 1.0f - fac;
154
155 switch (type) {
156 case BR_BLEND:
157 for (int i = 0; i < 4; i++) {
158 r_col[i] = facm * r_col[i] + fac * col[i];
159 }
160 break;
161 case BR_ADD:
162 for (int i = 0; i < 3; i++) {
163 r_col[i] += fac * col[i];
164 }
165 break;
166 case BR_MULT:
167 for (int i = 0; i < 3; i++) {
168 r_col[i] *= (facm + fac * col[i]);
169 }
170 break;
171 case BR_SCREEN:
172 for (int i = 0; i < 3; i++) {
173 r_col[i] = 1.0f - (facm + fac * (1.0f - col[i])) * (1.0f - r_col[i]);
174 }
175 break;
176 case BR_OVERLAY:
177 for (int i = 0; i < 3; i++) {
178 if (r_col[i] < 0.5f) {
179 r_col[i] *= (facm + 2.0f * fac * col[i]);
180 } else {
181 r_col[i] = 1.0f - (facm + 2.0f * fac * (1.0f - col[i])) * (1.0f - r_col[i]);
182 }
183 }
184 break;
185 case BR_SUB:
186 for (int i = 0; i < 3; i++) {
187 r_col[i] -= fac * col[i];
188 }
189 break;
190 case BR_DIV:
191 for (int i = 0; i < 3; i++) {
192 if (col[i] != 0.0f) {
193 r_col[i] = facm * r_col[i] + fac * r_col[i] / col[i];
194 }
195 }
196 break;
197 case BR_DIFF:
198 for (int i = 0; i < 3; i++) {
199 r_col[i] = facm * r_col[i] + fac * abs(r_col[i] - col[i]);
200 }
201 break;
202 case BR_EXCLUSION:
203 for (int i = 0; i < 3; i++) {
204 r_col[i] = max(facm * r_col[i] +
205 fac * (r_col[i] + col[i] - 2.0f * r_col[i] * col[i]),
206 0.0f);
207 }
208 break;
209 case BR_DARK:
210 for (int i = 0; i < 3; i++) {
211 r_col[i] = min(r_col[i], col[i]) * fac + r_col[i] * facm;
212 }
213 break;
214 case BR_LIGHT:
215 for (int i = 0; i < 3; i++) {
216 r_col[i] = max(r_col[i], col[i]) * fac + r_col[i] * facm;
217 }
218 break;
219 case BR_DODGE:
220 for (int i = 0; i < 3; i++) {
221 if (r_col[i] != 0.0f) {
222 tmp = 1.0f - fac * col[i];
223 if (tmp <= 0.0f) {
224 r_col[i] = 1.0f;
225 } else if ((tmp = r_col[i] / tmp) > 1.0f) {
226 r_col[i] = 1.0f;
227 } else {
228 r_col[i] = tmp;
229 }
230 }
231 }
232 break;
233 case BR_BURN:
234 for (int i = 0; i < 3; i++) {
235 tmp = facm + fac * col[i];
236 if (tmp <= 0.0f) {
237 r_col[i] = 0.0f;
238 } else if ((tmp = (1.0f - (1.0f - r_col[i]) / tmp)) < 0.0f) {
239 r_col[i] = 0.0f;
240 } else if (tmp > 1.0f) {
241 r_col[i] = 1.0f;
242 } else {
243 r_col[i] = tmp;
244 }
245 }
246 break;
247 case BR_HUE: {
248 float rH, rS, rV, colH, colS, colV, tr, tg, tb;
249 { vec3 _hsv1 = br_rgb_to_hsv(col[0], col[1], col[2]); colH = _hsv1.x; colS = _hsv1.y; colV = _hsv1.z; }
250 if (colS != 0.0f) {
251 { vec3 _hsv2 = br_rgb_to_hsv(r_col[0], r_col[1], r_col[2]); rH = _hsv2.x; rS = _hsv2.y; rV = _hsv2.z; }
252 { vec3 _hsv3 = br_hsv_to_rgb(colH, rS, rV); tr = _hsv3.x; tg = _hsv3.y; tb = _hsv3.z; }
253 r_col[0] = facm * r_col[0] + fac * tr;
254 r_col[1] = facm * r_col[1] + fac * tg;
255 r_col[2] = facm * r_col[2] + fac * tb;
256 }
257 break;
258 }
259 case BR_SAT: {
260 float rH, rS, rV, colH, colS, colV;
261 { vec3 _hsv4 = br_rgb_to_hsv(r_col[0], r_col[1], r_col[2]); rH = _hsv4.x; rS = _hsv4.y; rV = _hsv4.z; }
262 if (rS != 0.0f) {
263 { vec3 _hsv5 = br_rgb_to_hsv(col[0], col[1], col[2]); colH = _hsv5.x; colS = _hsv5.y; colV = _hsv5.z; }
264 { vec3 _hsv6 = br_hsv_to_rgb(rH, facm * rS + fac * colS, rV); r_col[0] = _hsv6.x; r_col[1] = _hsv6.y; r_col[2] = _hsv6.z; }
265 }
266 break;
267 }
268 case BR_VAL: {
269 float rH, rS, rV, colH, colS, colV;
270 { vec3 _hsv7 = br_rgb_to_hsv(r_col[0], r_col[1], r_col[2]); rH = _hsv7.x; rS = _hsv7.y; rV = _hsv7.z; }
271 { vec3 _hsv8 = br_rgb_to_hsv(col[0], col[1], col[2]); colH = _hsv8.x; colS = _hsv8.y; colV = _hsv8.z; }
272 { vec3 _hsv9 = br_hsv_to_rgb(rH, rS, facm * rV + fac * colV); r_col[0] = _hsv9.x; r_col[1] = _hsv9.y; r_col[2] = _hsv9.z; }
273 break;
274 }
275 case BR_COLOR: {
276 float rH, rS, rV, colH, colS, colV, tr, tg, tb;
277 { vec3 _hsv10 = br_rgb_to_hsv(col[0], col[1], col[2]); colH = _hsv10.x; colS = _hsv10.y; colV = _hsv10.z; }
278 if (colS != 0.0f) {
279 { vec3 _hsv11 = br_rgb_to_hsv(r_col[0], r_col[1], r_col[2]); rH = _hsv11.x; rS = _hsv11.y; rV = _hsv11.z; }
280 { vec3 _hsv12 = br_hsv_to_rgb(colH, colS, rV); tr = _hsv12.x; tg = _hsv12.y; tb = _hsv12.z; }
281 r_col[0] = facm * r_col[0] + fac * tr;
282 r_col[1] = facm * r_col[1] + fac * tg;
283 r_col[2] = facm * r_col[2] + fac * tb;
284 }
285 break;
286 }
287 case BR_SOFT: {
288 // The non-fac Screen mix first, then blended in.
289 float sc[3];
290 for (int i = 0; i < 3; i++) {
291 sc[i] = 1.0f - (1.0f - col[i]) * (1.0f - r_col[i]);
292 }
293 for (int i = 0; i < 3; i++) {
294 r_col[i] = facm * r_col[i] +
295 fac * (((1.0f - r_col[i]) * col[i] * r_col[i]) + (r_col[i] * sc[i]));
296 }
297 break;
298 }
299 case BR_LINEAR:
300 for (int i = 0; i < 3; i++) {
301 if (col[i] > 0.5f) {
302 r_col[i] = r_col[i] + fac * (2.0f * (col[i] - 0.5f));
303 } else {
304 r_col[i] = r_col[i] + fac * (2.0f * col[i] - 1.0f);
305 }
306 }
307 break;
308 }
309
310 return r_col;
311}
312
313#endif // PN_BLENDER_RAMP_BLEND
tests/test_shared_noise_parity.cpp +48 -4
……@@ -94,10 +94,11 @@ static const char *SUBJECT_FILES[] = {
9494 "src/gpu/blender_hash_glsl.gen.h",
9595 "src/gpu/blender_perlin_glsl.gen.h",
9696 "src/gpu/blender_voronoi_glsl.gen.h",
97 "src/gpu/blender_ramp_blend_glsl.gen.h",
9798};
9899static const char *SUBJECT_MARKS[] = { "BLENDER_HASH_GLSL", "BLENDER_PERLIN_GLSL",
99 "BLENDER_VORONOI_GLSL" };
100static const int SUBJECT_N = 3;
100 "BLENDER_VORONOI_GLSL", "BLENDER_RAMP_BLEND_GLSL" };
101static const int SUBJECT_N = 4;
101102
102103static const double TOL_REL = 1e-5;
103104static const double TOL_ABS = 1e-6;
……@@ -312,6 +313,7 @@ enum Op {
312313 OP_VORONOI_XFX, // dim, comp (0=dist,1..3=color,4..7=position)
313314 OP_VORONOI_EDGE, // dim
314315 OP_VORONOI_RADIUS, // dim
316 OP_RAMP_BLEND, // type = blend mode, comp = channel
315317 OP__COUNT
316318};
317319
……@@ -334,7 +336,7 @@ static const OpName OP_NAMES[] = {
334336 { OP_PERLIN_SELECT, "OP_PERLIN_SELECT" }, { OP_PERLIN_DISTORTED, "OP_PERLIN_DISTORTED" },
335337 { OP_PERLIN_FLOAT3, "OP_PERLIN_FLOAT3" },
336338 { OP_VORONOI_XFX, "OP_VORONOI_XFX" }, { OP_VORONOI_EDGE, "OP_VORONOI_EDGE" },
337 { OP_VORONOI_RADIUS, "OP_VORONOI_RADIUS" },
339 { OP_VORONOI_RADIUS, "OP_VORONOI_RADIUS" }, { OP_RAMP_BLEND, "OP_RAMP_BLEND" },
338340};
339341
340342struct Rec {
……@@ -485,6 +487,14 @@ static uint32_t eval_cpu(const uint32_t *w) {
485487 : (dim == 3) ? bv_fractal_voronoi_distance_to_edge_3(p, v3)
486488 : bv_fractal_voronoi_distance_to_edge_4(p, v4));
487489 }
490 case OP_RAMP_BLEND: {
491 // A in a..d, B in the fractal slots, factor in W_DIST -- the record
492 // is reused rather than widened again.
493 vec4 A(x, y, z, wv);
494 vec4 Bc(detail, rough, lac, off);
495 vec4 r = br_ramp_blend(type, A, dist, Bc);
496 return bits_of(comp == 0 ? r.x : (comp == 1 ? r.y : (comp == 2 ? r.z : r.w)));
497 }
488498 case OP_VORONOI_RADIUS: {
489499 BvParams p = params_from(w);
490500 return bits_of((dim == 1) ? bv_voronoi_n_sphere_radius(p, x)
……@@ -628,6 +638,12 @@ void main() {
628638 else { q = bp_perlin_float3_fractal_distorted(v4, detail, rough, lac, off, gain, dist, type, nrm); }
629639 r = floatBitsToUint(comp == 0u ? q.x : (comp == 1u ? q.y : q.z));
630640 }
641 else if (op == OP_RAMP_BLEND) {
642 vec4 A = vec4(x, y, z, w);
643 vec4 Bc = vec4(detail, rough, lac, off);
644 vec4 q = br_ramp_blend(type, A, dist, Bc);
645 r = floatBitsToUint(comp == 0u ? q.x : (comp == 1u ? q.y : (comp == 2u ? q.z : q.w)));
646 }
631647 else if (op == OP_VORONOI_XFX || op == OP_VORONOI_EDGE || op == OP_VORONOI_RADIUS) {
632648 BvParams p;
633649 p.scale = gF(b, 15u);
……@@ -943,6 +959,32 @@ static void build_voronoi_cases() {
943959 }
944960}
945961
962static void build_ramp_cases() {
963 // All nineteen blend modes on all four channels. Several of them are not a
964 // lerp of anything, four go through HSV, and the alpha channel is left
965 // alone by every mode except BLEND -- an asymmetry that is Blender's and
966 // that a "reasonable" implementation would smooth away.
967 const float CP[][9] = {
968 // A r,g,b,a B r,g,b,a fac
969 { 0.2f, 0.4f, 0.6f, 1.0f, 0.7f, 0.1f, 0.3f, 1.0f, 0.5f },
970 { 0.0f, 0.0f, 0.0f, 1.0f, 0.5f, 0.5f, 0.5f, 0.25f, 0.0f }, // black A, fac 0
971 { 0.5f, 0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f }, // grey A, black B, fac 1
972 { 0.9f, 0.05f, 0.35f, 0.8f, 0.15f, 0.95f, 0.6f, 0.2f, 0.75f },
973 };
974 const int NCP = (int)(sizeof(CP) / sizeof(CP[0]));
975 for (int i = 0; i < NCP; i++) {
976 for (int mode = 0; mode <= 18; mode++) {
977 for (uint32_t comp = 0; comp < 4; comp++) {
978 char nm[48];
979 snprintf(nm, sizeof(nm), "br_ramp_blend.m%d", mode);
980 add(nm, false, B(OP_RAMP_BLEND).type(mode).comp(comp)
981 .f(CP[i][0], CP[i][1], CP[i][2], CP[i][3])
982 .frac(CP[i][4], CP[i][5], CP[i][6], CP[i][7], 0.0f, CP[i][8]));
983 }
984 }
985 }
986}
987
946988static long peak_rss_kb() {
947989 std::ifstream f("/proc/self/status");
948990 std::string line;
……@@ -979,7 +1021,8 @@ int main(int argc, char **argv) {
9791021 }
9801022 if (lib.find("bn_hash_bit_final") == std::string::npos ||
9811023 lib.find("bp_perlin_signed") == std::string::npos ||
982 lib.find("bv_voronoi_f1") == std::string::npos) {
1024 lib.find("bv_voronoi_f1") == std::string::npos ||
1025 lib.find("br_ramp_blend") == std::string::npos) {
9831026 fprintf(stderr, "FATAL: subject text is missing bn_hash_bit_final, bp_perlin_signed"
9841027 " or bv_voronoi_f1 -- wrong subject?\n");
9851028 return 1;
……@@ -987,6 +1030,7 @@ int main(int argc, char **argv) {
9871030
9881031 build_cases();
9891032 build_voronoi_cases();
1033 build_ramp_cases();
9901034 if (g_cases.empty()) { fprintf(stderr, "FATAL: no cases\n"); return 1; }
9911035
9921036 int n_exact = 0;