1use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18
19#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
21pub struct Socket {
22 pub link: Option<u32>,
24 pub value: [f32; 3],
26 #[serde(default)]
28 pub override_default: bool,
29 #[serde(default, skip_serializing_if = "is_zero")]
31 pub output: u8,
32 #[serde(default, skip_serializing_if = "is_zero")]
34 pub convert: u8,
35}
36
37fn is_zero(v: &u8) -> bool {
38 *v == 0
39}
40
41impl Socket {
42 pub fn empty() -> Socket {
44 Socket { link: None, value: [0.0; 3], override_default: false, output: 0, convert: 0 }
45 }
46}
47
48#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
50#[serde(rename_all = "lowercase")]
51pub enum GraphMode {
52 #[default]
54 Material,
55 Shader,
57 Effect,
59}
60
61impl GraphMode {
62 pub fn from_path(path: &str) -> GraphMode {
64 if path.ends_with(".kshader") {
65 GraphMode::Shader
66 } else if path.ends_with(".kfx") {
67 GraphMode::Effect
68 } else {
69 GraphMode::Material
70 }
71 }
72
73 pub fn extension(self) -> &'static str {
75 match self {
76 GraphMode::Material => "kmat",
77 GraphMode::Shader => "kshader",
78 GraphMode::Effect => "kfx",
79 }
80 }
81
82 pub fn output_kind(self) -> &'static str {
84 match self {
85 GraphMode::Material => "Output",
86 GraphMode::Shader => "Shader output",
87 GraphMode::Effect => "Effect output",
88 }
89 }
90}
91
92pub fn available(kind: &str, mode: GraphMode) -> bool {
94 use crate::material_nodes::{EFFECT_ONLY, MESH_ONLY};
95 if is_output(kind) {
96 return kind == mode.output_kind();
97 }
98 match mode {
99 GraphMode::Effect => !MESH_ONLY.contains(&kind) && !crate::material_nodes_ext::MESH_ONLY.contains(&kind) && !crate::material_nodes_v4::MESH_ONLY.contains(&kind),
100 _ => !EFFECT_ONLY.contains(&kind),
101 }
102}
103
104pub fn is_output(kind: &str) -> bool {
106 matches!(kind, "Output" | "Shader output" | "Effect output")
107}
108
109#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
111pub struct MatNode {
112 pub id: u32,
114 pub kind: String,
116 pub pos: [f32; 2],
118 pub inputs: Vec<Socket>,
120 pub params: Vec<f32>,
122 #[serde(default)]
124 pub text: String,
125 #[serde(default)]
127 pub size: [f32; 2],
128 #[serde(default)]
130 pub preview: bool,
131}
132
133#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
135#[serde(default)]
136pub struct MaterialGraph {
137 pub nodes: Vec<MatNode>,
139 pub textures: [String; 3],
141 pub next_id: u32,
143 pub mode: GraphMode,
145}
146
147impl Default for MaterialGraph {
148 fn default() -> Self {
149 Self::default_graph()
150 }
151}
152
153pub struct NodeSpec {
155 pub kind: &'static str,
157 pub category: &'static str,
159 pub inputs: &'static [(&'static str, &'static str)],
161 pub params: &'static [(&'static str, f32, f32, f32)],
163 pub expr: &'static str,
165 pub color: bool,
167}
168
169const COORD: &str = "vec3<f32>(in.uv, 0.0)";
170const ZERO: &str = "vec3<f32>(0.0)";
171
172pub const SPECS: &[NodeSpec] = &[
174 NodeSpec { kind: "Output", category: "Output", inputs: &[("Base color", "vec3<f32>(0.8)"), ("Roughness", "vec3<f32>(0.6)"), ("Metallic", "vec3<f32>(0.0)"), ("Emission", "vec3<f32>(0.0)"), ("Alpha", "vec3<f32>(1.0)"), ("Occlusion", "vec3<f32>(1.0)"), ("Normal", "n"), ("Height", "vec3<f32>(0.0)")], params: &[], expr: "", color: false },
175 NodeSpec { kind: "Color", category: "Input", inputs: &[], params: &[("r", 0.8, 0.0, 1.0), ("g", 0.8, 0.0, 1.0), ("b", 0.8, 0.0, 1.0)], expr: "vec3<f32>({p0}, {p1}, {p2})", color: true },
177 NodeSpec { kind: "Value", category: "Input", inputs: &[], params: &[("value", 0.5, -100.0, 100.0)], expr: "vec3<f32>({p0})", color: false },
178 NodeSpec { kind: "Texture", category: "Input", inputs: &[("UV", COORD)], params: &[("slot (0 albedo, 1 normal, 2 ORM)", 0.0, 0.0, 2.0), ("tiling", 1.0, 0.01, 100.0)], expr: "", color: false },
179 NodeSpec { kind: "UV", category: "Geometry", inputs: &[], params: &[], expr: "vec3<f32>(in.uv, 0.0)", color: false },
180 NodeSpec { kind: "World position", category: "Geometry", inputs: &[], params: &[], expr: "in.world_pos", color: false },
181 NodeSpec { kind: "Normal", category: "Geometry", inputs: &[], params: &[], expr: "n", color: false },
182 NodeSpec { kind: "Height", category: "Geometry", inputs: &[], params: &[], expr: "vec3<f32>(in.world_pos.y)", color: false },
183 NodeSpec { kind: "Time", category: "Input", inputs: &[], params: &[], expr: "vec3<f32>(globals.time.x)", color: false },
184 NodeSpec { kind: "Object colour", category: "Input", inputs: &[], params: &[], expr: "object.color.rgb", color: false },
185 NodeSpec { kind: "Fresnel", category: "Light", inputs: &[("Power", "vec3<f32>(3.0)")], params: &[], expr: "vec3<f32>(pow(1.0 - max(dot(n, view), 0.0), max({0}.x, 0.01)))", color: false },
186 NodeSpec { kind: "Add", category: "Math", inputs: &[("A", ZERO), ("B", ZERO)], params: &[], expr: "({0} + {1})", color: false },
188 NodeSpec { kind: "Subtract", category: "Math", inputs: &[("A", ZERO), ("B", ZERO)], params: &[], expr: "({0} - {1})", color: false },
189 NodeSpec { kind: "Multiply", category: "Math", inputs: &[("A", "vec3<f32>(1.0)"), ("B", "vec3<f32>(1.0)")], params: &[], expr: "({0} * {1})", color: false },
190 NodeSpec { kind: "Divide", category: "Math", inputs: &[("A", "vec3<f32>(1.0)"), ("B", "vec3<f32>(1.0)")], params: &[], expr: "({0} / select(vec3<f32>(0.0001), {1}, abs({1}) >= vec3<f32>(0.0001)))", color: false },
191 NodeSpec { kind: "Mix", category: "Math", inputs: &[("A", ZERO), ("B", "vec3<f32>(1.0)"), ("Factor", "vec3<f32>(0.5)")], params: &[], expr: "mix({0}, {1}, clamp({2}.x, 0.0, 1.0))", color: false },
192 NodeSpec { kind: "Power", category: "Math", inputs: &[("Value", "vec3<f32>(0.5)"), ("Exponent", "vec3<f32>(2.0)")], params: &[], expr: "pow(max({0}, vec3<f32>(0.0)), {1})", color: false },
193 NodeSpec { kind: "Clamp", category: "Math", inputs: &[("Value", ZERO)], params: &[("min", 0.0, -100.0, 100.0), ("max", 1.0, -100.0, 100.0)], expr: "clamp({0}, vec3<f32>({p0}), vec3<f32>({p1}))", color: false },
194 NodeSpec { kind: "One minus", category: "Math", inputs: &[("Value", ZERO)], params: &[], expr: "(vec3<f32>(1.0) - {0})", color: false },
195 NodeSpec { kind: "Sine", category: "Math", inputs: &[("Value", ZERO)], params: &[("frequency", 1.0, 0.0, 100.0)], expr: "(sin({0} * {p0}) * 0.5 + vec3<f32>(0.5))", color: false },
196 NodeSpec { kind: "Smoothstep", category: "Math", inputs: &[("Value", "vec3<f32>(0.5)")], params: &[("edge 0", 0.3, -10.0, 10.0), ("edge 1", 0.7, -10.0, 10.0)], expr: "smoothstep(vec3<f32>({p0}), vec3<f32>({p1}), {0})", color: false },
197 NodeSpec { kind: "Abs", category: "Math", inputs: &[("Value", ZERO)], params: &[], expr: "abs({0})", color: false },
198 NodeSpec { kind: "Min", category: "Math", inputs: &[("A", ZERO), ("B", ZERO)], params: &[], expr: "min({0}, {1})", color: false },
199 NodeSpec { kind: "Max", category: "Math", inputs: &[("A", ZERO), ("B", ZERO)], params: &[], expr: "max({0}, {1})", color: false },
200 NodeSpec { kind: "Hue shift", category: "Colour", inputs: &[("Colour", "vec3<f32>(0.8)")], params: &[("turns", 0.0, -1.0, 1.0)], expr: "lib_hue_shift({0}, {p0})", color: false },
202 NodeSpec { kind: "Saturation", category: "Colour", inputs: &[("Colour", "vec3<f32>(0.8)")], params: &[("amount", 1.0, 0.0, 3.0)], expr: "lib_saturation({0}, {p0})", color: false },
203 NodeSpec { kind: "Contrast", category: "Colour", inputs: &[("Colour", "vec3<f32>(0.5)")], params: &[("amount", 1.0, 0.0, 3.0)], expr: "lib_contrast({0}, {p0})", color: false },
204 NodeSpec { kind: "Brightness", category: "Colour", inputs: &[("Colour", "vec3<f32>(0.5)")], params: &[("scale", 1.0, 0.0, 10.0)], expr: "({0} * {p0})", color: false },
205 NodeSpec { kind: "Gradient", category: "Colour", inputs: &[("Position", "vec3<f32>(0.5)"), ("From", "vec3<f32>(0.05, 0.1, 0.3)"), ("To", "vec3<f32>(0.9, 0.6, 0.2)")], params: &[], expr: "mix({1}, {2}, clamp({0}.x, 0.0, 1.0))", color: false },
206 NodeSpec { kind: "To grey", category: "Colour", inputs: &[("Colour", "vec3<f32>(0.5)")], params: &[], expr: "vec3<f32>(lib_luma({0}))", color: false },
207 NodeSpec { kind: "Noise", category: "Noise", inputs: &[("Coords", "in.world_pos")], params: &[("scale", 2.0, 0.01, 100.0)], expr: "vec3<f32>(lib_fbm({0}.xz * {p0}))", color: false },
209 NodeSpec { kind: "Checker", category: "Procedural", inputs: &[("Coords", COORD)], params: &[("scale", 8.0, 1.0, 200.0)], expr: "vec3<f32>(abs((floor({0}.x * {p0}) + floor({0}.y * {p0})) % 2.0))", color: false },
210 NodeSpec { kind: "Stripes", category: "Procedural", inputs: &[("Coords", COORD)], params: &[("scale", 10.0, 1.0, 200.0), ("sharpness", 0.5, 0.0, 1.0)], expr: "vec3<f32>(smoothstep(0.5 - {p1} * 0.5, 0.5 + {p1} * 0.5 + 0.001, abs(fract({0}.x * {p0}) - 0.5) * 2.0))", color: false },
211 NodeSpec { kind: "Voronoi", category: "Noise", inputs: &[("Coords", "in.world_pos")], params: &[("scale", 3.0, 0.01, 100.0)], expr: "vec3<f32>(lib_voronoi({0}.xz * {p0}).x)", color: false },
212 NodeSpec { kind: "Cell colour", category: "Noise", inputs: &[("Coords", "in.world_pos")], params: &[("scale", 3.0, 0.01, 100.0)], expr: "vec3<f32>(lib_voronoi({0}.xz * {p0}).y, lib_voronoi({0}.xz * {p0} + 11.0).y, lib_voronoi({0}.xz * {p0} + 23.0).y)", color: false },
213 NodeSpec { kind: "Bricks", category: "Procedural", inputs: &[("Coords", COORD)], params: &[("scale", 6.0, 1.0, 100.0), ("mortar", 0.06, 0.0, 0.3)], expr: "vec3<f32>(smoothstep({p1}, {p1} + 0.02, min(min(fract({0}.x * {p0} + 0.5 * (floor({0}.y * {p0} * 2.0) % 2.0)), 1.0 - fract({0}.x * {p0} + 0.5 * (floor({0}.y * {p0} * 2.0) % 2.0))), min(fract({0}.y * {p0} * 2.0), 1.0 - fract({0}.y * {p0} * 2.0)))))", color: false },
214 NodeSpec { kind: "Slope mask", category: "Geometry", inputs: &[], params: &[("from", 0.5, 0.0, 1.0), ("to", 0.9, 0.0, 1.0)], expr: "vec3<f32>(smoothstep({p0}, {p1} + 0.001, n.y))", color: false },
215 NodeSpec { kind: "Sun light", category: "Light", inputs: &[], params: &[], expr: "vec3<f32>(max(dot(n, normalize(globals.sun_dir.xyz)), 0.0))", color: false },
216 NodeSpec { kind: "Camera distance", category: "Geometry", inputs: &[], params: &[("range", 50.0, 1.0, 1000.0)], expr: "vec3<f32>(clamp(distance(globals.camera_pos.xyz, in.world_pos) / {p0}, 0.0, 1.0))", color: false },
217 NodeSpec { kind: "Fract", category: "Math", inputs: &[("Value", "vec3<f32>(0.5)")], params: &[], expr: "fract({0})", color: false },
218 NodeSpec { kind: "Step", category: "Math", inputs: &[("Value", "vec3<f32>(0.5)")], params: &[("edge", 0.5, 0.0, 1.0)], expr: "step(vec3<f32>({p0}), {0})", color: false },
219 NodeSpec { kind: "Remap", category: "Math", inputs: &[("Value", "vec3<f32>(0.5)")], params: &[("in min", 0.0, -10.0, 10.0), ("in max", 1.0, -10.0, 10.0), ("out min", 0.0, -10.0, 10.0), ("out max", 1.0, -10.0, 10.0)], expr: "(({0}) - vec3<f32>({p0})) / vec3<f32>({p1} - {p0} + 0.00001) * vec3<f32>({p3} - {p2}) + vec3<f32>({p2})", color: false },
220 NodeSpec { kind: "Dots", category: "Procedural", inputs: &[("Coords", COORD)], params: &[("scale", 10.0, 1.0, 200.0), ("size", 0.3, 0.01, 0.5)], expr: "vec3<f32>(1.0 - smoothstep({p1}, {p1} + 0.05, length(fract({0}.xy * {p0}) - vec2<f32>(0.5))))", color: false },
221];
222
223pub fn spec(kind: &str) -> Option<&'static NodeSpec> {
225 all_specs().find(|s| s.kind == kind)
226}
227
228pub fn all_specs() -> impl Iterator<Item = &'static NodeSpec> {
230 SPECS.iter().chain(crate::material_nodes::MORE_SPECS.iter()).chain(crate::material_nodes_ext::EXT_SPECS.iter()).chain(crate::material_nodes_v4::V4_SPECS.iter())
231}
232
233impl MaterialGraph {
234 pub fn default_graph() -> Self {
236 Self::default_graph_for(GraphMode::Material)
237 }
238
239 pub fn default_graph_for(mode: GraphMode) -> Self {
241 let mut g = MaterialGraph { nodes: Vec::new(), textures: Default::default(), next_id: 1, mode };
242 g.add_node(mode.output_kind(), [420.0, 80.0]);
243 let first = match mode {
244 GraphMode::Effect => g.add_node("Screen color", [60.0, 60.0]),
245 _ => g.add_node("Color", [60.0, 60.0]),
246 };
247 g.connect(first, 1, 0);
248 g
249 }
250
251 pub fn add_node(&mut self, kind: &str, pos: [f32; 2]) -> u32 {
253 let spec = spec(kind).expect("known node type");
254 let id = self.next_id;
255 self.next_id += 1;
256 self.nodes.push(MatNode {
257 id,
258 kind: kind.to_string(),
259 pos,
260 inputs: spec.inputs.iter().map(|_| Socket::empty()).collect(),
261 params: spec.params.iter().map(|p| p.1).collect(),
262 text: if kind == "Comment" { "Comment".into() } else { String::new() },
263 size: if kind == "Comment" { [260.0, 160.0] } else { [0.0, 0.0] },
264 preview: matches!(spec.category, "Noise" | "Procedural"),
265 });
266 id
267 }
268
269 pub fn connect(&mut self, from: u32, to: u32, input: usize) -> bool {
271 self.connect_output(from, 0, to, input, 0)
272 }
273
274 pub fn connect_output(&mut self, from: u32, output: u8, to: u32, input: usize, convert: u8) -> bool {
277 if !self.nodes.iter().any(|n| n.id == from && !is_output(&n.kind) && n.kind != "Comment") {
278 return false;
279 }
280 if from == to || self.reaches(from, to) {
281 return false;
282 }
283 match self.nodes.iter_mut().find(|n| n.id == to).and_then(|n| n.inputs.get_mut(input)) {
284 Some(socket) => {
285 socket.link = Some(from);
286 socket.output = output;
287 socket.convert = convert;
288 true
289 }
290 None => false,
291 }
292 }
293
294 fn reaches(&self, start: u32, target: u32) -> bool {
296 let mut stack = vec![start];
297 let mut seen = std::collections::HashSet::new();
298 while let Some(id) = stack.pop() {
299 if id == target {
300 return true;
301 }
302 if !seen.insert(id) {
303 continue;
304 }
305 if let Some(n) = self.nodes.iter().find(|n| n.id == id) {
306 stack.extend(n.inputs.iter().filter_map(|s| s.link));
307 }
308 }
309 false
310 }
311
312 pub fn remove_node(&mut self, id: u32) {
314 if self.nodes.iter().any(|n| n.id == id && is_output(&n.kind)) {
315 return;
316 }
317 self.nodes.retain(|n| n.id != id);
318 for n in &mut self.nodes {
319 for s in &mut n.inputs {
320 if s.link == Some(id) {
321 s.link = None;
322 }
323 }
324 }
325 }
326
327 pub fn from_json(text: &str) -> Result<Self, String> {
329 let mut g: MaterialGraph = serde_json::from_str(text).map_err(|e| e.to_string())?;
330 if g.nodes.iter().any(|n| n.id >= u32::MAX - 1) { return Err("node id space is exhausted".into()); }
331 for n in &mut g.nodes {
333 if let Some(spec) = spec(&n.kind) {
334 n.inputs.resize(spec.inputs.len(), Socket::empty());
335 while n.params.len() < spec.params.len() {
336 n.params.push(spec.params[n.params.len()].1);
337 }
338 }
339 }
340 for n in &mut g.nodes {
341 if n.kind == "Comment" && n.size == [0.0, 0.0] {
342 n.size = [260.0, 160.0];
343 }
344 }
345 g.next_id = g.next_id.max(g.nodes.iter().map(|n| n.id + 1).max().unwrap_or(1));
346 Ok(g)
347 }
348
349 pub fn to_json(&self) -> String {
351 serde_json::to_string_pretty(self).unwrap_or_default()
352 }
353
354 pub fn validate(&self) -> Result<(), String> {
357 let mut ids = std::collections::HashSet::new();
358 let mut outputs = 0;
359 for node in &self.nodes {
360 if !ids.insert(node.id) { return Err(format!("duplicate node id {}", node.id)); }
361 let s = spec(&node.kind).ok_or_else(|| format!("unknown node type '{}'", node.kind))?;
362 if !available(&node.kind, self.mode) { return Err(format!("node '{}' is unavailable in {:?} graphs", node.kind, self.mode)); }
363 outputs += usize::from(is_output(&node.kind));
364 if node.inputs.len() != s.inputs.len() || node.params.len() < s.params.len() {
365 return Err(format!("node {} ({}) has invalid sockets or parameters", node.id, node.kind));
366 }
367 if node.params.iter().chain(node.inputs.iter().flat_map(|s| &s.value)).any(|v| !v.is_finite()) {
368 return Err(format!("node {} contains a non-finite value", node.id));
369 }
370 }
371 if outputs != 1 { return Err(format!("expected one '{}' node, found {outputs}", self.mode.output_kind())); }
372 let nodes: HashMap<_, _> = self.nodes.iter().map(|n| (n.id, n)).collect();
373 let mut state = HashMap::new();
375 for node in &self.nodes {
376 let mut stack = vec![(node.id, false, 0usize)];
377 while let Some((id, leave, depth)) = stack.pop() {
378 if leave { state.insert(id, 2); continue; }
379 match state.get(&id) {
380 Some(1) => return Err(format!("the graph has a loop at node {id}")),
381 Some(2) => continue,
382 _ => {}
383 }
384 if depth > 256 { return Err("graph exceeds the 256-node dependency depth limit".into()); }
385 let n = nodes.get(&id).ok_or_else(|| format!("a link points to missing node {id}"))?;
386 state.insert(id, 1);
387 stack.push((id, true, depth));
388 for link in n.inputs.iter().filter_map(|s| s.link) {
389 let source = nodes.get(&link).ok_or_else(|| format!("node {id} links to missing node {link}"))?;
390 if is_output(&source.kind) || source.kind == "Comment" { return Err(format!("node {link} has no output socket")); }
391 stack.push((link, false, depth + 1));
392 }
393 }
394 }
395 Ok(())
396 }
397}
398
399fn f(v: f32) -> String {
400 if v.is_finite() { format!("{v:?}") } else { "0.0".into() }
401}
402
403struct Gen<'a> {
404 graph: &'a MaterialGraph,
405 lines: Vec<String>,
406 done: HashMap<u32, String>,
407 visiting: Vec<u32>,
408 extra: HashMap<u64, String>,
410 inputs_of: HashMap<u32, Vec<String>>,
412 vertex: bool,
414}
415
416fn for_effect(expr: &str) -> String {
418 expr.replace("in.world_pos", "vec3<f32>(uv, 0.0)").replace("in.uv", "uv").replace("globals.time.x", "time")
419}
420
421impl Gen<'_> {
422 fn ctx(&self, expr: String) -> String {
423 if self.graph.mode == GraphMode::Effect {
424 for_effect(&expr)
425 } else {
426 expr
427 }
428 }
429
430 fn input(&mut self, node: &MatNode, index: usize, spec: &NodeSpec) -> Result<String, String> {
431 let socket = node.inputs.get(index);
432 match socket.and_then(|s| s.link) {
433 Some(id) => {
434 let socket = socket.unwrap();
435 let value = self.eval_output(id, socket.output as usize)?;
436 match crate::material_nodes_v4::CONVERSIONS.get(socket.convert as usize) {
437 Some((_, conv)) if socket.convert > 0 => Ok(conv.replace("{v}", &value)),
438 _ => Ok(value),
439 }
440 }
441 None => {
442 match socket.map(|s| s.value) {
444 Some(v) if socket.is_some_and(|s| s.override_default) || v != [0.0; 3] => Ok(format!("vec3<f32>({}, {}, {})", f(v[0]), f(v[1]), f(v[2]))),
445 _ => Ok(self.ctx(spec.inputs.get(index).map_or(ZERO, |i| i.1).to_string())),
446 }
447 }
448 }
449 }
450
451 fn eval_output(&mut self, id: u32, output: usize) -> Result<String, String> {
453 let main = self.eval(id)?;
454 let node = self.graph.nodes.iter().find(|n| n.id == id).ok_or("a link points to a missing node")?;
455 let Some((_, template)) = crate::material_nodes_v4::outputs(&node.kind).get(output) else { return Ok(main) };
458 if *template == "{v}" {
459 return Ok(main);
460 }
461 let key = id as u64 | ((output as u64) << 32);
462 if let Some(v) = self.extra.get(&key) {
463 return Ok(v.clone());
464 }
465 let mut e = template.replace("{v}", &main);
466 let inputs = self.inputs_of.get(&id).cloned().unwrap_or_default();
467 for (i, input) in inputs.iter().enumerate() {
468 e = e.replace(&format!("{{{i}}}"), &format!("({input})"));
469 }
470 for (i, p) in node.params.iter().enumerate() {
471 e = e.replace(&format!("{{p{i}}}"), &f(*p));
472 }
473 let e = self.ctx(e);
474 let name = format!("v{id}_{output}");
475 self.lines.push(format!(" let {name} = {e};"));
476 self.extra.insert(key, name.clone());
477 Ok(name)
478 }
479
480 fn eval(&mut self, id: u32) -> Result<String, String> {
481 if let Some(v) = self.done.get(&id) {
482 return Ok(v.clone());
483 }
484 if self.visiting.contains(&id) {
485 return Err("the graph has a loop".into());
486 }
487 let node = self.graph.nodes.iter().find(|n| n.id == id).ok_or("a link points to a missing node")?.clone();
488 let spec = spec(&node.kind).ok_or_else(|| format!("unknown node type '{}'", node.kind))?;
489 if !available(&node.kind, self.graph.mode) {
490 return Err(format!("the node '{}' cannot be used in this kind of graph", node.kind));
491 }
492 if self.vertex && (matches!(node.kind.as_str(), "Texture" | "Normal map" | "Bump from height" | "Shadow" | "Lighting" | "Sky reflection" | "Screen position") || crate::material_nodes_ext::FRAGMENT_ONLY.contains(&node.kind.as_str()) || crate::material_nodes_v4::FRAGMENT_ONLY.contains(&node.kind.as_str())) {
493 return Err(format!("'{}' requires fragment-stage sampling or derivatives and cannot feed Vertex offset", node.kind));
494 }
495 self.visiting.push(id);
496 let mut inputs = Vec::new();
497 for i in 0..spec.inputs.len() {
498 inputs.push(self.input(&node, i, spec)?);
499 }
500 self.visiting.pop();
501 let mut short = Vec::new();
503 for (i, input) in inputs.iter().enumerate() {
504 if input.starts_with('v') && input[1..].chars().all(|c| c.is_ascii_digit() || c == '_') {
505 short.push(input.clone());
506 } else {
507 let name = format!("i{id}_{i}");
508 self.lines.push(format!(" let {name} = {input};"));
509 short.push(name);
510 }
511 }
512 let inputs = short;
513 self.inputs_of.insert(id, inputs.clone());
514 let expr = if node.kind == "Texture" {
515 let slot = node.params.first().copied().unwrap_or(0.0).round() as i32;
516 let tex = match slot {
517 1 => "normal_tex",
518 2 => "orm_tex",
519 _ => "albedo_tex",
520 };
521 let tiling = f(node.params.get(1).copied().unwrap_or(1.0));
522 format!("textureSample({tex}, albedo_smp, ({}).xy * {tiling}).rgb", inputs[0])
523 } else if node.kind == "Inspector number" {
524 let slot = node.params.first().copied().unwrap_or(0.0).round().clamp(0.0, 7.0) as usize;
525 if self.graph.mode == GraphMode::Effect && slot > 3 { return Err("camera effect Inspector number has only slots 0..3 (params.xyzw)".into()); }
526 let default = f(node.params.get(1).copied().unwrap_or(1.0));
527 let comp = ["x", "y", "z", "w"][slot % 4];
528 let source = if self.graph.mode == GraphMode::Effect { format!("params.{comp}") } else { format!("object.user{}.{comp}", slot / 4) };
529 if node.params.get(2).copied().unwrap_or(1.0) >= 0.5 { format!("vec3<f32>(select({source}, {default}, {source} == 0.0))") }
530 else { format!("vec3<f32>({source})") }
531 } else {
532 let mut e = spec.expr.to_string();
533 if e.contains("{tex}") {
534 let slot = node.params.first().copied().unwrap_or(0.0).round() as i32;
535 e = e.replace("{tex}", match slot { 1 => "normal_tex", 2 => "orm_tex", _ => "albedo_tex" });
536 }
537 for (i, input) in inputs.iter().enumerate() {
538 e = e.replace(&format!("{{{i}}}"), &format!("({input})"));
539 }
540 for (i, p) in node.params.iter().enumerate() {
541 e = e.replace(&format!("{{p{i}}}"), &f(*p));
542 }
543 self.ctx(e)
544 };
545 let name = format!("v{id}");
546 self.lines.push(format!(" let {name} = {expr};"));
547 self.done.insert(id, name.clone());
548 Ok(name)
549 }
550}
551
552fn new_gen(graph: &MaterialGraph, vertex: bool) -> Gen<'_> {
553 Gen { graph, lines: Vec::new(), done: HashMap::new(), visiting: Vec::new(), extra: HashMap::new(), inputs_of: HashMap::new(), vertex }
554}
555
556pub fn to_wgsl(graph: &MaterialGraph) -> Result<String, String> {
559 graph.validate()?;
560 let kind = graph.mode.output_kind();
561 let output = graph.nodes.iter().find(|n| n.kind == kind).ok_or_else(|| format!("the graph has no '{kind}' node"))?.clone();
562 let out_spec = spec(kind).unwrap();
563 let mut gen = new_gen(graph, false);
564 let mut results = Vec::new();
565 let skip = if graph.mode == GraphMode::Shader { Some(5) } else { None };
567 for i in 0..out_spec.inputs.len() {
568 if Some(i) == skip {
569 results.push(String::new());
570 continue;
571 }
572 results.push(gen.input(&output, i, out_spec)?);
573 }
574 let body = gen.lines.join("\n");
575 match graph.mode {
576 GraphMode::Material => Ok(format!(
577 "// Generated from a .kmat material graph.
578@vertex
579fn vs_main(v: VertexIn) -> VsOut {{ return standard_vertex(v); }}
580
581@fragment
582fn fs_main(in: VsOut) -> @location(0) vec4<f32> {{
583 let n0 = normalize(in.normal);
584 let n = n0;
585 let view = normalize(globals.camera_pos.xyz - in.world_pos);
586{body}
587 let albedo = max({0}, vec3<f32>(0.0));
588 let rough = clamp(({1}).x, 0.04, 1.0);
589 let metal = clamp(({2}).x, 0.0, 1.0);
590 let emission = max({3}, vec3<f32>(0.0));
591 let alpha = clamp(({4}).x, 0.0, 1.0);
592 let occlusion = clamp(({5}).x, 0.0, 1.0);
593 let bumped = lib_bump(({7}).x, in.world_pos, n, 1.0);
594 let nrm = normalize({6} + (bumped - n) + vec3<f32>(0.00001));
595 if (alpha < 0.003) {{ discard; }}
596 let lit = lighting_pbr(in.world_pos, nrm, albedo, rough, metal, occlusion) + emission;
597 return vec4<f32>(to_display_at(apply_fog(lit, in.world_pos), in.clip), alpha * object.color.a);
598}}
599",
600 results[0], results[1], results[2], results[3], results[4], results[5], results[6], results[7]
601 )),
602 GraphMode::Shader => {
603 let mut vgen = new_gen(graph, true);
605 let offset = vgen.input(&output, 5, out_spec)?;
606 let vbody = vgen.lines.join("\n");
607 Ok(format!(
608 "// Generated from a .kshader graph.
609@vertex
610fn vs_main(v: VertexIn) -> VsOut {{
611 var in = standard_vertex(v);
612 let n = in.normal;
613 let view = normalize(globals.camera_pos.xyz - in.world_pos);
614{vbody}
615 in.world_pos = in.world_pos + {offset};
616 in.clip = globals.view_proj * vec4<f32>(in.world_pos, 1.0);
617 return in;
618}}
619
620@fragment
621fn fs_main(in: VsOut) -> @location(0) vec4<f32> {{
622 let n = normalize(in.normal);
623 let view = normalize(globals.camera_pos.xyz - in.world_pos);
624{body}
625 let base = max({0}, vec3<f32>(0.0));
626 let alpha = clamp(({1}).x, 0.0, 1.0);
627 let emission = max({2}, vec3<f32>(0.0));
628 let amount = clamp(({3}).x, 0.0, 1.0);
629 let rough = clamp(({4}).x, 0.04, 1.0);
630 let lit = mix(base, lighting(in.world_pos, n, base, rough), amount) + emission;
631 if (alpha < 0.003) {{ discard; }}
632 return vec4<f32>(to_display_at(apply_fog(lit, in.world_pos), in.clip), alpha * object.color.a);
633}}
634",
635 results[0], results[1], results[2], results[3], results[4]
636 ))
637 }
638 GraphMode::Effect => Ok(format!(
639 "// Generated from a .kfx camera effect graph.
640fn effect(uv: vec2<f32>, color: vec3<f32>, params: vec4<f32>, time: f32) -> vec3<f32> {{
641{body}
642 return max({0}, vec3<f32>(0.0));
643}}
644",
645 results[0]
646 )),
647 }
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653
654 #[test]
655 fn explicit_zero_and_legacy_defaults_round_trip() {
656 let mut g = MaterialGraph::default_graph();
657 g.nodes[0].inputs[1].override_default = true;
658 let src = to_wgsl(&MaterialGraph::from_json(&g.to_json()).unwrap()).unwrap();
659 assert!(src.contains("let rough = clamp((vec3<f32>(0.0, 0.0, 0.0)).x"));
660 g.nodes[0].inputs[1].override_default = false;
661 assert!(to_wgsl(&g).unwrap().contains("clamp((vec3<f32>(0.6)).x"));
662 }
663
664 #[test]
665 fn malformed_links_and_duplicate_ids_report_errors() {
666 let mut g = MaterialGraph::default_graph();
667 assert!(!g.connect(999, 1, 0));
668 assert!(!g.connect(1, 2, 0));
669 g.nodes[0].inputs[0].link = Some(999);
670 assert!(to_wgsl(&g).unwrap_err().contains("missing node"));
671 g.nodes[0].inputs[0].link = Some(2);
672 g.nodes[1].id = 1;
673 assert!(to_wgsl(&g).unwrap_err().contains("duplicate node id"));
674 }
675
676 #[test]
677 fn hand_edited_cycles_and_vertex_derivatives_are_rejected() {
678 let mut g = MaterialGraph::default_graph_for(GraphMode::Shader);
679 let id = g.add_node("Bump from height", [0.0; 2]);
680 assert!(g.connect(id, 1, 5));
681 assert!(to_wgsl(&g).unwrap_err().contains("fragment-stage"));
682 let a = g.add_node("Add", [0.0; 2]);
683 let b = g.add_node("Add", [0.0; 2]);
684 g.nodes.iter_mut().find(|n| n.id == a).unwrap().inputs[0].link = Some(b);
685 g.nodes.iter_mut().find(|n| n.id == b).unwrap().inputs[0].link = Some(a);
686 assert!(g.validate().unwrap_err().contains("loop"));
687 }
688
689 #[test]
690 fn graph_with_every_node_generates() {
691 for mode in [GraphMode::Material, GraphMode::Shader, GraphMode::Effect] {
692 for spec in all_specs().filter(|s| !is_output(s.kind) && available(s.kind, mode)) {
693 let mut g = MaterialGraph::default_graph_for(mode);
694 let id = g.add_node(spec.kind, [0.0, 0.0]);
695 g.connect(id, 1, 0);
696 let wgsl = to_wgsl(&g).unwrap_or_else(|e| panic!("{:?} {}: {e}", mode, spec.kind));
697 assert!(wgsl.contains("fn "), "{}", spec.kind);
698 }
699 }
700 }
701
702 #[test]
703 fn loops_and_json_round_trip() {
704 let mut g = MaterialGraph::default_graph();
705 let a = g.add_node("Add", [0.0, 0.0]);
706 let b = g.add_node("Multiply", [0.0, 0.0]);
707 assert!(g.connect(a, b, 0));
708 assert!(!g.connect(b, a, 0), "a loop must be refused");
709 let back = MaterialGraph::from_json(&g.to_json()).unwrap();
710 assert_eq!(back, g);
711 }
712}