1use std::collections::HashMap;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum MaterialSocket {
18 Color, Float, Vec2, Vec3, Mask, }
24
25#[derive(Debug, Clone)]
27pub enum MaterialOp {
28 InputColor,
31
32 ConstantColor {
36 r: f32,
37 g: f32,
38 b: f32,
39 a: f32,
40 },
41
42 SampleTexture {
45 tex_index: u32,
46 },
47
48 PremultipliedBlend,
52
53 SDFRoundRect,
57
58 SDFEllipse,
61
62 LinearGradient {
66 start: [f32; 4],
67 end: [f32; 4],
68 },
69
70 RadialGradient {
74 start: [f32; 4],
75 end: [f32; 4],
76 },
77
78 NeonGlow {
82 radius: f32,
83 intensity: f32,
84 },
85
86 GlassBlur,
90
91 LayerBlend {
95 mode: BlendMode,
96 },
97
98 PBRLighting,
102
103 DropShadow,
107
108 NineSlice,
112
113 Heatmap,
117
118 Raymarch {
121 shape: RaymarchShape,
122 },
123
124 Lightning,
125 RuneGlow,
126 RaymarchReflections,
127 Stroke,
128 DashedStroke,
129}
130
131#[derive(Debug, Clone, Copy)]
132pub enum BlendMode {
133 Add,
134 Screen,
135 Multiply,
136 Overlay,
137}
138
139#[derive(Debug, Clone, Copy)]
140pub enum RaymarchShape {
141 Sphere,
142 Box,
143}
144
145#[derive(Debug, Clone)]
147pub struct MaterialEdge {
148 pub from_node: u32,
149 pub from_socket: MaterialSocket,
150 pub to_node: u32,
151 pub to_socket: MaterialSocket,
152}
153
154pub type MatNodeId = u32;
156
157#[derive(Debug, Clone)]
159pub struct MaterialGraph {
160 pub nodes: Vec<(MatNodeId, MaterialOp)>,
161 pub edges: Vec<MaterialEdge>,
162 pub output: Option<MatNodeId>,
163}
164
165impl MaterialGraph {
166 pub fn new() -> Self {
167 Self {
168 nodes: Vec::new(),
169 edges: Vec::new(),
170 output: None,
171 }
172 }
173
174 pub fn add_node(&mut self, op: MaterialOp) -> MatNodeId {
175 let id = self.nodes.len() as MatNodeId;
176 self.nodes.push((id, op));
177 id
178 }
179
180 pub fn connect(
181 &mut self,
182 from: MatNodeId,
183 from_socket: MaterialSocket,
184 to: MatNodeId,
185 to_socket: MaterialSocket,
186 ) {
187 self.edges.push(MaterialEdge {
188 from_node: from,
189 from_socket,
190 to_node: to,
191 to_socket,
192 });
193 }
194
195 pub fn set_output(&mut self, node: MatNodeId) {
196 self.output = Some(node);
197 }
198
199 pub fn validate(&self) -> Result<(), MaterialError> {
201 self.validate_with_config(&MaterialValidationConfig::default())
202 }
203
204 pub fn validate_with_config(
206 &self,
207 config: &MaterialValidationConfig,
208 ) -> Result<(), MaterialError> {
209 if self.output.is_none() {
210 return Err(MaterialError::NoOutput);
211 }
212 if self.nodes.len() > config.max_nodes {
213 return Err(MaterialError::TooManyNodes(
214 self.nodes.len(),
215 config.max_nodes,
216 ));
217 }
218 let mut visited = vec![false; self.nodes.len()];
220 let mut in_stack = vec![false; self.nodes.len()];
221
222 for &(id, _) in &self.nodes {
223 if !visited[id as usize] {
224 self.dfs_check(id, &mut visited, &mut in_stack)?;
225 }
226 }
227 Ok(())
228 }
229
230 fn dfs_check(
231 &self,
232 node: MatNodeId,
233 visited: &mut [bool],
234 in_stack: &mut [bool],
235 ) -> Result<(), MaterialError> {
236 let idx = node as usize;
237 if in_stack[idx] {
238 return Err(MaterialError::Cycle);
239 }
240 if visited[idx] {
241 return Ok(());
242 }
243 visited[idx] = true;
244 in_stack[idx] = true;
245
246 for edge in &self.edges {
248 if edge.to_node == node {
249 self.dfs_check(edge.from_node, visited, in_stack)?;
250 }
251 }
252
253 in_stack[idx] = false;
254 Ok(())
255 }
256}
257
258impl Default for MaterialGraph {
259 fn default() -> Self {
260 Self::new()
261 }
262}
263
264#[derive(Debug)]
265pub enum MaterialError {
266 NoOutput,
267 Cycle,
268 DisconnectedInput {
269 node: MatNodeId,
270 socket: MaterialSocket,
271 },
272 TypeMismatch {
273 from: MaterialSocket,
274 to: MaterialSocket,
275 },
276 CompileError(String),
277 TooManyNodes(usize, usize),
278 UnsupportedNodeType(String),
279}
280
281pub struct MaterialValidationConfig {
282 pub max_nodes: usize,
283}
284
285impl Default for MaterialValidationConfig {
286 fn default() -> Self {
287 Self { max_nodes: 1024 } }
289}
290
291impl std::error::Error for MaterialError {}
292
293impl std::fmt::Display for MaterialError {
294 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295 match self {
296 Self::NoOutput => write!(f, "material graph has no output node"),
297 Self::Cycle => write!(f, "material graph contains a cycle"),
298 Self::DisconnectedInput { node, socket } => {
299 write!(f, "node {:?} missing input {:?}", node, socket)
300 }
301 Self::TypeMismatch { from, to } => {
302 write!(f, "type mismatch: {:?} -> {:?}", from, to)
303 }
304 Self::CompileError(msg) => write!(f, "WGSL compilation error: {}", msg),
305 Self::TooManyNodes(count, max) => write!(f, "too many nodes: {} (max {})", count, max),
306 Self::UnsupportedNodeType(kind) => write!(f, "unsupported node type: {}", kind),
307 }
308 }
309}
310
311#[derive(Debug, Clone)]
313pub struct CompiledMaterial {
314 pub wgsl_fn: String,
316 pub fn_name: String,
318}
319
320impl CompiledMaterial {
321 pub fn hash_code(&self) -> u64 {
322 use std::hash::{Hash, Hasher};
323 let mut hasher = std::collections::hash_map::DefaultHasher::new();
324 self.wgsl_fn.hash(&mut hasher);
325 hasher.finish()
326 }
327}
328
329pub struct MaterialCompiler;
331
332impl MaterialCompiler {
333 pub fn compile(graph: &MaterialGraph) -> Result<CompiledMaterial, MaterialError> {
344 graph.validate()?;
345
346 let order = Self::topo_sort(graph)?;
348
349 let mut lines: Vec<String> = Vec::new();
351 let mut var_names: HashMap<(MatNodeId, MaterialSocket), String> = HashMap::new();
352 let mut next_var = 0;
353
354 let mut mk_var = |prefix: &str| -> String {
355 let v = format!("{}_{}", prefix, next_var);
356 next_var += 1;
357 v
358 };
359
360 for &node_id in &order {
361 let (_, op) = &graph.nodes[node_id as usize];
362 let result_var = mk_var("v");
363
364 let expr = match op {
365 MaterialOp::InputColor => {
366 "col".to_string()
367 }
368 MaterialOp::ConstantColor { r, g, b, a } => {
369 format!("vec4<f32>({:.6}, {:.6}, {:.6}, {:.6})", r, g, b, a)
370 }
371 MaterialOp::SampleTexture { tex_index } => {
372 format!(
373 "textureSample(t_diffuse[{}u], s_diffuse, in.uv)",
374 tex_index
375 )
376 }
377 MaterialOp::PremultipliedBlend => {
378 let color_var = Self::find_input(&var_names, node_id, MaterialSocket::Color, graph)
379 .unwrap_or_else(|| "col".to_string());
380 let alpha_var = Self::find_input(&var_names, node_id, MaterialSocket::Float, graph)
382 .unwrap_or_else(|| "1.0".to_string());
383 format!(
384 "vec4<f32>(({}).rgb, ({}).a * ({}))",
385 color_var, color_var, alpha_var
386 )
387 }
388 MaterialOp::SDFRoundRect => {
389 let half = "in.size * 0.5";
390 format!(
391 r#"
392 let _d = sd_round_rect(in.logical - {0}, {0} - in.radius, in.radius);
393 let _aa = fwidth(_d);
394 __RESULT__ = vec4<f32>(col.rgb, col.a * (1.0 - smoothstep(0.0, _aa, _d)));"#,
395 half
396 ).trim().to_string()
397 }
398 MaterialOp::SDFEllipse => {
399 let half = "in.size * 0.5";
400 format!(
401 r#"
402 let _sh = max({0}, vec2<f32>(0.001));
403 let _d = length((in.logical - {0}) / _sh) - 1.0;
404 let _aa = fwidth(_d);
405 __RESULT__ = vec4<f32>(col.rgb, col.a * (1.0 - smoothstep(0.0, _aa, _d)));"#,
406 half
407 ).trim().to_string()
408 }
409 MaterialOp::LinearGradient { start, end } => {
410 let t_var = Self::find_input(&var_names, node_id, MaterialSocket::Float, graph)
411 .unwrap_or_else(|| "in.uv.x".to_string());
412 format!(
413 "mix(vec4<f32>({:.6},{:.6},{:.6},{:.6}), vec4<f32>({:.6},{:.6},{:.6},{:.6}), clamp({}, 0.0, 1.0))",
414 start[0], start[1], start[2], start[3],
415 end[0], end[1], end[2], end[3],
416 t_var
417 )
418 }
419 MaterialOp::RadialGradient { start, end } => {
420 format!(
421 r#"
422 let _dist = length(in.uv - 0.5) * 2.0;
423 __RESULT__ = mix(vec4<f32>({:.6},{:.6},{:.6},{:.6}), vec4<f32>({:.6},{:.6},{:.6},{:.6}), clamp(_dist, 0.0, 1.0));"#,
424 start[0], start[1], start[2], start[3],
425 end[0], end[1], end[2], end[3],
426 ).trim().to_string()
427 }
428 MaterialOp::NeonGlow { radius, intensity } => {
429 let dist_var = Self::find_input(&var_names, node_id, MaterialSocket::Float, graph)
430 .unwrap_or_else(|| "length(in.logical - in.size * 0.5) / max(in.size.x, in.size.y)".to_string());
431 format!(
432 "vec4<f32>(col.rgb * exp(-{} * {:.6}), col.a)",
433 dist_var, intensity / radius.max(0.001)
434 )
435 }
436 MaterialOp::GlassBlur => {
437 r#"
438 let uv = clamp(in.uv, vec2<f32>(0.0), vec2<f32>(1.0));
439 let local = in.logical / in.size;
440 let centered = local - vec2<f32>(0.5, 0.5);
441 let lens_dir = normalize(centered + vec2<f32>(1e-5, 1e-5));
442 let lens_dist = length(centered);
443 let fresnel = pow(lens_dist * 1.8, 2.5);
444 let lens = lens_dir * lens_dist * 0.08;
445 let blur_mip = theme.glass_blur_strength;
446 let env_base = textureSampleLevel(t_env, s_env, uv, blur_mip).rgb;
447 let brightness = dot(env_base, vec3<f32>(0.299, 0.587, 0.114));
448 var distortion = lens * 1.2;
449 distortion *= (1.0 + brightness * 0.7);
450 distortion *= 2.0;
451 let ab_offset = distortion * 0.04;
452 let r_sample = textureSampleLevel(t_env, s_env, uv + distortion + ab_offset * 1.2, blur_mip).r;
453 let g_sample = textureSampleLevel(t_env, s_env, uv + distortion, blur_mip).g;
454 let b_sample = textureSampleLevel(t_env, s_env, uv + distortion - ab_offset * 1.2, blur_mip).b;
455 let refracted = vec3<f32>(r_sample, g_sample, b_sample);
456 let tint = vec3<f32>(0.85, 0.9, 1.0);
457 var final_rgb = refracted * tint;
458 final_rgb += (brightness * 0.2) * (0.9 + vnoise(uv * 20.0 + scene.time * 3.0) * 0.1);
459 let half_size = in.size * 0.5;
460 let p_sdf = in.logical - half_size;
461 let q_sdf = abs(p_sdf) - (half_size - in.radius);
462 let d_sdf = length(max(q_sdf, vec2(0.0))) + min(max(q_sdf.x, q_sdf.y), 0.0) - in.radius;
463 let d_norm = clamp(-d_sdf / 20.0, 0.0, 1.0);
464 let flicker = 0.9 + vnoise(uv * 20.0 + scene.time * 3.0) * 0.1;
465 final_rgb += smoothstep(1.0, 0.96, d_norm) * 0.25 * flicker * vec3<f32>(0.7, 1.0, 1.3);
466 final_rgb -= smoothstep(0.96, 0.88, d_norm) * 0.15;
467 let light_dir_h = normalize(vec2<f32>(-0.4, -0.8));
468 let l = dot(uv, light_dir_h);
469 final_rgb += smoothstep(0.45, 0.55, l) * 0.12;
470 __RESULT__ = vec4<f32>(final_rgb, 0.02 + fresnel * 0.15) * (1.0 - smoothstep(-length(vec2(dpdx(in.logical.x), dpdy(in.logical.y))), length(vec2(dpdx(in.logical.x), dpdy(in.logical.y))), d_sdf));"#.trim().to_string()
471 }
472 MaterialOp::LayerBlend { mode } => {
473 let bottom = Self::find_input(&var_names, node_id, MaterialSocket::Color, graph)
474 .unwrap_or_else(|| "col".to_string());
475 let top = Self::find_input_map(&var_names, node_id, MaterialSocket::Color, graph, 1)
476 .unwrap_or_else(|| "col".to_string());
477 let opacity = Self::find_input(&var_names, node_id, MaterialSocket::Float, graph)
478 .unwrap_or_else(|| "1.0".to_string());
479 match mode {
480 BlendMode::Add => {
481 format!("mix({}, {}, {})", bottom, top, opacity)
482 }
483 BlendMode::Screen => {
484 format!("mix({}, 1.0 - (1.0 - {}) * (1.0 - {}), {})", bottom, bottom, top, opacity)
485 }
486 BlendMode::Multiply => {
487 format!("mix({}, {} * {}, {})", bottom, bottom, top, opacity)
488 }
489 BlendMode::Overlay => {
490 format!("mix({}, select(2.0 * {} * {}, 1.0 - 2.0 * (1.0 - {}) * (1.0 - {}), step(vec4<f32>(0.5), {})), {})", bottom, bottom, top, bottom, top, bottom, opacity)
491 }
492 }
493 }
494 MaterialOp::PBRLighting => {
495 r#"
496 let _n = normalize(in.normal);
497 let _metallic = in.slice.x;
498 let _roughness = in.slice.y;
499 let _opacity = in.slice.z;
500 let _ld = normalize(vec3<f32>(0.5, 0.8, 0.6));
501 let _lc = vec3<f32>(1.0, 0.95, 0.9);
502 let _ndl = max(dot(_n, _ld), 0.0);
503 let _diffuse = _ndl * _lc;
504 let _vd = vec3<f32>(0.0, 0.0, 1.0);
505 let _hd = normalize(_ld + _vd);
506 let _ndh = max(dot(_n, _hd), 0.0);
507 let _shiny = mix(8.0, 256.0, 1.0 - _roughness);
508 let _spec = pow(_ndh, _shiny) * _lc;
509 let _f0 = mix(vec3<f32>(0.04), col.rgb, _metallic);
510 let _fresnel = _f0 + (vec3<f32>(1.0) - _f0) * pow(1.0 - max(dot(_n, -_vd), 0.0), 5.0);
511 let _amb = vec3<f32>(0.06, 0.07, 0.1);
512 var _lit = col.rgb * (_amb + _diffuse);
513 _lit += _spec * mix(vec3<f32>(1.0), col.rgb, _metallic) * _fresnel;
514 let _depth = in.clip_position.z;
515 let _fog = clamp(1.0 - _depth * 0.0005, 0.7, 1.0);
516 _lit *= _fog;
517 __RESULT__ = vec4<f32>(_lit, col.a * _opacity);"#.trim().to_string()
518 }
519 MaterialOp::DropShadow => {
520 r#"
521 let margin = in.uv.x;
522 let blur = max(in.uv.y, 1.0);
523 let original_size = in.size - 2.0 * margin;
524 let half_size = original_size * 0.5;
525 let p = in.logical - margin - half_size;
526 let d = sd_round_rect(p, half_size - in.radius, in.radius);
527 __RESULT__ = vec4<f32>(col.rgb, col.a * smoothstep(blur, 0.0, d));"#.trim().to_string()
528 }
529 MaterialOp::NineSlice => {
530 "col".to_string() }
532 MaterialOp::Heatmap => {
533 let val_var = Self::find_input(&var_names, node_id, MaterialSocket::Float, graph)
534 .unwrap_or_else(|| "textureSample(t_diffuse[0], s_diffuse, in.uv).r".to_string());
535 format!("vec4<f32>(heatmap_palette({}), col.a)", val_var)
536 }
537 MaterialOp::Raymarch { shape } => {
538 match shape {
539 RaymarchShape::Box => {
540 r#"
541 let uv = (in.uv - 0.5) * 2.0;
542 let ro = vec3<f32>(0.0, 0.0, -2.5);
543 let rd = normalize(vec3<f32>(uv.x, uv.y, 1.5));
544 let m = rotX(in.slice.x) * rotY(in.slice.y) * rotZ(in.slice.z);
545 var t = 0.0;
546 var hit = false;
547 var d = 0.0;
548 for (var i = 0; i < 40; i++) {
549 let p = m * (ro + rd * t);
550 d = sd_box_3d(p, vec3(0.5, 0.5, 0.5));
551 if d < 0.001 {
552 hit = true;
553 break;
554 }
555 t += d;
556 if t > 5.0 { break; }
557 }
558 if hit {
559 let p = m * (ro + rd * t);
560 let eps = vec2(0.001, 0.0);
561 let n = normalize(vec3(
562 sd_box_3d(p + eps.xyy, vec3(0.5)) - sd_box_3d(p - eps.xyy, vec3(0.5)),
563 sd_box_3d(p + eps.yxy, vec3(0.5)) - sd_box_3d(p - eps.yxy, vec3(0.5)),
564 sd_box_3d(p + eps.yyx, vec3(0.5)) - sd_box_3d(p - eps.yyx, vec3(0.5))
565 ));
566 let light_dir = normalize(vec3(1.0, 1.0, -2.0));
567 let diff = max(dot(n, light_dir), 0.1);
568 let rim = pow(1.0 - max(dot(n, -rd), 0.0), 3.0) * 0.5;
569 __RESULT__ = vec4<f32>(col.rgb * diff + rim, col.a);
570 } else {
571 discard;
572 }"#.trim().to_string()
573 }
574 RaymarchShape::Sphere => {
575 r#"
576 let ro = vec3<f32>(in.uv * 2.0 - 1.0, -2.0);
577 let rd = normalize(vec3<f32>(0.0, 0.0, 1.0));
578 var t = 0.0;
579 var hit = false;
580 for (var i = 0; i < 32; i++) {
581 let p = ro + rd * t;
582 let d = length(p) - 1.0;
583 if d < 0.01 { hit = true; break; }
584 t += d;
585 }
586 if hit {
587 let p = ro + rd * t;
588 let n = normalize(p);
589 let ld = normalize(vec3<f32>(1.0, 1.0, -1.0));
590 let diff = max(dot(n, ld), 0.0);
591 __RESULT__ = vec4<f32>(col.rgb * diff, col.a);
592 } else {
593 discard;
594 }"#.trim().to_string()
595 }
596 }
597 }
598 MaterialOp::Lightning => {
599 r#"
600 let d = length((in.uv - 0.5) * vec2<f32>(1.0, 4.0));
601 __RESULT__ = theme.primary_neon * neon_glow(d, 0.01, 0.2);"#.trim().to_string()
602 }
603 MaterialOp::RuneGlow => {
604 r#"
605 let p = (in.uv - 0.5) * 2.0;
606 let d = min(sd_segment(p, vec2(-0.5, -0.8), vec2(0.5, 0.8)), sd_segment(p, vec2(0.5, -0.8), vec2(-0.5, 0.8)));
607 __RESULT__ = theme.rune_glow * neon_glow(d, 0.02, 0.15) * theme.rune_opacity;"#.trim().to_string()
608 }
609 MaterialOp::RaymarchReflections => {
610 r#"
611 let ro = vec3<f32>(in.uv.x - 0.5, in.uv.y - 0.5, -2.0);
612 let rd = normalize(vec3<f32>(in.uv.x - 0.5, in.uv.y - 0.5, 1.0));
613 let t = ray_march(ro, rd);
614 if t > 0.0 {
615 let p = ro + rd * t;
616 let n = calc_normal(p);
617 let light_dir = normalize(vec3<f32>(1.0, 1.0, -1.0));
618 let diff = max(dot(n, light_dir), 0.2);
619 let ref_rd = reflect(rd, n);
620 let ref_t = ray_march(p + n * 0.01, ref_rd);
621 var reflection_color = vec3<f32>(0.05, 0.05, 0.1);
622 if ref_t > 0.0 { reflection_color = mix(theme.primary_neon.rgb, theme.shatter_neon.rgb, 0.5); }
623 __RESULT__ = vec4<f32>(mix(col.rgb * diff, reflection_color, 0.3), 1.0);
624 } else { discard; }"#.trim().to_string()
625 }
626 MaterialOp::Stroke => {
627 r#"
628 let half_size = in.size * 0.5;
629 let d = sd_round_rect(in.logical - half_size, half_size - in.radius, in.radius);
630 let thickness = max(in.slice.x, 1.0);
631 let fw = length(vec2(dpdx(in.logical.x), dpdy(in.logical.y)));
632 __RESULT__ = vec4<f32>(col.rgb, col.a * (1.0 - smoothstep(-fw, fw, abs(d + thickness * 0.5) - thickness * 0.5)));"#.trim().to_string()
633 }
634 MaterialOp::DashedStroke => {
635 r#"
636 let half_size = in.size * 0.5;
637 let d = sd_round_rect(in.logical - half_size, half_size - in.radius, in.radius);
638 let thickness = max(in.slice.x, 1.0);
639 let perimeter = (in.uv.x + in.uv.y) * max(in.size.x, in.size.y);
640 var alpha = 1.0 - smoothstep(-length(vec2(dpdx(in.logical.x), dpdy(in.logical.y))), length(vec2(dpdx(in.logical.x), dpdy(in.logical.y))), abs(d + thickness * 0.5) - thickness * 0.5);
641 if (perimeter + scene.time * 20.0) % (max(in.slice.y, 1.0) + max(in.slice.z, 1.0)) > max(in.slice.y, 1.0) { alpha = 0.0; }
642 __RESULT__ = vec4<f32>(col.rgb, col.a * alpha);"#.trim().to_string()
643 }
644 };
645
646 if expr.contains("__RESULT__") {
647 lines.push(format!(" var {}: vec4<f32>;", result_var));
648 lines.push(" {".to_string());
649 lines.push(expr.replace("__RESULT__", &result_var));
650 lines.push(" }".to_string());
651 } else {
652 lines.push(format!(" var {} = {};", result_var, expr));
653 }
654 var_names.insert((node_id, MaterialSocket::Color), result_var);
655 }
656
657 let body = lines.join("\n");
658 let out_id = graph.output.ok_or(MaterialError::NoOutput)?;
659 let fn_name = "material_entry".to_string();
660
661 let wgsl_fn = format!(
662 "fn {}(in: VertexOutput, col: vec4<f32>) -> vec4<f32> {{\n{}\n return v_{};\n}}",
663 fn_name, body, out_id
664 );
665
666 Ok(CompiledMaterial { wgsl_fn, fn_name })
667 }
668
669 fn find_input(
670 names: &HashMap<(MatNodeId, MaterialSocket), String>,
671 node: MatNodeId,
672 socket: MaterialSocket,
673 graph: &MaterialGraph,
674 ) -> Option<String> {
675 for edge in &graph.edges {
676 if edge.to_node == node && edge.to_socket == socket {
677 return names.get(&(edge.from_node, edge.from_socket)).cloned();
678 }
679 }
680 None
681 }
682
683 fn find_input_map(
684 names: &HashMap<(MatNodeId, MaterialSocket), String>,
685 node: MatNodeId,
686 socket: MaterialSocket,
687 graph: &MaterialGraph,
688 offset: usize,
689 ) -> Option<String> {
690 let mut matches = graph
691 .edges
692 .iter()
693 .filter(|e| e.to_node == node && e.to_socket == socket);
694 let edge = matches.nth(offset)?;
695 names.get(&(edge.from_node, edge.from_socket)).cloned()
696 }
697
698 fn topo_sort(graph: &MaterialGraph) -> Result<Vec<MatNodeId>, MaterialError> {
699 let n = graph.nodes.len();
700 let mut in_degree = vec![0u32; n];
701 let mut adj: Vec<Vec<MatNodeId>> = vec![Vec::new(); n];
702
703 for edge in &graph.edges {
704 adj[edge.from_node as usize].push(edge.to_node);
705 in_degree[edge.to_node as usize] += 1;
706 }
707
708 let mut queue: std::collections::VecDeque<MatNodeId> = std::collections::VecDeque::new();
709 for (i, °) in in_degree.iter().enumerate() {
710 if deg == 0 {
711 queue.push_back(i as MatNodeId);
712 }
713 }
714
715 let mut order = Vec::with_capacity(n);
716 while let Some(node) = queue.pop_front() {
717 order.push(node);
718 for &next in &adj[node as usize] {
719 in_degree[next as usize] -= 1;
720 if in_degree[next as usize] == 0 {
721 queue.push_back(next);
722 }
723 }
724 }
725
726 if order.len() != n {
727 return Err(MaterialError::Cycle);
728 }
729
730 Ok(order)
731 }
732}
733
734pub mod builtins {
737 use super::*;
738
739 pub fn rounded_rect() -> MaterialGraph {
741 let mut g = MaterialGraph::new();
742 let input = g.add_node(MaterialOp::InputColor);
743 let sdf = g.add_node(MaterialOp::SDFRoundRect);
744 g.connect(input, MaterialSocket::Color, sdf, MaterialSocket::Color);
746 g.set_output(sdf);
747 g
748 }
749
750 pub fn glass() -> MaterialGraph {
752 let mut g = MaterialGraph::new();
753 let glass = g.add_node(MaterialOp::GlassBlur);
754 g.set_output(glass);
755 g
756 }
757
758 pub fn solid() -> MaterialGraph {
760 let mut g = MaterialGraph::new();
761 let input = g.add_node(MaterialOp::InputColor);
762 g.set_output(input);
763 g
764 }
765
766 pub fn pbr() -> MaterialGraph {
768 let mut g = MaterialGraph::new();
769 let input = g.add_node(MaterialOp::InputColor);
770 let pbr = g.add_node(MaterialOp::PBRLighting);
771 g.connect(input, MaterialSocket::Color, pbr, MaterialSocket::Color);
772 g.set_output(pbr);
773 g
774 }
775
776 pub fn text(tex_index: u32) -> MaterialGraph {
778 let mut g = MaterialGraph::new();
779 let input = g.add_node(MaterialOp::InputColor);
780 let tex = g.add_node(MaterialOp::SampleTexture { tex_index });
781 let blend = g.add_node(MaterialOp::PremultipliedBlend);
782 g.connect(input, MaterialSocket::Color, blend, MaterialSocket::Color);
783 g.connect(tex, MaterialSocket::Float, blend, MaterialSocket::Float);
784 g.set_output(blend);
785 g
786 }
787
788 pub fn textured(tex_index: u32) -> MaterialGraph {
790 let mut g = MaterialGraph::new();
791 let input = g.add_node(MaterialOp::InputColor);
792 let tex = g.add_node(MaterialOp::SampleTexture { tex_index });
793 let blend = g.add_node(MaterialOp::LayerBlend {
794 mode: BlendMode::Multiply,
795 });
796 g.connect(input, MaterialSocket::Color, blend, MaterialSocket::Color);
797 g.connect(tex, MaterialSocket::Color, blend, MaterialSocket::Color);
798 g.set_output(blend);
799 g
800 }
801
802 pub fn neon_glow(radius: f32, intensity: f32) -> MaterialGraph {
804 let mut g = MaterialGraph::new();
805 let input = g.add_node(MaterialOp::InputColor);
806 let glow = g.add_node(MaterialOp::NeonGlow { radius, intensity });
807 g.connect(input, MaterialSocket::Color, glow, MaterialSocket::Color);
808 g.set_output(glow);
809 g
810 }
811
812 pub fn linear_gradient(start: [f32; 4], end: [f32; 4]) -> MaterialGraph {
814 let mut g = MaterialGraph::new();
815 let grad = g.add_node(MaterialOp::LinearGradient { start, end });
816 g.set_output(grad);
817 g
818 }
819
820 pub fn radial_gradient(start: [f32; 4], end: [f32; 4]) -> MaterialGraph {
822 let mut g = MaterialGraph::new();
823 let grad = g.add_node(MaterialOp::RadialGradient { start, end });
824 g.set_output(grad);
825 g
826 }
827
828 pub fn ellipse() -> MaterialGraph {
830 let mut g = MaterialGraph::new();
831 let input = g.add_node(MaterialOp::InputColor);
832 let sdf = g.add_node(MaterialOp::SDFEllipse);
833 g.connect(input, MaterialSocket::Color, sdf, MaterialSocket::Color);
834 g.set_output(sdf);
835 g
836 }
837
838 pub fn neon_line() -> MaterialGraph {
840 let mut g = MaterialGraph::new();
841 let color = g.add_node(MaterialOp::ConstantColor {
842 r: 1.5,
843 g: 1.5,
844 b: 1.5,
845 a: 1.0,
846 });
847 g.set_output(color);
848 g
849 }
850
851 pub fn heatmap(tex_index: u32) -> MaterialGraph {
853 let mut g = MaterialGraph::new();
854 let tex = g.add_node(MaterialOp::SampleTexture { tex_index });
855 let hm = g.add_node(MaterialOp::Heatmap);
856 g.connect(tex, MaterialSocket::Float, hm, MaterialSocket::Float);
857 g.set_output(hm);
858 g
859 }
860
861 pub fn nine_slice(tex_index: u32) -> MaterialGraph {
863 let mut g = MaterialGraph::new();
864 let input = g.add_node(MaterialOp::InputColor);
865 let tex = g.add_node(MaterialOp::SampleTexture { tex_index });
866 let blend = g.add_node(MaterialOp::LayerBlend {
867 mode: BlendMode::Multiply,
868 });
869 g.connect(input, MaterialSocket::Color, blend, MaterialSocket::Color);
870 g.connect(tex, MaterialSocket::Color, blend, MaterialSocket::Color);
871 g.set_output(blend);
872 g
873 }
874
875 pub fn raymarch_cube() -> MaterialGraph {
877 let mut g = MaterialGraph::new();
878 let input = g.add_node(MaterialOp::InputColor);
879 let rm = g.add_node(MaterialOp::Raymarch {
880 shape: RaymarchShape::Box,
881 });
882 g.connect(input, MaterialSocket::Color, rm, MaterialSocket::Color);
883 g.set_output(rm);
884 g
885 }
886
887 pub fn stroke() -> MaterialGraph {
889 let mut g = MaterialGraph::new();
890 let input = g.add_node(MaterialOp::InputColor);
891 let sdf = g.add_node(MaterialOp::Stroke);
892 g.connect(input, MaterialSocket::Color, sdf, MaterialSocket::Color);
893 g.set_output(sdf);
894 g
895 }
896
897 pub fn drop_shadow() -> MaterialGraph {
899 let mut g = MaterialGraph::new();
900 let input = g.add_node(MaterialOp::InputColor);
901 let shadow = g.add_node(MaterialOp::DropShadow);
902 g.connect(input, MaterialSocket::Color, shadow, MaterialSocket::Color);
903 g.set_output(shadow);
904 g
905 }
906
907 pub fn dashed_stroke() -> MaterialGraph {
909 let mut g = MaterialGraph::new();
910 let input = g.add_node(MaterialOp::InputColor);
911 let sdf = g.add_node(MaterialOp::DashedStroke);
912 g.connect(input, MaterialSocket::Color, sdf, MaterialSocket::Color);
913 g.set_output(sdf);
914 g
915 }
916
917 pub fn lightning() -> MaterialGraph {
918 let mut g = MaterialGraph::new();
919 let l = g.add_node(MaterialOp::Lightning);
920 g.set_output(l);
921 g
922 }
923
924 pub fn rune_glow() -> MaterialGraph {
925 let mut g = MaterialGraph::new();
926 let r = g.add_node(MaterialOp::RuneGlow);
927 g.set_output(r);
928 g
929 }
930
931 pub fn raymarch() -> MaterialGraph {
932 let mut g = MaterialGraph::new();
933 let input = g.add_node(MaterialOp::InputColor);
934 let rm = g.add_node(MaterialOp::RaymarchReflections);
935 g.connect(input, MaterialSocket::Color, rm, MaterialSocket::Color);
936 g.set_output(rm);
937 g
938 }
939}
940
941pub fn generate_builtins_wgsl() -> String {
942 let mut out = String::new();
943 out.push_str("// ── Auto-generated material functions (Runtime) ──\n\n");
944
945 let builtins = vec![
946 (0, "solid", builtins::solid()),
947 (1, "neon_line", builtins::neon_line()),
948 (2, "textured", builtins::textured(0)),
949 (3, "rounded_rect", builtins::rounded_rect()),
950 (4, "ellipse", builtins::ellipse()),
951 (6, "text", builtins::text(0)),
952 (7, "glass", builtins::glass()),
953 (8, "neon_glow", builtins::neon_glow(1.0, 1.0)),
954 (9, "lightning", builtins::lightning()),
955 (10, "rune_glow", builtins::rune_glow()),
956 (12, "heatmap", builtins::heatmap(0)),
957 (13, "pbr", builtins::pbr()),
958 (14, "raymarch", builtins::raymarch()),
959 (
960 15,
961 "linear_grad",
962 builtins::linear_gradient([0.0; 4], [0.0; 4]),
963 ),
964 (
965 16,
966 "radial_grad",
967 builtins::radial_gradient([0.0; 4], [0.0; 4]),
968 ),
969 (17, "stroke", builtins::stroke()),
970 (18, "drop_shadow", builtins::drop_shadow()),
971 (19, "dashed", builtins::dashed_stroke()),
972 (20, "nine_slice", builtins::nine_slice(0)),
973 (21, "raymarch_cube", builtins::raymarch_cube()),
974 ];
975
976 let mut dispatch = String::new();
977 dispatch.push_str(
978 "fn dispatch_material(material_id: u32, in: VertexOutput, col: vec4<f32>) -> vec4<f32> {\n",
979 );
980 dispatch.push_str(" switch material_id {\n");
981
982 for (id, name, graph) in builtins {
983 let compiled = MaterialCompiler::compile(&graph).unwrap();
984 let fn_name = format!("material_{}_{}", id, name);
985 let fn_code = compiled.wgsl_fn.replace("material_entry", &fn_name);
986 out.push_str(&fn_code);
987 out.push_str("\n\n");
988
989 dispatch.push_str(&format!(
990 " case {}u: {{ return {}(in, col); }}\n",
991 id, fn_name
992 ));
993 }
994
995 dispatch.push_str(" default: { return col; }\n");
996 dispatch.push_str(" }\n}\n");
997
998 out.push_str(&dispatch);
999 out
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004 use super::*;
1005
1006 #[test]
1007 fn test_solid_material_compiles() {
1008 let graph = builtins::solid();
1009 let compiled = MaterialCompiler::compile(&graph).unwrap();
1010 assert!(compiled.wgsl_fn.contains("fn material_"));
1011 assert!(compiled.wgsl_fn.contains("col"));
1012 }
1013
1014 #[test]
1015 fn test_rounded_rect_compiles() {
1016 let graph = builtins::rounded_rect();
1017 let compiled = MaterialCompiler::compile(&graph).unwrap();
1018 assert!(compiled.wgsl_fn.contains("sd_round_rect"));
1019 }
1020
1021 #[test]
1022 fn test_pbr_compiles() {
1023 let graph = builtins::pbr();
1024 let compiled = MaterialCompiler::compile(&graph).unwrap();
1025 assert!(compiled.wgsl_fn.contains("PBRLighting") || compiled.wgsl_fn.contains("_n"));
1026 }
1027
1028 #[test]
1029 fn test_graph_validation_no_output() {
1030 let mut g = MaterialGraph::new();
1031 g.add_node(MaterialOp::InputColor);
1032 assert!(g.validate().is_err());
1033 }
1034
1035 #[test]
1036 fn test_graph_validation_cycle() {
1037 let mut g = MaterialGraph::new();
1038 let a = g.add_node(MaterialOp::InputColor);
1039 let b = g.add_node(MaterialOp::NeonGlow {
1040 radius: 1.0,
1041 intensity: 1.0,
1042 });
1043 g.connect(a, MaterialSocket::Color, b, MaterialSocket::Color);
1044 g.connect(b, MaterialSocket::Color, a, MaterialSocket::Color); g.set_output(b);
1046 assert!(g.validate().is_err());
1047 }
1048
1049 #[test]
1050 fn test_all_builtins_compile() {
1051 let graphs: Vec<MaterialGraph> = vec![
1052 builtins::solid(),
1053 builtins::rounded_rect(),
1054 builtins::glass(),
1055 builtins::pbr(),
1056 builtins::text(0),
1057 builtins::textured(0),
1058 builtins::neon_glow(4.0, 1.5),
1059 builtins::linear_gradient([1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0]),
1060 builtins::radial_gradient([1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0, 1.0]),
1061 builtins::ellipse(),
1062 builtins::neon_line(),
1063 builtins::heatmap(0),
1064 builtins::nine_slice(0),
1065 builtins::raymarch_cube(),
1066 builtins::stroke(),
1067 builtins::drop_shadow(),
1068 builtins::dashed_stroke(),
1069 ];
1070
1071 for (i, graph) in graphs.iter().enumerate() {
1072 match MaterialCompiler::compile(graph) {
1073 Ok(compiled) => {
1074 assert!(
1075 !compiled.wgsl_fn.is_empty(),
1076 "graph {} produced empty WGSL",
1077 i
1078 );
1079 assert!(
1080 !compiled.fn_name.is_empty(),
1081 "graph {} produced empty fn name",
1082 i
1083 );
1084 }
1085 Err(e) => {
1086 panic!("graph {} failed to compile: {}", i, e);
1087 }
1088 }
1089 }
1090 }
1091}