| … | … | @@ -72,6 +72,12 @@ glsl_inc_gen = [ |
| 72 | 72 | "src/shared/blender_voronoi.inc", |
| 73 | 73 | "python3 tools/gen_glsl_inc.py $SOURCE $TARGET BLENDER_VORONOI_GLSL", |
| 74 | 74 | ), |
| 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 | ), | |
| 75 | 81 | ] |
| 76 | 82 | |
| 77 | 83 | sources = ( |
| … | … | @@ -198,17 +198,56 @@ func _init() -> void: |
| 198 | 198 | _check("geometry chain still displaces", et.find("return pos.y - query_y;") != -1, |
| 199 | 199 | "returns the displacement") |
| 200 | 200 | |
| 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. | |
| 205 | 208 | var mg := _erosion_graph() |
| 206 | 209 | var mixn := MixNode.new() |
| 207 | 210 | mixn.set("data_type", 2) # RGBA |
| 211 | mixn.set("blend_type", 12) # HUE, which is nothing like a lerp | |
| 208 | 212 | mg.add_node(mixn) |
| 209 | 213 | 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") | |
| 212 | 251 | |
| 213 | 252 | var mg2 := _erosion_graph() |
| 214 | 253 | var mixn2 := MixNode.new() |
| … | … | @@ -399,6 +399,57 @@ func _init() -> void: |
| 399 | 399 | PackedStringArray()), |
| 400 | 400 | }) |
| 401 | 401 | |
| 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 | ||
| 402 | 453 | # A DIRECTORY, not JSON: stage 2 is a standalone C++ program with no JSON |
| 403 | 454 | # parser, and adding one to read our own test data would be more code than |
| 404 | 455 | # the gate. One manifest line per case, one .glsl file per case. |
| … | … | @@ -4,6 +4,7 @@ |
| 4 | 4 | #include "../shared/blender_curvemapping.h" |
| 5 | 5 | #include "../shared/blender_math_ops.h" |
| 6 | 6 | #include "../shared/blender_ramp_blend.h" |
| 7 | #include "../gpu/blender_ramp_blend_glsl.gen.h" | |
| 7 | 8 | #include "../graph/graph_executor.h" |
| 8 | 9 | #include <godot_cpp/variant/utility_functions.hpp> |
| 9 | 10 | #include <cmath> |
| … | … | @@ -833,14 +834,75 @@ MixNode::MixNode() { |
| 833 | 834 | output_defs.push_back({ "Result_Rotation", SocketType::Rotation, Variant() }); |
| 834 | 835 | } |
| 835 | 836 | |
| 837 | static 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. | |
| 855 | vec4 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. | |
| 879 | vec4 pn_quat_normalize(vec4 q) { return q / length(q); } | |
| 880 | #endif | |
| 881 | )GLSL"; | |
| 882 | ||
| 883 | String 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 | ||
| 836 | 893 | String MixNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const { |
| 837 | 894 | // The socket LAYOUT depends on data_type, exactly as execute() reads it: |
| 838 | 895 | // Float takes 2/3, Vector 4/5, and a NON-UNIFORM vector factor reads socket |
| 839 | 896 | // 1 rather than 0. Getting that wrong reads someone else's socket and looks |
| 840 | 897 | // like a wrong blend rather than a wrong address. |
| 841 | 898 | const bool is_vector = (data_type == 1); |
| 899 | const bool is_color = (data_type == 2); | |
| 900 | const bool is_rotation = (data_type == 14); | |
| 842 | 901 | 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)); | |
| 844 | 906 | |
| 845 | 907 | String t = p_ctx.in(p_node_idx, vector_factor ? 1 : 0); |
| 846 | 908 | 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 |
| 857 | 919 | String out; |
| 858 | 920 | const String f_decl = p_ctx.declare(p_node_idx, 0); |
| 859 | 921 | 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 | } | |
| 860 | 945 | if (is_vector) { |
| 861 | 946 | // a * (1 - t) + b * t componentwise -- the same algebra as mix() and a |
| 862 | 947 | // different rounding, which is what the CPU does and therefore what the |
| … | … | @@ -275,15 +275,15 @@ public: |
| 275 | 275 | StringName get_node_type() const override { return "Mix"; } |
| 276 | 276 | String get_node_title() const override { return "Mix"; } |
| 277 | 277 | 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; } | |
| 286 | 285 | String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override; |
| 286 | String compile_glsl_prelude(int p_node_idx) const override; | |
| 287 | 287 | |
| 288 | 288 | void set_data_type(int p_t) { data_type = p_t; } |
| 289 | 289 | int get_data_type() const { return data_type; } |
| … | … | @@ -1068,6 +1068,90 @@ WaveTextureNode::WaveTextureNode() { |
| 1068 | 1068 | output_defs.push_back({ "Fac", SocketType::Float, Variant() }); |
| 1069 | 1069 | } |
| 1070 | 1070 | |
| 1071 | String 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 | ||
| 1076 | String 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 | ||
| 1071 | 1155 | void WaveTextureNode::execute(ProcEvalContext &p_ctx, int p_node_idx) { |
| 1072 | 1156 | SocketValue in[7]; |
| 1073 | 1157 | bool is_field = false; |
| … | … | @@ -167,8 +167,12 @@ public: |
| 167 | 167 | StringName get_node_type() const override { return "WaveTexture"; } |
| 168 | 168 | String get_node_title() const override { return "Wave Texture"; } |
| 169 | 169 | 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; | |
| 172 | 176 | |
| 173 | 177 | void set_wave_type(int p_t) { wave_type = p_t; } |
| 174 | 178 | int get_wave_type() const { return wave_type; } |
| … | … | @@ -43,8 +43,13 @@ namespace bnoisegl { |
| 43 | 43 | |
| 44 | 44 | typedef uint32_t uint; |
| 45 | 45 | |
| 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. | |
| 46 | 49 | struct vec2 { |
| 47 | 50 | 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; } | |
| 48 | 53 | vec2() : x(0.0f), y(0.0f) {} |
| 49 | 54 | explicit vec2(float s) : x(s), y(s) {} |
| 50 | 55 | vec2(float a, float b) : x(a), y(b) {} |
| … | … | @@ -52,6 +57,8 @@ struct vec2 { |
| 52 | 57 | |
| 53 | 58 | struct vec3 { |
| 54 | 59 | 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); } | |
| 55 | 62 | vec3() : x(0.0f), y(0.0f), z(0.0f) {} |
| 56 | 63 | explicit vec3(float s) : x(s), y(s), z(s) {} |
| 57 | 64 | vec3(float a, float b, float c) : x(a), y(b), z(c) {} |
| … | … | @@ -59,6 +66,8 @@ struct vec3 { |
| 59 | 66 | |
| 60 | 67 | struct vec4 { |
| 61 | 68 | 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)); } | |
| 62 | 71 | vec4() : x(0.0f), y(0.0f), z(0.0f), w(0.0f) {} |
| 63 | 72 | explicit vec4(float s) : x(s), y(s), z(s), w(s) {} |
| 64 | 73 | 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); } |
| 189 | 198 | #include "blender_hash.inc" |
| 190 | 199 | #include "blender_perlin.inc" |
| 191 | 200 | #include "blender_voronoi.inc" |
| 201 | #include "blender_ramp_blend.inc" | |
| 192 | 202 | |
| 193 | 203 | } // namespace bnoisegl |
| 194 | 204 | } // namespace godot |
| … | … | @@ -1,30 +1,25 @@ |
| 1 | 1 | #ifndef PROC_NODES_BLENDER_RAMP_BLEND_H |
| 2 | 2 | #define PROC_NODES_BLENDER_RAMP_BLEND_H |
| 3 | 3 | |
| 4 | // BLENDER'S COLOUR BLEND MODES, PORTED FROM BLENDER'S SOURCE. | |
| 4 | // BLENDER'S COLOUR BLEND MODES — NOW AN ADAPTER, NOT AN IMPLEMENTATION. | |
| 5 | 5 | // |
| 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. | |
| 10 | 11 | // |
| 11 | 12 | // 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. | |
| 16 | 16 | // |
| 17 | 17 | // 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. | |
| 24 | 19 | |
| 25 | #include <godot_cpp/variant/color.hpp> | |
| 20 | #include "blender_noise_cpp_shim.h" | |
| 26 | 21 | |
| 27 | #include <cmath> | |
| 22 | #include <godot_cpp/variant/color.hpp> | |
| 28 | 23 | |
| 29 | 24 | namespace godot { |
| 30 | 25 | namespace bramp { |
| … | … | @@ -36,247 +31,38 @@ enum { |
| 36 | 31 | RAMP_COLOR = 15, RAMP_SOFT = 16, RAMP_LINEAR = 17, RAMP_EXCLUSION = 18, |
| 37 | 32 | }; |
| 38 | 33 | |
| 34 | // The conversions kept their out-parameter shape for callers; the .inc returns | |
| 35 | // a vec3 because GLSL has no references. | |
| 39 | 36 | static 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; | |
| 59 | 39 | } |
| 60 | ||
| 61 | 40 | static 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; | |
| 71 | 43 | } |
| 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. | |
| 77 | 44 | static 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; | |
| 96 | 47 | } |
| 97 | ||
| 98 | 48 | static 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; | |
| 111 | 51 | } |
| 112 | 52 | |
| 113 | 53 | // r_col is BOTH the A input and the result, exactly as Blender's signature has |
| 114 | 54 | // it; col is B. |
| 115 | 55 | static 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; | |
| 273 | 60 | } |
| 274 | 61 | |
| 275 | 62 | static 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); | |
| 280 | 66 | } |
| 281 | 67 | |
| 282 | 68 | } // namespace bramp |
| … | … | @@ -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 | ||
| 42 | const int BR_BLEND = 0; | |
| 43 | const int BR_ADD = 1; | |
| 44 | const int BR_MULT = 2; | |
| 45 | const int BR_SUB = 3; | |
| 46 | const int BR_SCREEN = 4; | |
| 47 | const int BR_DIV = 5; | |
| 48 | const int BR_DIFF = 6; | |
| 49 | const int BR_DARK = 7; | |
| 50 | const int BR_LIGHT = 8; | |
| 51 | const int BR_OVERLAY = 9; | |
| 52 | const int BR_DODGE = 10; | |
| 53 | const int BR_BURN = 11; | |
| 54 | const int BR_HUE = 12; | |
| 55 | const int BR_SAT = 13; | |
| 56 | const int BR_VAL = 14; | |
| 57 | const int BR_COLOR = 15; | |
| 58 | const int BR_SOFT = 16; | |
| 59 | const int BR_LINEAR = 17; | |
| 60 | const int BR_EXCLUSION = 18; | |
| 61 | ||
| 62 | ||
| 63 | BN_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 | ||
| 88 | BN_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. | |
| 107 | BN_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 | ||
| 131 | BN_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. | |
| 151 | BN_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 |
| … | … | @@ -94,10 +94,11 @@ static const char *SUBJECT_FILES[] = { |
| 94 | 94 | "src/gpu/blender_hash_glsl.gen.h", |
| 95 | 95 | "src/gpu/blender_perlin_glsl.gen.h", |
| 96 | 96 | "src/gpu/blender_voronoi_glsl.gen.h", |
| 97 | "src/gpu/blender_ramp_blend_glsl.gen.h", | |
| 97 | 98 | }; |
| 98 | 99 | static const char *SUBJECT_MARKS[] = { "BLENDER_HASH_GLSL", "BLENDER_PERLIN_GLSL", |
| 99 | "BLENDER_VORONOI_GLSL" }; | |
| 100 | static const int SUBJECT_N = 3; | |
| 100 | "BLENDER_VORONOI_GLSL", "BLENDER_RAMP_BLEND_GLSL" }; | |
| 101 | static const int SUBJECT_N = 4; | |
| 101 | 102 | |
| 102 | 103 | static const double TOL_REL = 1e-5; |
| 103 | 104 | static const double TOL_ABS = 1e-6; |
| … | … | @@ -312,6 +313,7 @@ enum Op { |
| 312 | 313 | OP_VORONOI_XFX, // dim, comp (0=dist,1..3=color,4..7=position) |
| 313 | 314 | OP_VORONOI_EDGE, // dim |
| 314 | 315 | OP_VORONOI_RADIUS, // dim |
| 316 | OP_RAMP_BLEND, // type = blend mode, comp = channel | |
| 315 | 317 | OP__COUNT |
| 316 | 318 | }; |
| 317 | 319 | |
| … | … | @@ -334,7 +336,7 @@ static const OpName OP_NAMES[] = { |
| 334 | 336 | { OP_PERLIN_SELECT, "OP_PERLIN_SELECT" }, { OP_PERLIN_DISTORTED, "OP_PERLIN_DISTORTED" }, |
| 335 | 337 | { OP_PERLIN_FLOAT3, "OP_PERLIN_FLOAT3" }, |
| 336 | 338 | { 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" }, | |
| 338 | 340 | }; |
| 339 | 341 | |
| 340 | 342 | struct Rec { |
| … | … | @@ -485,6 +487,14 @@ static uint32_t eval_cpu(const uint32_t *w) { |
| 485 | 487 | : (dim == 3) ? bv_fractal_voronoi_distance_to_edge_3(p, v3) |
| 486 | 488 | : bv_fractal_voronoi_distance_to_edge_4(p, v4)); |
| 487 | 489 | } |
| 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 | } | |
| 488 | 498 | case OP_VORONOI_RADIUS: { |
| 489 | 499 | BvParams p = params_from(w); |
| 490 | 500 | return bits_of((dim == 1) ? bv_voronoi_n_sphere_radius(p, x) |
| … | … | @@ -628,6 +638,12 @@ void main() { |
| 628 | 638 | else { q = bp_perlin_float3_fractal_distorted(v4, detail, rough, lac, off, gain, dist, type, nrm); } |
| 629 | 639 | r = floatBitsToUint(comp == 0u ? q.x : (comp == 1u ? q.y : q.z)); |
| 630 | 640 | } |
| 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 | } | |
| 631 | 647 | else if (op == OP_VORONOI_XFX || op == OP_VORONOI_EDGE || op == OP_VORONOI_RADIUS) { |
| 632 | 648 | BvParams p; |
| 633 | 649 | p.scale = gF(b, 15u); |
| … | … | @@ -943,6 +959,32 @@ static void build_voronoi_cases() { |
| 943 | 959 | } |
| 944 | 960 | } |
| 945 | 961 | |
| 962 | static 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 | ||
| 946 | 988 | static long peak_rss_kb() { |
| 947 | 989 | std::ifstream f("/proc/self/status"); |
| 948 | 990 | std::string line; |
| … | … | @@ -979,7 +1021,8 @@ int main(int argc, char **argv) { |
| 979 | 1021 | } |
| 980 | 1022 | if (lib.find("bn_hash_bit_final") == std::string::npos || |
| 981 | 1023 | 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) { | |
| 983 | 1026 | fprintf(stderr, "FATAL: subject text is missing bn_hash_bit_final, bp_perlin_signed" |
| 984 | 1027 | " or bv_voronoi_f1 -- wrong subject?\n"); |
| 985 | 1028 | return 1; |
| … | … | @@ -987,6 +1030,7 @@ int main(int argc, char **argv) { |
| 987 | 1030 | |
| 988 | 1031 | build_cases(); |
| 989 | 1032 | build_voronoi_cases(); |
| 1033 | build_ramp_cases(); | |
| 990 | 1034 | if (g_cases.empty()) { fprintf(stderr, "FATAL: no cases\n"); return 1; } |
| 991 | 1035 | |
| 992 | 1036 | int n_exact = 0; |