use crate::brush::eval::{BrushNodeEvaluator, EvalContext};
use crate::brush::node::BrushNodeRegistration;
use crate::brush::wgsl::{CompileWgslCtx, NodeWgsl};
use crate::brush::wire::BrushWireType;
use crate::brush::wire::ScalarValue;
use crate::gpu::params::{ParamDef, ParamValue};
use crate::nodegraph::{NodeRegistration, PortDef};
pub const TYPE_ID: &str = "noise";
pub fn register() -> BrushNodeRegistration {
BrushNodeRegistration::compute(
NodeRegistration {
type_id: TYPE_ID,
category: "texture",
display_name: "Noise",
description: "Procedural noise sampled where the brush touches the canvas — for grain, jitter, and texture.",
ports: vec![
PortDef::output("color", BrushWireType::Vec4).with_description(
"Grayscale RGBA value noise at the fragment's canvas-pixel position",
),
],
params: &[
ParamDef::Float {
name: "scale",
min: 0.1,
max: 16.0,
default: 1.0,
},
ParamDef::Int {
name: "seed",
min: 0,
max: 65535,
default: 1,
},
],
is_gpu: false,
is_terminal: false,
supports_erase: true,
},
|| Box::new(NoiseEvaluator),
)
}
pub struct NoiseEvaluator;
impl BrushNodeEvaluator for NoiseEvaluator {
fn evaluate_cpu(&self, _ctx: &EvalContext) -> Vec<(String, ScalarValue)> {
vec![("color".into(), ScalarValue::Vec4([0.5, 0.5, 0.5, 1.0]))]
}
fn compile_wgsl(&self, cctx: &CompileWgslCtx) -> Result<NodeWgsl, String> {
let mut wgsl = NodeWgsl::default();
if !cctx.consumed_outputs.contains("color") {
return Ok(wgsl);
}
let scale = cctx
.params
.first()
.and_then(param_as_f32)
.unwrap_or(1.0)
.max(1e-3);
let seed = cctx.params.get(1).and_then(param_as_u32).unwrap_or(1);
let var = cctx.ident("noise_c");
wgsl.body = format!(
" let {var}_n = node_noise_value(target_pos / {scale:.6}, {seed}u);\n\
\x20 let {var} = vec4<f32>({var}_n, {var}_n, {var}_n, 1.0);\n"
);
wgsl.outputs.insert("color".into(), var);
Ok(wgsl)
}
}
fn param_as_f32(p: &ParamValue) -> Option<f32> {
match p {
ParamValue::Float(v) => Some(*v),
ParamValue::Int(v) => Some(*v as f32),
_ => None,
}
}
fn param_as_u32(p: &ParamValue) -> Option<u32> {
match p {
ParamValue::Int(v) => Some((*v).max(0) as u32),
ParamValue::Float(v) => Some(v.max(0.0) as u32),
_ => None,
}
}
fn cpu_pcg(n: u32) -> u32 {
let mut h = n.wrapping_mul(747796405).wrapping_add(2891336453);
let shift = (h >> 28).wrapping_add(4);
h = ((h >> shift) ^ h).wrapping_mul(277803737);
(h >> 22) ^ h
}
fn cpu_hash2(cx: i32, cy: i32, seed: u32) -> f32 {
let cxu = cx as u32;
let cyu = cy as u32;
let h = cpu_pcg(cxu.wrapping_add(cpu_pcg(cyu.wrapping_add(cpu_pcg(seed)))));
h as f32 / u32::MAX as f32
}
fn cpu_noise_value(px: f32, py: f32, seed: u32) -> f32 {
cpu_hash2(px.floor() as i32, py.floor() as i32, seed)
}
pub fn render_preview_png(params: &[ParamValue], size: u32) -> Vec<u8> {
let scale = params
.first()
.and_then(param_as_f32)
.unwrap_or(1.0)
.max(1e-3);
let seed = params.get(1).and_then(param_as_u32).unwrap_or(1);
let mut img = vec![0u8; (size * size * 4) as usize];
for y in 0..size {
for x in 0..size {
let n = cpu_noise_value(x as f32 / scale, y as f32 / scale, seed).clamp(0.0, 1.0);
let v = (n * 255.0) as u8;
let i = ((y * size + x) * 4) as usize;
img[i] = v;
img[i + 1] = v;
img[i + 2] = v;
img[i + 3] = 255;
}
}
crate::engine::rendering::encode_rgba_as_png(&img, size, size)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registration_shape() {
let reg = register();
assert_eq!(reg.node.type_id, "noise");
assert_eq!(reg.node.category, "texture");
assert_eq!(reg.node.ports.len(), 1);
assert_eq!(reg.node.ports[0].name, "color");
assert_eq!(reg.node.params.len(), 2);
}
#[test]
fn cpu_noise_is_deterministic_per_seed() {
let a = cpu_noise_value(3.7, 12.1, 42);
let b = cpu_noise_value(3.7, 12.1, 42);
assert_eq!(a, b);
let c = cpu_noise_value(3.7, 12.1, 43);
assert!((a - c).abs() > 1e-6, "seed must perturb the hash");
}
#[test]
fn cpu_noise_stays_in_unit_range() {
for y in 0..50 {
for x in 0..50 {
let n = cpu_noise_value(x as f32 * 0.31, y as f32 * 0.47, 7);
assert!((0.0..=1.0).contains(&n), "x={x} y={y} n={n} out of [0, 1]");
}
}
}
#[test]
fn render_preview_png_returns_png_bytes() {
let params = vec![ParamValue::Float(2.0), ParamValue::Int(1)];
let png = render_preview_png(¶ms, 32);
assert!(!png.is_empty(), "preview PNG must be non-empty");
assert_eq!(
&png[..8],
&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A],
"preview must be a valid PNG"
);
}
}