| … | … | @@ -188,6 +188,49 @@ func _init() -> void: |
| 188 | 188 | PackedStringArray()), |
| 189 | 189 | }) |
| 190 | 190 | |
| 191 | # --- White Noise ------------------------------------------------------ | |
| 192 | # The first texture node back on the GPU, and the one with nothing above | |
| 193 | # the hash: Blender's White Noise IS hash_float_to_float, so this compares | |
| 194 | # the dual-compiled blender_hash.inc through the node that exposes it most | |
| 195 | # directly. All four noise_dimensions, because each keys a DIFFERENT hash | |
| 196 | # arity -- 1D takes W alone, 2D drops z, 4D appends W -- and a node that | |
| 197 | # emitted the 3D form for all of them would still look perfectly plausible. | |
| 198 | # | |
| 199 | # The Value output only. The Colour is hash_float_to_float3 of the same | |
| 200 | # key, and that family is gated exactly, all 1056 cases of it, by | |
| 201 | # tests/test_hash_parity.cpp -- which is a better instrument for it than a | |
| 202 | # float socket. | |
| 203 | var wn_inputs := [ | |
| 204 | [Vector3(0.0, 0.0, 0.0), 0.0], | |
| 205 | [Vector3(1.0, 2.0, 3.0), 0.5], | |
| 206 | [Vector3(-7.25, 0.5, 1e7), -1.5], | |
| 207 | [Vector3(1e-7, -0.001, 65536.0), 3.14159265], | |
| 208 | [Vector3(0.5, 0.5, 0.5), 0.0], | |
| 209 | ] | |
| 210 | for dims in [1, 2, 3, 4]: | |
| 211 | for wi in wn_inputs: | |
| 212 | var g5 := ProcNodeGraph.new() | |
| 213 | var wn := WhiteNoiseNode.new() | |
| 214 | wn.set("noise_dimensions", dims) | |
| 215 | var wn_idx := g5.add_node(wn) | |
| 216 | var vn := VectorNode.new() | |
| 217 | vn.set("value", wi[0]) | |
| 218 | g5.connect_sockets(g5.add_node(vn), 0, wn_idx, 0) | |
| 219 | var wv := ValueNode.new() | |
| 220 | wv.set("value", wi[1]) | |
| 221 | g5.connect_sockets(g5.add_node(wv), 0, wn_idx, 1) | |
| 222 | var go5 := GroupOutputNode.new() | |
| 223 | go5.set_interface([{"name": "Density", "type": 0}]) | |
| 224 | var go5_idx := g5.add_node(go5) | |
| 225 | g5.connect_sockets(wn_idx, 0, go5_idx, 0) | |
| 226 | cases.append({ | |
| 227 | "node": "WhiteNoise.d%d" % dims, "op": dims, | |
| 228 | "inputs": [wi[0].x, wi[0].y, wi[1]], | |
| 229 | "cpu": float(g5.evaluate_socket(wn_idx, 0)), | |
| 230 | "glsl": g5.compile_to_glsl_function("proc_density", false, | |
| 231 | PackedStringArray()), | |
| 232 | }) | |
| 233 | ||
| 191 | 234 | # A DIRECTORY, not JSON: stage 2 is a standalone C++ program with no JSON |
| 192 | 235 | # parser, and adding one to read our own test data would be more code than |
| 193 | 236 | # the gate. One manifest line per case, one .glsl file per case. |
| … | … | @@ -10,6 +10,7 @@ |
| 10 | 10 | #include "../graph/graph_executor.h" |
| 11 | 11 | #include "../graph/proc_node_graph.h" |
| 12 | 12 | #include <cmath> |
| 13 | #include "../gpu/blender_hash_glsl.gen.h" | |
| 13 | 14 | |
| 14 | 15 | namespace godot { |
| 15 | 16 | |
| … | … | @@ -1021,6 +1022,37 @@ void WhiteNoiseNode::execute(ProcEvalContext &p_ctx, int p_node_idx) { |
| 1021 | 1022 | p_ctx.write_color_field(p_node_idx, 1, cols, domain); |
| 1022 | 1023 | } |
| 1023 | 1024 | |
| 1025 | String WhiteNoiseNode::compile_glsl_prelude(int p_node_idx) const { | |
| 1026 | // The hash layer itself, guarded, so several texture nodes can each ask | |
| 1027 | // for it. This is the GENERATED text -- the same characters execute() | |
| 1028 | // compiles through blender_noise_cpp_shim.h -- not a GLSL rendering of it. | |
| 1029 | return String(BLENDER_HASH_GLSL); | |
| 1030 | } | |
| 1031 | ||
| 1032 | String WhiteNoiseNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const { | |
| 1033 | // Mirrors execute() case for case. Blender's White Noise is | |
| 1034 | // hash_float_to_float for the Value and hash_float_to_float3 for the | |
| 1035 | // Colour, keyed on noise_dimensions, and that is the whole node. | |
| 1036 | const String v = p_ctx.in(p_node_idx, 0); | |
| 1037 | const String w = p_ctx.in(p_node_idx, 1); | |
| 1038 | ||
| 1039 | // Parenthesised: `in` returns an EXPRESSION, which may be a producer's | |
| 1040 | // variable or a literal like vec3(0.0, 0.0, 0.0), and `.x` binds tighter | |
| 1041 | // than anything inside it. | |
| 1042 | String key; | |
| 1043 | switch (noise_dimensions) { | |
| 1044 | case 1: key = w; break; | |
| 1045 | case 2: key = "vec2((" + v + ").x, (" + v + ").y)"; break; | |
| 1046 | case 4: key = "vec4(" + v + ", " + w + ")"; break; | |
| 1047 | default: key = v; break; | |
| 1048 | } | |
| 1049 | ||
| 1050 | // Both outputs, always, as every other multi-output node does. The alpha | |
| 1051 | // is 1.0 because the CPU writes Color(c.x, c.y, c.z, 1.0f). | |
| 1052 | return p_ctx.declare(p_node_idx, 0) + " = bn_hash_float_to_float(" + key + ");\n\t" + | |
| 1053 | p_ctx.declare(p_node_idx, 1) + " = vec4(bn_hash_float_to_float3(" + key + "), 1.0);"; | |
| 1054 | } | |
| 1055 | ||
| 1024 | 1056 | // --- GradientTextureNode --- |
| 1025 | 1057 | |
| 1026 | 1058 | void GradientTextureNode::_bind_methods() { |
| … | … | @@ -196,10 +196,16 @@ public: |
| 196 | 196 | StringName get_node_type() const override { return "WhiteNoise"; } |
| 197 | 197 | String get_node_title() const override { return "White Noise"; } |
| 198 | 198 | String get_node_category() const override { return "Texture"; } |
| 199 | // The GLSL is NOT this function. Claiming GPU support would let a batch | |
| 200 | // evaluate a different hash from the CPU path, silently -- the exact drift | |
| 201 | // the shared erosion filter exists to prevent. Off until it is ported. | |
| 202 | bool supports_gpu() const override { return false; } | |
| 199 | // ON since 2026-09-05, and this is the first of the five texture nodes to | |
| 200 | // come back, because it is the one with nothing above the hash. There is | |
| 201 | // no second implementation to drift from: compile_glsl emits calls into | |
| 202 | // blender_hash.inc, which is the same text execute() runs through the C++ | |
| 203 | // shim. What used to be here -- "the GLSL is NOT this function" -- was | |
| 204 | // true of a hand-written copy, and the way to stop saying it was to | |
| 205 | // remove the copy rather than to write a better one. | |
| 206 | bool supports_gpu() const override { return true; } | |
| 207 | String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override; | |
| 208 | String compile_glsl_prelude(int p_node_idx) const override; | |
| 203 | 209 | |
| 204 | 210 | void set_noise_dimensions(int p_d) { noise_dimensions = p_d; } |
| 205 | 211 | int get_noise_dimensions() const { return noise_dimensions; } |
| … | … | @@ -45,6 +45,18 @@ |
| 45 | 45 | // That is why bn_int_to_float_01 below writes `float(1)` and not `1.0`. |
| 46 | 46 | // ============================================================================ |
| 47 | 47 | |
| 48 | // GUARDED AS A WHOLE, because every texture node that stands on this hash | |
| 49 | // emits it as a prelude and a prelude is emitted per node. GpuCompiler drops | |
| 50 | // one it has already emitted by string comparison, but Math's helpers record | |
| 51 | // why that is not enough on its own: when one prelude CONTAINS another, | |
| 52 | // string-dedup either drops them or defines them twice depending on which | |
| 53 | // node came first. GLSL has a preprocessor; let it do the work. | |
| 54 | // | |
| 55 | // Harmless on the C++ side, which includes this exactly once through | |
| 56 | // blender_noise_cpp_shim.h. | |
| 57 | #ifndef PN_BLENDER_HASH | |
| 58 | #define PN_BLENDER_HASH | |
| 59 | ||
| 48 | 60 | // The C++ side defines this as `inline`; GLSL leaves it empty. blender_noise.h |
| 49 | 61 | // reaches more than one translation unit, so without it the second one is a |
| 50 | 62 | // multiple-definition link error -- the trap erosion_cpp_shim.h already has, |
| … | … | @@ -299,3 +311,5 @@ BN_FN vec3 bn_hash_int_to_float3(ivec4 k) { |
| 299 | 311 | vec4 h = bn_hash_int_to_float4(k); |
| 300 | 312 | return vec3(h.x, h.y, h.z); |
| 301 | 313 | } |
| 314 | ||
| 315 | #endif // PN_BLENDER_HASH |