1use rayon::prelude::*;
2use rustc_hash::FxHashSet;
3use std::sync::RwLock;
4use theframework::prelude::*;
5
6#[inline(always)]
7fn hash21(p: Vec2<f32>) -> f32 {
8 let mut p3 = Vec3::new(
9 (p.x * 0.1031).fract(),
10 (p.y * 0.1031).fract(),
11 (p.x * 0.1031).fract(),
12 );
13 let dot = p3.dot(Vec3::new(p3.y + 33.333, p3.z + 33.333, p3.x + 33.333));
14
15 p3.x += dot;
16 p3.y += dot;
17 p3.z += dot;
18
19 ((p3.x + p3.y) * p3.z).fract()
20}
21
22fn rot(a: f32) -> Mat2<f32> {
23 Mat2::new(a.cos(), -a.sin(), a.sin(), a.cos())
24}
25
26fn box_divide(
27 p: Vec2<f32>,
28 cell: Vec2<f32>,
29 gap: f32,
30 rotation: f32,
31 rounding: f32,
32 iterations: i32,
33) -> (f32, f32) {
34 fn s_box(p: Vec2<f32>, b: Vec2<f32>, r: f32) -> f32 {
35 let d = p.map(|v| v.abs()) - b + Vec2::new(r, r);
36 d.x.max(d.y).min(0.0) + (d.map(|v| v.max(0.0))).magnitude() - r
37 }
38
39 let mut p = p;
40
41 let mut l = Vec2::new(1.0, 1.0);
42 let mut last_l;
43 let mut r = hash21(cell);
44
45 for _ in 0..iterations.max(1) {
46 r = (l + Vec2::new(r, r)).dot(Vec2::new(123.71, 439.43)).fract() * 0.4 + 0.3;
47
48 last_l = l;
49 if l.x > l.y {
50 p = Vec2::new(p.y, p.x);
51 l = Vec2::new(l.y, l.x);
52 }
53
54 if p.x < r {
55 l.x /= r;
56 p.x /= r;
57 } else {
58 l.x /= 1.0 - r;
59 p.x = (p.x - r) / (1.0 - r);
60 }
61
62 if last_l.x > last_l.y {
63 p = Vec2::new(p.y, p.x);
64 l = Vec2::new(l.y, l.x);
65 }
66 }
67 p -= 0.5;
68
69 let id = hash21(cell + l);
70 p = rot((id - 0.5) * rotation) * p;
71
72 let th = l * 0.02 * gap;
73 let c = s_box(p, Vec2::new(0.5, 0.5) - th, rounding);
74
75 (c, id)
76}
77
78fn default_tile_node_nodes() -> Vec<TileNodeState> {
79 vec![TileNodeState {
80 kind: TileNodeKind::default_output_root(),
81 position: (420, 40),
82 preview_open: true,
83 bypass: false,
84 mute: false,
85 solo: false,
86 }]
87}
88
89fn default_voronoi_warp_amount() -> f32 {
90 0.0
91}
92
93fn default_voronoi_falloff() -> f32 {
94 1.0
95}
96
97fn default_layout_warp_amount() -> f32 {
98 0.0
99}
100
101fn default_layout_falloff() -> f32 {
102 1.0
103}
104
105fn default_height_shape_rim() -> f32 {
106 0.0
107}
108
109fn default_brick_staggered() -> bool {
110 true
111}
112
113fn default_colorize4_color_1() -> u16 {
114 0
115}
116
117fn default_colorize4_color_2() -> u16 {
118 1
119}
120
121fn default_colorize4_color_3() -> u16 {
122 2
123}
124
125fn default_colorize4_color_4() -> u16 {
126 3
127}
128
129fn default_colorize4_pixel_size() -> u16 {
130 1
131}
132
133fn default_colorize4_dither() -> bool {
134 false
135}
136
137fn default_colorize4_auto_range() -> bool {
138 true
139}
140
141fn default_particle_color_1() -> u16 {
142 0
143}
144
145fn default_particle_color_2() -> u16 {
146 1
147}
148
149fn default_particle_color_3() -> u16 {
150 2
151}
152
153fn default_particle_color_4() -> u16 {
154 3
155}
156
157#[derive(Serialize, Deserialize, Default, Clone, Debug)]
158pub struct TileNodeGraphState {
159 #[serde(default = "default_tile_node_nodes")]
160 pub nodes: Vec<TileNodeState>,
161 #[serde(default)]
162 pub connections: Vec<(u16, u8, u16, u8)>,
163 #[serde(default)]
164 pub offset: (i32, i32),
165 #[serde(default)]
166 pub selected_node: Option<usize>,
167 #[serde(default)]
168 pub preview_mode: u8,
169}
170
171#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
172pub enum TileGraphPaletteSource {
173 #[default]
174 Local,
175 Project,
176}
177
178#[derive(Serialize, Deserialize, Clone, Debug)]
179pub struct TileNodeGraphExchange {
180 #[serde(default)]
181 pub version: u32,
182 #[serde(default)]
183 pub graph_name: String,
184 #[serde(default)]
185 pub palette_source: TileGraphPaletteSource,
186 #[serde(default)]
187 pub palette_colors: Vec<TheColor>,
188 pub output_grid_width: u16,
189 pub output_grid_height: u16,
190 pub tile_pixel_width: u16,
191 pub tile_pixel_height: u16,
192 #[serde(default)]
193 pub graph_state: TileNodeGraphState,
194}
195
196#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
197pub struct TileParticleOutput {
198 pub rate: f32,
199 pub spread: f32,
200 pub lifetime_min: f32,
201 pub lifetime_max: f32,
202 pub radius_min: f32,
203 pub radius_max: f32,
204 pub speed_min: f32,
205 pub speed_max: f32,
206 pub flame_base: bool,
207 pub color_variation: u8,
208 pub ramp_colors: [[u8; 4]; 4],
209}
210
211#[derive(Clone, Debug)]
212pub struct TileLightOutput {
213 pub intensity: f32,
214 pub range: f32,
215 pub flicker: f32,
216 pub lift: f32,
217}
218
219#[derive(Serialize, Deserialize, Clone, Debug)]
220pub struct TileNodeState {
221 pub kind: TileNodeKind,
222 pub position: (i32, i32),
223 #[serde(default = "default_node_preview_open")]
224 pub preview_open: bool,
225 #[serde(default)]
226 pub bypass: bool,
227 #[serde(default)]
228 pub mute: bool,
229 #[serde(default)]
230 pub solo: bool,
231}
232
233impl Default for TileNodeState {
234 fn default() -> Self {
235 Self {
236 kind: TileNodeKind::default_output_root(),
237 position: (420, 40),
238 preview_open: true,
239 bypass: false,
240 mute: false,
241 solo: false,
242 }
243 }
244}
245
246fn default_node_preview_open() -> bool {
247 true
248}
249
250#[derive(Serialize, Deserialize, Clone, Debug)]
251pub enum TileNodeKind {
252 OutputRoot {
253 #[serde(default = "default_output_roughness")]
254 roughness: f32,
255 #[serde(default = "default_output_metallic")]
256 metallic: f32,
257 #[serde(default = "default_output_opacity")]
258 opacity: f32,
259 #[serde(default = "default_output_emissive")]
260 emissive: f32,
261 #[serde(default = "default_output_particle_enabled")]
262 particle_enabled: bool,
263 #[serde(default = "default_output_light_enabled")]
264 light_enabled: bool,
265 },
266 LayerInput {
267 name: String,
268 value: f32,
269 },
270 ImportLayer {
271 source: String,
272 },
273 GroupUV,
274 Scalar {
275 value: f32,
276 },
277 Colorize4 {
278 #[serde(default = "default_colorize4_color_1")]
279 color_1: u16,
280 #[serde(default = "default_colorize4_color_2")]
281 color_2: u16,
282 #[serde(default = "default_colorize4_color_3")]
283 color_3: u16,
284 #[serde(default = "default_colorize4_color_4")]
285 color_4: u16,
286 #[serde(default = "default_colorize4_pixel_size")]
287 pixel_size: u16,
288 #[serde(default = "default_colorize4_dither")]
289 dither: bool,
290 #[serde(default = "default_colorize4_auto_range")]
291 auto_range: bool,
292 },
293 Color {
294 color: TheColor,
295 },
296 PaletteColor {
297 index: u16,
298 },
299 NearestPalette,
300 Mix {
301 factor: f32,
302 },
303 Checker {
304 scale: u16,
305 },
306 Gradient {
307 mode: u8,
308 },
309 Curve {
310 power: f32,
311 },
312 Noise {
313 scale: f32,
314 seed: u32,
315 wrap: bool,
316 },
317 Voronoi {
318 scale: f32,
319 seed: u32,
320 jitter: f32,
321 #[serde(default = "default_voronoi_warp_amount")]
322 warp_amount: f32,
323 #[serde(default = "default_voronoi_falloff")]
324 falloff: f32,
325 },
326 BoxDivide {
327 scale: f32,
328 gap: f32,
329 rotation: f32,
330 rounding: f32,
331 #[serde(default = "default_layout_warp_amount")]
332 warp_amount: f32,
333 #[serde(default = "default_layout_falloff")]
334 falloff: f32,
335 },
336 Offset {
337 x: f32,
338 y: f32,
339 },
340 Scale {
341 x: f32,
342 y: f32,
343 },
344 Repeat {
345 repeat_x: f32,
346 repeat_y: f32,
347 },
348 Rotate {
349 angle: f32,
350 },
351 DirectionalWarp {
352 amount: f32,
353 angle: f32,
354 },
355 Brick {
356 columns: u16,
357 rows: u16,
358 #[serde(default = "default_brick_staggered")]
359 staggered: bool,
360 offset: f32,
361 #[serde(default = "default_layout_warp_amount")]
362 warp_amount: f32,
363 #[serde(default = "default_layout_falloff")]
364 falloff: f32,
365 },
366 Disc {
367 scale: f32,
368 seed: u32,
369 jitter: f32,
370 radius: f32,
371 #[serde(default = "default_layout_warp_amount")]
372 warp_amount: f32,
373 #[serde(default = "default_layout_falloff")]
374 falloff: f32,
375 },
376 IdRandom,
377 Min,
378 Max,
379 Add,
380 Subtract,
381 Multiply,
382 MakeMaterial,
383 Material {
384 roughness: f32,
385 metallic: f32,
386 opacity: f32,
387 emissive: f32,
388 },
389 MaterialMix {
390 factor: f32,
391 },
392 ParticleEmitter {
393 rate: f32,
394 spread: f32,
395 lifetime_min: f32,
396 lifetime_max: f32,
397 radius_min: f32,
398 radius_max: f32,
399 speed_min: f32,
400 speed_max: f32,
401 color_variation: u8,
402 },
403 ParticleSpawn {
404 rate: f32,
405 spread: f32,
406 },
407 ParticleMotion {
408 lifetime_min: f32,
409 lifetime_max: f32,
410 speed_min: f32,
411 speed_max: f32,
412 },
413 ParticleRender {
414 radius_min: f32,
415 radius_max: f32,
416 #[serde(default)]
417 flame_base: bool,
418 color_variation: u8,
419 #[serde(default = "default_particle_color_1")]
420 color_1: u16,
421 #[serde(default = "default_particle_color_2")]
422 color_2: u16,
423 #[serde(default = "default_particle_color_3")]
424 color_3: u16,
425 #[serde(default = "default_particle_color_4")]
426 color_4: u16,
427 },
428 LightEmitter {
429 intensity: f32,
430 range: f32,
431 flicker: f32,
432 lift: f32,
433 },
434 MaskBlend {
435 factor: f32,
436 },
437 Levels {
438 level: f32,
439 width: f32,
440 },
441 HeightShape {
442 contrast: f32,
443 bias: f32,
444 plateau: f32,
445 #[serde(default = "default_height_shape_rim")]
446 rim: f32,
447 #[serde(default = "default_layout_warp_amount")]
448 warp_amount: f32,
449 },
450 Threshold {
451 cutoff: f32,
452 },
453 Blur {
454 radius: f32,
455 },
456 SlopeBlur {
457 radius: f32,
458 amount: f32,
459 },
460 HeightEdge {
461 radius: f32,
462 },
463 Warp {
464 amount: f32,
465 },
466 Invert,
467}
468
469impl Default for TileNodeKind {
470 fn default() -> Self {
471 Self::default_output_root()
472 }
473}
474
475fn default_output_roughness() -> f32 {
476 0.9
477}
478
479fn default_output_metallic() -> f32 {
480 0.0
481}
482
483fn default_output_opacity() -> f32 {
484 1.0
485}
486
487fn default_output_emissive() -> f32 {
488 0.0
489}
490
491fn default_output_particle_enabled() -> bool {
492 true
493}
494
495fn default_output_light_enabled() -> bool {
496 true
497}
498
499impl TileNodeKind {
500 pub fn default_output_root() -> Self {
501 Self::OutputRoot {
502 roughness: default_output_roughness(),
503 metallic: default_output_metallic(),
504 opacity: default_output_opacity(),
505 emissive: default_output_emissive(),
506 particle_enabled: default_output_particle_enabled(),
507 light_enabled: default_output_light_enabled(),
508 }
509 }
510}
511
512#[derive(Clone, Copy)]
513pub struct TileEvalContext {
514 pub cell_x: u16,
515 pub cell_y: u16,
516 pub group_width: u16,
517 pub group_height: u16,
518 pub tile_pixel_width: u16,
519 pub tile_pixel_height: u16,
520 pub u: f32,
521 pub v: f32,
522}
523
524impl TileEvalContext {
525 pub fn group_u(&self) -> f32 {
526 ((self.cell_x as f32) + self.u) / (self.group_width.max(1) as f32)
527 }
528
529 pub fn group_v(&self) -> f32 {
530 ((self.cell_y as f32) + self.v) / (self.group_height.max(1) as f32)
531 }
532
533 pub fn with_group_uv(&self, group_u: f32, group_v: f32) -> Self {
534 let width = self.group_width.max(1) as f32;
535 let height = self.group_height.max(1) as f32;
536 let gx = group_u.clamp(0.0, 0.999_999) * width;
537 let gy = group_v.clamp(0.0, 0.999_999) * height;
538 let cell_x = gx.floor() as u16;
539 let cell_y = gy.floor() as u16;
540 Self {
541 cell_x,
542 cell_y,
543 group_width: self.group_width,
544 group_height: self.group_height,
545 tile_pixel_width: self.tile_pixel_width,
546 tile_pixel_height: self.tile_pixel_height,
547 u: gx.fract(),
548 v: gy.fract(),
549 }
550 }
551}
552
553impl TileNodeGraphState {
554 pub fn from_graph_data(graph_data: &str) -> Self {
555 let mut state = serde_json::from_str::<TileNodeGraphState>(graph_data).unwrap_or_default();
556 state.ensure_root();
557 state
558 }
559
560 pub fn ensure_root(&mut self) {
561 if self.nodes.is_empty() {
562 self.nodes = default_tile_node_nodes();
563 } else if !matches!(
564 self.nodes.first().map(|n| &n.kind),
565 Some(TileNodeKind::OutputRoot { .. })
566 ) {
567 self.nodes.insert(
568 0,
569 TileNodeState {
570 kind: TileNodeKind::default_output_root(),
571 position: (420, 40),
572 preview_open: true,
573 bypass: false,
574 mute: false,
575 solo: false,
576 },
577 );
578 }
579 }
580}
581
582impl TileNodeGraphExchange {
583 pub fn new(
584 graph_name: String,
585 output_grid_width: u16,
586 output_grid_height: u16,
587 tile_pixel_width: u16,
588 tile_pixel_height: u16,
589 graph_state: TileNodeGraphState,
590 ) -> Self {
591 Self {
592 version: 1,
593 graph_name,
594 palette_source: TileGraphPaletteSource::Local,
595 palette_colors: vec![],
596 output_grid_width,
597 output_grid_height,
598 tile_pixel_width,
599 tile_pixel_height,
600 graph_state,
601 }
602 }
603}
604
605#[derive(Clone, Debug)]
606pub struct RenderedTileGraph {
607 pub grid_width: usize,
608 pub grid_height: usize,
609 pub tile_width: usize,
610 pub tile_height: usize,
611 pub sheet_color: Vec<u8>,
612 pub sheet_material: Vec<u8>,
613 pub sheet_height: Vec<u8>,
614 pub tiles_color: Vec<Vec<u8>>,
615 pub tiles_material: Vec<Vec<u8>>,
616 pub tiles_height: Vec<Vec<u8>>,
617 pub particle_output: Option<TileParticleOutput>,
618 pub light_output: Option<TileLightOutput>,
619}
620
621pub trait TileGraphSubgraphResolver {
622 fn resolve_subgraph_state(&self, source: &str) -> Option<TileNodeGraphState>;
623
624 fn resolve_subgraph_exchange(&self, source: &str) -> Option<TileNodeGraphExchange> {
625 self.resolve_subgraph_state(source)
626 .map(|graph_state| TileNodeGraphExchange {
627 version: 1,
628 graph_name: String::new(),
629 palette_source: TileGraphPaletteSource::Local,
630 palette_colors: Vec::new(),
631 output_grid_width: 1,
632 output_grid_height: 1,
633 tile_pixel_width: 32,
634 tile_pixel_height: 32,
635 graph_state,
636 })
637 }
638}
639
640pub struct NoTileGraphSubgraphs;
641
642impl TileGraphSubgraphResolver for NoTileGraphSubgraphs {
643 fn resolve_subgraph_state(&self, _source: &str) -> Option<TileNodeGraphState> {
644 None
645 }
646}
647
648#[derive(Clone, Copy, Default)]
649struct FlatSubgraphOutputs {
650 outputs: [Option<u16>; 8],
651}
652
653#[derive(Clone, Default)]
654struct FlatSubgraphInputs {
655 inputs: Vec<Option<u16>>,
656}
657
658pub fn flatten_graph_exchange_with<R: TileGraphSubgraphResolver>(
659 graph: &TileNodeGraphExchange,
660 resolver: &R,
661) -> TileNodeGraphExchange {
662 let mut flattened = graph.clone();
663 flattened.graph_state = flatten_graph_state_recursive(
664 &graph.graph_state,
665 resolver,
666 &mut FxHashSet::default(),
667 graph.palette_source,
668 &graph.palette_colors,
669 );
670 flattened
671}
672
673pub fn flatten_graph_state_with<R: TileGraphSubgraphResolver>(
674 state: &TileNodeGraphState,
675 resolver: &R,
676) -> TileNodeGraphState {
677 let mut state = state.clone();
678 state.ensure_root();
679 flatten_graph_state_recursive(
680 &state,
681 resolver,
682 &mut FxHashSet::default(),
683 TileGraphPaletteSource::Local,
684 &[],
685 )
686}
687
688fn flatten_graph_state_recursive<R: TileGraphSubgraphResolver>(
689 state: &TileNodeGraphState,
690 resolver: &R,
691 stack: &mut FxHashSet<String>,
692 target_palette_source: TileGraphPaletteSource,
693 target_palette: &[TheColor],
694) -> TileNodeGraphState {
695 let mut nodes = Vec::new();
696 let mut node_map: Vec<Option<u16>> = vec![None; state.nodes.len()];
697 let mut subgraph_outputs: Vec<FlatSubgraphOutputs> =
698 vec![FlatSubgraphOutputs::default(); state.nodes.len()];
699 let mut subgraph_inputs: Vec<FlatSubgraphInputs> =
700 vec![FlatSubgraphInputs::default(); state.nodes.len()];
701 let mut connections = Vec::new();
702
703 if let Some(root) = state.nodes.first() {
704 nodes.push(root.clone());
705 node_map[0] = Some(0);
706 }
707
708 for (old_index, node) in state.nodes.iter().enumerate().skip(1) {
709 match &node.kind {
710 TileNodeKind::ImportLayer { source } => {
711 if !stack.insert(source.clone()) {
712 continue;
713 }
714 let Some(mut sub_exchange) = resolver.resolve_subgraph_exchange(source) else {
715 stack.remove(source);
716 continue;
717 };
718 sub_exchange.graph_state.ensure_root();
719 remap_exchange_palette_for_instancing(
720 &mut sub_exchange,
721 target_palette_source,
722 target_palette,
723 );
724 let sub_flat = flatten_graph_state_recursive(
725 &sub_exchange.graph_state,
726 resolver,
727 stack,
728 target_palette_source,
729 target_palette,
730 );
731 stack.remove(source);
732
733 let base = nodes.len() as u16;
734 let mut sub_map: Vec<Option<u16>> = vec![None; sub_flat.nodes.len()];
735 let mut input_slots = Vec::new();
736 for (sub_index, sub_node) in sub_flat.nodes.iter().enumerate().skip(1) {
737 let new_index = nodes.len() as u16;
738 nodes.push(sub_node.clone());
739 sub_map[sub_index] = Some(new_index);
740 if matches!(sub_node.kind, TileNodeKind::LayerInput { .. }) {
741 input_slots.push(Some(new_index));
742 }
743 }
744
745 let mut outputs = [None; 8];
746 for (terminal, slot) in outputs.iter_mut().enumerate() {
747 *slot = input_connection_source(&sub_flat, 0, terminal as u8)
748 .and_then(|src| remap_sub_index(src, &sub_map, base));
749 }
750 subgraph_outputs[old_index] = FlatSubgraphOutputs { outputs };
751 subgraph_inputs[old_index] = FlatSubgraphInputs {
752 inputs: input_slots,
753 };
754
755 for (src_node, src_terminal, dest_node, dest_terminal) in &sub_flat.connections {
756 if *src_node == 0 || *dest_node == 0 {
757 continue;
758 }
759 if let (Some(src), Some(dest)) =
760 (sub_map[*src_node as usize], sub_map[*dest_node as usize])
761 {
762 connections.push((src, *src_terminal, dest, *dest_terminal));
763 }
764 }
765 }
766 _ => {
767 let new_index = nodes.len() as u16;
768 nodes.push(node.clone());
769 node_map[old_index] = Some(new_index);
770 }
771 }
772 }
773
774 for (src_node, src_terminal, dest_node, dest_terminal) in &state.connections {
775 let src = if matches!(
776 state.nodes.get(*src_node as usize).map(|n| &n.kind),
777 Some(TileNodeKind::ImportLayer { .. })
778 ) {
779 let outputs = subgraph_outputs[*src_node as usize];
780 outputs.outputs.get(*src_terminal as usize).and_then(|v| *v)
781 } else {
782 node_map.get(*src_node as usize).and_then(|v| *v)
783 };
784 let dest = if matches!(
785 state.nodes.get(*dest_node as usize).map(|n| &n.kind),
786 Some(TileNodeKind::ImportLayer { .. })
787 ) {
788 subgraph_inputs
789 .get(*dest_node as usize)
790 .and_then(|i| i.inputs.get(*dest_terminal as usize))
791 .and_then(|v| *v)
792 } else {
793 node_map.get(*dest_node as usize).and_then(|v| *v)
794 };
795 if let (Some(src), Some(dest)) = (src, dest) {
796 let target_terminal = if matches!(
797 nodes.get(dest as usize).map(|n| &n.kind),
798 Some(TileNodeKind::LayerInput { .. })
799 ) {
800 0
801 } else {
802 *dest_terminal
803 };
804 connections.push((src, *src_terminal, dest, target_terminal));
805 }
806 }
807
808 TileNodeGraphState {
809 nodes,
810 connections,
811 offset: state.offset,
812 selected_node: state
813 .selected_node
814 .and_then(|index| node_map.get(index).and_then(|v| *v).map(|v| v as usize)),
815 preview_mode: state.preview_mode,
816 }
817}
818
819fn remap_exchange_palette_for_instancing(
820 exchange: &mut TileNodeGraphExchange,
821 target_palette_source: TileGraphPaletteSource,
822 target_palette: &[TheColor],
823) {
824 if exchange.palette_source != TileGraphPaletteSource::Local
825 || exchange.palette_colors.is_empty()
826 || target_palette.is_empty()
827 {
828 exchange.palette_source = target_palette_source;
829 if !target_palette.is_empty() {
830 exchange.palette_colors = target_palette.to_vec();
831 }
832 return;
833 }
834
835 for node in &mut exchange.graph_state.nodes {
836 match &mut node.kind {
837 TileNodeKind::PaletteColor { index } => {
838 *index = nearest_palette_index(
839 palette_color_for_index(&exchange.palette_colors, *index),
840 target_palette,
841 ) as u16;
842 }
843 TileNodeKind::Colorize4 {
844 color_1,
845 color_2,
846 color_3,
847 color_4,
848 ..
849 } => {
850 *color_1 = nearest_palette_index(
851 palette_color_for_index(&exchange.palette_colors, *color_1),
852 target_palette,
853 ) as u16;
854 *color_2 = nearest_palette_index(
855 palette_color_for_index(&exchange.palette_colors, *color_2),
856 target_palette,
857 ) as u16;
858 *color_3 = nearest_palette_index(
859 palette_color_for_index(&exchange.palette_colors, *color_3),
860 target_palette,
861 ) as u16;
862 *color_4 = nearest_palette_index(
863 palette_color_for_index(&exchange.palette_colors, *color_4),
864 target_palette,
865 ) as u16;
866 }
867 _ => {}
868 }
869 }
870
871 exchange.palette_source = target_palette_source;
872 exchange.palette_colors = target_palette.to_vec();
873}
874
875fn palette_color_for_index(palette: &[TheColor], index: u16) -> TheColor {
876 palette
877 .get(index as usize)
878 .cloned()
879 .or_else(|| palette.last().cloned())
880 .unwrap_or_else(|| TheColor::from_u8_array([0, 0, 0, 255]))
881}
882
883fn nearest_palette_index(color: TheColor, palette: &[TheColor]) -> usize {
884 if palette.is_empty() {
885 return 0;
886 }
887 let src = color.to_u8_array();
888 let mut best = 0usize;
889 let mut best_dist = i64::MAX;
890 for (i, candidate) in palette.iter().enumerate() {
891 let c = candidate.to_u8_array();
892 let dr = src[0] as i64 - c[0] as i64;
893 let dg = src[1] as i64 - c[1] as i64;
894 let db = src[2] as i64 - c[2] as i64;
895 let dist = dr * dr + dg * dg + db * db;
896 if dist < best_dist {
897 best_dist = dist;
898 best = i;
899 }
900 }
901 best
902}
903
904fn input_connection_source(
905 state: &TileNodeGraphState,
906 node_index: usize,
907 input_terminal: u8,
908) -> Option<u16> {
909 state
910 .connections
911 .iter()
912 .find(|(_, _, dest_node, dest_terminal)| {
913 *dest_node as usize == node_index && *dest_terminal == input_terminal
914 })
915 .map(|(src_node, _, _, _)| *src_node)
916}
917
918fn remap_sub_index(index: u16, sub_map: &[Option<u16>], _base: u16) -> Option<u16> {
919 if index == 0 {
920 None
921 } else {
922 sub_map.get(index as usize).and_then(|v| *v)
923 }
924}
925
926pub struct TileGraphRenderer {
927 palette: Vec<TheColor>,
928 colorize4_ranges: RwLock<Vec<Option<(f32, f32)>>>,
929}
930
931impl TileGraphRenderer {
932 pub fn new(palette: Vec<TheColor>) -> Self {
933 Self {
934 palette,
935 colorize4_ranges: RwLock::new(Vec::new()),
936 }
937 }
938
939 pub fn render_graph(&self, graph: &TileNodeGraphExchange) -> RenderedTileGraph {
940 let mut state = graph.graph_state.clone();
941 state.ensure_root();
942
943 let tile_width = graph.tile_pixel_width.max(1) as usize;
944 let tile_height = graph.tile_pixel_height.max(1) as usize;
945 let grid_width = graph.output_grid_width.max(1) as usize;
946 let grid_height = graph.output_grid_height.max(1) as usize;
947
948 let sheet_width = tile_width * grid_width;
949 let sheet_height = tile_height * grid_height;
950 let colorize4_ranges = self.compute_colorize4_ranges(
951 &state,
952 graph,
953 grid_width,
954 grid_height,
955 tile_width,
956 tile_height,
957 );
958 if let Ok(mut ranges) = self.colorize4_ranges.write() {
959 *ranges = colorize4_ranges;
960 }
961 let mut sheet_color = vec![0_u8; sheet_width * sheet_height * 4];
962 let mut sheet_material = vec![0_u8; sheet_width * sheet_height * 4];
963 let mut sheet_height_data = vec![0_u8; sheet_width * sheet_height];
964
965 for sy in 0..sheet_height {
966 for sx in 0..sheet_width {
967 let gx = if sheet_width <= 1 {
968 0.5
969 } else {
970 sx as f32 / (sheet_width - 1) as f32
971 };
972 let gy = if sheet_height <= 1 {
973 0.5
974 } else {
975 sy as f32 / (sheet_height - 1) as f32
976 };
977 let scaled_x = gx * grid_width as f32;
978 let scaled_y = gy * grid_height as f32;
979 let cell_x = scaled_x.floor().min((grid_width - 1) as f32) as u16;
980 let cell_y = scaled_y.floor().min((grid_height - 1) as f32) as u16;
981 let local_u = (scaled_x - cell_x as f32).clamp(0.0, 1.0);
982 let local_v = (scaled_y - cell_y as f32).clamp(0.0, 1.0);
983 let eval = TileEvalContext {
984 cell_x,
985 cell_y,
986 group_width: graph.output_grid_width.max(1),
987 group_height: graph.output_grid_height.max(1),
988 tile_pixel_width: graph.tile_pixel_width.max(1),
989 tile_pixel_height: graph.tile_pixel_height.max(1),
990 u: local_u,
991 v: local_v,
992 };
993 let color = self
994 .evaluate_node_color(&state, 0, eval, &mut FxHashSet::default())
995 .unwrap_or_else(|| TheColor::from_u8_array_3([96, 96, 96]))
996 .to_u8_array();
997 let material = self.evaluate_output_material(&state, eval);
998 let height = self.evaluate_output_height(&state, eval);
999 let i = (sy * sheet_width + sx) * 4;
1000 sheet_color[i..i + 4].copy_from_slice(&color);
1001 sheet_material[i] = unit_to_u8(material.0);
1002 sheet_material[i + 1] = unit_to_u8(material.1);
1003 sheet_material[i + 2] = unit_to_u8(material.2);
1004 sheet_material[i + 3] = unit_to_u8(material.3);
1005 sheet_height_data[sy * sheet_width + sx] = unit_to_u8(height);
1006 }
1007 }
1008
1009 let tile_count = grid_width * grid_height;
1010 let rendered_tiles: Vec<(Vec<u8>, Vec<u8>, Vec<u8>)> = (0..tile_count)
1011 .into_par_iter()
1012 .map(|tile_index| {
1013 let cell_x = tile_index % grid_width;
1014 let cell_y = tile_index / grid_width;
1015 let mut tile_color = vec![0_u8; tile_width * tile_height * 4];
1016 let mut tile_material = vec![0_u8; tile_width * tile_height * 4];
1017 let mut tile_height_data = vec![0_u8; tile_width * tile_height];
1018
1019 for py in 0..tile_height {
1020 for px in 0..tile_width {
1021 let u = if tile_width <= 1 {
1022 0.5
1023 } else {
1024 px as f32 / (tile_width - 1) as f32
1025 };
1026 let v = if tile_height <= 1 {
1027 0.5
1028 } else {
1029 py as f32 / (tile_height - 1) as f32
1030 };
1031 let eval = TileEvalContext {
1032 cell_x: cell_x as u16,
1033 cell_y: cell_y as u16,
1034 group_width: graph.output_grid_width.max(1),
1035 group_height: graph.output_grid_height.max(1),
1036 tile_pixel_width: graph.tile_pixel_width.max(1),
1037 tile_pixel_height: graph.tile_pixel_height.max(1),
1038 u,
1042 v,
1043 };
1044
1045 let color = self
1046 .evaluate_node_color(&state, 0, eval, &mut FxHashSet::default())
1047 .unwrap_or_else(|| TheColor::from_u8_array_3([96, 96, 96]))
1048 .to_u8_array();
1049 let material = self.evaluate_output_material(&state, eval);
1050 let height = self.evaluate_output_height(&state, eval);
1051
1052 let i = (py * tile_width + px) * 4;
1053 tile_color[i..i + 4].copy_from_slice(&color);
1054 tile_material[i] = unit_to_u8(material.0);
1055 tile_material[i + 1] = unit_to_u8(material.1);
1056 tile_material[i + 2] = unit_to_u8(material.2);
1057 tile_material[i + 3] = unit_to_u8(material.3);
1058 tile_height_data[py * tile_width + px] = unit_to_u8(height);
1059 }
1060 }
1061
1062 (tile_color, tile_material, tile_height_data)
1063 })
1064 .collect();
1065
1066 let mut tiles_color = Vec::with_capacity(tile_count);
1067 let mut tiles_material = Vec::with_capacity(tile_count);
1068 let mut tiles_height = Vec::with_capacity(tile_count);
1069
1070 for (_tile_index, (tile_color, tile_material, tile_height_data)) in
1071 rendered_tiles.into_iter().enumerate()
1072 {
1073 tiles_color.push(tile_color);
1074 tiles_material.push(tile_material);
1075 tiles_height.push(tile_height_data);
1076 }
1077
1078 RenderedTileGraph {
1079 grid_width,
1080 grid_height,
1081 tile_width,
1082 tile_height,
1083 sheet_color,
1084 sheet_material,
1085 sheet_height: sheet_height_data,
1086 tiles_color,
1087 tiles_material,
1088 tiles_height,
1089 particle_output: self.output_particle(&state),
1090 light_output: self.output_light(&state),
1091 }
1092 }
1093
1094 pub fn output_particle(&self, state: &TileNodeGraphState) -> Option<TileParticleOutput> {
1095 let Some(root) = state.nodes.first() else {
1096 return None;
1097 };
1098 let particle_enabled = match &root.kind {
1099 TileNodeKind::OutputRoot {
1100 particle_enabled, ..
1101 } => *particle_enabled,
1102 _ => false,
1103 };
1104 if !particle_enabled {
1105 return None;
1106 }
1107 let emitter_index =
1108 state
1109 .connections
1110 .iter()
1111 .find_map(|(src_node, src_term, dst_node, dst_term)| {
1112 if *dst_node == 0 && *dst_term == 6 && *src_term == 0 {
1113 Some(*src_node as usize)
1114 } else {
1115 None
1116 }
1117 })?;
1118 match state.nodes.get(emitter_index).map(|node| &node.kind) {
1119 Some(TileNodeKind::ParticleEmitter {
1120 rate,
1121 spread,
1122 lifetime_min,
1123 lifetime_max,
1124 radius_min,
1125 radius_max,
1126 speed_min,
1127 speed_max,
1128 color_variation,
1129 }) => Some(TileParticleOutput {
1130 rate: (*rate).max(0.0),
1131 spread: (*spread).clamp(0.0, std::f32::consts::PI),
1132 lifetime_min: (*lifetime_min).max(0.01),
1133 lifetime_max: (*lifetime_max).max(*lifetime_min),
1134 radius_min: (*radius_min).max(0.001),
1135 radius_max: (*radius_max).max(*radius_min),
1136 speed_min: (*speed_min).max(0.0),
1137 speed_max: (*speed_max).max(*speed_min),
1138 flame_base: false,
1139 color_variation: *color_variation,
1140 ramp_colors: [
1141 [255, 240, 200, 255],
1142 [255, 176, 72, 255],
1143 [224, 84, 24, 255],
1144 [40, 36, 36, 255],
1145 ],
1146 }),
1147 Some(TileNodeKind::ParticleRender {
1148 radius_min,
1149 radius_max,
1150 flame_base,
1151 color_variation,
1152 color_1,
1153 color_2,
1154 color_3,
1155 color_4,
1156 }) => {
1157 let spawn = state.connections.iter().find_map(
1158 |(src_node, src_term, dst_node, dst_term)| {
1159 if *dst_node as usize == emitter_index && *dst_term == 0 && *src_term == 0 {
1160 Some(*src_node as usize)
1161 } else {
1162 None
1163 }
1164 },
1165 );
1166 let motion = state.connections.iter().find_map(
1167 |(src_node, src_term, dst_node, dst_term)| {
1168 if *dst_node as usize == emitter_index && *dst_term == 1 && *src_term == 0 {
1169 Some(*src_node as usize)
1170 } else {
1171 None
1172 }
1173 },
1174 );
1175 let (rate, spread) = match spawn
1176 .and_then(|index| state.nodes.get(index))
1177 .map(|node| &node.kind)
1178 {
1179 Some(TileNodeKind::ParticleSpawn { rate, spread }) => {
1180 ((*rate).max(0.0), (*spread).clamp(0.0, std::f32::consts::PI))
1181 }
1182 _ => (24.0, 0.75),
1183 };
1184 let (lifetime_min, lifetime_max, speed_min, speed_max) = match motion
1185 .and_then(|index| state.nodes.get(index))
1186 .map(|node| &node.kind)
1187 {
1188 Some(TileNodeKind::ParticleMotion {
1189 lifetime_min,
1190 lifetime_max,
1191 speed_min,
1192 speed_max,
1193 }) => (
1194 (*lifetime_min).max(0.01),
1195 (*lifetime_max).max(*lifetime_min),
1196 (*speed_min).max(0.0),
1197 (*speed_max).max(*speed_min),
1198 ),
1199 _ => (0.35, 0.9, 0.35, 1.1),
1200 };
1201 Some(TileParticleOutput {
1202 rate,
1203 spread,
1204 lifetime_min,
1205 lifetime_max,
1206 radius_min: (*radius_min).max(0.001),
1207 radius_max: (*radius_max).max(*radius_min),
1208 speed_min,
1209 speed_max,
1210 flame_base: *flame_base,
1211 color_variation: *color_variation,
1212 ramp_colors: self.particle_ramp_colors(
1213 state,
1214 emitter_index,
1215 [*color_1, *color_2, *color_3, *color_4],
1216 ),
1217 })
1218 }
1219 _ => None,
1220 }
1221 }
1222
1223 pub fn output_light(&self, state: &TileNodeGraphState) -> Option<TileLightOutput> {
1224 let Some(root) = state.nodes.first() else {
1225 return None;
1226 };
1227 let light_enabled = match &root.kind {
1228 TileNodeKind::OutputRoot { light_enabled, .. } => *light_enabled,
1229 _ => false,
1230 };
1231 if !light_enabled {
1232 return None;
1233 }
1234 let light_index =
1235 state
1236 .connections
1237 .iter()
1238 .find_map(|(src_node, src_term, dst_node, dst_term)| {
1239 if *dst_node == 0 && *dst_term == 7 && *src_term == 0 {
1240 Some(*src_node as usize)
1241 } else {
1242 None
1243 }
1244 })?;
1245 match state.nodes.get(light_index).map(|node| &node.kind) {
1246 Some(TileNodeKind::LightEmitter {
1247 intensity,
1248 range,
1249 flicker,
1250 lift,
1251 }) => Some(TileLightOutput {
1252 intensity: (*intensity).max(0.0),
1253 range: (*range).max(0.0),
1254 flicker: (*flicker).clamp(0.0, 1.0),
1255 lift: (*lift).max(0.0),
1256 }),
1257 _ => None,
1258 }
1259 }
1260
1261 fn evaluate_output_height(&self, state: &TileNodeGraphState, eval: TileEvalContext) -> f32 {
1262 self.evaluate_output_height_internal(
1263 state,
1264 eval,
1265 &mut FxHashSet::default(),
1266 &mut FxHashSet::default(),
1267 )
1268 .unwrap_or(0.5)
1269 .clamp(0.0, 1.0)
1270 }
1271
1272 fn evaluate_output_height_internal(
1273 &self,
1274 state: &TileNodeGraphState,
1275 eval: TileEvalContext,
1276 visiting: &mut FxHashSet<usize>,
1277 visiting_subgraphs: &mut FxHashSet<Uuid>,
1278 ) -> Option<f32> {
1279 self.evaluate_connected_scalar(state, 0, 1, eval, visiting, visiting_subgraphs)
1280 .or_else(|| {
1281 self.evaluate_connected_color(state, 0, 0, eval, visiting, visiting_subgraphs)
1282 .map(Self::color_to_mask)
1283 })
1284 }
1285
1286 fn palette_color(&self, index: u16) -> Option<TheColor> {
1287 self.palette.get(index as usize).cloned().or_else(|| {
1288 let v = (index.min(255)) as u8;
1289 Some(TheColor::from_u8_array([v, v, v, 255]))
1290 })
1291 }
1292
1293 fn nearest_palette_color(&self, color: TheColor) -> TheColor {
1294 if self.palette.is_empty() {
1295 return color;
1296 }
1297 let rgba = color.to_u8_array();
1298 let mut best = self.palette[0].clone();
1299 let mut best_dist = f32::MAX;
1300 for candidate in &self.palette {
1301 let c = candidate.to_u8_array();
1302 let dr = rgba[0] as f32 - c[0] as f32;
1303 let dg = rgba[1] as f32 - c[1] as f32;
1304 let db = rgba[2] as f32 - c[2] as f32;
1305 let dist = dr * dr + dg * dg + db * db;
1306 if dist < best_dist {
1307 best_dist = dist;
1308 best = candidate.clone();
1309 }
1310 }
1311 best
1312 }
1313
1314 fn colorize4_palette_color(
1315 &self,
1316 slot: usize,
1317 color_1: u16,
1318 color_2: u16,
1319 color_3: u16,
1320 color_4: u16,
1321 ) -> TheColor {
1322 let index = match slot {
1323 0 => color_1,
1324 1 => color_2,
1325 2 => color_3,
1326 _ => color_4,
1327 };
1328 self.palette_color(index)
1329 .unwrap_or_else(|| TheColor::from_u8_array([255, 255, 255, 255]))
1330 }
1331
1332 fn particle_ramp_colors(
1333 &self,
1334 state: &TileNodeGraphState,
1335 node_index: usize,
1336 fallback: [u16; 4],
1337 ) -> [[u8; 4]; 4] {
1338 let mut colors = fallback.map(|index| {
1339 self.palette_color(index)
1340 .unwrap_or_else(|| TheColor::from_u8_array([255, 255, 255, 255]))
1341 .to_u8_array()
1342 });
1343
1344 for terminal in 2..=5u8 {
1345 let Some(source_index) =
1346 state
1347 .connections
1348 .iter()
1349 .find_map(|(src_node, src_term, dst_node, dst_term)| {
1350 if *dst_node as usize == node_index
1351 && *dst_term == terminal
1352 && *src_term == 0
1353 {
1354 Some(*src_node as usize)
1355 } else {
1356 None
1357 }
1358 })
1359 else {
1360 continue;
1361 };
1362
1363 let Some(source_kind) = state.nodes.get(source_index).map(|node| &node.kind) else {
1364 continue;
1365 };
1366
1367 match source_kind {
1368 TileNodeKind::PaletteColor { index } => {
1369 if let Some(color) = self.palette_color(*index) {
1370 colors[(terminal - 2) as usize] = color.to_u8_array();
1371 }
1372 }
1373 TileNodeKind::Color { color } => {
1374 colors[(terminal - 2) as usize] = color.to_u8_array();
1375 }
1376 TileNodeKind::Colorize4 {
1377 color_1,
1378 color_2,
1379 color_3,
1380 color_4,
1381 ..
1382 } if terminal == 2 => {
1383 colors = [
1384 self.colorize4_palette_color(0, *color_1, *color_2, *color_3, *color_4)
1385 .to_u8_array(),
1386 self.colorize4_palette_color(1, *color_1, *color_2, *color_3, *color_4)
1387 .to_u8_array(),
1388 self.colorize4_palette_color(2, *color_1, *color_2, *color_3, *color_4)
1389 .to_u8_array(),
1390 self.colorize4_palette_color(3, *color_1, *color_2, *color_3, *color_4)
1391 .to_u8_array(),
1392 ];
1393 }
1394 _ => {}
1395 }
1396 }
1397
1398 colors
1399 }
1400
1401 fn bayer4(x: usize, y: usize) -> f32 {
1402 const BAYER: [[u8; 4]; 4] = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];
1403 BAYER[y & 3][x & 3] as f32 / 16.0
1404 }
1405
1406 fn colorize4_range(&self, node_index: usize) -> Option<(f32, f32)> {
1407 self.colorize4_ranges
1408 .read()
1409 .ok()
1410 .and_then(|ranges| ranges.get(node_index).copied().flatten())
1411 }
1412
1413 fn compute_colorize4_ranges(
1414 &self,
1415 state: &TileNodeGraphState,
1416 graph: &TileNodeGraphExchange,
1417 grid_width: usize,
1418 grid_height: usize,
1419 tile_width: usize,
1420 tile_height: usize,
1421 ) -> Vec<Option<(f32, f32)>> {
1422 let mut ranges = vec![None; state.nodes.len()];
1423 for (node_index, node) in state.nodes.iter().enumerate() {
1424 let TileNodeKind::Colorize4 { auto_range, .. } = &node.kind else {
1425 continue;
1426 };
1427 if !*auto_range || self.input_connection(state, node_index, 0).is_none() {
1428 continue;
1429 }
1430
1431 let mut min_v = f32::INFINITY;
1432 let mut max_v = f32::NEG_INFINITY;
1433
1434 for cell_y in 0..grid_height {
1435 for cell_x in 0..grid_width {
1436 for py in 0..tile_height {
1437 for px in 0..tile_width {
1438 let u = if tile_width <= 1 {
1439 0.5
1440 } else {
1441 px as f32 / (tile_width - 1) as f32
1442 };
1443 let v = if tile_height <= 1 {
1444 0.5
1445 } else {
1446 py as f32 / (tile_height - 1) as f32
1447 };
1448 let eval = TileEvalContext {
1449 cell_x: cell_x as u16,
1450 cell_y: cell_y as u16,
1451 group_width: graph.output_grid_width.max(1),
1452 group_height: graph.output_grid_height.max(1),
1453 tile_pixel_width: graph.tile_pixel_width.max(1),
1454 tile_pixel_height: graph.tile_pixel_height.max(1),
1455 u,
1456 v,
1457 };
1458 if let Some(color) = self.evaluate_connected_color(
1459 state,
1460 node_index,
1461 0,
1462 eval,
1463 &mut FxHashSet::default(),
1464 &mut FxHashSet::default(),
1465 ) {
1466 let value = Self::color_to_mask(color).clamp(0.0, 1.0);
1467 min_v = min_v.min(value);
1468 max_v = max_v.max(value);
1469 }
1470 }
1471 }
1472 }
1473 }
1474
1475 if min_v.is_finite() && max_v.is_finite() {
1476 if (max_v - min_v).abs() < 1e-5 {
1477 let center = min_v.clamp(0.0, 1.0);
1478 let lo = (center - 0.5).clamp(0.0, 1.0);
1479 let hi = (center + 0.5).clamp(0.0, 1.0);
1480 ranges[node_index] = Some((lo, hi.max(lo + 1e-5)));
1481 } else {
1482 ranges[node_index] = Some((min_v, max_v));
1483 }
1484 }
1485 }
1486 ranges
1487 }
1488
1489 fn evaluate_output_material(
1490 &self,
1491 state: &TileNodeGraphState,
1492 eval: TileEvalContext,
1493 ) -> (f32, f32, f32, f32) {
1494 self.evaluate_output_material_internal(
1495 state,
1496 eval,
1497 &mut FxHashSet::default(),
1498 &mut FxHashSet::default(),
1499 )
1500 .unwrap_or((0.5, 0.0, 1.0, 0.0))
1501 }
1502
1503 fn evaluate_output_material_internal(
1504 &self,
1505 state: &TileNodeGraphState,
1506 eval: TileEvalContext,
1507 visiting: &mut FxHashSet<usize>,
1508 visiting_subgraphs: &mut FxHashSet<Uuid>,
1509 ) -> Option<(f32, f32, f32, f32)> {
1510 self.evaluate_connected_material(state, 0, 1, eval, visiting, visiting_subgraphs)
1511 }
1512
1513 fn solo_node_index(&self, state: &TileNodeGraphState) -> Option<usize> {
1514 state.nodes.iter().position(|n| n.solo)
1515 }
1516
1517 fn evaluate_node_scalar_internal(
1518 &self,
1519 state: &TileNodeGraphState,
1520 node_index: usize,
1521 eval: TileEvalContext,
1522 visiting: &mut FxHashSet<usize>,
1523 visiting_subgraphs: &mut FxHashSet<Uuid>,
1524 ) -> Option<f32> {
1525 self.evaluate_node_scalar_output_internal(
1526 state,
1527 node_index,
1528 0,
1529 eval,
1530 visiting,
1531 visiting_subgraphs,
1532 )
1533 }
1534
1535 fn evaluate_node_scalar_output_internal(
1536 &self,
1537 state: &TileNodeGraphState,
1538 node_index: usize,
1539 output_terminal: u8,
1540 eval: TileEvalContext,
1541 visiting: &mut FxHashSet<usize>,
1542 visiting_subgraphs: &mut FxHashSet<Uuid>,
1543 ) -> Option<f32> {
1544 state
1545 .nodes
1546 .get(node_index)
1547 .and_then(|node| match &node.kind {
1548 TileNodeKind::Scalar { value } => Some(*value),
1549 TileNodeKind::LayerInput { value, .. } => self
1550 .evaluate_connected_scalar(
1551 state,
1552 node_index,
1553 0,
1554 eval,
1555 visiting,
1556 visiting_subgraphs,
1557 )
1558 .or(Some(*value)),
1559 _ => self
1560 .evaluate_node_color_output_internal(
1561 state,
1562 node_index,
1563 output_terminal,
1564 eval,
1565 visiting,
1566 visiting_subgraphs,
1567 )
1568 .map(Self::color_to_mask),
1569 })
1570 }
1571
1572 fn evaluate_node_material_internal(
1573 &self,
1574 state: &TileNodeGraphState,
1575 node_index: usize,
1576 eval: TileEvalContext,
1577 visiting: &mut FxHashSet<usize>,
1578 visiting_subgraphs: &mut FxHashSet<Uuid>,
1579 ) -> Option<(f32, f32, f32, f32)> {
1580 if !visiting.insert(node_index) {
1581 return None;
1582 }
1583 let result = state.nodes.get(node_index).and_then(|node| {
1584 if node.bypass && !matches!(node.kind, TileNodeKind::OutputRoot { .. }) {
1585 if let Some(value) = self.evaluate_connected_material(
1586 state,
1587 node_index,
1588 0,
1589 eval,
1590 visiting,
1591 visiting_subgraphs,
1592 ) {
1593 return Some(value);
1594 }
1595 }
1596 match &node.kind {
1597 TileNodeKind::OutputRoot {
1598 roughness,
1599 metallic,
1600 opacity,
1601 emissive,
1602 ..
1603 } => {
1604 if let Some(solo) = self.solo_node_index(state)
1605 && solo != node_index
1606 {
1607 self.evaluate_node_material_internal(
1608 state,
1609 solo,
1610 eval,
1611 visiting,
1612 visiting_subgraphs,
1613 )
1614 } else {
1615 let mut channel = |input_terminal: u8, default: f32| -> f32 {
1616 self.evaluate_connected_scalar(
1617 state,
1618 node_index,
1619 input_terminal,
1620 eval,
1621 visiting,
1622 visiting_subgraphs,
1623 )
1624 .unwrap_or(default)
1625 .clamp(0.0, 1.0)
1626 };
1627 Some((
1628 channel(2, *roughness),
1629 channel(3, *metallic),
1630 channel(4, *opacity),
1631 channel(5, *emissive),
1632 ))
1633 }
1634 }
1635 TileNodeKind::ImportLayer { .. } => None,
1636 TileNodeKind::Material {
1637 roughness,
1638 metallic,
1639 opacity,
1640 emissive,
1641 } => Some((*roughness, *metallic, *opacity, *emissive)),
1642 TileNodeKind::MakeMaterial => {
1643 let mut channel = |input_terminal: u8, default: f32| -> f32 {
1644 self.evaluate_connected_scalar(
1645 state,
1646 node_index,
1647 input_terminal,
1648 eval,
1649 visiting,
1650 visiting_subgraphs,
1651 )
1652 .unwrap_or(default)
1653 .clamp(0.0, 1.0)
1654 };
1655 Some((
1656 channel(0, 0.5),
1657 channel(1, 0.0),
1658 channel(2, 1.0),
1659 channel(3, 0.0),
1660 ))
1661 }
1662 TileNodeKind::MaterialMix { factor } => {
1663 let a = self.evaluate_connected_material(
1664 state,
1665 node_index,
1666 0,
1667 eval,
1668 visiting,
1669 visiting_subgraphs,
1670 );
1671 let b = self.evaluate_connected_material(
1672 state,
1673 node_index,
1674 1,
1675 eval,
1676 visiting,
1677 visiting_subgraphs,
1678 );
1679 let mask = self
1680 .evaluate_connected_scalar(
1681 state,
1682 node_index,
1683 2,
1684 eval,
1685 visiting,
1686 visiting_subgraphs,
1687 )
1688 .unwrap_or(0.0)
1689 .clamp(0.0, 1.0)
1690 * factor.clamp(0.0, 1.0);
1691 match (a, b) {
1692 (Some(a), Some(b)) => Some((
1693 a.0 * (1.0 - mask) + b.0 * mask,
1694 a.1 * (1.0 - mask) + b.1 * mask,
1695 a.2 * (1.0 - mask) + b.2 * mask,
1696 a.3 * (1.0 - mask) + b.3 * mask,
1697 )),
1698 (Some(a), None) => Some(a),
1699 (None, Some(b)) => Some(b),
1700 (None, None) => None,
1701 }
1702 }
1703 _ => {
1704 if node.mute {
1705 return Some((0.5, 0.0, 0.0, 0.0));
1706 }
1707 let roughness = self
1708 .evaluate_node_scalar_internal(
1709 state,
1710 node_index,
1711 eval,
1712 visiting,
1713 visiting_subgraphs,
1714 )
1715 .unwrap_or(0.5)
1716 .clamp(0.0, 1.0);
1717 Some((roughness, 0.0, 1.0, 0.0))
1718 }
1719 }
1720 });
1721 visiting.remove(&node_index);
1722 result
1723 }
1724
1725 fn input_connection(
1726 &self,
1727 state: &TileNodeGraphState,
1728 node_index: usize,
1729 input_terminal: u8,
1730 ) -> Option<(usize, u8)> {
1731 state
1732 .connections
1733 .iter()
1734 .find(|(_, _, dest_node, dest_terminal)| {
1735 *dest_node as usize == node_index && *dest_terminal == input_terminal
1736 })
1737 .map(|(src_node, src_terminal, _, _)| (*src_node as usize, *src_terminal))
1738 }
1739
1740 fn evaluate_connected_color(
1741 &self,
1742 state: &TileNodeGraphState,
1743 node_index: usize,
1744 input_terminal: u8,
1745 eval: TileEvalContext,
1746 visiting: &mut FxHashSet<usize>,
1747 visiting_subgraphs: &mut FxHashSet<Uuid>,
1748 ) -> Option<TheColor> {
1749 self.input_connection(state, node_index, input_terminal)
1750 .and_then(|(src, output_terminal)| {
1751 self.evaluate_node_color_output_internal(
1752 state,
1753 src,
1754 output_terminal,
1755 eval,
1756 visiting,
1757 visiting_subgraphs,
1758 )
1759 })
1760 }
1761
1762 fn input_connection_source(
1763 &self,
1764 state: &TileNodeGraphState,
1765 node_index: usize,
1766 input_terminal: u8,
1767 ) -> Option<usize> {
1768 self.input_connection(state, node_index, input_terminal)
1769 .map(|(src, _)| src)
1770 }
1771
1772 fn evaluate_connected_scalar(
1773 &self,
1774 state: &TileNodeGraphState,
1775 node_index: usize,
1776 input_terminal: u8,
1777 eval: TileEvalContext,
1778 visiting: &mut FxHashSet<usize>,
1779 visiting_subgraphs: &mut FxHashSet<Uuid>,
1780 ) -> Option<f32> {
1781 self.input_connection(state, node_index, input_terminal)
1782 .and_then(|(src, output_terminal)| {
1783 self.evaluate_node_scalar_output_internal(
1784 state,
1785 src,
1786 output_terminal,
1787 eval,
1788 visiting,
1789 visiting_subgraphs,
1790 )
1791 })
1792 }
1793
1794 fn connected_warp_vector(
1795 &self,
1796 state: &TileNodeGraphState,
1797 node_index: usize,
1798 input_terminal: u8,
1799 eval: TileEvalContext,
1800 amount: f32,
1801 visiting: &mut FxHashSet<usize>,
1802 visiting_subgraphs: &mut FxHashSet<Uuid>,
1803 ) -> Vec2<f32> {
1804 if amount <= f32::EPSILON
1805 || self
1806 .input_connection(state, node_index, input_terminal)
1807 .is_none()
1808 {
1809 return Vec2::new(0.0, 0.0);
1810 }
1811
1812 let sx = self
1813 .evaluate_connected_scalar(
1814 state,
1815 node_index,
1816 input_terminal,
1817 eval,
1818 visiting,
1819 visiting_subgraphs,
1820 )
1821 .unwrap_or(0.5);
1822 let wrapped_u = (eval.group_u() + 0.173).rem_euclid(1.0);
1823 let wrapped_v = (eval.group_v() + 0.317).rem_euclid(1.0);
1824 let sy = self
1825 .evaluate_connected_scalar(
1826 state,
1827 node_index,
1828 input_terminal,
1829 eval.with_group_uv(wrapped_u, wrapped_v),
1830 visiting,
1831 visiting_subgraphs,
1832 )
1833 .unwrap_or(sx);
1834
1835 Vec2::new((sx - 0.5) * 2.0 * amount, (sy - 0.5) * 2.0 * amount)
1836 }
1837
1838 fn evaluate_connected_material(
1839 &self,
1840 state: &TileNodeGraphState,
1841 node_index: usize,
1842 input_terminal: u8,
1843 eval: TileEvalContext,
1844 visiting: &mut FxHashSet<usize>,
1845 visiting_subgraphs: &mut FxHashSet<Uuid>,
1846 ) -> Option<(f32, f32, f32, f32)> {
1847 self.input_connection(state, node_index, input_terminal)
1848 .and_then(|(src, _output_terminal)| {
1849 self.evaluate_node_material_internal(state, src, eval, visiting, visiting_subgraphs)
1850 })
1851 }
1852
1853 fn evaluate_node_color(
1854 &self,
1855 state: &TileNodeGraphState,
1856 node_index: usize,
1857 eval: TileEvalContext,
1858 visiting: &mut FxHashSet<usize>,
1859 ) -> Option<TheColor> {
1860 self.evaluate_node_color_output_internal(
1861 state,
1862 node_index,
1863 0,
1864 eval,
1865 visiting,
1866 &mut FxHashSet::default(),
1867 )
1868 }
1869
1870 fn evaluate_node_color_internal(
1871 &self,
1872 state: &TileNodeGraphState,
1873 node_index: usize,
1874 eval: TileEvalContext,
1875 visiting: &mut FxHashSet<usize>,
1876 visiting_subgraphs: &mut FxHashSet<Uuid>,
1877 ) -> Option<TheColor> {
1878 self.evaluate_node_color_output_internal(
1879 state,
1880 node_index,
1881 0,
1882 eval,
1883 visiting,
1884 visiting_subgraphs,
1885 )
1886 }
1887
1888 fn evaluate_node_color_output_internal(
1889 &self,
1890 state: &TileNodeGraphState,
1891 node_index: usize,
1892 output_terminal: u8,
1893 eval: TileEvalContext,
1894 visiting: &mut FxHashSet<usize>,
1895 visiting_subgraphs: &mut FxHashSet<Uuid>,
1896 ) -> Option<TheColor> {
1897 if !visiting.insert(node_index) {
1898 return None;
1899 }
1900 let result = state.nodes.get(node_index).and_then(|node| {
1901 if node.mute {
1902 return Some(TheColor::from_u8_array([0, 0, 0, 0]));
1903 }
1904 if node.bypass
1905 && !matches!(
1906 node.kind,
1907 TileNodeKind::OutputRoot { .. } | TileNodeKind::GroupUV
1908 )
1909 {
1910 if let Some(color) = self.evaluate_connected_color(
1911 state,
1912 node_index,
1913 0,
1914 eval,
1915 visiting,
1916 visiting_subgraphs,
1917 ) {
1918 return Some(color);
1919 }
1920 }
1921 match &node.kind {
1922 TileNodeKind::OutputRoot { .. } => {
1923 if let Some(solo) = self.solo_node_index(state)
1924 && solo != node_index
1925 {
1926 self.evaluate_node_color_output_internal(
1927 state,
1928 solo,
1929 output_terminal,
1930 eval,
1931 visiting,
1932 visiting_subgraphs,
1933 )
1934 } else {
1935 self.evaluate_connected_color(
1936 state,
1937 node_index,
1938 0,
1939 eval,
1940 visiting,
1941 visiting_subgraphs,
1942 )
1943 }
1944 }
1945 TileNodeKind::LayerInput { value, .. } => self
1946 .evaluate_connected_color(
1947 state,
1948 node_index,
1949 0,
1950 eval,
1951 visiting,
1952 visiting_subgraphs,
1953 )
1954 .or_else(|| {
1955 let v = unit_to_u8(*value);
1956 Some(TheColor::from_u8_array([v, v, v, 255]))
1957 }),
1958 TileNodeKind::ImportLayer { .. } => None,
1959 TileNodeKind::ParticleEmitter { .. }
1960 | TileNodeKind::ParticleSpawn { .. }
1961 | TileNodeKind::ParticleMotion { .. }
1962 | TileNodeKind::ParticleRender { .. }
1963 | TileNodeKind::LightEmitter { .. } => None,
1964 TileNodeKind::GroupUV => Some(TheColor::from_u8_array([
1965 unit_to_u8(eval.group_u()),
1966 unit_to_u8(eval.group_v()),
1967 0,
1968 255,
1969 ])),
1970 TileNodeKind::Scalar { value } => {
1971 let v = unit_to_u8(*value);
1972 Some(TheColor::from_u8_array([v, v, v, 255]))
1973 }
1974 TileNodeKind::Colorize4 {
1975 color_1,
1976 color_2,
1977 color_3,
1978 color_4,
1979 pixel_size,
1980 dither,
1981 auto_range,
1982 } => {
1983 let quantized_eval = if *pixel_size > 1 {
1984 let px_size = (*pixel_size).max(1) as f32;
1985 let tile_w = eval.tile_pixel_width.max(1) as f32;
1986 let tile_h = eval.tile_pixel_height.max(1) as f32;
1987 let x = (eval.u * (tile_w - 1.0)).round();
1988 let y = (eval.v * (tile_h - 1.0)).round();
1989 let qx =
1990 ((x / px_size).floor() * px_size).clamp(0.0, (tile_w - 1.0).max(0.0));
1991 let qy =
1992 ((y / px_size).floor() * px_size).clamp(0.0, (tile_h - 1.0).max(0.0));
1993 let u = if tile_w <= 1.0 {
1994 0.5
1995 } else {
1996 qx / (tile_w - 1.0)
1997 };
1998 let v = if tile_h <= 1.0 {
1999 0.5
2000 } else {
2001 qy / (tile_h - 1.0)
2002 };
2003 TileEvalContext { u, v, ..eval }
2004 } else {
2005 eval
2006 };
2007 self.evaluate_connected_color(
2008 state,
2009 node_index,
2010 0,
2011 quantized_eval,
2012 visiting,
2013 visiting_subgraphs,
2014 )
2015 .map(|color| {
2016 let mut t = Self::color_to_mask(color).clamp(0.0, 1.0);
2017 if *auto_range
2018 && let Some((min_v, max_v)) = self.colorize4_range(node_index)
2019 {
2020 let width = (max_v - min_v).max(1e-5);
2021 t = ((t - min_v) / width).clamp(0.0, 1.0);
2022 }
2023 t = t.clamp(0.0, 0.999_999);
2024 if *dither {
2025 let x = (quantized_eval.u
2026 * (quantized_eval.tile_pixel_width.max(1) - 1) as f32)
2027 .round()
2028 .max(0.0) as usize;
2029 let y = (quantized_eval.v
2030 * (quantized_eval.tile_pixel_height.max(1) - 1) as f32)
2031 .round()
2032 .max(0.0) as usize;
2033 let threshold = Self::bayer4(x, y) - 0.5;
2034 t = (t + threshold * 0.18).clamp(0.0, 0.999_999);
2035 }
2036 let slot = (t * 4.0).floor() as usize;
2037 self.colorize4_palette_color(slot, *color_1, *color_2, *color_3, *color_4)
2038 })
2039 }
2040 TileNodeKind::Gradient { mode } => {
2041 let gu = eval.group_u().clamp(0.0, 1.0);
2042 let gv = eval.group_v().clamp(0.0, 1.0);
2043 let value = match mode {
2044 0 => gu,
2045 1 => gv,
2046 _ => {
2047 let dx = gu - 0.5;
2048 let dy = gv - 0.5;
2049 (1.0 - (dx * dx + dy * dy).sqrt() * 2.0).clamp(0.0, 1.0)
2050 }
2051 };
2052 let v = unit_to_u8(value);
2053 Some(TheColor::from_u8_array([v, v, v, 255]))
2054 }
2055 TileNodeKind::Curve { power } => self
2056 .evaluate_connected_color(
2057 state,
2058 node_index,
2059 0,
2060 eval,
2061 visiting,
2062 visiting_subgraphs,
2063 )
2064 .map(|color| {
2065 let value = Self::color_to_mask(color)
2066 .clamp(0.0, 1.0)
2067 .powf(power.clamp(0.1, 4.0));
2068 let v = unit_to_u8(value);
2069 TheColor::from_u8_array([v, v, v, 255])
2070 }),
2071 TileNodeKind::Color { color } => Some(color.clone()),
2072 TileNodeKind::PaletteColor { index } => self.palette_color(*index),
2073 TileNodeKind::NearestPalette => self
2074 .evaluate_connected_color(
2075 state,
2076 node_index,
2077 0,
2078 eval,
2079 visiting,
2080 visiting_subgraphs,
2081 )
2082 .map(|color| self.nearest_palette_color(color)),
2083 TileNodeKind::Mix { factor } => {
2084 let a = self.evaluate_connected_color(
2085 state,
2086 node_index,
2087 0,
2088 eval,
2089 visiting,
2090 visiting_subgraphs,
2091 );
2092 let b = self.evaluate_connected_color(
2093 state,
2094 node_index,
2095 1,
2096 eval,
2097 visiting,
2098 visiting_subgraphs,
2099 );
2100 match (a, b) {
2101 (Some(a), Some(b)) => Some(Self::mix_colors(a, b, *factor)),
2102 (Some(a), None) => Some(a),
2103 (None, Some(b)) => Some(b),
2104 (None, None) => None,
2105 }
2106 }
2107 TileNodeKind::Checker { scale } => {
2108 let s = (*scale).max(1) as f32;
2109 let cx = (eval.group_u() * s).floor() as i32;
2110 let cy = (eval.group_v() * s).floor() as i32;
2111 let a = self
2112 .evaluate_connected_color(
2113 state,
2114 node_index,
2115 0,
2116 eval,
2117 visiting,
2118 visiting_subgraphs,
2119 )
2120 .unwrap_or_else(|| TheColor::from_u8_array_3([255, 255, 255]));
2121 let b = self
2122 .evaluate_connected_color(
2123 state,
2124 node_index,
2125 1,
2126 eval,
2127 visiting,
2128 visiting_subgraphs,
2129 )
2130 .unwrap_or_else(|| TheColor::from_u8_array_3([0, 0, 0]));
2131 if (cx + cy) & 1 == 0 { Some(a) } else { Some(b) }
2132 }
2133 TileNodeKind::Noise { scale, seed, wrap } => {
2134 let s = (*scale).clamp(0.0, 1.0).max(0.0001);
2135 let frequency = (s * 64.0).round().max(1.0) as i32;
2136 let v = unit_to_u8(Self::value_noise(
2137 eval.group_u(),
2138 eval.group_v(),
2139 frequency,
2140 *seed,
2141 *wrap,
2142 ));
2143 Some(TheColor::from_u8_array([v, v, v, 255]))
2144 }
2145 TileNodeKind::Voronoi {
2146 scale,
2147 seed,
2148 jitter,
2149 warp_amount,
2150 falloff,
2151 } => {
2152 let warp = self.connected_warp_vector(
2153 state,
2154 node_index,
2155 0,
2156 eval,
2157 warp_amount.clamp(0.0, 0.25),
2158 visiting,
2159 visiting_subgraphs,
2160 );
2161 let value = match output_terminal {
2162 1 => Self::voronoi_height(eval, warp, *scale, *seed, *jitter, *falloff),
2163 2 => Self::voronoi_cell_id(eval, warp, *scale, *seed, *jitter),
2164 _ => Self::voronoi_center(eval, warp, *scale, *seed, *jitter),
2165 };
2166 let v = unit_to_u8(value);
2167 Some(TheColor::from_u8_array([v, v, v, 255]))
2168 }
2169 TileNodeKind::BoxDivide {
2170 scale,
2171 gap,
2172 rotation,
2173 rounding,
2174 warp_amount,
2175 falloff,
2176 } => {
2177 let warp = self.connected_warp_vector(
2178 state,
2179 node_index,
2180 0,
2181 eval,
2182 warp_amount.clamp(0.0, 0.25),
2183 visiting,
2184 visiting_subgraphs,
2185 );
2186 let value = match output_terminal {
2187 1 => Self::box_divide_height(
2188 eval, warp, *scale, *gap, *rotation, *rounding, *falloff,
2189 ),
2190 2 => {
2191 Self::box_divide_cell_id(eval, warp, *scale, *gap, *rotation, *rounding)
2192 }
2193 _ => {
2194 Self::box_divide_center(eval, warp, *scale, *gap, *rotation, *rounding)
2195 }
2196 };
2197 let v = unit_to_u8(value);
2198 Some(TheColor::from_u8_array([v, v, v, 255]))
2199 }
2200 TileNodeKind::Offset { x, y } => self
2201 .input_connection(state, node_index, 0)
2202 .and_then(|(src, out)| {
2203 self.evaluate_node_color_output_internal(
2204 state,
2205 src,
2206 out,
2207 eval.with_group_uv(eval.group_u() + *x, eval.group_v() + *y),
2208 visiting,
2209 visiting_subgraphs,
2210 )
2211 }),
2212 TileNodeKind::Scale { x, y } => self
2213 .input_connection(state, node_index, 0)
2214 .and_then(|(src, out)| {
2215 let gu = (eval.group_u() - 0.5) * x.max(0.1) + 0.5;
2216 let gv = (eval.group_v() - 0.5) * y.max(0.1) + 0.5;
2217 self.evaluate_node_color_output_internal(
2218 state,
2219 src,
2220 out,
2221 eval.with_group_uv(gu, gv),
2222 visiting,
2223 visiting_subgraphs,
2224 )
2225 }),
2226 TileNodeKind::Repeat { repeat_x, repeat_y } => self
2227 .input_connection(state, node_index, 0)
2228 .and_then(|(src, out)| {
2229 let wrapped_u = (eval.group_u() * repeat_x.max(0.1)).fract();
2230 let wrapped_v = (eval.group_v() * repeat_y.max(0.1)).fract();
2231 self.evaluate_node_color_output_internal(
2232 state,
2233 src,
2234 out,
2235 eval.with_group_uv(wrapped_u, wrapped_v),
2236 visiting,
2237 visiting_subgraphs,
2238 )
2239 }),
2240 TileNodeKind::Rotate { angle } => self
2241 .input_connection(state, node_index, 0)
2242 .and_then(|(src, out)| {
2243 let radians = angle.to_radians();
2244 let s = radians.sin();
2245 let c = radians.cos();
2246 let dx = eval.group_u() - 0.5;
2247 let dy = eval.group_v() - 0.5;
2248 let ru = dx * c - dy * s + 0.5;
2249 let rv = dx * s + dy * c + 0.5;
2250 self.evaluate_node_color_output_internal(
2251 state,
2252 src,
2253 out,
2254 eval.with_group_uv(ru, rv),
2255 visiting,
2256 visiting_subgraphs,
2257 )
2258 }),
2259 TileNodeKind::DirectionalWarp { amount, angle } => self
2260 .input_connection_source(state, node_index, 0)
2261 .and_then(|src| {
2262 let warp = self
2263 .input_connection_source(state, node_index, 1)
2264 .and_then(|warp_src| {
2265 self.evaluate_node_color_internal(
2266 state,
2267 warp_src,
2268 eval,
2269 visiting,
2270 visiting_subgraphs,
2271 )
2272 })
2273 .map(Self::color_to_mask)
2274 .unwrap_or(0.5);
2275 let radians = angle.to_radians();
2276 let delta = (warp - 0.5) * amount.clamp(0.0, 1.0);
2277 let du = radians.cos() * delta;
2278 let dv = radians.sin() * delta;
2279 self.evaluate_node_color_internal(
2280 state,
2281 src,
2282 eval.with_group_uv(eval.group_u() + du, eval.group_v() + dv),
2283 visiting,
2284 visiting_subgraphs,
2285 )
2286 }),
2287 TileNodeKind::Brick {
2288 columns,
2289 rows,
2290 staggered,
2291 offset,
2292 warp_amount,
2293 falloff,
2294 } => {
2295 let warp = self.connected_warp_vector(
2296 state,
2297 node_index,
2298 0,
2299 eval,
2300 warp_amount.clamp(0.0, 0.25),
2301 visiting,
2302 visiting_subgraphs,
2303 );
2304 let value = match output_terminal {
2305 1 => Self::brick_height(
2306 eval, warp, *columns, *rows, *staggered, *offset, *falloff,
2307 ),
2308 2 => Self::brick_cell_id(eval, warp, *columns, *rows, *staggered, *offset),
2309 _ => Self::brick_center(eval, warp, *columns, *rows, *staggered, *offset),
2310 };
2311 let v = unit_to_u8(value);
2312 Some(TheColor::from_u8_array([v, v, v, 255]))
2313 }
2314 TileNodeKind::Disc {
2315 scale,
2316 seed,
2317 jitter,
2318 radius,
2319 warp_amount,
2320 falloff,
2321 } => {
2322 let warp = self.connected_warp_vector(
2323 state,
2324 node_index,
2325 0,
2326 eval,
2327 warp_amount.clamp(0.0, 0.25),
2328 visiting,
2329 visiting_subgraphs,
2330 );
2331 let density = self
2332 .evaluate_connected_scalar(
2333 state,
2334 node_index,
2335 1,
2336 eval,
2337 visiting,
2338 visiting_subgraphs,
2339 )
2340 .unwrap_or(*scale)
2341 .clamp(0.05, 2.0);
2342 let jitter = self
2343 .evaluate_connected_scalar(
2344 state,
2345 node_index,
2346 2,
2347 eval,
2348 visiting,
2349 visiting_subgraphs,
2350 )
2351 .unwrap_or(*jitter)
2352 .clamp(0.0, 1.0);
2353 let radius = self
2354 .evaluate_connected_scalar(
2355 state,
2356 node_index,
2357 3,
2358 eval,
2359 visiting,
2360 visiting_subgraphs,
2361 )
2362 .unwrap_or(*radius)
2363 .clamp(0.05, 1.0);
2364 let falloff = self
2365 .evaluate_connected_scalar(
2366 state,
2367 node_index,
2368 4,
2369 eval,
2370 visiting,
2371 visiting_subgraphs,
2372 )
2373 .unwrap_or(*falloff)
2374 .clamp(0.1, 4.0);
2375 let value = match output_terminal {
2376 1 => Self::disc_height(eval, warp, density, *seed, jitter, radius, falloff),
2377 2 => Self::disc_cell_id(eval, warp, density, *seed, jitter),
2378 _ => Self::disc_center(eval, warp, density, *seed, jitter, radius),
2379 };
2380 let v = unit_to_u8(value);
2381 Some(TheColor::from_u8_array([v, v, v, 255]))
2382 }
2383 TileNodeKind::IdRandom => {
2384 let id = self
2385 .evaluate_connected_color(
2386 state,
2387 node_index,
2388 0,
2389 eval,
2390 visiting,
2391 visiting_subgraphs,
2392 )
2393 .map(Self::color_to_mask)
2394 .unwrap_or(0.0);
2395 let key = (id.clamp(0.0, 1.0) * 65535.0).round() as i32;
2396 let v = unit_to_u8(Self::hash2(key, key ^ 0x45d9f3, 0x9e37_79b9));
2397 Some(TheColor::from_u8_array([v, v, v, 255]))
2398 }
2399 TileNodeKind::Min => {
2400 let a = self.evaluate_connected_color(
2401 state,
2402 node_index,
2403 0,
2404 eval,
2405 visiting,
2406 visiting_subgraphs,
2407 );
2408 let b = self.evaluate_connected_color(
2409 state,
2410 node_index,
2411 1,
2412 eval,
2413 visiting,
2414 visiting_subgraphs,
2415 );
2416 match (a, b) {
2417 (Some(a), Some(b)) => {
2418 let av = Self::color_to_mask(a);
2419 let bv = Self::color_to_mask(b);
2420 let v = unit_to_u8(av.min(bv));
2421 Some(TheColor::from_u8_array([v, v, v, 255]))
2422 }
2423 (Some(a), None) => Some(a),
2424 (None, Some(b)) => Some(b),
2425 (None, None) => None,
2426 }
2427 }
2428 TileNodeKind::Max => {
2429 let a = self.evaluate_connected_color(
2430 state,
2431 node_index,
2432 0,
2433 eval,
2434 visiting,
2435 visiting_subgraphs,
2436 );
2437 let b = self.evaluate_connected_color(
2438 state,
2439 node_index,
2440 1,
2441 eval,
2442 visiting,
2443 visiting_subgraphs,
2444 );
2445 match (a, b) {
2446 (Some(a), Some(b)) => {
2447 let av = Self::color_to_mask(a);
2448 let bv = Self::color_to_mask(b);
2449 let v = unit_to_u8(av.max(bv));
2450 Some(TheColor::from_u8_array([v, v, v, 255]))
2451 }
2452 (Some(a), None) => Some(a),
2453 (None, Some(b)) => Some(b),
2454 (None, None) => None,
2455 }
2456 }
2457 TileNodeKind::Add => {
2458 let a = self.evaluate_connected_color(
2459 state,
2460 node_index,
2461 0,
2462 eval,
2463 visiting,
2464 visiting_subgraphs,
2465 );
2466 let b = self.evaluate_connected_color(
2467 state,
2468 node_index,
2469 1,
2470 eval,
2471 visiting,
2472 visiting_subgraphs,
2473 );
2474 match (a, b) {
2475 (Some(a), Some(b)) => {
2476 let av = Self::color_to_mask(a);
2477 let bv = Self::color_to_mask(b);
2478 let v = unit_to_u8((av + bv).clamp(0.0, 1.0));
2479 Some(TheColor::from_u8_array([v, v, v, 255]))
2480 }
2481 (Some(a), None) => Some(a),
2482 (None, Some(b)) => Some(b),
2483 (None, None) => None,
2484 }
2485 }
2486 TileNodeKind::Subtract => {
2487 let a = self.evaluate_connected_color(
2488 state,
2489 node_index,
2490 0,
2491 eval,
2492 visiting,
2493 visiting_subgraphs,
2494 );
2495 let b = self.evaluate_connected_color(
2496 state,
2497 node_index,
2498 1,
2499 eval,
2500 visiting,
2501 visiting_subgraphs,
2502 );
2503 match (a, b) {
2504 (Some(a), Some(b)) => {
2505 let av = Self::color_to_mask(a);
2506 let bv = Self::color_to_mask(b);
2507 let v = unit_to_u8((av - bv).clamp(0.0, 1.0));
2508 Some(TheColor::from_u8_array([v, v, v, 255]))
2509 }
2510 (Some(a), None) => Some(a),
2511 (None, Some(_)) => Some(TheColor::from_u8_array([0, 0, 0, 255])),
2512 (None, None) => None,
2513 }
2514 }
2515 TileNodeKind::Multiply => {
2516 let a = self.evaluate_connected_color(
2517 state,
2518 node_index,
2519 0,
2520 eval,
2521 visiting,
2522 visiting_subgraphs,
2523 );
2524 let b = self.evaluate_connected_color(
2525 state,
2526 node_index,
2527 1,
2528 eval,
2529 visiting,
2530 visiting_subgraphs,
2531 );
2532 match (a, b) {
2533 (Some(a), Some(b)) => Some(Self::multiply_colors(a, b)),
2534 (Some(a), None) => Some(a),
2535 (None, Some(b)) => Some(b),
2536 (None, None) => None,
2537 }
2538 }
2539 TileNodeKind::MakeMaterial => self
2540 .evaluate_node_material_internal(
2541 state,
2542 node_index,
2543 eval,
2544 visiting,
2545 visiting_subgraphs,
2546 )
2547 .map(material_to_color),
2548 TileNodeKind::Material {
2549 roughness,
2550 metallic,
2551 opacity,
2552 emissive,
2553 } => Some(material_to_color((
2554 *roughness, *metallic, *opacity, *emissive,
2555 ))),
2556 TileNodeKind::MaterialMix { .. } => self
2557 .evaluate_node_material_internal(
2558 state,
2559 node_index,
2560 eval,
2561 visiting,
2562 visiting_subgraphs,
2563 )
2564 .map(material_to_color),
2565 TileNodeKind::MaskBlend { factor } => {
2566 let a = self.evaluate_connected_color(
2567 state,
2568 node_index,
2569 0,
2570 eval,
2571 visiting,
2572 visiting_subgraphs,
2573 );
2574 let b = self.evaluate_connected_color(
2575 state,
2576 node_index,
2577 1,
2578 eval,
2579 visiting,
2580 visiting_subgraphs,
2581 );
2582 let mask = self
2583 .evaluate_connected_color(
2584 state,
2585 node_index,
2586 2,
2587 eval,
2588 visiting,
2589 visiting_subgraphs,
2590 )
2591 .map(Self::color_to_mask)
2592 .unwrap_or(0.0);
2593 match (a, b) {
2594 (Some(a), Some(b)) => Some(Self::mix_colors(a, b, mask * *factor)),
2595 (Some(a), None) => Some(a),
2596 (None, Some(b)) => Some(b),
2597 (None, None) => None,
2598 }
2599 }
2600 TileNodeKind::Levels { level, width } => self
2601 .evaluate_connected_color(
2602 state,
2603 node_index,
2604 0,
2605 eval,
2606 visiting,
2607 visiting_subgraphs,
2608 )
2609 .map(|color| {
2610 let width = width.clamp(0.001, 1.0);
2611 let level = level.clamp(0.0, 1.0);
2612 let half = width * 0.5;
2613 let low = (level - half).clamp(0.0, 1.0);
2614 let high = (level + half).clamp(0.0, 1.0);
2615 let v = unit_to_u8(Self::remap_unit(Self::color_to_mask(color), low, high));
2616 TheColor::from_u8_array([v, v, v, 255])
2617 }),
2618 TileNodeKind::HeightShape {
2619 contrast,
2620 bias,
2621 plateau,
2622 rim,
2623 warp_amount,
2624 } => self
2625 .evaluate_connected_color(
2626 state,
2627 node_index,
2628 0,
2629 eval,
2630 visiting,
2631 visiting_subgraphs,
2632 )
2633 .map(|fallback| {
2634 let warp = self.connected_warp_vector(
2635 state,
2636 node_index,
2637 1,
2638 eval,
2639 warp_amount.clamp(0.0, 0.25),
2640 visiting,
2641 visiting_subgraphs,
2642 );
2643 let input = self
2644 .evaluate_connected_color(
2645 state,
2646 node_index,
2647 0,
2648 eval.with_group_uv(
2649 (eval.group_u() + warp.x).rem_euclid(1.0),
2650 (eval.group_v() + warp.y).rem_euclid(1.0),
2651 ),
2652 visiting,
2653 visiting_subgraphs,
2654 )
2655 .unwrap_or(fallback);
2656 let mut v = Self::color_to_mask(input).clamp(0.0, 1.0);
2657 let contrast = self
2658 .evaluate_connected_scalar(
2659 state,
2660 node_index,
2661 2,
2662 eval,
2663 visiting,
2664 visiting_subgraphs,
2665 )
2666 .unwrap_or(*contrast)
2667 .clamp(0.1, 4.0);
2668 v = ((v - 0.5) * contrast + 0.5).clamp(0.0, 1.0);
2669
2670 let bias = self
2671 .evaluate_connected_scalar(
2672 state,
2673 node_index,
2674 3,
2675 eval,
2676 visiting,
2677 visiting_subgraphs,
2678 )
2679 .unwrap_or(*bias)
2680 .clamp(-1.0, 1.0);
2681 if bias < 0.0 {
2682 let power = (1.0 + (-bias * 3.0)).clamp(1.0, 4.0);
2683 v = v.powf(power);
2684 } else if bias > 0.0 {
2685 let power = (1.0 + (bias * 3.0)).clamp(1.0, 4.0);
2686 v = 1.0 - (1.0 - v).powf(power);
2687 }
2688
2689 let plateau = self
2690 .evaluate_connected_scalar(
2691 state,
2692 node_index,
2693 4,
2694 eval,
2695 visiting,
2696 visiting_subgraphs,
2697 )
2698 .unwrap_or(*plateau)
2699 .clamp(0.0, 3.0);
2700 let rim = self
2701 .evaluate_connected_scalar(
2702 state,
2703 node_index,
2704 5,
2705 eval,
2706 visiting,
2707 visiting_subgraphs,
2708 )
2709 .unwrap_or(*rim)
2710 .clamp(0.0, 4.0);
2711 if rim > 0.0 {
2712 let shoulder = (4.0 * v * (1.0 - v)).clamp(0.0, 1.0);
2713 v = (v - shoulder * rim * 0.18).clamp(0.0, 1.0);
2714 }
2715 if plateau > 0.0 {
2716 let top = (1.0 - plateau * 0.18).clamp(0.35, 0.999);
2717 if v > top {
2718 let t = ((v - top) / (1.0 - top).max(0.0001)).clamp(0.0, 1.0);
2719 let flatten = (0.2 / (1.0 + plateau * 1.2)).clamp(0.03, 0.2);
2720 let curve = t * t * (3.0 - 2.0 * t);
2721 v = top + curve * (1.0 - top) * flatten;
2722 }
2723 }
2724
2725 let out = unit_to_u8(v);
2726 TheColor::from_u8_array([out, out, out, 255])
2727 }),
2728 TileNodeKind::Threshold { cutoff } => self
2729 .evaluate_connected_color(
2730 state,
2731 node_index,
2732 0,
2733 eval,
2734 visiting,
2735 visiting_subgraphs,
2736 )
2737 .map(|color| {
2738 let v = if Self::color_to_mask(color) >= *cutoff {
2739 255
2740 } else {
2741 0
2742 };
2743 TheColor::from_u8_array([v, v, v, 255])
2744 }),
2745 TileNodeKind::Blur { radius } => {
2746 let radius = radius.clamp(0.001, 0.08);
2747 let offsets = [
2748 (-1.0f32, -1.0f32),
2749 (0.0, -1.0),
2750 (1.0, -1.0),
2751 (-1.0, 0.0),
2752 (0.0, 0.0),
2753 (1.0, 0.0),
2754 (-1.0, 1.0),
2755 (0.0, 1.0),
2756 (1.0, 1.0),
2757 ];
2758 let mut sum = 0.0;
2759 let mut weight_sum = 0.0;
2760 for (ox, oy) in offsets {
2761 let weight = if ox == 0.0 && oy == 0.0 {
2762 2.0
2763 } else if ox == 0.0 || oy == 0.0 {
2764 1.0
2765 } else {
2766 0.75
2767 };
2768 let value = self
2769 .evaluate_connected_color(
2770 state,
2771 node_index,
2772 0,
2773 eval.with_group_uv(
2774 eval.group_u() + ox * radius,
2775 eval.group_v() + oy * radius,
2776 ),
2777 visiting,
2778 visiting_subgraphs,
2779 )
2780 .map(Self::color_to_mask)
2781 .unwrap_or(0.0);
2782 sum += value * weight;
2783 weight_sum += weight;
2784 }
2785 let v = unit_to_u8((sum / weight_sum.max(0.0001)).clamp(0.0, 1.0));
2786 Some(TheColor::from_u8_array([v, v, v, 255]))
2787 }
2788 TileNodeKind::SlopeBlur { radius, amount } => {
2789 let radius = radius.clamp(0.001, 0.08);
2790 let amount = amount.clamp(0.0, 1.0);
2791 let center = self
2792 .evaluate_connected_color(
2793 state,
2794 node_index,
2795 0,
2796 eval,
2797 visiting,
2798 visiting_subgraphs,
2799 )
2800 .map(Self::color_to_mask)
2801 .unwrap_or(0.0);
2802 let directions = [
2803 Vec2::new(1.0f32, 0.0),
2804 Vec2::new(0.707, 0.707),
2805 Vec2::new(0.0, 1.0),
2806 Vec2::new(-0.707, 0.707),
2807 Vec2::new(-1.0, 0.0),
2808 Vec2::new(-0.707, -0.707),
2809 Vec2::new(0.0, -1.0),
2810 Vec2::new(0.707, -0.707),
2811 ];
2812 let mut sum = center * 2.0;
2813 let mut weight_sum = 2.0;
2814 for dir in directions {
2815 let near_u = eval.group_u() + dir.x * radius;
2816 let near_v = eval.group_v() + dir.y * radius;
2817 let near = self
2818 .evaluate_connected_color(
2819 state,
2820 node_index,
2821 0,
2822 eval.with_group_uv(near_u, near_v),
2823 visiting,
2824 visiting_subgraphs,
2825 )
2826 .map(Self::color_to_mask)
2827 .unwrap_or(center);
2828 let shifted_u =
2829 eval.group_u() + dir.x * radius * (1.0 + near * amount * 2.0);
2830 let shifted_v =
2831 eval.group_v() + dir.y * radius * (1.0 + near * amount * 2.0);
2832 let shifted = self
2833 .evaluate_connected_color(
2834 state,
2835 node_index,
2836 0,
2837 eval.with_group_uv(shifted_u, shifted_v),
2838 visiting,
2839 visiting_subgraphs,
2840 )
2841 .map(Self::color_to_mask)
2842 .unwrap_or(near);
2843 let weight = 0.75 + near * amount;
2844 sum += shifted * weight;
2845 weight_sum += weight;
2846 }
2847 let v = unit_to_u8((sum / weight_sum.max(0.0001)).clamp(0.0, 1.0));
2848 Some(TheColor::from_u8_array([v, v, v, 255]))
2849 }
2850 TileNodeKind::HeightEdge { radius } => {
2851 let radius = radius.clamp(0.001, 0.08);
2852 let sample = |u: f32,
2853 v: f32,
2854 visiting: &mut FxHashSet<usize>,
2855 visiting_subgraphs: &mut FxHashSet<Uuid>|
2856 -> f32 {
2857 self.evaluate_connected_color(
2858 state,
2859 node_index,
2860 0,
2861 eval.with_group_uv(u, v),
2862 visiting,
2863 visiting_subgraphs,
2864 )
2865 .map(Self::color_to_mask)
2866 .unwrap_or(0.0)
2867 };
2868 let c = sample(eval.group_u(), eval.group_v(), visiting, visiting_subgraphs);
2869 let l = sample(
2870 eval.group_u() - radius,
2871 eval.group_v(),
2872 visiting,
2873 visiting_subgraphs,
2874 );
2875 let r = sample(
2876 eval.group_u() + radius,
2877 eval.group_v(),
2878 visiting,
2879 visiting_subgraphs,
2880 );
2881 let t = sample(
2882 eval.group_u(),
2883 eval.group_v() - radius,
2884 visiting,
2885 visiting_subgraphs,
2886 );
2887 let b = sample(
2888 eval.group_u(),
2889 eval.group_v() + radius,
2890 visiting,
2891 visiting_subgraphs,
2892 );
2893 let edge = (((c - l).abs() + (c - r).abs() + (c - t).abs() + (c - b).abs())
2894 * 0.5)
2895 .clamp(0.0, 1.0);
2896 let v = unit_to_u8(edge);
2897 Some(TheColor::from_u8_array([v, v, v, 255]))
2898 }
2899 TileNodeKind::Warp { amount } => self
2900 .input_connection_source(state, node_index, 0)
2901 .and_then(|src| {
2902 let warp = self
2903 .input_connection_source(state, node_index, 1)
2904 .and_then(|warp_src| {
2905 self.evaluate_node_color_internal(
2906 state,
2907 warp_src,
2908 eval,
2909 visiting,
2910 visiting_subgraphs,
2911 )
2912 })
2913 .map(Self::color_to_mask)
2914 .unwrap_or(0.5);
2915 let delta = (warp - 0.5) * amount * 0.5;
2916 self.evaluate_node_color_internal(
2917 state,
2918 src,
2919 eval.with_group_uv(eval.group_u() + delta, eval.group_v() + delta),
2920 visiting,
2921 visiting_subgraphs,
2922 )
2923 }),
2924 TileNodeKind::Invert => self
2925 .input_connection_source(state, node_index, 0)
2926 .and_then(|src| {
2927 self.evaluate_node_color_internal(
2928 state,
2929 src,
2930 eval,
2931 visiting,
2932 visiting_subgraphs,
2933 )
2934 })
2935 .map(|color| {
2936 let rgba = color.to_u8_array();
2937 TheColor::from_u8_array([
2938 255 - rgba[0],
2939 255 - rgba[1],
2940 255 - rgba[2],
2941 rgba[3],
2942 ])
2943 }),
2944 }
2945 });
2946 visiting.remove(&node_index);
2947 result
2948 }
2949
2950 fn mix_colors(a: TheColor, b: TheColor, factor: f32) -> TheColor {
2951 let t = factor.clamp(0.0, 1.0);
2952 let aa = a.to_u8_array();
2953 let bb = b.to_u8_array();
2954 let lerp = |x: u8, y: u8| -> u8 {
2955 ((x as f32 * (1.0 - t) + y as f32 * t).round()).clamp(0.0, 255.0) as u8
2956 };
2957 TheColor::from_u8_array([
2958 lerp(aa[0], bb[0]),
2959 lerp(aa[1], bb[1]),
2960 lerp(aa[2], bb[2]),
2961 lerp(aa[3], bb[3]),
2962 ])
2963 }
2964
2965 fn multiply_colors(a: TheColor, b: TheColor) -> TheColor {
2966 let aa = a.to_u8_array();
2967 let bb = b.to_u8_array();
2968 let mul = |x: u8, y: u8| -> u8 { ((x as u16 * y as u16) / 255) as u8 };
2969 TheColor::from_u8_array([
2970 mul(aa[0], bb[0]),
2971 mul(aa[1], bb[1]),
2972 mul(aa[2], bb[2]),
2973 mul(aa[3], bb[3]),
2974 ])
2975 }
2976
2977 fn color_to_mask(color: TheColor) -> f32 {
2978 let rgba = color.to_u8_array();
2979 (0.2126 * rgba[0] as f32 + 0.7152 * rgba[1] as f32 + 0.0722 * rgba[2] as f32) / 255.0
2980 }
2981
2982 fn hash2(x: i32, y: i32, seed: u32) -> f32 {
2983 let mut n = x as u32;
2984 n = n
2985 .wrapping_mul(374761393)
2986 .wrapping_add((y as u32).wrapping_mul(668265263));
2987 n ^= seed.wrapping_mul(2246822519);
2988 n = (n ^ (n >> 13)).wrapping_mul(1274126177);
2989 ((n ^ (n >> 16)) & 0x00ff_ffff) as f32 / 0x00ff_ffff as f32
2990 }
2991
2992 fn smoothstep_unit(t: f32) -> f32 {
2993 let t = t.clamp(0.0, 1.0);
2994 t * t * (3.0 - 2.0 * t)
2995 }
2996
2997 fn value_noise(u: f32, v: f32, frequency: i32, seed: u32, wrap: bool) -> f32 {
2998 let frequency = frequency.max(1);
2999 let (u, v) = if wrap {
3000 (u.rem_euclid(1.0), v.rem_euclid(1.0))
3001 } else {
3002 (u, v)
3003 };
3004 let x = u * frequency as f32;
3005 let y = v * frequency as f32;
3006 let x0 = x.floor() as i32;
3007 let y0 = y.floor() as i32;
3008 let x1 = x0 + 1;
3009 let y1 = y0 + 1;
3010 let fx = Self::smoothstep_unit(x.fract());
3011 let fy = Self::smoothstep_unit(y.fract());
3012
3013 let sample = |ix: i32, iy: i32| -> f32 {
3014 let (sx, sy) = if wrap {
3015 (ix.rem_euclid(frequency), iy.rem_euclid(frequency))
3016 } else {
3017 (ix, iy)
3018 };
3019 Self::hash2(sx, sy, seed)
3020 };
3021
3022 let v00 = sample(x0, y0);
3023 let v10 = sample(x1, y0);
3024 let v01 = sample(x0, y1);
3025 let v11 = sample(x1, y1);
3026 let vx0 = v00 * (1.0 - fx) + v10 * fx;
3027 let vx1 = v01 * (1.0 - fx) + v11 * fx;
3028 (vx0 * (1.0 - fy) + vx1 * fy).clamp(0.0, 1.0)
3029 }
3030
3031 fn remap_unit(value: f32, low: f32, high: f32) -> f32 {
3032 let span = (high - low).max(0.000_1);
3033 ((value - low) / span).clamp(0.0, 1.0)
3034 }
3035
3036 fn layout_repeat_from_scale(scale: f32) -> i32 {
3037 ((scale.clamp(0.05, 2.0) * 6.0).round() as i32).max(1)
3038 }
3039
3040 fn box_divide_repeat_from_density(density: f32) -> i32 {
3041 (1 + (density.clamp(0.0, 0.2) * 20.0).round() as i32).max(1)
3042 }
3043
3044 fn box_divide_iterations_from_density(density: f32) -> i32 {
3045 (1 + (density.clamp(0.0, 0.2) / 0.2 * 5.0).round() as i32).max(1)
3046 }
3047
3048 fn voronoi_data(
3049 eval: TileEvalContext,
3050 warp: Vec2<f32>,
3051 scale: f32,
3052 seed: u32,
3053 jitter: f32,
3054 falloff: f32,
3055 ) -> (f32, f32, f32) {
3056 let repeat = Self::layout_repeat_from_scale(scale);
3057 let u = (eval.group_u() + warp.x).rem_euclid(1.0);
3058 let v = (eval.group_v() + warp.y).rem_euclid(1.0);
3059 let x = u * repeat as f32;
3060 let y = v * repeat as f32;
3061 let cell_x = x.floor() as i32;
3062 let cell_y = y.floor() as i32;
3063 let frac_x = x.fract();
3064 let frac_y = y.fract();
3065 let jitter = jitter.clamp(0.0, 1.0);
3066 let mut min_dist = f32::MAX;
3067 let mut second_dist = f32::MAX;
3068 let mut nearest = (cell_x, cell_y);
3069 for oy in -1..=1 {
3070 for ox in -1..=1 {
3071 let sx = cell_x + ox;
3072 let sy = cell_y + oy;
3073 let wx = sx.rem_euclid(repeat);
3074 let wy = sy.rem_euclid(repeat);
3075 let px = 0.5 + (Self::hash2(wx, wy, seed) - 0.5) * jitter;
3076 let py = 0.5 + (Self::hash2(wx, wy, seed ^ 0x9e37_79b9) - 0.5) * jitter;
3077 let dx = ox as f32 + px - frac_x;
3078 let dy = oy as f32 + py - frac_y;
3079 let dist = (dx * dx + dy * dy).sqrt();
3080 if dist < min_dist {
3081 second_dist = min_dist;
3082 min_dist = dist;
3083 nearest = (wx, wy);
3084 } else if dist < second_dist {
3085 second_dist = dist;
3086 }
3087 }
3088 }
3089 let center = (1.0 - (min_dist / 1.4142)).clamp(0.0, 1.0);
3090 let edge_distance = ((second_dist - min_dist) / second_dist.max(0.0001)).clamp(0.0, 1.0);
3091 let height = edge_distance.powf(falloff.clamp(0.1, 4.0));
3092 let id = Self::hash2(nearest.0, nearest.1, seed ^ 0x51f1_5e11);
3093 (center, height, id)
3094 }
3095
3096 fn voronoi_center(
3097 eval: TileEvalContext,
3098 warp: Vec2<f32>,
3099 scale: f32,
3100 seed: u32,
3101 jitter: f32,
3102 ) -> f32 {
3103 Self::voronoi_data(eval, warp, scale, seed, jitter, 1.0).0
3104 }
3105
3106 fn voronoi_height(
3107 eval: TileEvalContext,
3108 warp: Vec2<f32>,
3109 scale: f32,
3110 seed: u32,
3111 jitter: f32,
3112 falloff: f32,
3113 ) -> f32 {
3114 Self::voronoi_data(eval, warp, scale, seed, jitter, falloff).1
3115 }
3116
3117 fn voronoi_cell_id(
3118 eval: TileEvalContext,
3119 warp: Vec2<f32>,
3120 scale: f32,
3121 seed: u32,
3122 jitter: f32,
3123 ) -> f32 {
3124 Self::voronoi_data(eval, warp, scale, seed, jitter, 1.0).2
3125 }
3126
3127 fn brick_data(
3128 eval: TileEvalContext,
3129 warp: Vec2<f32>,
3130 columns: u16,
3131 rows: u16,
3132 staggered: bool,
3133 offset: f32,
3134 falloff: f32,
3135 ) -> (f32, f32, f32) {
3136 let cols = columns.max(1) as i32;
3137 let rows = rows.max(1) as i32;
3138 let u = (eval.group_u() + warp.x).rem_euclid(1.0);
3139 let v = (eval.group_v() + warp.y).rem_euclid(1.0);
3140 let gv = v * rows as f32;
3141 let row = gv.floor() as i32;
3142 let gu = u * cols as f32
3143 + if staggered && row & 1 == 1 {
3144 offset
3145 } else {
3146 0.0
3147 };
3148 let brick_x = gu.rem_euclid(cols as f32);
3149 let col = brick_x.floor() as i32;
3150 let local_x = brick_x.fract();
3151 let local_y = gv.fract();
3152
3153 let dx = ((local_x - 0.5).abs() * 2.0).clamp(0.0, 1.0);
3154 let dy = ((local_y - 0.5).abs() * 2.0).clamp(0.0, 1.0);
3155 let center = (1.0 - ((dx * dx + dy * dy).sqrt() / 1.4142)).clamp(0.0, 1.0);
3156
3157 let edge =
3158 (local_x.min(1.0 - local_x).min(local_y.min(1.0 - local_y)) * 2.0).clamp(0.0, 1.0);
3159 let height = edge.powf(falloff.clamp(0.1, 4.0));
3160
3161 let id = Self::hash2(col.rem_euclid(cols), row.rem_euclid(rows), 0x61c8_8647);
3162 (center, height, id)
3163 }
3164
3165 fn brick_center(
3166 eval: TileEvalContext,
3167 warp: Vec2<f32>,
3168 columns: u16,
3169 rows: u16,
3170 staggered: bool,
3171 offset: f32,
3172 ) -> f32 {
3173 Self::brick_data(eval, warp, columns, rows, staggered, offset, 1.0).0
3174 }
3175
3176 fn brick_height(
3177 eval: TileEvalContext,
3178 warp: Vec2<f32>,
3179 columns: u16,
3180 rows: u16,
3181 staggered: bool,
3182 offset: f32,
3183 falloff: f32,
3184 ) -> f32 {
3185 Self::brick_data(eval, warp, columns, rows, staggered, offset, falloff).1
3186 }
3187
3188 fn brick_cell_id(
3189 eval: TileEvalContext,
3190 warp: Vec2<f32>,
3191 columns: u16,
3192 rows: u16,
3193 staggered: bool,
3194 offset: f32,
3195 ) -> f32 {
3196 Self::brick_data(eval, warp, columns, rows, staggered, offset, 1.0).2
3197 }
3198
3199 fn disc_data(
3200 eval: TileEvalContext,
3201 warp: Vec2<f32>,
3202 scale: f32,
3203 seed: u32,
3204 jitter: f32,
3205 radius: f32,
3206 falloff: f32,
3207 ) -> (f32, f32, f32) {
3208 let repeat = Self::layout_repeat_from_scale(scale);
3209 let u = (eval.group_u() + warp.x).rem_euclid(1.0);
3210 let v = (eval.group_v() + warp.y).rem_euclid(1.0);
3211 let x = u * repeat as f32;
3212 let y = v * repeat as f32;
3213 let cell_x = x.floor() as i32;
3214 let cell_y = y.floor() as i32;
3215 let frac_x = x.fract();
3216 let frac_y = y.fract();
3217 let jitter = jitter.clamp(0.0, 1.0);
3218 let radius = radius.clamp(0.05, 1.0);
3219
3220 let mut min_dist = f32::MAX;
3221 let mut nearest = (cell_x, cell_y);
3222 for oy in -1..=1 {
3223 for ox in -1..=1 {
3224 let sx = cell_x + ox;
3225 let sy = cell_y + oy;
3226 let wx = sx.rem_euclid(repeat);
3227 let wy = sy.rem_euclid(repeat);
3228 let px = 0.5 + (Self::hash2(wx, wy, seed) - 0.5) * jitter;
3229 let py = 0.5 + (Self::hash2(wx, wy, seed ^ 0x9e37_79b9) - 0.5) * jitter;
3230 let dx = ox as f32 + px - frac_x;
3231 let dy = oy as f32 + py - frac_y;
3232 let dist = (dx * dx + dy * dy).sqrt();
3233 if dist < min_dist {
3234 min_dist = dist;
3235 nearest = (wx, wy);
3236 }
3237 }
3238 }
3239
3240 let radius_cells = 0.5 * radius;
3241 let center = (1.0 - (min_dist / radius_cells.max(0.0001))).clamp(0.0, 1.0);
3242 let height = center.powf(falloff.clamp(0.1, 4.0));
3243 let id = Self::hash2(nearest.0, nearest.1, seed ^ 0x2f6e_2b1d);
3244 (center, height, id)
3245 }
3246
3247 fn disc_center(
3248 eval: TileEvalContext,
3249 warp: Vec2<f32>,
3250 scale: f32,
3251 seed: u32,
3252 jitter: f32,
3253 radius: f32,
3254 ) -> f32 {
3255 Self::disc_data(eval, warp, scale, seed, jitter, radius, 1.0).0
3256 }
3257
3258 fn disc_height(
3259 eval: TileEvalContext,
3260 warp: Vec2<f32>,
3261 scale: f32,
3262 seed: u32,
3263 jitter: f32,
3264 radius: f32,
3265 falloff: f32,
3266 ) -> f32 {
3267 Self::disc_data(eval, warp, scale, seed, jitter, radius, falloff).1
3268 }
3269
3270 fn disc_cell_id(
3271 eval: TileEvalContext,
3272 warp: Vec2<f32>,
3273 scale: f32,
3274 seed: u32,
3275 jitter: f32,
3276 ) -> f32 {
3277 Self::disc_data(eval, warp, scale, seed, jitter, 1.0, 1.0).2
3278 }
3279
3280 fn box_divide_data(
3281 eval: TileEvalContext,
3282 warp: Vec2<f32>,
3283 scale: f32,
3284 gap: f32,
3285 rotation: f32,
3286 rounding: f32,
3287 falloff: f32,
3288 ) -> (f32, f32, f32) {
3289 let repeat = Self::box_divide_repeat_from_density(scale);
3290 let iterations = Self::box_divide_iterations_from_density(scale);
3291 let u = (eval.group_u() + warp.x).rem_euclid(1.0);
3292 let v = (eval.group_v() + warp.y).rem_euclid(1.0);
3293 let x = u * repeat as f32;
3294 let y = v * repeat as f32;
3295 let cell_x = x.floor() as i32;
3296 let cell_y = y.floor() as i32;
3297 let local = Vec2::new(x.fract(), y.fract());
3298 let wrapped_cell = Vec2::new(
3299 cell_x.rem_euclid(repeat) as f32,
3300 cell_y.rem_euclid(repeat) as f32,
3301 );
3302 let (dist, id) = box_divide(
3303 local,
3304 wrapped_cell,
3305 gap.clamp(0.0, 4.0),
3306 rotation,
3307 rounding.clamp(0.0, 0.5),
3308 iterations,
3309 );
3310 let center = (1.0 - (dist.abs() * 6.0)).clamp(0.0, 1.0);
3311 let height = (1.0 - (dist.max(0.0) * 12.0))
3312 .clamp(0.0, 1.0)
3313 .powf(falloff.clamp(0.1, 4.0));
3314 (center, height, id)
3315 }
3316
3317 fn box_divide_center(
3318 eval: TileEvalContext,
3319 warp: Vec2<f32>,
3320 scale: f32,
3321 gap: f32,
3322 rotation: f32,
3323 rounding: f32,
3324 ) -> f32 {
3325 Self::box_divide_data(eval, warp, scale, gap, rotation, rounding, 1.0).0
3326 }
3327
3328 fn box_divide_height(
3329 eval: TileEvalContext,
3330 warp: Vec2<f32>,
3331 scale: f32,
3332 gap: f32,
3333 rotation: f32,
3334 rounding: f32,
3335 falloff: f32,
3336 ) -> f32 {
3337 Self::box_divide_data(eval, warp, scale, gap, rotation, rounding, falloff).1
3338 }
3339
3340 fn box_divide_cell_id(
3341 eval: TileEvalContext,
3342 warp: Vec2<f32>,
3343 scale: f32,
3344 gap: f32,
3345 rotation: f32,
3346 rounding: f32,
3347 ) -> f32 {
3348 Self::box_divide_data(eval, warp, scale, gap, rotation, rounding, 1.0).2
3349 }
3350}
3351
3352fn material_to_color(material: (f32, f32, f32, f32)) -> TheColor {
3353 TheColor::from_u8_array([
3354 unit_to_u8(material.0),
3355 unit_to_u8(material.1),
3356 unit_to_u8(material.2),
3357 unit_to_u8(material.3),
3358 ])
3359}
3360
3361pub fn unit_to_u8(value: f32) -> u8 {
3362 (value.clamp(0.0, 1.0) * 255.0).round() as u8
3363}