Skip to main content

kvas_engine/
material_nodes.rs

1//! The second half of the node library: shader-graph outputs, vector / maths / colour / procedural
2//! nodes, camera-effect nodes and the node help texts. See [`crate::material_graph`].
3
4use crate::material_graph::NodeSpec;
5
6const COORD: &str = "vec3<f32>(in.uv, 0.0)";
7const ZERO: &str = "vec3<f32>(0.0)";
8const ONE: &str = "vec3<f32>(1.0)";
9const HALF: &str = "vec3<f32>(0.5)";
10const WORLD: &str = "in.world_pos";
11
12macro_rules! node {
13    ($kind:expr, $cat:expr, [$($i:expr),*], [$($p:expr),*], $expr:expr) => {
14        NodeSpec { kind: $kind, category: $cat, inputs: &[$($i),*], params: &[$($p),*], expr: $expr, color: false }
15    };
16}
17
18/// Nodes added after the first release; searched together with [`crate::material_graph::SPECS`].
19pub const MORE_SPECS: &[NodeSpec] = &[
20    // ---- outputs of the other graph kinds
21    NodeSpec {
22        kind: "Shader output",
23        category: "Output",
24        inputs: &[("Color", "vec3<f32>(0.8)"), ("Alpha", ONE), ("Emission", ZERO), ("Light amount", ONE), ("Roughness", "vec3<f32>(0.6)"), ("Vertex offset", ZERO)],
25        params: &[],
26        expr: "",
27        color: false,
28    },
29    NodeSpec { kind: "Effect output", category: "Output", inputs: &[("Color", "color")], params: &[], expr: "", color: false },
30    // ---- utility
31    node!("Reroute", "Utility", [("Value", ZERO)], [], "{0}"),
32    node!("Comment", "Utility", [], [], "vec3<f32>(0.0)"),
33    node!("Inspector number", "Utility", [], [("slot 0-7 (user N)", 0.0, 0.0, 7.0), ("default", 1.0, -1000.0, 1000.0), ("zero uses default (0/1)", 1.0, 0.0, 1.0)], "vec3<f32>(0.0)"),
34    node!("Time scaled", "Utility", [], [("speed", 1.0, -50.0, 50.0)], "vec3<f32>(globals.time.x * {p0})"),
35    node!("Pulse", "Utility", [], [("hertz", 1.0, 0.0, 20.0)], "vec3<f32>(0.5 + 0.5 * sin(globals.time.x * {p0} * 6.2831853))"),
36    // ---- scene inputs (surface shaders)
37    node!("View direction", "Geometry", [], [], "view"),
38    node!("Reflection", "Geometry", [], [], "reflect(-view, n)"),
39    node!("Sky reflection", "Light", [], [], "env_color(reflect(-view, n))"),
40    node!("Sun direction", "Light", [], [], "normalize(globals.sun_dir.xyz)"),
41    node!("Sun colour", "Light", [], [], "globals.sun_color.rgb"),
42    node!("Ambient light", "Light", [], [], "globals.ambient.rgb"),
43    node!("Shadow", "Light", [], [], "vec3<f32>(shadow_factor(in.world_pos, n))"),
44    node!("Lighting", "Light", [("Base", "vec3<f32>(0.8)")], [("roughness", 0.6, 0.0, 1.0)], "lighting(in.world_pos, n, {0}, {p0})"),
45    node!("Camera position", "Geometry", [], [], "globals.camera_pos.xyz"),
46    node!("Object position", "Geometry", [], [], "object.model[3].xyz"),
47    node!("Object random", "Geometry", [], [], "vec3<f32>(hash31(object.model[3].xyz))"),
48    node!("Screen position", "Geometry", [], [], "vec3<f32>(in.clip.xy * globals.viewport.zw, 0.0)"),
49    // ---- camera effect inputs
50    node!("Screen color", "Effect", [("Coords", COORD)], [], "scene_color(({0}).xy)"),
51    node!("Pixel colour", "Effect", [], [], "color"),
52    node!("Pixel size", "Effect", [], [], "vec3<f32>(pixel_size(), 0.0)"),
53    node!("Aspect ratio", "Effect", [], [], "vec3<f32>(screen_aspect())"),
54    node!("Blur", "Effect", [("Coords", COORD)], [("radius px", 2.0, 0.0, 30.0)], "((scene_color(({0}).xy) + scene_color(({0}).xy + vec2<f32>({p0}, 0.0) * pixel_size()) + scene_color(({0}).xy - vec2<f32>({p0}, 0.0) * pixel_size()) + scene_color(({0}).xy + vec2<f32>(0.0, {p0}) * pixel_size()) + scene_color(({0}).xy - vec2<f32>(0.0, {p0}) * pixel_size())) / 5.0)"),
55    node!("Chromatic split", "Effect", [("Coords", COORD)], [("amount px", 3.0, 0.0, 30.0)], "vec3<f32>(scene_color(({0}).xy + vec2<f32>({p0}, 0.0) * pixel_size()).r, scene_color(({0}).xy).g, scene_color(({0}).xy - vec2<f32>({p0}, 0.0) * pixel_size()).b)"),
56    node!("Edge detect", "Effect", [("Coords", COORD)], [("strength", 4.0, 0.0, 20.0)], "vec3<f32>(clamp((abs(lib_luma(scene_color(({0}).xy + vec2<f32>(pixel_size().x, 0.0))) - lib_luma(scene_color(({0}).xy - vec2<f32>(pixel_size().x, 0.0)))) + abs(lib_luma(scene_color(({0}).xy + vec2<f32>(0.0, pixel_size().y))) - lib_luma(scene_color(({0}).xy - vec2<f32>(0.0, pixel_size().y))))) * {p0}, 0.0, 1.0))"),
57    node!("Pixelate", "Effect", [("Coords", COORD)], [("cells across", 160.0, 8.0, 1000.0)], "scene_color(floor(({0}).xy * {p0}) / {p0})"),
58    node!("Vignette", "Effect", [("Coords", COORD)], [("strength", 0.4, 0.0, 2.0)], "vec3<f32>(lib_vignette(({0}).xy, {p0}))"),
59    node!("Scanlines", "Effect", [("Coords", COORD)], [("lines", 300.0, 10.0, 2000.0)], "vec3<f32>(0.5 + 0.5 * sin(({0}).y * {p0} * 3.1415926))"),
60    // ---- maths
61    node!("Negate", "Math", [("Value", ZERO)], [], "(-{0})"),
62    node!("Square root", "Math", [("Value", HALF)], [], "sqrt(max({0}, vec3<f32>(0.0)))"),
63    node!("Floor", "Math", [("Value", HALF)], [], "floor({0})"),
64    node!("Ceil", "Math", [("Value", HALF)], [], "ceil({0})"),
65    node!("Round", "Math", [("Value", HALF)], [], "round({0})"),
66    node!("Sign", "Math", [("Value", ZERO)], [], "sign({0})"),
67    node!("Saturate", "Math", [("Value", ZERO)], [], "clamp({0}, vec3<f32>(0.0), vec3<f32>(1.0))"),
68    node!("Modulo", "Math", [("A", ZERO), ("B", ONE)], [], "({0} - {1} * floor({0} / max({1}, vec3<f32>(0.0001))))"),
69    node!("Multiply add", "Math", [("A", ONE), ("B", ONE), ("C", ZERO)], [], "({0} * {1} + {2})"),
70    node!("Cosine", "Math", [("Value", ZERO)], [("frequency", 1.0, 0.0, 100.0)], "(cos({0} * {p0}) * 0.5 + vec3<f32>(0.5))"),
71    node!("Triangle wave", "Math", [("Value", ZERO)], [("frequency", 1.0, 0.0, 100.0)], "(abs(fract({0} * {p0}) - vec3<f32>(0.5)) * 2.0)"),
72    node!("Sawtooth", "Math", [("Value", ZERO)], [("frequency", 1.0, 0.0, 100.0)], "fract({0} * {p0})"),
73    node!("Square wave", "Math", [("Value", ZERO)], [("frequency", 1.0, 0.0, 100.0)], "step(vec3<f32>(0.5), fract({0} * {p0}))"),
74    node!("Posterize", "Math", [("Value", HALF)], [("steps", 4.0, 2.0, 64.0)], "(floor({0} * {p0} + vec3<f32>(0.5)) / {p0})"),
75    node!("Greater than", "Math", [("A", ZERO), ("B", HALF)], [], "select(vec3<f32>(0.0), vec3<f32>(1.0), {0} > {1})"),
76    node!("Branch", "Math", [("A", ZERO), ("B", HALF), ("If A > B", ONE), ("Otherwise", ZERO)], [], "select({3}, {2}, {0} > {1})"),
77    // ---- vector
78    node!("Red", "Vector", [("Vector", ZERO)], [], "vec3<f32>(({0}).x)"),
79    node!("Green", "Vector", [("Vector", ZERO)], [], "vec3<f32>(({0}).y)"),
80    node!("Blue", "Vector", [("Vector", ZERO)], [], "vec3<f32>(({0}).z)"),
81    node!("Combine", "Vector", [("R", ZERO), ("G", ZERO), ("B", ZERO)], [], "vec3<f32>(({0}).x, ({1}).x, ({2}).x)"),
82    node!("Length", "Vector", [("Vector", ZERO)], [], "vec3<f32>(length({0}))"),
83    node!("Distance", "Vector", [("A", ZERO), ("B", ZERO)], [], "vec3<f32>(distance({0}, {1}))"),
84    node!("Dot", "Vector", [("A", ZERO), ("B", ZERO)], [], "vec3<f32>(dot({0}, {1}))"),
85    node!("Cross", "Vector", [("A", ZERO), ("B", ZERO)], [], "cross({0}, {1})"),
86    node!("Normalize", "Vector", [("Vector", "vec3<f32>(0.0, 1.0, 0.0)")], [], "({0} / max(length({0}), 0.00001))"),
87    node!("Reflect", "Vector", [("Incident", "vec3<f32>(0.0, -1.0, 0.0)"), ("Normal", "vec3<f32>(0.0, 1.0, 0.0)")], [], "reflect({0}, normalize({1} + vec3<f32>(0.00001)))"),
88    node!("Rotate coords", "Vector", [("Coords", COORD)], [("angle", 0.0, -6.3, 6.3)], "vec3<f32>(lib_rot2(({0}).xy - vec2<f32>(0.5), {p0}) + vec2<f32>(0.5), 0.0)"),
89    node!("Scale offset coords", "Vector", [("Coords", COORD)], [("scale x", 1.0, 0.01, 100.0), ("scale y", 1.0, 0.01, 100.0), ("offset x", 0.0, -100.0, 100.0), ("offset y", 0.0, -100.0, 100.0)], "vec3<f32>((({0}).xy - vec2<f32>(0.5)) * vec2<f32>({p0}, {p1}) + vec2<f32>(0.5) + vec2<f32>({p2}, {p3}), 0.0)"),
90    node!("Scroll coords", "Vector", [("Coords", COORD)], [("speed x", 0.1, -10.0, 10.0), ("speed y", 0.0, -10.0, 10.0)], "vec3<f32>(({0}).xy + vec2<f32>({p0}, {p1}) * globals.time.x, 0.0)"),
91    node!("Polar coords", "Vector", [("Coords", COORD)], [], "vec3<f32>(lib_polar(({0}).xy), 0.0)"),
92    node!("Mirror tile", "Vector", [("Coords", COORD)], [("tiles", 2.0, 0.1, 50.0)], "vec3<f32>(abs(fract(({0}).xy * {p0} * 0.5) * 2.0 - vec2<f32>(1.0)), 0.0)"),
93    node!("Flipbook", "Vector", [("Coords", COORD)], [("columns", 4.0, 1.0, 32.0), ("rows", 4.0, 1.0, 32.0), ("frames per second", 8.0, 0.0, 60.0)], "vec3<f32>(lib_flipbook(({0}).xy, {p0}, {p1}, floor(globals.time.x * {p2})), 0.0)"),
94    node!("Distort coords", "Vector", [("Coords", COORD), ("Amount", "vec3<f32>(0.05)")], [("scale", 4.0, 0.1, 100.0)], "({0} + vec3<f32>(lib_noise(({0}).xy * {p0}) - 0.5, lib_noise(({0}).xy * {p0} + vec2<f32>(7.3, 1.7)) - 0.5, 0.0) * ({1}).x)"),
95    node!("Normal map", "Normal", [("Map", "vec3<f32>(0.5, 0.5, 1.0)"), ("UV", COORD)], [("strength", 1.0, 0.0, 4.0)], "lib_normal_map(n, in.world_pos, ({1}).xy, {0}, {p0})"),
96    node!("Bump from height", "Normal", [("Height", ZERO)], [("strength", 1.0, 0.0, 10.0)], "lib_bump(({0}).x, in.world_pos, n, {p0})"),
97    // ---- colour
98    node!("Overlay", "Colour", [("Base", HALF), ("Blend", HALF)], [], "lib_overlay(clamp({0}, vec3<f32>(0.0), vec3<f32>(1.0)), {1})"),
99    node!("Screen blend", "Colour", [("Base", HALF), ("Blend", HALF)], [], "lib_screen({0}, {1})"),
100    node!("Colourize", "Colour", [("Value", HALF), ("Tint", "vec3<f32>(1.0, 0.7, 0.4)")], [], "(vec3<f32>(lib_luma({0})) * {1})"),
101    node!("Ramp 3 colours", "Colour", [("Position", HALF), ("Low", "vec3<f32>(0.02, 0.03, 0.15)"), ("Middle", "vec3<f32>(0.8, 0.2, 0.3)"), ("High", "vec3<f32>(1.0, 0.9, 0.4)")], [], "mix(mix({1}, {2}, clamp(({0}).x * 2.0, 0.0, 1.0)), {3}, clamp(({0}).x * 2.0 - 1.0, 0.0, 1.0))"),
102    node!("Gamma", "Colour", [("Colour", HALF)], [("gamma", 2.2, 0.1, 5.0)], "pow(max({0}, vec3<f32>(0.0)), vec3<f32>(1.0 / {p0}))"),
103    node!("Exposure", "Colour", [("Colour", HALF)], [("stops", 0.0, -8.0, 8.0)], "({0} * exp2({p0}))"),
104    node!("Levels", "Colour", [("Colour", HALF)], [("black", 0.0, 0.0, 1.0), ("white", 1.0, 0.0, 2.0)], "clamp(({0} - vec3<f32>({p0})) / vec3<f32>({p1} - {p0} + 0.0001), vec3<f32>(0.0), vec3<f32>(1.0))"),
105    node!("Threshold", "Colour", [("Colour", HALF)], [("level", 0.5, 0.0, 1.0)], "vec3<f32>(step({p0}, lib_luma({0})))"),
106    node!("Tone map", "Colour", [("Colour", HALF)], [], "lib_tonemap({0})"),
107    // ---- procedural
108    node!("Noise 3D", "Noise", [("Coords", WORLD)], [("scale", 2.0, 0.01, 100.0)], "vec3<f32>(fbm3({0} * {p0}, 4))"),
109    node!("Ridged noise", "Noise", [("Coords", WORLD)], [("scale", 2.0, 0.01, 100.0)], "vec3<f32>(lib_ridged(({0}).xz * {p0}))"),
110    node!("Turbulence", "Noise", [("Coords", WORLD)], [("scale", 2.0, 0.01, 100.0)], "vec3<f32>(lib_turbulence(({0}).xz * {p0}))"),
111    node!("White noise", "Noise", [("Coords", COORD)], [("cells", 64.0, 1.0, 1000.0)], "vec3<f32>(lib_hash21(floor(({0}).xy * {p0})))"),
112    node!("Sparkle", "Noise", [("Coords", COORD)], [("cells", 80.0, 4.0, 1000.0), ("density", 0.03, 0.0, 1.0)], "vec3<f32>(step(1.0 - {p1}, lib_hash21(floor(({0}).xy * {p0}))))"),
113    node!("Caustics", "Procedural", [("Coords", WORLD)], [("scale", 1.5, 0.05, 50.0), ("speed", 0.6, 0.0, 10.0)], "vec3<f32>(lib_caustic(({0}).xz * {p0}, globals.time.x * {p1}))"),
114    node!("Hexagons", "Procedural", [("Coords", COORD)], [("scale", 6.0, 0.5, 100.0), ("edge width", 0.06, 0.0, 0.5)], "vec3<f32>(smoothstep(0.5 - {p1}, 0.5, lib_hex(({0}).xy * {p0})))"),
115    node!("Wave rings", "Procedural", [("Coords", COORD)], [("frequency", 20.0, 1.0, 200.0), ("speed", 2.0, -20.0, 20.0)], "vec3<f32>(sin(length(({0}).xy - vec2<f32>(0.5)) * {p0} - globals.time.x * {p1}) * 0.5 + 0.5)"),
116    node!("Linear gradient", "Procedural", [("Coords", COORD)], [], "vec3<f32>(clamp(({0}).x, 0.0, 1.0))"),
117    node!("Radial gradient", "Procedural", [("Coords", COORD)], [], "vec3<f32>(clamp(1.0 - length(({0}).xy - vec2<f32>(0.5)) * 2.0, 0.0, 1.0))"),
118    node!("Angular gradient", "Procedural", [("Coords", COORD)], [], "vec3<f32>(atan2(({0}).y - 0.5, ({0}).x - 0.5) / 6.2831853 + 0.5)"),
119    node!("Circle", "Procedural", [("Coords", COORD)], [("radius", 0.3, 0.0, 1.0), ("softness", 0.02, 0.0, 0.5)], "vec3<f32>(1.0 - smoothstep({p0}, {p0} + {p1} + 0.0001, length(({0}).xy - vec2<f32>(0.5))))"),
120    node!("Box", "Procedural", [("Coords", COORD)], [("width", 0.6, 0.0, 1.0), ("height", 0.4, 0.0, 1.0), ("softness", 0.02, 0.0, 0.5)], "vec3<f32>(1.0 - smoothstep(0.0, {p2} + 0.0001, lib_sdf_box(({0}).xy - vec2<f32>(0.5), vec2<f32>({p0}, {p1}) * 0.5)))"),
121    node!("Wood rings", "Procedural", [("Coords", WORLD)], [("scale", 4.0, 0.1, 50.0), ("wobble", 0.3, 0.0, 2.0)], "vec3<f32>(lib_wood({0} * {p0}, {p1}))"),
122    node!("Marble veins", "Procedural", [("Coords", WORLD)], [("scale", 1.5, 0.05, 20.0), ("warp", 4.0, 0.0, 20.0)], "vec3<f32>(sin((({0}).x * {p0} + fbm3({0} * {p0}, 5) * {p1}) * 6.2831853) * 0.5 + 0.5)"),
123];
124
125/// Nodes that only exist for surfaces (they read the mesh, light or camera).
126pub const MESH_ONLY: &[&str] = &[
127    "Texture", "World position", "Normal", "Height", "Object colour", "Fresnel", "Slope mask", "Sun light", "Camera distance", "View direction", "Reflection", "Sky reflection", "Sun direction", "Sun colour",
128    "Ambient light", "Shadow", "Lighting", "Camera position", "Object position", "Object random", "Screen position", "Normal map", "Bump from height", "Noise 3D", "Ridged noise", "Turbulence", "Caustics", "Wood rings",
129    "Marble veins",
130];
131
132/// Nodes that only exist inside camera effects.
133pub const EFFECT_ONLY: &[&str] = &["Screen color", "Pixel colour", "Pixel size", "Aspect ratio", "Blur", "Chromatic split", "Edge detect", "Pixelate", "Vignette", "Scanlines"];
134
135/// One-line explanation of a node (tooltip in the editor).
136pub fn help(kind: &str) -> &'static str {
137    match kind {
138        "Output" => "The material result: colour, roughness, metal, glow, opacity, occlusion and a normal.",
139        "Shader output" => "The shader result. Light amount 0 = flat colour, 1 = fully lit. Vertex offset moves vertices in world space (waves, wind).",
140        "Effect output" => "The final colour of the camera image.",
141        "Color" => "A constant colour.",
142        "Value" => "A constant number.",
143        "Texture" => "Samples one of the material images (slot 0 albedo, 1 normal, 2 ORM).",
144        "UV" => "Texture coordinates of the surface (0..1).",
145        "World position" => "Position of the pixel in the world.",
146        "Normal" => "Direction the surface faces.",
147        "Height" => "World height (Y) of the pixel.",
148        "Time" => "Seconds since the game started.",
149        "Fresnel" => "Brighter at grazing angles (rim light, glass, water edges).",
150        "Reroute" => "A tiny pass-through point to tidy long links.",
151        "Comment" => "A frame with a note. Nodes inside it move with it.",
152        "Inspector number" => "A number you can change per object in the Inspector (MeshRenderer user 1..8) or from a script.",
153        "Time scaled" => "Time multiplied by a speed.",
154        "Pulse" => "A smooth 0..1 wave over time.",
155        "Screen color" => "Colour of the camera image at the given coordinates (blur, distortion).",
156        "Pixel colour" => "The current pixel of the camera image.",
157        "Blur" => "Cheap 5-tap blur of the camera image.",
158        "Chromatic split" => "Shifts the red and blue channels apart.",
159        "Edge detect" => "White where the picture changes sharply.",
160        "Pixelate" => "Blocky retro look.",
161        "Vignette" => "Darkens the screen edges.",
162        "Scanlines" => "CRT-like horizontal lines.",
163        "Mix" => "Blends A and B by Factor (0 = A, 1 = B).",
164        "Branch" => "If A > B returns the third input, otherwise the fourth.",
165        "Combine" => "Builds a vector from the red channels of three inputs.",
166        "Normal map" => "Turns a normal-map colour into a surface direction. Connect the result to Output > Normal.",
167        "Bump from height" => "Makes bumps from a height value. Connect the result to Output > Normal.",
168        "Flipbook" => "Plays a grid of frames (sprite sheet) as animation.",
169        "Distort coords" => "Wobbles coordinates with noise (heat haze, water).",
170        "Ramp 3 colours" => "Maps 0..1 to a three-colour gradient.",
171        "Voronoi" => "Cell pattern: distance to the nearest random point.",
172        "Caustics" => "Moving light patterns like under water.",
173        "Hexagons" => "Honeycomb pattern edges.",
174        "Wood rings" => "Tree-ring pattern for wood.",
175        "Marble veins" => "Wavy veins for marble.",
176        "Lighting" => "Applies scene lighting (sun, shadows, lamps) to a colour.",
177        "Shadow" => "1 in light, 0 in the sun shadow.",
178        "Sky reflection" => "What the sky looks like in the mirror direction.",
179        _ => "",
180    }
181}
182
183/// Help text in the requested language (new nodes have Russian text; the rest fall back to English).
184pub fn help_for(kind: &str, russian: bool) -> &'static str {
185    if let Some(h) = crate::material_nodes_v4::HELP.iter().find(|h| h.0 == kind) {
186        return if russian { h.2 } else { h.1 };
187    }
188    crate::material_nodes_ext::help(kind, russian).unwrap_or_else(|| help(kind))
189}