diff --git a/crates/compositor/src/compositor_linux.rs b/crates/compositor/src/compositor_linux.rs index 198e6e637..d6f14670f 100644 --- a/crates/compositor/src/compositor_linux.rs +++ b/crates/compositor/src/compositor_linux.rs @@ -5697,7 +5697,7 @@ mod tests { for state in ["arrow", "pointer"] { let still = model_track(state, false, 0.5); let sprite = compose_model(&comp, &blue, &extruded, &still); - for theme in ["studio-ink", "prism-glow", "pop-coral", "pixel-candy", "star-sprout"] { + for theme in ["studio-ink", "pop-coral", "pixel-candy", "star-sprout"] { // Le sprite reste celui du theme par defaut : seul le nom pose le modele. let json = extruded .replace(&format!(r#"/{state}.png","#), &format!(r#"/{state}.png","sculpt":"{theme}/{state}","#)); @@ -5740,6 +5740,69 @@ mod tests { assert!(failures.is_empty(), "{failures:#?}"); } + /// Les thèmes cerclés gardent le trait de leur dessin (`design/cursors/`) : sur la + /// fleche comme sur la main, le modele est pour une bonne part de la couleur du trait (le + /// plateau, le jonc, les rainures entre les doigts), autour de la couleur du corps, et montre + /// ce qu'il porte devant : l'etoile jaune de Star Sprout, le calque et les tirets jaunes de la + /// fleche de Pop Coral, les tirets corail de sa main. Les parts comptent les pixels francs : + /// ceux que l'antialiasing mêle aux bords n'entrent dans aucune. + #[test] + fn the_rimmed_models_keep_the_outline_of_their_art() { + type Rgb = [i32; 3]; + let navy = |[r, g, b]: Rgb| b > r + 20 && r < 110 && g < 130; + let black = |[r, g, b]: Rgb| r.max(g).max(b) < 70; + let mint = |[r, g, b]: Rgb| g > 200 && g > r + 15 && b > 150; + let ivory = |[r, g, b]: Rgb| r > 200 && g > 190 && b > 160 && r - b < 70; + let coral = |[r, g, b]: Rgb| r > 200 && g < 150 && b < 140; + let yellow = |[r, g, b]: Rgb| r > 200 && g > 150 && b < 120; + // (thème, état, trait, corps, ornement, parts minimales du trait, du corps, de l'ornement) + let cases: [(&str, &str, &dyn Fn(Rgb) -> bool, &dyn Fn(Rgb) -> bool, &dyn Fn(Rgb) -> bool, [f32; 3]); 6] = [ + ("studio-ink", "arrow", &black, &ivory, &|_| false, [0.3, 0.15, 0.0]), + ("studio-ink", "pointer", &black, &ivory, &|_| false, [0.2, 0.3, 0.0]), + ("pop-coral", "arrow", &navy, &coral, &yellow, [0.2, 0.2, 0.05]), + ("pop-coral", "pointer", &navy, &yellow, &coral, [0.18, 0.3, 0.02]), + ("star-sprout", "arrow", &navy, &mint, &yellow, [0.25, 0.2, 0.03]), + ("star-sprout", "pointer", &navy, &ivory, &yellow, [0.25, 0.2, 0.03]), + ]; + let Some(gpu) = gpu() else { return }; + let comp = Compositor::new_sized(&gpu, 1280, 720).expect("Compositor::new_sized"); + let (y, uv) = model_screen_planes(false); + let blue = FakeFrame::from_planes(&gpu, 640, 360, &y, &uv); + let (y, uv) = model_screen_planes(true); + let orange = FakeFrame::from_planes(&gpu, 640, 360, &y, &uv); + let extruded = model_scene_json("null", Some(true), "default", true, 5.0); + let hidden = model_scene_json("null", Some(true), "default", false, 5.0); + let bare = compose_model(&comp, &blue, &hidden, &model_track("arrow", false, 0.5)); + let mut failures = Vec::new(); + for (theme, state, rim, body, extra, [min_rim, min_body, min_extra]) in cases { + let still = model_track(state, false, 0.5); + let json = extruded + .replace(&format!(r#"/{state}.png","#), &format!(r#"/{state}.png","sculpt":"{theme}/{state}","#)); + let (hover, hover_b) = (compose_model(&comp, &blue, &json, &still), compose_model(&comp, &orange, &json, &still)); + let mask = model_opaque(&hover, &hover_b, &bare); + let (mut total, mut counts) = (0usize, [0usize; 3]); + for (i, _) in mask.iter().enumerate().filter(|(_, m)| **m) { + let p = [hover[i * 4] as i32, hover[i * 4 + 1] as i32, hover[i * 4 + 2] as i32]; + total += 1; + for (k, class) in [rim, body, extra].iter().enumerate() { + counts[k] += usize::from(class(p)); + } + } + let [rim_share, body_share, extra_share] = counts.map(|k| k as f32 / total.max(1) as f32); + println!("{theme}/{state} : {total} px, trait {rim_share:.3}, corps {body_share:.3}, ornement {extra_share:.3}"); + if !(min_rim..0.7).contains(&rim_share) { + failures.push(format!("{theme}/{state}: {rim_share:.3} de trait, il a disparu ou tout mange")); + } + if body_share < min_body { + failures.push(format!("{theme}/{state}: {body_share:.3} de couleur du corps")); + } + if extra_share < min_extra { + failures.push(format!("{theme}/{state}: {extra_share:.3} d'ornement")); + } + } + assert!(failures.is_empty(), "{failures:#?}"); + } + /// Pendant de `tests/cursor_tap_render.rs` (Windows) : sous un lissage qui traine loin /// derriere la souris, la pointe du modele se pose sur la pastille rouge du clic, et l'anneau /// de l'impact (mode 16) l'entoure, a plat et incline. diff --git a/crates/compositor/src/sculpt.rs b/crates/compositor/src/sculpt.rs index de9c4e65a..612c2c65f 100644 --- a/crates/compositor/src/sculpt.rs +++ b/crates/compositor/src/sculpt.rs @@ -1,17 +1,17 @@ -//! Curseurs sculptés : la flèche et la main des cinq thèmes d'origine, modelées en volumes dans les -//! shaders (mode 15, `sculpt_proto`) au lieu d'extruder leur PNG — capsules et unions lissées, -//! extrusions arrondies, voxels, polyèdres taillés. Ce module en tient ce que la géométrie doit -//! savoir côté CPU : l'identifiant que lit le shader, et la boîte du modèle (`SpriteShape`) qui pose -//! le hotspot, règle la garde au sol et borne la boîte de dessin. +//! Curseurs sculptés : la flèche et la main des thèmes d'origine, modelées en volumes dans les +//! shaders (mode 15, `sculpt_proto`) au lieu d'extruder leur PNG — voxels (Pixel Candy), pièces +//! cerclées du trait de leur dessin (Studio Ink, Pop Coral, Star Sprout). Prism Glow n'en a pas : +//! son dessin à facettes est extrudé comme tout sprite. Ce module en tient ce que la géométrie +//! doit savoir côté CPU : l'identifiant que lit le shader, et la boîte du modèle (`SpriteShape`) +//! qui pose le hotspot, règle la garde au sol et borne la boîte de dessin. //! //! Les formes sont écrites dans le repère du PROTOTYPE où elles ont été dessinées : hauteur du //! curseur 1, x à droite, y VERS LE HAUT, z vers la caméra, l'écran en z = 0. Les constantes //! ci-dessous sont le miroir des `SCULPT_*` des trois shaders. //! -//! Les voxels de la flèche de Pixel Candy (`SCULPT_ARROWPIX`, `SCULPT_ARROWBACK`) sont son polygone -//! échantillonné : une cellule de 1/16 porte un voxel quand son centre est à plus de 0,01 dedans, un -//! bloc de la couche violette quand il est à moins de 0,9 cellule. Changer le polygone, c'est -//! régénérer ces deux tables. +//! Les grilles de voxels de Pixel Candy sont des tables que +//! `scripts/generate-pixel-candy-voxels.mjs` écrit dans les trois shaders : on change la grille +//! dans le script, on le relance, et on ajuste ici la boîte si la forme a bougé. use crate::frame_geometry::SpriteShape; @@ -24,40 +24,56 @@ const ZREF_ARROW: f32 = 0.2; const ZREF_HAND: f32 = 0.185; /// Le dessous de tous les modèles, dans le prototype. const Z_LOW: f32 = 0.05; -/// Le plus haut : la table de la flèche taillée, l'étoile de Star Sprout sur la manchette. -const Z_HIGH_ARROW: f32 = 0.29; +/// Le côté d'un cube de Pixel Candy : seize lignes font un curseur haut de 1. +const VOX: f32 = 0.0625; +/// Le plus haut : les yeux de l'étoile de Star Sprout, sur la flèche comme sur la manchette. +const Z_HIGH_ARROW: f32 = 0.31; const Z_HIGH_HAND: f32 = 0.4; -/// Boîtes des deux formes dans le prototype (x0, x1, haut), qui contiennent leurs cinq variantes : -/// couche violette des voxels, étoile, cristal compris. Leur hauteur est celle du sprite, 1 / SCULPT_SCALE : -/// le mode 15 tient le plus grand côté de la boîte pour 1 (`sprite_size`). -const BOX_ARROW: [f32; 3] = [-0.1, 0.75, 0.09]; -const BOX_HAND: [f32; 3] = [-0.27, 0.73, 0.07]; +/// Boîtes des modèles dans le prototype (x0, x1, haut). Leur hauteur est celle du sprite, +/// 1 / SCULPT_SCALE : le mode 15 tient le plus grand côté de la boîte pour 1 (`sprite_size`). +/// Celles des cubes de Pixel Candy ; puis des thèmes cerclés, que leur trait élargit et que +/// débordent l'étoile et les feuilles (Star Sprout), les tirets du clic et les calques (Pop +/// Coral), le pouce. +const BOX_PIXEL_ARROW: [f32; 3] = [-0.03, 0.72, 0.03]; +const BOX_PIXEL_HAND: [f32; 3] = [-0.345, 0.72, 0.03]; +const BOX_INK_ARROW: [f32; 3] = [-0.08, 0.6, 0.03]; +const BOX_INK_HAND: [f32; 3] = [-0.36, 0.6, 0.03]; +const BOX_CORAL_ARROW: [f32; 3] = [-0.35, 0.6, 0.06]; +const BOX_CORAL_HAND: [f32; 3] = [-0.38, 0.62, 0.04]; +const BOX_SPROUT_ARROW: [f32; 3] = [-0.08, 0.87, 0.03]; +const BOX_SPROUT_HAND: [f32; 3] = [-0.33, 0.56, 0.03]; -/// Dans l'ordre des identifiants du shader. +/// Dans l'ordre des identifiants du shader. Prism Glow y garde sa place, sans modèle. const THEMES: [&str; 5] = ["studio-ink", "prism-glow", "pop-coral", "pixel-candy", "star-sprout"]; +const EXTRUDED: &str = "prism-glow"; -/// Le haut de la silhouette (y du prototype) : la couche violette des voxels dépasse la pointe -/// d'une cellule, la pointe arrondie de la flèche et le sommet de sa table taillée dépassent le -/// hotspot de 0,03, le bout de l'index y est. -fn silhouette_top(theme: usize, arrow: bool) -> f32 { +/// La boîte du modèle (x0, x1, haut) dans le prototype. +fn model_box(theme: usize, arrow: bool) -> [f32; 3] { match (THEMES[theme], arrow) { - ("pixel-candy", _) => 0.0625, - (_, true) => 0.03, - (_, false) => 0.0, + ("studio-ink", true) => BOX_INK_ARROW, + ("studio-ink", false) => BOX_INK_HAND, + ("pop-coral", true) => BOX_CORAL_ARROW, + ("pop-coral", false) => BOX_CORAL_HAND, + ("pixel-candy", true) => BOX_PIXEL_ARROW, + ("pixel-candy", false) => BOX_PIXEL_HAND, + ("star-sprout", true) => BOX_SPROUT_ARROW, + ("star-sprout", false) => BOX_SPROUT_HAND, + (other, _) => unreachable!("thème sculpté inconnu : {other}"), } } /// Le curseur sculpté que nomme la scène (`"/<état>"`, cf. `resolveCursorSprites`), sous -/// la forme que le mode 15 attend. `None` pour un nom inconnu : l'appelant extrude le sprite. +/// la forme que le mode 15 attend. `None` pour un nom inconnu ou un thème sans modèle (Prism Glow, +/// qu'un ancien projet peut encore nommer) : l'appelant extrude le sprite. pub fn sculpted_shape(name: &str) -> Option { let (theme, state) = name.split_once('/')?; - let theme = THEMES.iter().position(|t| *t == theme)?; + let theme = THEMES.iter().position(|t| *t == theme && *t != EXTRUDED)?; let arrow = match state { "arrow" => true, "pointer" => false, _ => return None, }; - let [x0, x1, y1] = if arrow { BOX_ARROW } else { BOX_HAND }; + let [x0, x1, y1] = model_box(theme, arrow); let (zref, z_high) = if arrow { (ZREF_ARROW, Z_HIGH_ARROW) } else { (ZREF_HAND, Z_HIGH_HAND) }; let s = SCULPT_SCALE; // Repère du modèle : y vers le bas, le coin haut-gauche de la boîte est donc (x0, y1). @@ -65,7 +81,7 @@ pub fn sculpted_shape(name: &str) -> Option { Some(SpriteShape { size, hotspot: [-x0 * s / size[0], y1 * s], - top: (y1 - silhouette_top(theme, arrow)) * s, + top: y1 * s, max_height: (z_high - zref) * s, thick: (zref - Z_LOW) * s, sculpt: 1 + 2 * theme as u32 + u32::from(!arrow), @@ -92,14 +108,16 @@ mod tests { #[test] fn each_theme_arrow_and_hand_has_its_own_id_in_shader_order() { for (k, name) in NAMES.iter().enumerate() { - let shape = sculpted_shape(name).expect(name); - assert_eq!(shape.sculpt, k as u32 + 1, "{name}"); + match sculpted_shape(name) { + Some(shape) => assert_eq!(shape.sculpt, k as u32 + 1, "{name}"), + None => assert!(name.starts_with(EXTRUDED), "{name} : pas de modèle"), + } } } #[test] fn other_states_and_themes_keep_the_extruded_sprite() { - for name in ["default/arrow", "pop-coral/text", "pop-coral", "", "pop-coral/arrow/x"] { + for name in ["default/arrow", "pop-coral/text", "pop-coral", "", "pop-coral/arrow/x", "prism-glow/arrow"] { assert!(sculpted_shape(name).is_none(), "{name}"); } } @@ -108,7 +126,7 @@ mod tests { /// une hauteur ; la boîte a la hauteur du sprite, son plus grand côté, comme le veut le mode 15. #[test] fn the_box_holds_the_hotspot_and_the_silhouette_top() { - for name in NAMES { + for name in NAMES.into_iter().filter(|n| !n.starts_with(EXTRUDED)) { let s = sculpted_shape(name).unwrap(); assert!((0.0..1.0).contains(&s.hotspot[0]) && (0.0..1.0).contains(&s.hotspot[1]), "{name}"); assert!(s.top <= s.hotspot[1] + 1e-6, "{name} : haut de silhouette sous le hotspot"); @@ -131,6 +149,7 @@ mod tests { ("SCULPT_ZREF_ARROW", ZREF_ARROW), ("SCULPT_ZREF_HAND", ZREF_HAND), ("SCULPT_HOVER", Z_LOW), + ("SCULPT_VOX", VOX), ] { let line = src .lines() @@ -145,4 +164,52 @@ mod tests { } } } + + /// Les nombres de la table `name` d'un shader, du `=` à la fin de la liste, constructeurs de + /// vecteurs retirés : la même table s'écrit alors pareil dans les trois langages. + fn table(src: &str, name: &str) -> Vec { + // La déclaration : `NAME =` en WGSL, `NAME[taille]` en HLSL et en Metal (pas un `NAME[i]` du code). + let decl = format!("{name}["); + let sized = |&i: &usize| src[i + decl.len()..].starts_with(|c: char| c.is_ascii_digit()); + let at = src.find(&format!("{name} =")).or_else(|| src.match_indices(&decl).map(|(i, _)| i).find(sized)); + let rest = &src[at.expect(name)..]; + let rest = &rest[rest.find('=').unwrap()..]; + let open = rest.find(['(', '{']).unwrap() + 1; + let close = [rest.find(");"), rest.find("};")].into_iter().flatten().min().unwrap(); + let mut list = rest[open..close].to_string(); + for ty in ["vec2", "vec3", "vec4", "vec4", "float2", "float3", "float4", "int4"] { + list = list.replace(ty, ""); + } + list.split(|c: char| !(c.is_ascii_digit() || c == '.' || c == '-')) + .filter(|t| !t.is_empty()) + .map(|t| t.parse().unwrap_or_else(|_| panic!("{name} : nombre illisible {t}"))) + .collect() + } + + /// Les grilles de Pixel Candy, que `scripts/generate-pixel-candy-voxels.mjs` écrit dans les + /// trois shaders, y sont les mêmes ; chaque pixel coloré est plein, et d'une seule couleur. + #[test] + fn the_voxel_tables_match_in_the_three_shaders() { + let hlsl = include_str!("shaders.hlsl"); + let metal = include_str!("shaders.metal"); + let wgsl = include_str!("vk_shaders/layer.wgsl"); + let names = ["PIX_BODY", "PIX_LINE", "PIX_HI", "PIX_SHADE", "PIX_RECT_N", "PIX_GRID", "PIX_ORIGIN", "PIX_BOX", "PIX_RECT"]; + for name in names { + let reference = table(wgsl, name); + assert!(!reference.is_empty(), "{name} vide"); + assert_eq!(table(hlsl, name), reference, "{name} : HLSL et WGSL diffèrent"); + assert_eq!(table(metal, name), reference, "{name} : Metal et WGSL diffèrent"); + } + let body = table(wgsl, "PIX_BODY"); + let classes = ["PIX_LINE", "PIX_HI", "PIX_SHADE"].map(|name| table(wgsl, name)); + for (i, &face) in body.iter().enumerate() { + let mut seen = 0u32; + for class in &classes { + let m = class[i] as u32; + assert_eq!(m & !(face as u32), 0, "couleur hors de la forme, ligne {i}"); + assert_eq!(m & seen, 0, "deux couleurs sur un pixel, ligne {i}"); + seen |= m; + } + } + } } diff --git a/crates/compositor/src/shaders.hlsl b/crates/compositor/src/shaders.hlsl index 30fbf1bd5..16c62192d 100644 --- a/crates/compositor/src/shaders.hlsl +++ b/crates/compositor/src/shaders.hlsl @@ -587,13 +587,13 @@ static const float MODEL_CONTACT_ALPHA = 0.5; // curseur 1, x à droite, y VERS LE HAUT, z vers la caméra, l'écran en z = 0 et le modèle posé à // SCULPT_HOVER au-dessus. `sculpt_point` y amène un point du repère du modèle. Identifiant : // 1 + 2 × thème + forme ; thèmes 0 Studio Ink, 1 Prism Glow, 2 Pop Coral, 3 Pixel Candy, -// 4 Star Sprout ; formes 0 flèche, 1 main. Id des matières : 1 corps, 2 liseré ou manchette, -// 3 étoile, 4 feuilles, 5 yeux, 6 face des voxels, 7 couche violette, 8 cristal. +// 4 Star Sprout ; formes 0 flèche, 1 main. Id des matières : 1 corps, 2 bande, calque ou +// manchette, 3 tirets, calque corail ou étoile, 4 feuilles, 5 trait des thèmes cerclés (et +// yeux), 6 cubes de Pixel Candy. Prism Glow n'a pas de modèle : son dessin est extrudé, comme un +// sprite (id 0). static const float SCULPT_SCALE = 0.85; static const float SCULPT_HOVER = 0.05; static const float SCULPT_VOX = 0.0625; -static const float SCULPT_AR_ROUND = 0.03; -static const float SCULPT_HAND_ZC = 0.185; // Hauteur, dans le prototype, du z = 0 du modèle : le dessus de la pointe de la flèche, l'axe du // bout de l'index. static const float SCULPT_ZREF_ARROW = 0.2; @@ -619,13 +619,6 @@ float2 s_opu(float2 a, float2 b) return a.x < b.x ? a : b; } -float s_capsule(float3 p, float3 a, float3 b, float r) -{ - float3 pa = p - a, ba = b - a; - float h = saturate(dot(pa, ba) / dot(ba, ba)); - return length(pa - ba * h) - r; -} - float s_round_box(float3 p, float3 b, float r) { float3 q = abs(p) - b + r; @@ -652,35 +645,6 @@ float2 s_rot(float2 v, float a) return float2(c * v.x - s * v.y, s * v.x + c * v.y); } -// La flèche : polygone à sept sommets, pointe à l'origine. -static const float2 SCULPT_ARROW[7] = { - float2(0.0, 0.0), float2(0.0, -0.86), float2(0.215, -0.665), float2(0.37, -1.0), - float2(0.53, -0.93), float2(0.38, -0.60), float2(0.64, -0.60) -}; - -float s_arrow2(float2 p) -{ - float d = dot(p - SCULPT_ARROW[0], p - SCULPT_ARROW[0]); - float s = 1.0; - int j = 6; - [loop] for (int i = 0; i < 7; i++) - { - float2 e = SCULPT_ARROW[j] - SCULPT_ARROW[i]; - float2 w = p - SCULPT_ARROW[i]; - float2 b = w - e * saturate(dot(w, e) / dot(e, e)); - d = min(d, dot(b, b)); - bool c0 = p.y >= SCULPT_ARROW[i].y; - bool c1 = p.y < SCULPT_ARROW[j].y; - bool c2 = e.x * w.y > e.y * w.x; - if ((c0 && c1 && c2) || (!c0 && !c1 && !c2)) - { - s = -s; - } - j = i; - } - return s * sqrt(d); -} - float s_star5(float2 p, float r, float rf) { const float2 k1 = float2(0.809016994375, -0.587785252292); @@ -695,254 +659,390 @@ float s_star5(float2 p, float r, float rf) return length(p - ba * h) * sign(p.y * ba.x - p.x * ba.y); } -// La flèche en volume : extrusion arrondie, bombée d'un dôme qui plafonne avant l'axe médian de -// la silhouette (un dôme qui monterait encore y plierait le dessus). -float s_arrow_solid(float3 p, float h, float re, float dome) -{ - float d2 = s_arrow2(p.xy) - SCULPT_AR_ROUND; - float hh = h + dome * smoothstep(0.0, 0.07, -d2); - return s_extrude(d2, p.z - (SCULPT_HOVER + h + dome), hh, re); -} - -// Le liseré de Studio Ink : un jonc posé sur le dessus, en retrait du bord. -float s_piping(float3 p, float ztop) -{ - float d2 = s_arrow2(p.xy) - SCULPT_AR_ROUND; - return length(float2(d2 + 0.075, p.z - ztop)) - 0.017; -} - -// Le gant : l'index levé, trois doigts repliés, le pouce, fondus dans la paume. -float s_glove(float3 p, float zc) -{ - float index = s_capsule(p, float3(0.0, -0.10, zc), float3(0.0, -0.52, zc), 0.098); - float palm = s_round_box(p - float3(0.185, -0.70, zc), float3(0.255, 0.19, 0.105), 0.1); - float f1 = s_capsule(p, float3(0.17, -0.57, zc + 0.012), float3(0.17, -0.41, zc + 0.045), 0.086); - float f2 = s_capsule(p, float3(0.31, -0.59, zc + 0.01), float3(0.31, -0.45, zc + 0.04), 0.08); - float f3 = s_capsule(p, float3(0.435, -0.625, zc + 0.005), float3(0.435, -0.52, zc + 0.03), 0.07); - float thumb = s_capsule(p, float3(0.03, -0.77, zc + 0.03), float3(-0.165, -0.60, zc + 0.065), 0.082); - float d = s_smin(palm, min(f1, min(f2, f3)), 0.035); - d = s_smin(d, index, 0.05); - return s_smin(d, thumb, 0.05); -} +// Lentille : deux disques de rayon r aux centres à ±d sur x, une feuille pointue à ses deux bouts +// (0, ±sqrt(r² - d²)). +float s_vesica(float2 p, float r, float d) +{ + p = abs(p); + float b = sqrt(r * r - d * d); + return (p.y - b) * d > p.x * b ? length(p - float2(0.0, b)) : length(p + float2(d, 0.0)) - r; +} + +// Les thèmes CERCLÉS (Studio Ink, Pop Coral, Star Sprout) suivent la planche 3D de référence +// (`design/cursors/3d-concept.png`) : le trait de leur dessin 2D n'est pas jeté, il devient la +// matière qui porte tout. Chaque pièce (la flèche, le gant, la manchette, les feuilles, l'étoile) +// est un plateau sous sa silhouette, un jonc sur le trait et un coussin de couleur bombé dedans. +// RIM_POLY : sept sommets par polygone (un sommet coupe un côté en deux quand il en faut six). +// D'abord la couleur de la flèche, commune aux trois thèmes (le trait l'élargit de 0,06, sa pointe +// arrondie touche le hotspot), puis les paumes de Star Sprout, de Studio Ink et de Pop Coral, +// arrondies de 0,05. +static const float2 RIM_POLY[28] = { + float2(0.0, -0.06), float2(0.0, -0.7712), float2(0.165, -0.6325), float2(0.313, -0.9318), + float2(0.4234, -0.8557), float2(0.2672, -0.5686), float2(0.456, -0.5393), + float2(-0.173, -0.5237), float2(-0.016, -0.75), float2(0.329, -0.75), float2(0.411, -0.592), + float2(0.411, -0.43), float2(0.19, -0.435), float2(-0.03, -0.44), + float2(-0.1825, -0.6215), float2(0.0037, -0.9195), float2(0.3462, -0.9195), float2(0.467, -0.6906), + float2(0.467, -0.49), float2(0.2, -0.49), float2(-0.03, -0.49), + float2(-0.1807, -0.5534), float2(0.0194, -0.872), float2(0.3485, -0.872), float2(0.47, -0.6795), + float2(0.47, -0.49), float2(0.2, -0.49), float2(-0.03, -0.49) +}; -// La manchette, ronde autour du poignet (elliptique en xz) : le gant y entre. -float s_cuff(float3 p, float zc) -{ - float3 q = p - float3(0.185, -0.925, zc); - float2 ab = float2(0.272, 0.12); - float e = (length(q.xz / ab) - 1.0) * min(ab.x, ab.y); - return s_extrude(e, q.y, 0.07, 0.06); -} +// Les trois gants, mesurés sur leur dessin (g : 0 Star Sprout, 1 Studio Ink, 2 Pop Coral), neuf +// lignes chacun : les segments de l'index, des trois doigts repliés et du pouce, ceux des trois +// fentes entre les doigts, puis le V entre le pouce et l'index (un point du bord du pouce et la +// normale qui entre dans le V). RIM_GLOVE_R, trois lignes par gant : les rayons des quatre doigts ; +// ceux du pouce et des trois fentes ; le bord de l'index que longe le V, le fond du V, le trait. +static const float4 RIM_GLOVE[27] = { + float4(0.0, -0.1077, 0.0, -0.6), float4(0.1493, -0.3279, 0.1493, -0.6), + float4(0.2863, -0.3654, 0.2863, -0.6), float4(0.4193, -0.4043, 0.4193, -0.6), + float4(-0.2, -0.478, -0.075, -0.625), float4(0.0788, -0.25, 0.0788, -0.3992), + float4(0.2188, -0.3, 0.2188, -0.4234), float4(0.3552, -0.33, 0.3552, -0.4534), + float4(-0.0845, -0.5386, 0.762, 0.648), + float4(0.0, -0.1186, 0.0, -0.65), float4(0.1614, -0.3838, 0.1614, -0.65), + float4(0.3177, -0.427, 0.3177, -0.65), float4(0.4645, -0.4848, 0.4645, -0.65), + float4(-0.215, -0.535, -0.1208, -0.6864), float4(0.0832, -0.3, 0.0832, -0.48), + float4(0.2414, -0.35, 0.2414, -0.482), float4(0.3959, -0.41, 0.3959, -0.533), + float4(-0.122, -0.5718, 0.867, 0.498), + float4(0.0, -0.122, 0.0, -0.65), float4(0.172, -0.387, 0.172, -0.65), + float4(0.3267, -0.427, 0.3267, -0.65), float4(0.4727, -0.4867, 0.4727, -0.65), + float4(-0.199, -0.483, -0.102, -0.645), float4(0.0927, -0.3, 0.0927, -0.476), + float4(0.2493, -0.35, 0.2493, -0.478), float4(0.402, -0.41, 0.402, -0.52), + float4(-0.0993, -0.5267, 0.858, 0.514) +}; +static const float4 RIM_GLOVE_R[9] = { + float4(0.0552, 0.047, 0.045, 0.0375), float4(0.052, 0.0235, 0.0225, 0.0225), float4(0.0552, -0.548, 0.0525, 0.0), + float4(0.064, 0.0585, 0.0585, 0.0515), float4(0.06, 0.0197, 0.0178, 0.0172), float4(0.0648, -0.6, 0.058, 0.0), + float4(0.066, 0.0553, 0.0553, 0.0507), float4(0.063, 0.024, 0.022, 0.02), float4(0.0647, -0.555, 0.056, 0.0) +}; -// L'étoile de Star Sprout, face à la caméra, centrée en c : son visage et ses deux feuilles. -float2 s_sprout(float3 p, float3 c) +// Distance signée au polygone de RIM_POLY qui commence en `base` : distance aux arêtes, signe par +// la parité des traversées d'une demi-droite. +float s_rim_poly(float2 p, int base) { - float3 q = p - c; - float st2 = s_star5(q.xy, 0.125, 0.52) - 0.022; - float2 r = float2(s_extrude(st2, q.z, 0.038, 0.034), 3.0); - float3 e = float3(abs(q.x) - 0.032, q.y + 0.005, q.z - 0.036); - r = s_opu(r, float2(length(e) - 0.0135, 5.0)); - float3 l1 = q - float3(-0.04, 0.15, 0.0); - l1.xy = s_rot(l1.xy, -0.6); - float3 l2 = q - float3(0.045, 0.155, 0.0); - l2.xy = s_rot(l2.xy, 0.7); - float leaves = min(s_ellipsoid(l1, float3(0.03, 0.058, 0.02)), s_ellipsoid(l2, float3(0.03, 0.058, 0.02))); - return s_opu(r, float2(leaves, 4.0)); + float d = dot(p - RIM_POLY[base], p - RIM_POLY[base]); + float s = 1.0; + int j = base + 6; + [loop] for (int i = base; i < base + 7; i++) + { + float2 e = RIM_POLY[j] - RIM_POLY[i]; + float2 w = p - RIM_POLY[i]; + float2 b = w - e * saturate(dot(w, e) / dot(e, e)); + d = min(d, dot(b, b)); + bool c0 = p.y >= RIM_POLY[i].y; + bool c1 = p.y < RIM_POLY[j].y; + bool c2 = e.x * w.y > e.y * w.x; + if ((c0 && c1 && c2) || (!c0 && !c1 && !c2)) + { + s = -s; + } + j = i; + } + return s * sqrt(d); } -// Pixel Candy : une ligne par entier, bit c = colonne c, ligne 0 juste sous la pointe. La flèche -// est le polygone échantillonné à 16 cellules par unité (`sculpt.rs` dit comment la régénérer) ; -// la main est dessinée à la main, un gant échantillonné fondant ses doigts. Les tables *BACK -// portent la couche violette, la face dilatée d'une cellule, décalée d'une ligne et d'une colonne. -static const int SCULPT_ARROWPIX[16] = { 0, 1, 3, 7, 31, 63, 127, 255, 511, 63, 55, 115, 113, 224, 224, 64 }; -static const int SCULPT_ARROWBACK[18] = { - 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 255, 511, 511, 999, 995, 960, 192 +// Le gant `g` dans le plan, autour de sa paume `palm` : (silhouette, couleur). La silhouette unit +// la paume, l'index levé, trois doigts repliés et le pouce ; la couleur en est creusée des +// rainures du dessin, trois fentes entre les doigts et le V entre le pouce et l'index. +float2 s_rim_glove(float2 p, int g, float palm) +{ + float4 r0 = RIM_GLOVE_R[3 * g]; + float4 r1 = RIM_GLOVE_R[3 * g + 1]; + float4 r2 = RIM_GLOVE_R[3 * g + 2]; + int i = 9 * g; + float f = min(sd_segment(p, RIM_GLOVE[i].xy, RIM_GLOVE[i].zw) - r0.x, + sd_segment(p, RIM_GLOVE[i + 1].xy, RIM_GLOVE[i + 1].zw) - r0.y); + f = min(f, sd_segment(p, RIM_GLOVE[i + 2].xy, RIM_GLOVE[i + 2].zw) - r0.z); + f = min(f, sd_segment(p, RIM_GLOVE[i + 3].xy, RIM_GLOVE[i + 3].zw) - r0.w); + f = min(f, sd_segment(p, RIM_GLOVE[i + 4].xy, RIM_GLOVE[i + 4].zw) - r1.x); + float grooves = min(sd_segment(p, RIM_GLOVE[i + 5].xy, RIM_GLOVE[i + 5].zw) - r1.y, + sd_segment(p, RIM_GLOVE[i + 6].xy, RIM_GLOVE[i + 6].zw) - r1.z); + grooves = min(grooves, sd_segment(p, RIM_GLOVE[i + 7].xy, RIM_GLOVE[i + 7].zw) - r1.w); + float sil = s_smin(palm, f, 0.03); + float4 v = RIM_GLOVE[i + 8]; + float wedge = max(max(-dot(p - v.xy, v.zw), p.x + r2.x), r2.y - p.y); + return float2(sil, max(sil, -min(grooves, wedge))); +} + +// Une pièce cerclée : (distance, matière). `d` : le bord intérieur du trait dans le plan, `sil` : +// le même avant les rainures, `dc` : le coussin. Le plateau (matière 5) monte de SCULPT_HOVER à +// `zt` sous la silhouette élargie du trait `w`. Le jonc (matière 5), un tore de rayon w/2 à la +// hauteur `zt`, suit le milieu du trait : dans une rainure plus étroite que le trait il n'en reste +// que la crête, un muret entre les doigts. Le coussin de la matière `mat` sort du plateau, haut de +// `h` au bord, bombé sur 0,045 puis de `bump` ; sa hauteur variable penche le champ, d'où la +// distance minorée (× 0,8). +float2 s_piece(float d, float dc, float sil, float z, float w, float zt, float h, float bump, float mat) +{ + float tray = s_extrude(sil - w, z - 0.5 * (SCULPT_HOVER + zt), 0.5 * (zt - SCULPT_HOVER), 0.012); + float bead = length(float2(d - 0.5 * w, z - zt)) - 0.5 * w; + float u = saturate(-dc / 0.045); + float cushion = 0.8 * s_extrude(dc, z - zt, h + 0.022 * u * (2.0 - u) + bump, 0.75 * h); + return s_opu(float2(min(tray, bead), 5.0), float2(cushion, mat)); +} + +// Pop Coral est du papier découpé : des feuilles à plat, au dessus plat et au bord à peine +// cassé. La feuille marine sous la silhouette élargie du trait `w`, de SCULPT_HOVER à 0,15 ; la +// feuille de couleur (matière 1) posée dessus, 0,02 d'épaisseur, découpée des rainures entre les +// doigts qui laissent voir la marine. +float2 s_paper(float d, float sil, float z, float w) +{ + float navy = s_extrude(sil - w, z - 0.5 * (SCULPT_HOVER + 0.15), 0.5 * (0.15 - SCULPT_HOVER), 0.006); + float sheet = s_extrude(d, z - 0.16, 0.01, 0.004); + return s_opu(float2(navy, 5.0), float2(sheet, 1.0)); +} + +// Bosse de rayon r en c, 1 au centre et 0 au bord : un coussin bombé sans pli. Une hauteur tirée +// de la distance au bord plierait le coussin sur l'axe médian de sa silhouette. +float s_bump(float2 p, float2 c, float r) +{ + float k = saturate(1.0 - dot(p - c, p - c) / (r * r)); + return k * k; +} + +// Un tiret du clic de Pop Coral : capsule inégale de a (rayon ra) à b (rayon rb), exacte. +float s_dash(float2 p, float2 a, float2 b, float ra, float rb) +{ + p -= a; + b -= a; + float hb = dot(b, b); + float2 q = float2(abs(dot(p, float2(b.y, -b.x))), dot(p, b)) / hb; + float2 c = float2(sqrt(hb - (ra - rb) * (ra - rb)), ra - rb); + float k = c.x * q.y - c.y * q.x; + if (k < 0.0) + { + return sqrt(hb * dot(q, q)) - ra; + } + if (k > c.x) + { + return sqrt(hb * (dot(q, q) + 1.0 - 2.0 * q.y)) - rb; + } + return dot(c, q) - ra; +} + +// Les thèmes cerclés. La flèche : Star Sprout menthe, Pop Coral corail, et Studio Ink noir, dont le +// trait se partage en un jonc noir au bord et une bande ivoire en relief, le champ restant le +// dessus du plateau. Le gant ivoire, jaune ou ivoire. Pop Coral est en papier découpé +// (`s_paper`), ses calques et ses tirets aussi : des feuilles au dessus plat. Puis ce que chacun ajoute devant : le calque +// jaune de la flèche, le calque corail du gant et les tirets du clic de Pop Coral ; la manchette, +// l'étoile, ses feuilles et ses yeux de Star Sprout, lus dans le repère de l'étoile (centre `c`, +// tournée, en unités de son rayon `rs`). Matières : 1 corps, 2 bande ivoire, calque jaune ou +// manchette, 3 tirets, calque corail ou étoile, 4 feuilles, 5 le trait (plateaux, joncs, yeux). +float2 s_rimmed(float3 p, int theme, int shape) +{ + bool arrow = shape == 0; + int g = theme == 4 ? 0 : (theme == 0 ? 1 : 2); + float poly = s_rim_poly(p.xy, arrow ? 0 : 7 * g + 7); + float2 body = float2(poly, poly); + float w = 0.06, zt = 0.17, h = 0.03; + float bump = 0.022 * s_bump(p.xy, float2(0.15, -0.45), 0.3); + float back = 1e9; + if (!arrow) + { + w = RIM_GLOVE_R[3 * g + 2].z; + // Le gant, puis pour Pop Coral son calque corail : le même gant décalé en bas à gauche. Une + // boucle et non deux appels (FXC recopierait le gant) ; le calque est sauté quand `p` est + // au-dessus du plateau du gant, qui en est alors plus près que lui. + [loop] for (int i = 0; i < 2; i++) + { + float2 q = p.xy + (i == 1 ? float2(0.03, 0.04) : float2(0.0, 0.0)); + float2 v = s_rim_glove(q, g, (i == 1 ? s_rim_poly(q, 7 * g + 7) : poly) - 0.05); + if (i == 1) + { + back = v.x - w; + } + else + { + body = v; + if (theme != 2 || v.x - w <= 0.0) + { + break; + } + } + } + zt = 0.185; + h = 0.028; + bump = 0.03 * s_bump(p.xy, float2(0.19, -0.62), 0.28); + } + float dc = body.y; + if (arrow && theme == 0) + { + // Studio Ink : pas de coussin, le jonc sur le bord extérieur du trait. + body -= 0.015; + w = 0.045; + dc = 1.0; + } + float2 r = theme == 2 ? s_paper(body.y, body.x, p.z, w) + : s_piece(body.y, dc, body.x, p.z, w, zt, h, bump, 1.0); + if (theme == 0) + { + if (arrow) + { + float band = s_extrude(abs(poly + 0.0075) - 0.0225, p.z - zt, 0.028, 0.012); + r = s_opu(r, float2(band, 2.0)); + } + return r; + } + if (theme == 2) + { + float dash; + if (arrow) + { + // Le calque jaune, décalé en bas à gauche, derrière le plateau. + float back = s_rim_poly(p.xy + float2(0.025, 0.05), 0) - 0.06; + r = s_opu(r, float2(s_extrude(back, p.z - 0.08, 0.03, 0.006), 2.0)); + dash = min(s_dash(p.xy, float2(-0.1179, -0.1374), float2(-0.1799, -0.0443), 0.028, 0.045), + s_dash(p.xy, float2(-0.1347, -0.2493), float2(-0.248, -0.2056), 0.026, 0.042)); + } + else + { + r = s_opu(r, float2(s_extrude(back, p.z - 0.08, 0.03, 0.006), 3.0)); + dash = min(s_dash(p.xy, float2(0.216, -0.1695), float2(0.2593, -0.0685), 0.028, 0.042), + s_dash(p.xy, float2(0.3054, -0.2339), float2(0.3949, -0.1652), 0.0275, 0.041)); + } + return s_opu(r, float2(s_extrude(dash, p.z - 0.11, 0.06, 0.006), 3.0)); + } + if (!arrow) + { + // La manchette : un rectangle arrondi, cintré en sourire comme au dessin. + float2 cq = p.xy - float2(0.1541, -0.9047); + float2 bq = abs(float2(cq.x, cq.y - 0.2066 * cq.x * cq.x)) - float2(0.214, 0.0361); + float cuff = length(max(bq, 0.0)) + min(max(bq.x, bq.y), 0.0) - 0.03; + r = s_opu(r, s_piece(cuff, cuff, cuff, p.z, 0.0477, 0.225, 0.025, 0.0, 2.0)); + } + float2 c = arrow ? float2(0.6118, -0.8079) : float2(0.15, -0.885); + float rs = arrow ? 0.1678 : 0.128; + float zs = arrow ? 0.23 : 0.285; + float2 q = s_rot(p.xy - c, arrow ? 0.2443 : 0.0) / rs; + float star = (s_star5(q, 0.82, 0.55) - 0.18) * rs; + float leaves = min(s_vesica(s_rot(q - float2(-0.33, 1.38), -0.925), 0.4296, 0.2626), + s_vesica(s_rot(q - float2(0.53, 1.37), 0.873), 0.4296, 0.2626)) * rs; + r = s_opu(r, s_piece(leaves, leaves, leaves, p.z, 0.038, zs - 0.02, 0.018, 0.0, 4.0)); + r = s_opu(r, s_piece(star, star, star, p.z, 0.038, zs, 0.022, 0.018 * s_bump(q, float2(0.0, 0.0), 1.0), 3.0)); + // Les yeux : deux ovales marine qui affleurent du coussin. + float3 e = float3(abs(q.x) - 0.25, q.y - 0.08, (p.z - zs - 0.056) / rs); + return s_opu(r, float2(s_ellipsoid(e, float3(0.075, 0.13, 0.1)) * rs, 5.0)); +} + +// Pixel Candy est un pixel art, dessiné une fois : la même grille donne ses PNG 2D et, ici, un cube +// par pixel. Une ligne par entier, bit c = colonne c, ligne 0 en haut. `PIX_BODY` : les pixels +// pleins ; `PIX_LINE`, `PIX_HI`, `PIX_SHADE` : ceux du contour prune, du reflet rose pâle et de +// l'ombre rose foncé, le reste est rose. `PIX_GRID` : colonnes, lignes et début de chaque forme +// dans les tables ; `PIX_ORIGIN` : le coin haut-gauche du pixel (0, 0) ; `PIX_BOX` (centre, +// demi-côtés) : la boîte de la forme ; `PIX_RECT`, bornés par `PIX_RECT_N` : la forme en +// rectangles, pour l'ombre. Tables générées par scripts/generate-pixel-candy-voxels.mjs, qui +// réécrit les trois shaders et les PNG : c'est là qu'on redessine une grille. +// +static const int PIX_BODY[32] = { 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 127, 247, 243, 480, 192, 48, 120, 120, 120, 504, 4088, 32760, 65534, 65535, 65535, 65534, 32766, 32764, 16380, 16376, 16376 }; +static const int PIX_LINE[32] = { 1, 3, 5, 9, 17, 33, 65, 129, 257, 513, 1985, 73, 149, 147, 288, 192, 48, 72, 72, 72, 456, 3656, 29256, 37454, 32777, 32769, 32770, 16386, 16388, 8196, 8200, 16376 }; +static const int PIX_HI[32] = { 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 18, 34, 32, 64, 0, 0, 16, 16, 16, 16, 16, 16, 16, 22, 18, 4, 4, 8, 8, 16, 0 }; +static const int PIX_SHADE[32] = { 0, 0, 0, 4, 8, 16, 32, 64, 128, 448, 40, 36, 64, 64, 128, 0, 0, 32, 32, 32, 32, 288, 2336, 18720, 16384, 16384, 16384, 8192, 8192, 4096, 8160, 0 }; +static const int PIX_RECT_N[3] = { 0, 17, 29 }; +static const int4 PIX_GRID[2] = { + int4(11, 16, 0, 0), + int4(16, 16, 16, 0) }; -static const int SCULPT_HANDPIX[15] = { 4, 14, 14, 14, 110, 878, 7022, 7022, 8190, 8191, 8191, 8191, 8190, 4092, 4092 }; -static const int SCULPT_HANDBACK[17] = { - 28, 62, 62, 62, 510, 4094, 32766, 32766, 32766, 32767, 32767, 32767, 32767, 32767, 32766, 16380, 16380 +static const float2 PIX_ORIGIN[2] = { + float2(0.0, 0.0), + float2(-0.3125, 0.0) }; -// La couche violette de la main, une plage de colonnes par ligne (toutes pleines) : la distance -// exacte à son contour, pour l'ombre portée. -static const float2 SCULPT_HANDSPAN[17] = { - float2(2.0, 4.0), float2(1.0, 5.0), float2(1.0, 5.0), float2(1.0, 5.0), float2(1.0, 8.0), - float2(1.0, 11.0), float2(1.0, 14.0), float2(1.0, 14.0), float2(1.0, 14.0), float2(0.0, 14.0), - float2(0.0, 14.0), float2(0.0, 14.0), float2(0.0, 14.0), float2(0.0, 14.0), float2(1.0, 14.0), - float2(2.0, 13.0), float2(2.0, 13.0) +static const float4 PIX_BOX[2] = { + float4(0.3438, -0.5, 0.3438, 0.5), + float4(0.1875, -0.5, 0.5, 0.5) }; +static const float4 PIX_RECT[29] = { + float4(0.0313, -0.0313, 0.0313, 0.0313), + float4(0.0625, -0.0938, 0.0625, 0.0313), + float4(0.0938, -0.1563, 0.0938, 0.0313), + float4(0.125, -0.2188, 0.125, 0.0313), + float4(0.1563, -0.2813, 0.1563, 0.0313), + float4(0.1875, -0.3438, 0.1875, 0.0313), + float4(0.2188, -0.4063, 0.2188, 0.0313), + float4(0.25, -0.4688, 0.25, 0.0313), + float4(0.2813, -0.5313, 0.2813, 0.0313), + float4(0.3125, -0.5938, 0.3125, 0.0313), + float4(0.3438, -0.6563, 0.3438, 0.0313), + float4(0.2188, -0.7188, 0.2188, 0.0313), + float4(0.0938, -0.7813, 0.0938, 0.0313), + float4(0.375, -0.8125, 0.125, 0.0625), + float4(0.0625, -0.8438, 0.0625, 0.0313), + float4(0.4375, -0.9063, 0.125, 0.0313), + float4(0.4375, -0.9688, 0.0625, 0.0313), + float4(0.0, -0.0313, 0.0625, 0.0313), + float4(0.0, -0.1563, 0.125, 0.0938), + float4(0.0625, -0.2813, 0.1875, 0.0313), + float4(0.1563, -0.3438, 0.2813, 0.0313), + float4(0.25, -0.4063, 0.375, 0.0313), + float4(0.2188, -0.4688, 0.4688, 0.0313), + float4(0.1875, -0.5625, 0.5, 0.0625), + float4(0.2188, -0.6563, 0.4688, 0.0313), + float4(0.1875, -0.7188, 0.4375, 0.0313), + float4(0.2188, -0.7813, 0.4063, 0.0313), + float4(0.1875, -0.8438, 0.375, 0.0313), + float4(0.2188, -0.9375, 0.3438, 0.0625) +}; +// -// Origine de la grille : la flèche part de sa pointe, la main de 2,5 cellules à sa gauche. -float2 s_grid_origin(int shape) -{ - return shape == 0 ? float2(0.0, 0.0) : float2(-2.5 * SCULPT_VOX, 0.0); -} - -// Le bit `c` de la ligne `r` d'une table de `rows` lignes et `cols` colonnes. FXC évalue les deux -// côtés d'un `&&` : l'index est ramené dans la table avant la lecture, le test fait le reste. -bool s_bit(int row, int r, int c, int rows, int cols) +// Le bit `c` d'une ligne. FXC évalue les deux côtés d'un `&&` : chaque index est ramené dans sa +// table avant la lecture, les tests de bornes font le reste. +bool s_pix_bit(int m, int c) { - return r >= 0 && r < rows && c >= 0 && c < cols && ((row >> clamp(c, 0, 31)) & 1) == 1; + return ((m >> clamp(c, 0, 31)) & 1) == 1; } -// La cellule `id` (colonne, ligne, y vers le haut) porte-t-elle un voxel de la face ? -bool s_occ(float2 id, int shape) +// Le pixel (c, r) de la forme `shape`, r compté vers le bas depuis la ligne 0, est-il plein ? +bool s_pix_body(int shape, int c, int r) { - int c = (int)id.x; - int r = -(int)id.y - 1; - if (shape == 0) - { - return s_bit(SCULPT_ARROWPIX[clamp(r, 0, 15)], r, c, 16, 16); - } - return s_bit(SCULPT_HANDPIX[clamp(r, 0, 14)], r, c, 15, 13); + int4 g = PIX_GRID[shape]; + return c >= 0 && c < g.x && r >= 0 && r < g.y && s_pix_bit(PIX_BODY[g.z + clamp(r, 0, g.y - 1)], c); } -// … et un bloc de la couche violette. -bool s_occ_back(float2 id, int shape) +// La cellule qui contient `p` (prototype, y vers le haut). +int2 s_pix_cell(float2 p, int shape) { - int c = (int)id.x + 1; - int r = -(int)id.y; - if (shape == 0) - { - return s_bit(SCULPT_ARROWBACK[clamp(r, 0, 17)], r, c, 18, 17); - } - return s_bit(SCULPT_HANDBACK[clamp(r, 0, 16)], r, c, 17, 15); + float2 o = PIX_ORIGIN[shape]; + return int2((int)floor((p.x - o.x) / SCULPT_VOX), (int)floor((o.y - p.y) / SCULPT_VOX)); } -// Chaque cellule pleine est un cube biseauté ; la couche violette est un étage plus bas. Hors du -// voisinage 3×3, une borne : une cellule au moins, et jamais plus près que la boîte de la forme. +// Un cube par pixel plein, tous de même hauteur, l'arête à peine cassée pour que chaque cube se +// lise. Hors du voisinage 3×3, une borne : une cellule au moins, et jamais plus près que la boîte +// de la forme. float2 s_voxels(float3 p, int shape) { - float2 o = s_grid_origin(shape); - float2 cell = floor((p.xy - o) / SCULPT_VOX); - float2 bc = shape == 0 ? float2(0.33, -0.5) : float2(0.25, -0.47); - float2 bh = shape == 0 ? float2(0.44, 0.61) : float2(0.48, 0.55); - float2 bq = max(abs(p.xy - bc) - bh, 0.0); + float4 b = PIX_BOX[shape]; + float2 bq = max(abs(p.xy - b.xy) - b.zw, 0.0); float bz = max(abs(p.z - (SCULPT_HOVER + 0.07)) - 0.07, 0.0); - float far = max(SCULPT_VOX, sqrt(dot(bq, bq) + bz * bz)); - float dF = far; - float dB = far; - const float3 half_cell = float3(0.5 * SCULPT_VOX, 0.5 * SCULPT_VOX, 0.04); + float d = max(SCULPT_VOX, sqrt(dot(bq, bq) + bz * bz)); + float2 o = PIX_ORIGIN[shape]; + int2 cell = s_pix_cell(p.xy, shape); + const float3 cube = float3(0.5 * SCULPT_VOX, 0.5 * SCULPT_VOX, 0.07); [loop] for (int j = -1; j <= 1; j++) { [loop] for (int i = -1; i <= 1; i++) { - float2 id = cell + float2(i, j); - float3 q = float3(p.xy - (o + (id + 0.5) * SCULPT_VOX), p.z); - if (s_occ(id, shape)) + int c = cell.x + i; + int r = cell.y + j; + if (s_pix_body(shape, c, r)) { - dF = min(dF, s_round_box(q - float3(0.0, 0.0, SCULPT_HOVER + 0.1), half_cell, 0.009)); - } - if (s_occ_back(id, shape)) - { - dB = min(dB, s_round_box(q - float3(0.0, 0.0, SCULPT_HOVER + 0.04), half_cell, 0.009)); + float3 q = float3(p.x - o.x - (c + 0.5) * SCULPT_VOX, p.y - o.y + (r + 0.5) * SCULPT_VOX, + p.z - SCULPT_HOVER - 0.07); + d = min(d, s_round_box(q, cube, 0.006)); } } } - return dF < dB ? float2(dF, 6.0) : float2(dB, 7.0); + return float2(d, 6.0); } -// Distance, dans le plan de l'écran, au contour de la couche violette de la main. -float s_pixel_hand_dist(float2 p) +// Distance, dans le plan de l'écran, au contour de la forme. +float s_pixel_outline(float2 p, int shape) { float d = 1e9; - [loop] for (int r = 0; r < 17; r++) - { - float2 x = (SCULPT_HANDSPAN[r] + float2(-3.5, -2.5)) * SCULPT_VOX; - float2 y = float2(-r, 1.0 - r) * SCULPT_VOX; - float2 q = max(max(float2(x.x, y.x) - p, p - float2(x.y, y.y)), 0.0); - d = min(d, length(q)); - } - return d; -} - -// Prism Glow est taillé, pas peint : chaque pièce est un polyèdre convexe (le max de ses plans), -// les facettes sont planes et la silhouette polygonale. - -// Une arête d'un contour convexe de gemme (a -> b, sens trigonométrique) : sa paroi, une facette -// de rondiste raide, et deux facettes de couronne qui se rejoignent au milieu de l'arête. Les -// couronnes de toutes les arêtes se rejoignent en arêtes vives au-dessus du centre. -float s_gem_edge(float3 p, float2 a, float2 b, float z0) -{ - float2 e = b - a; - float len = length(e); - float2 d = e / len; - float2 q = p.xy - a; - float dist = dot(q, float2(-d.y, d.x)); - float along = abs(dot(q, d) - 0.5 * len); - float z = p.z - z0; - float girdle = (z - 0.035 - 2.2 * dist) * 0.4138; - float crown = (z - 0.07 - 0.6 * dist + 0.18 * along) * 0.8438; - return max(-dist, max(girdle, crown)); -} - -float s_gem_arrow(float3 p) -{ - float z0 = SCULPT_HOVER + 0.03; - float slab = max(p.z - z0 - 0.2, z0 - 0.03 - p.z); - float head = max(slab, max(s_gem_edge(p, float2(-0.02, 0.03), float2(-0.02, -0.88), z0), - max(s_gem_edge(p, float2(-0.02, -0.88), float2(0.66, -0.61), z0), - s_gem_edge(p, float2(0.66, -0.61), float2(-0.02, 0.03), z0)))); - float tail = max(slab, max(max(s_gem_edge(p, float2(0.215, -0.665), float2(0.37, -1.0), z0), - s_gem_edge(p, float2(0.37, -1.0), float2(0.53, -0.93), z0)), - max(s_gem_edge(p, float2(0.53, -0.93), float2(0.38, -0.60), z0), - s_gem_edge(p, float2(0.38, -0.60), float2(0.215, -0.665), z0)))); - return min(head, tail); -} - -// Capsule taillée a -> b : un prisme hexagonal coiffé en b de deux couronnes de facettes -// décalées et d'une facette sommitale, tous les plans tangents à la sphère du bout. -float s_facet_capsule(float3 p, float3 a, float3 b, float r, float spin) -{ - float3 u = normalize(b - a); - float3 v = normalize(cross(u, float3(0.0, 0.0, 1.0))); - float3 w = cross(u, v); - float3 q = p - b; - float h = dot(q, u); - float2 rad = float2(dot(q, v), dot(q, w)); - float d = max(h - r, -dot(p - a, u) - r); - [loop] for (int k = 0; k < 6; k++) - { - float an = spin + k * 1.0471976; - float s = dot(rad, float2(cos(an), sin(an))); - d = max(d, s - r); - d = max(d, dot(rad, float2(cos(an + 0.5236), sin(an + 0.5236))) * 0.8660 + h * 0.5 - r); - d = max(d, s * 0.5 + h * 0.8660 - r); - } - return d; -} - -// Ellipsoïde taillé : les plans tangents dans dix directions du premier octant, reportées dans -// les huit. Une pierre aux facettes de taille égale. -static const float3 SCULPT_FACETS[10] = { - float3(1.0, 0.0, 0.0), float3(0.0, 1.0, 0.0), float3(0.0, 0.0, 1.0), - float3(0.7071, 0.7071, 0.0), float3(0.7071, 0.0, 0.7071), float3(0.0, 0.7071, 0.7071), - float3(0.5774, 0.5774, 0.5774), float3(0.4472, 0.0, 0.8944), float3(0.0, 0.4472, 0.8944), - float3(0.3015, 0.3015, 0.9045) -}; - -float s_facet_ellipsoid(float3 p, float3 r) -{ - float3 q = abs(p); - float d = -1e9; - [loop] for (int i = 0; i < 10; i++) + [loop] for (int i = PIX_RECT_N[shape]; i < PIX_RECT_N[shape + 1]; i++) { - d = max(d, dot(q, SCULPT_FACETS[i]) - length(r * SCULPT_FACETS[i])); + float2 q = abs(p - PIX_RECT[i].xy) - PIX_RECT[i].zw; + d = min(d, length(max(q, 0.0)) + min(max(q.x, q.y), 0.0)); } return d; } -// Le gant des autres thèmes, mêmes os, taillé en facettes. -float s_crystal_hand(float3 p) -{ - float zc = SCULPT_HAND_ZC; - float d = s_facet_ellipsoid(p - float3(0.185, -0.70, zc), float3(0.3, 0.235, 0.125)); - d = min(d, s_facet_capsule(p, float3(0.0, -0.52, zc), float3(0.0, -0.10, zc), 0.098, 0.3)); - d = min(d, s_facet_capsule(p, float3(0.17, -0.57, zc + 0.012), float3(0.17, -0.41, zc + 0.045), 0.086, 0.1)); - d = min(d, s_facet_capsule(p, float3(0.31, -0.59, zc + 0.01), float3(0.31, -0.45, zc + 0.04), 0.08, 0.5)); - d = min(d, s_facet_capsule(p, float3(0.435, -0.625, zc + 0.005), float3(0.435, -0.52, zc + 0.03), 0.07, 0.2)); - return min(d, s_facet_capsule(p, float3(0.03, -0.77, zc + 0.03), float3(-0.165, -0.60, zc + 0.065), 0.082, 0.7)); -} - // Le modèle `shape` du thème `theme`, repère du prototype : (distance, id de matière). float2 sculpt_proto(float3 p, int theme, int shape) { @@ -950,42 +1050,8 @@ float2 sculpt_proto(float3 p, int theme, int shape) { return s_voxels(p, shape); } - if (theme == 1) - { - return float2(shape == 0 ? s_gem_arrow(p) : s_crystal_hand(p), 8.0); - } - float2 body; - float3 star; - if (shape == 0) - { - float h = 0.075, re = 0.03, dome = 0.0; - if (theme == 2) - { - h = 0.07; - re = 0.06; - dome = 0.035; - } - if (theme == 4) - { - h = 0.07; - re = 0.055; - dome = 0.025; - } - float2 r = float2(s_arrow_solid(p, h, re, dome), 1.0); - if (theme == 0) - { - r = s_opu(r, float2(s_piping(p, SCULPT_HOVER + 2.0 * h - 0.004), 2.0)); - } - body = r; - star = float3(0.57, -0.86, SCULPT_HOVER + 2.0 * h + dome); - } - else - { - body = s_opu(float2(s_glove(p, SCULPT_HAND_ZC), 1.0), float2(s_cuff(p, SCULPT_HAND_ZC), 2.0)); - star = float3(0.185, -0.93, SCULPT_HAND_ZC + 0.15); - } - // L'étoile de Star Sprout : un seul appel pour les deux formes (FXC recopie chaque appel). - return theme == 4 ? s_opu(body, s_sprout(p, star)) : body; + // Un seul appel pour les trois thèmes cerclés et leurs deux formes (FXC recopie chaque appel). + return s_rimmed(p, theme, shape); } // Repère du modèle -> prototype. L'écrasement du clic ne raccourcit que z, autour du hotspot. @@ -1005,8 +1071,7 @@ float sculpt_units() // Le curseur sculpté en `q` (repère du modèle) : distance et id de matière. `occ` : ce qui porte // l'ombre sur l'écran. La borne des plans d'une gemme et le champ des voxels sont de mauvaises // distances loin de la surface : la pénombre s'y strie, ou s'arrête net au bord de la boîte des -// voxels. Le cristal porte donc l'ombre de la forme lisse de Pop Coral, les voxels celle de leur -// contour extrudé sur toute leur hauteur. +// voxels. Pixel Candy porte donc l'ombre de sa forme, extrudée sur la hauteur des cubes. float2 sculpt_eval(float3 q, bool occ) { int id = sculpt_id() - 1; @@ -1015,13 +1080,13 @@ float2 sculpt_eval(float3 q, bool occ) float2 r; if (occ && theme == 3) { - float d2 = shape == 0 ? s_arrow2(p.xy) - 1.2 * SCULPT_VOX : s_pixel_hand_dist(p.xy); + float d2 = s_pixel_outline(p.xy, shape); float dz = abs(p.z - (SCULPT_HOVER + 0.07)) - 0.07; r = float2(length(max(float2(d2, dz), 0.0)) + min(max(d2, dz), 0.0), 7.0); } else { - r = sculpt_proto(p, occ && theme == 1 ? 2 : theme, shape); + r = sculpt_proto(p, theme, shape); } return float2(r.x * sculpt_units(), r.y); } @@ -1138,20 +1203,16 @@ SculptMat s_mat(float3 alb, float rough, float spec, float sss, float refl) return m; } -// Couleur de face du voxel qui porte `p` (prototype) : menthe où le bord regarde en bas à gauche, -// rose pâle où il regarde en haut à droite, rose dedans. +// Couleur du cube qui porte `p` (prototype) : celle de son pixel. float3 s_pixel_colour(float3 p, int shape) { - float2 id = floor((p.xy - s_grid_origin(shape)) / SCULPT_VOX); - if (!s_occ(id + float2(-1.0, 0.0), shape) || !s_occ(id + float2(0.0, -1.0), shape)) - { - return s_lin(0.52, 0.91, 0.77); - } - if (!s_occ(id + float2(1.0, 0.0), shape) || !s_occ(id + float2(0.0, 1.0), shape)) - { - return s_lin(1.0, 0.78, 0.87); - } - return s_lin(1.0, 0.50, 0.71); + int2 cell = s_pix_cell(p.xy, shape); + int4 g = PIX_GRID[shape]; + int row = g.z + clamp(cell.y, 0, g.y - 1); + if (s_pix_bit(PIX_LINE[row], cell.x)) return s_lin(0.29, 0.12, 0.36); + if (s_pix_bit(PIX_HI[row], cell.x)) return s_lin(1.0, 0.78, 0.87); + if (s_pix_bit(PIX_SHADE[row], cell.x)) return s_lin(0.87, 0.27, 0.51); + return s_lin(1.0, 0.435, 0.66); } // La matière `mat` (id de `sculpt_proto`) au point `p` du prototype. @@ -1160,28 +1221,31 @@ SculptMat sculpt_material(float mat, float3 p, int theme, int shape) bool primary = mat < 1.5; if (theme == 0) { - if (shape == 0 && primary) return s_mat(s_lin(0.10, 0.10, 0.115), 0.3, 0.2, 0.0, 0.9); - if (shape == 0) return s_mat(s_lin(0.94, 0.91, 0.84), 0.45, 0.4, 0.2, 0.3); - if (primary) return s_mat(s_lin(0.95, 0.92, 0.85), 0.55, 0.35, 0.35, 0.25); - return s_mat(s_lin(0.17, 0.18, 0.22), 0.35, 0.6, 0.0, 0.6); + // Ivoire (le gant, la bande de la flèche) ; noir satiné, presque sans reflet : un reflet + // large sur le champ plat de la flèche le virait au gris. + if (mat < 2.5) return s_mat(s_lin(0.95, 0.92, 0.85), 0.3, 0.6, 0.35, 0.25); + return s_mat(s_lin(0.1, 0.1, 0.11), 0.55, 0.05, 0.0, 0.1); } if (theme == 2) { - if (shape == 0) return s_mat(s_lin(1.0, 0.40, 0.30), 0.5, 0.45, 0.4, 0.25); - if (primary) return s_mat(s_lin(1.0, 0.79, 0.16), 0.5, 0.45, 0.4, 0.25); - return s_mat(s_lin(0.18, 0.20, 0.29), 0.4, 0.5, 0.0, 0.4); + // Corail : la flèche, le calque et les tirets de la main ; jaune : le gant, le calque et les + // tirets de la flèche ; le trait marine. + if ((primary && shape == 0) || (mat > 2.5 && mat < 3.5 && shape == 1)) return s_mat(s_lin(1.0, 0.40, 0.30), 0.85, 0.08, 0.15, 0.03); + if (mat < 3.5) return s_mat(s_lin(1.0, 0.80, 0.10), 0.85, 0.08, 0.15, 0.03); + return s_mat(s_lin(0.09, 0.13, 0.45), 0.9, 0.05, 0.05, 0.02); } if (theme == 4) { - if (primary && shape == 0) return s_mat(s_lin(0.62, 0.91, 0.78), 0.22, 0.8, 0.25, 0.6); - if (primary) return s_mat(s_lin(0.96, 0.94, 0.88), 0.5, 0.35, 0.35, 0.25); - if (mat < 2.5) return s_mat(s_lin(0.62, 0.91, 0.78), 0.25, 0.7, 0.25, 0.5); - if (mat < 3.5) return s_mat(s_lin(1.0, 0.80, 0.20), 0.3, 0.6, 0.3, 0.4); - if (mat < 4.5) return s_mat(s_lin(0.38, 0.80, 0.55), 0.35, 0.5, 0.35, 0.3); - return s_mat(s_lin(0.16, 0.12, 0.10), 0.2, 0.8, 0.0, 0.5); + // Coussins laqués ; le marine satiné, un reflet large et sombre plutôt qu'un filet blanc. + if (primary && shape == 0) return s_mat(s_lin(0.68, 0.93, 0.80), 0.25, 0.7, 0.3, 0.3); + if (primary) return s_mat(s_lin(0.97, 0.95, 0.90), 0.3, 0.6, 0.35, 0.25); + if (mat < 2.5) return s_mat(s_lin(0.52, 0.87, 0.78), 0.25, 0.7, 0.3, 0.3); + if (mat < 3.5) return s_mat(s_lin(1.0, 0.75, 0.25), 0.25, 0.7, 0.3, 0.3); + if (mat < 4.5) return s_mat(s_lin(0.62, 0.92, 0.72), 0.3, 0.6, 0.3, 0.35); + return s_mat(s_lin(0.07, 0.15, 0.33), 0.7, 0.15, 0.05, 0.08); } - if (mat < 6.5) return s_mat(s_pixel_colour(p, shape), 0.45, 0.35, 0.15, 0.2); - return s_mat(s_lin(0.36, 0.18, 0.54), 0.45, 0.35, 0.1, 0.2); + // Plastique mat : sur les faces plates des cubes, un reflet de la lampe blanchirait le prune. + return s_mat(s_pixel_colour(p, shape), 0.65, 0.1, 0.12, 0.04); } // L'environnement du studio vu du modèle : l'écran dessous, la pièce au-dessus (z du modèle), une @@ -1197,39 +1261,6 @@ float3 model_env(float3 d, float rough, float3 l, float3 fill) return col; } -// Cyan, bleu, violet, orchidée. -float3 s_gem(float k) -{ - k = saturate(k) * 3.0; - float3 a = s_lin(0.20, 0.95, 1.0); - float3 b = s_lin(0.15, 0.42, 1.0); - float3 c = s_lin(0.45, 0.25, 0.95); - float3 d = s_lin(0.88, 0.50, 1.0); - if (k < 1.0) return lerp(a, b, k); - if (k < 2.0) return lerp(b, c, k - 1.0); - return lerp(c, d, k - 2.0); -} - -// Le cristal : la teinte vient d'où la facette regarde et d'où elle plie la vue (lues y vers le -// haut, comme au prototype) ; la dispersion sépare le décalage par canal. Les facettes tournées -// vers la lampe sont cyan, les autres violettes. -float3 model_shade_crystal(float3 n, float3 rd, float3 L, float fall, float3 l, float3 fill) -{ - float cosi = saturate(dot(-rd, n)); - float F = 0.04 + 0.96 * pow(1.0 - cosi, 5.0); - float3 t = refract(rd, n, 1.0 / 1.6); - float k = 0.42 + 0.9 * dot(float2(n.x, -n.y), float2(0.7557, -0.6549)) - + 0.6 * dot(float2(t.x, -t.y), float2(0.6, -0.8)); - float3 body = float3(s_gem(k - 0.08).r, s_gem(k).g, s_gem(k + 0.08).b); - float3 col = body * (0.1 + 1.8 * fall * pow(max(dot(n, L), 0.0), 2.5)); - // Éclat : la vue rebondit sur le dos plat et allume la facette quand elle trouve la lampe. - col += body * model_env(reflect(t, float3(0.0, 0.0, 1.0)) * float3(1.0, 1.0, -1.0), 0.15, l, fill) * 0.5; - col += pow(max(dot(reflect(rd, n), L), 0.0), 30.0) * 2.0; - col += model_env(reflect(rd, n), 0.05, l, fill) * F; - col += s_lin(0.5, 0.9, 1.0) * pow(1.0 - cosi, 4.0) * 0.6; - return col; -} - // Khronos PBR Neutral : garde les teintes, ne comprime que les hautes lumières ; puis sRGB. float3 model_tonemap(float3 c) { @@ -1260,10 +1291,6 @@ float3 model_shade(float3 q, float3 n, float3 rd, float3 L, float fall, float sh float3 l, float3 fill) { int id = sculpt_id(); - if (id > 0 && (id - 1) / 2 == 1) - { - return model_tonemap(model_shade_crystal(n, rd, L, fall, l, fill)); - } SculptMat m; // Sur un sprite, reflets et brillance sur les arrondis seulement : le dessus plat d'un sprite // sombre virerait au gris sous la lampe. @@ -1306,7 +1333,12 @@ static const int STAGE_NORMAL = 1; // quatre sondes en tétraèdre autour du poi static const int STAGE_AO = 2; // cinq sondes le long de la normale, jusqu'à 0,08 du prototype static const int STAGE_SELF = 3; // pénombre du modèle sur lui-même, vers la lampe (32 pas) static const int STAGE_PLANE = 4; // le point du plan : son contact, puis sa pénombre (32 pas) -static const int STAGE_DONE = 5; +static const int STAGE_EDGE = 5; // quatre sondes à un demi-pixel du point touché : un bord ? +static const int STAGE_SHADE = 6; // l'ombrage d'un échantillon, puis le rayon suivant s'il en reste +static const int STAGE_DONE = 7; + +// Les trois rayons de plus d'un pixel au bord, en pixels autour de son centre. +static const float2 MODEL_SUBPIXEL[3] = { float2(0.35, 0.2), float2(-0.35, 0.2), float2(0.0, -0.4) }; // Sonde `k` de la normale, sommet d'un tétraèdre. float3 model_tetra(int k) @@ -1347,8 +1379,14 @@ float4 cursor_model(float2 local) float3 lamp = float3(color.rg + sprite_size() * 0.5, 0.0) + l * SCULPT_LAMP_DIST; // Silhouette antialiasée : un rayon qui frôle le modèle à moins d'un pixel le couvre en partie - // (`best`, la plus petite distance rencontrée, en pixels). - float2 tb = ray_box(ro, rd, lo - 0.02, hi + 0.02); + // (`best`, la plus petite distance rencontrée, en pixels). Les bords intérieurs (deux matières, + // une arête, un arrondi serré) sont suréchantillonnés : `STAGE_EDGE` sonde le modèle à un + // demi-pixel du point touché, dans son plan tangent ; si la matière change ou que la surface + // s'écarte, trois rayons de plus (`MODEL_SUBPIXEL`) sont marchés et ombrés, avec l'occlusion et + // les ombres du premier. Chaque échantillon passe par `STAGE_SHADE`, seul appel de + // `model_shade` ; le pixel est leur moyenne. + float3 ray = rd; + float2 tb = ray_box(ro, ray, lo - 0.02, hi + 0.02); int stage = tb.x < tb.y && tb.y > 0.0 ? STAGE_MARCH : STAGE_DONE; // Le point du plan derrière le pixel est à régler avant le prochain tour. bool plane_next = stage == STAGE_DONE; @@ -1356,8 +1394,10 @@ float4 cursor_model(float2 local) float t = max(tb.x, 0.0); float best = 1e9; float t_best = t; + float d_best = 0.0; float mat = 0.0; float cov = 0.0; + float cov_s = 0.0; float3 q = 0.0; float3 n = 0.0; float3 L = 0.0; @@ -1371,14 +1411,19 @@ float4 cursor_model(float2 local) float inside = 0.0; float contact = 0.0; float dropped = 0.0; - [loop] for (int it = 0; it < 180; it++) + // Sous-échantillons restants, échantillons ombrés, et leur somme (couleur × couverture, + // couverture). + int sub = 0; + int shaded = 0; + float4 acc = 0.0; + [loop] for (int it = 0; it < 500; it++) { if (plane_next) { // Le plan, là où le modèle ne couvre pas tout le pixel : ombre portée et ombre de // contact, seulement à l'intérieur de l'écran (`mb.xy` = sa demi-taille, px du plan). plane_next = false; - stage = STAGE_DONE; + stage = cov > 0.0 ? STAGE_SHADE : STAGE_DONE; float denom = dot(rd, nz); if (cov < 1.0 && denom < -1e-4) { @@ -1393,15 +1438,54 @@ float4 cursor_model(float2 local) } } } + if (stage == STAGE_SHADE) + { + if (cov_s > 0.0) + { + float3 tl = lamp - q; + float fall = SCULPT_LAMP_DIST * SCULPT_LAMP_DIST / dot(tl, tl); + float3 c = model_shade(q, n, ray, normalize(tl), fall, sh, ao, mat, l, fill); + acc += float4(c * cov_s, cov_s); + } + shaded++; + stage = STAGE_DONE; + if (sub > 0) + { + // Le rayon suivant, décalé dans le pixel ; il repart de sa propre entrée dans la + // boîte, pour ne pas manquer ce qui passerait devant le premier. + float3 dws = float3(local + MODEL_SUBPIXEL[3 - sub] + src.xy, -persp); + sub--; + dlen = length(dws); + ray = plane_to_model(world_to_plane(dws / dlen, f), f); + tb = ray_box(ro, ray, lo - 0.02, hi + 0.02); + cov_s = 0.0; + stage = STAGE_SHADE; + if (tb.x < tb.y && tb.y > 0.0) + { + stage = STAGE_MARCH; + k = 0; + t = max(tb.x, 0.0); + best = 1e9; + t_best = t; + } + } + continue; + } float3 pos; if (stage == STAGE_MARCH) { - pos = ro + rd * t; + pos = ro + ray * t; } else if (stage == STAGE_NORMAL) { pos = q + 0.002 * model_tetra(k); } + else if (stage == STAGE_EDGE) + { + float3 side = normalize(cross(n, ray)); + float3 e = (k >= 2 ? cross(n, side) : side) * (k % 2 == 1 ? -1.0 : 1.0); + pos = q + e * 0.5 * t_best / dlen; + } else if (stage == STAGE_AO) { pos = q + (0.01 + 0.0175 * k) * SCULPT_SCALE * n; @@ -1428,22 +1512,38 @@ float4 cursor_model(float2 local) { best = hit ? 0.0 : d / fp; t_best = t; + d_best = d; mat = m.y; } t += d * stride; k++; if (hit || t > tb.y || k == 96) { - cov = saturate(1.0 - best); - if (cov > 0.0) + cov_s = saturate(1.0 - best); + if (shaded > 0) { - q = ro + rd * t_best; - stage = STAGE_NORMAL; - k = 0; + stage = STAGE_SHADE; + if (cov_s > 0.0) + { + q = ro + ray * t_best; + n = 0.0; + stage = STAGE_NORMAL; + k = 0; + } } else { - plane_next = true; + cov = cov_s; + if (cov > 0.0) + { + q = ro + ray * t_best; + stage = STAGE_NORMAL; + k = 0; + } + else + { + plane_next = true; + } } } } @@ -1454,6 +1554,21 @@ float4 cursor_model(float2 local) if (k == 4) { n = normalize(n); + k = 0; + stage = shaded > 0 ? STAGE_SHADE : STAGE_EDGE; + } + } + else if (stage == STAGE_EDGE) + { + // Autre matière, ou surface qui s'écarte du plan tangent de plus de 2 % d'un pixel + // (le point touché flotte déjà de `d_best` au-dessus d'elle). + if (m.y != mat || abs(d - d_best) > 0.02 * t_best / dlen) + { + sub = 3; + } + k++; + if (k == 4) + { stage = STAGE_AO; k = 0; ao = 0.0; @@ -1504,7 +1619,7 @@ float4 cursor_model(float2 local) k = 1; if (!(tbp.x < tbp.y && tbp.y > 0.0)) { - stage = STAGE_DONE; + stage = cov > 0.0 ? STAGE_SHADE : STAGE_DONE; } } else @@ -1516,21 +1631,15 @@ float4 cursor_model(float2 local) { res = saturate(res); dropped = 1.0 - res * res * (3.0 - 2.0 * res); - stage = STAGE_DONE; + stage = cov > 0.0 ? STAGE_SHADE : STAGE_DONE; } } } - float3 rgb = 0.0; - if (cov > 0.0) - { - float3 tl = lamp - q; - float fall = SCULPT_LAMP_DIST * SCULPT_LAMP_DIST / dot(tl, tl); - rgb = model_shade(q, n, rd, normalize(tl), fall, sh, ao, mat, l, fill); - } + float w = 1.0 / max(shaded, 1); float shadow = inside * max(dropped * MODEL_SHADOW_ALPHA, contact * MODEL_CONTACT_ALPHA); - float a = cov * color.a; - return float4(rgb * a, a + (1.0 - a) * shadow * color.a); // prémultiplié, ombre noire + float a = acc.a * w * color.a; + return float4(acc.rgb * w * color.a, a + (1.0 - a) * shadow * color.a); // prémultiplié, ombre noire } // ============ Impact du clic (mode 16) ============ diff --git a/crates/compositor/src/shaders.metal b/crates/compositor/src/shaders.metal index 6c019c3ec..01cce9358 100644 --- a/crates/compositor/src/shaders.metal +++ b/crates/compositor/src/shaders.metal @@ -590,8 +590,6 @@ inline int sculpt_id(constant Layer &layer) constant float SCULPT_SCALE = 0.85; constant float SCULPT_HOVER = 0.05; constant float SCULPT_VOX = 0.0625; -constant float SCULPT_AR_ROUND = 0.03; -constant float SCULPT_HAND_ZC = 0.185; constant float SCULPT_ZREF_ARROW = 0.2; constant float SCULPT_ZREF_HAND = 0.185; constant float SCULPT_LAMP_DIST = 1.9; @@ -613,13 +611,6 @@ inline float2 s_opu(float2 a, float2 b) return a.x < b.x ? a : b; } -static float s_capsule(float3 p, float3 a, float3 b, float r) -{ - float3 pa = p - a, ba = b - a; - float h = saturate(dot(pa, ba) / dot(ba, ba)); - return length(pa - ba * h) - r; -} - static float s_round_box(float3 p, float3 b, float r) { float3 q = abs(p) - b + r; @@ -646,34 +637,6 @@ inline float2 s_rot(float2 v, float a) return float2(c * v.x - s * v.y, s * v.x + c * v.y); } -constant float2 SCULPT_ARROW[7] = { - float2(0.0, 0.0), float2(0.0, -0.86), float2(0.215, -0.665), float2(0.37, -1.0), - float2(0.53, -0.93), float2(0.38, -0.60), float2(0.64, -0.60) -}; - -static float s_arrow2(float2 p) -{ - float d = dot(p - SCULPT_ARROW[0], p - SCULPT_ARROW[0]); - float s = 1.0; - int j = 6; - for (int i = 0; i < 7; i++) - { - float2 e = SCULPT_ARROW[j] - SCULPT_ARROW[i]; - float2 w = p - SCULPT_ARROW[i]; - float2 b = w - e * saturate(dot(w, e) / dot(e, e)); - d = min(d, dot(b, b)); - bool c0 = p.y >= SCULPT_ARROW[i].y; - bool c1 = p.y < SCULPT_ARROW[j].y; - bool c2 = e.x * w.y > e.y * w.x; - if ((c0 && c1 && c2) || (!c0 && !c1 && !c2)) - { - s = -s; - } - j = i; - } - return s * sqrt(d); -} - static float s_star5(float2 p, float r, float rf) { const float2 k1 = float2(0.809016994375, -0.587785252292); @@ -688,262 +651,335 @@ static float s_star5(float2 p, float r, float rf) return length(p - ba * h) * sign(p.y * ba.x - p.x * ba.y); } -static float s_arrow_solid(float3 p, float h, float re, float dome) +static float s_vesica(float2 p, float r, float d) { - float d2 = s_arrow2(p.xy) - SCULPT_AR_ROUND; - float hh = h + dome * smoothstep(0.0, 0.07, -d2); - return s_extrude(d2, p.z - (SCULPT_HOVER + h + dome), hh, re); + p = abs(p); + float b = sqrt(r * r - d * d); + return (p.y - b) * d > p.x * b ? length(p - float2(0.0, b)) : length(p + float2(d, 0.0)) - r; } -static float s_piping(float3 p, float ztop) -{ - float d2 = s_arrow2(p.xy) - SCULPT_AR_ROUND; - return length(float2(d2 + 0.075, p.z - ztop)) - 0.017; -} +// Curseurs cercles (cf. HLSL) : Studio Ink, Pop Coral, Star Sprout. +constant float2 RIM_POLY[28] = { + float2(0.0, -0.06), float2(0.0, -0.7712), float2(0.165, -0.6325), float2(0.313, -0.9318), + float2(0.4234, -0.8557), float2(0.2672, -0.5686), float2(0.456, -0.5393), + float2(-0.173, -0.5237), float2(-0.016, -0.75), float2(0.329, -0.75), float2(0.411, -0.592), + float2(0.411, -0.43), float2(0.19, -0.435), float2(-0.03, -0.44), + float2(-0.1825, -0.6215), float2(0.0037, -0.9195), float2(0.3462, -0.9195), float2(0.467, -0.6906), + float2(0.467, -0.49), float2(0.2, -0.49), float2(-0.03, -0.49), + float2(-0.1807, -0.5534), float2(0.0194, -0.872), float2(0.3485, -0.872), float2(0.47, -0.6795), + float2(0.47, -0.49), float2(0.2, -0.49), float2(-0.03, -0.49) +}; -static float s_glove(float3 p, float zc) +constant float4 RIM_GLOVE[27] = { + float4(0.0, -0.1077, 0.0, -0.6), float4(0.1493, -0.3279, 0.1493, -0.6), + float4(0.2863, -0.3654, 0.2863, -0.6), float4(0.4193, -0.4043, 0.4193, -0.6), + float4(-0.2, -0.478, -0.075, -0.625), float4(0.0788, -0.25, 0.0788, -0.3992), + float4(0.2188, -0.3, 0.2188, -0.4234), float4(0.3552, -0.33, 0.3552, -0.4534), + float4(-0.0845, -0.5386, 0.762, 0.648), + float4(0.0, -0.1186, 0.0, -0.65), float4(0.1614, -0.3838, 0.1614, -0.65), + float4(0.3177, -0.427, 0.3177, -0.65), float4(0.4645, -0.4848, 0.4645, -0.65), + float4(-0.215, -0.535, -0.1208, -0.6864), float4(0.0832, -0.3, 0.0832, -0.48), + float4(0.2414, -0.35, 0.2414, -0.482), float4(0.3959, -0.41, 0.3959, -0.533), + float4(-0.122, -0.5718, 0.867, 0.498), + float4(0.0, -0.122, 0.0, -0.65), float4(0.172, -0.387, 0.172, -0.65), + float4(0.3267, -0.427, 0.3267, -0.65), float4(0.4727, -0.4867, 0.4727, -0.65), + float4(-0.199, -0.483, -0.102, -0.645), float4(0.0927, -0.3, 0.0927, -0.476), + float4(0.2493, -0.35, 0.2493, -0.478), float4(0.402, -0.41, 0.402, -0.52), + float4(-0.0993, -0.5267, 0.858, 0.514) +}; + +constant float4 RIM_GLOVE_R[9] = { + float4(0.0552, 0.047, 0.045, 0.0375), float4(0.052, 0.0235, 0.0225, 0.0225), float4(0.0552, -0.548, 0.0525, 0.0), + float4(0.064, 0.0585, 0.0585, 0.0515), float4(0.06, 0.0197, 0.0178, 0.0172), float4(0.0648, -0.6, 0.058, 0.0), + float4(0.066, 0.0553, 0.0553, 0.0507), float4(0.063, 0.024, 0.022, 0.02), float4(0.0647, -0.555, 0.056, 0.0) +}; + +static float s_rim_poly(float2 p, int base) { - float index = s_capsule(p, float3(0.0, -0.10, zc), float3(0.0, -0.52, zc), 0.098); - float palm = s_round_box(p - float3(0.185, -0.70, zc), float3(0.255, 0.19, 0.105), 0.1); - float f1 = s_capsule(p, float3(0.17, -0.57, zc + 0.012), float3(0.17, -0.41, zc + 0.045), 0.086); - float f2 = s_capsule(p, float3(0.31, -0.59, zc + 0.01), float3(0.31, -0.45, zc + 0.04), 0.08); - float f3 = s_capsule(p, float3(0.435, -0.625, zc + 0.005), float3(0.435, -0.52, zc + 0.03), 0.07); - float thumb = s_capsule(p, float3(0.03, -0.77, zc + 0.03), float3(-0.165, -0.60, zc + 0.065), 0.082); - float d = s_smin(palm, min(f1, min(f2, f3)), 0.035); - d = s_smin(d, index, 0.05); - return s_smin(d, thumb, 0.05); + float d = dot(p - RIM_POLY[base], p - RIM_POLY[base]); + float s = 1.0; + int j = base + 6; + for (int i = base; i < base + 7; i++) + { + float2 e = RIM_POLY[j] - RIM_POLY[i]; + float2 w = p - RIM_POLY[i]; + float2 b = w - e * saturate(dot(w, e) / dot(e, e)); + d = min(d, dot(b, b)); + bool c0 = p.y >= RIM_POLY[i].y; + bool c1 = p.y < RIM_POLY[j].y; + bool c2 = e.x * w.y > e.y * w.x; + if ((c0 && c1 && c2) || (!c0 && !c1 && !c2)) + { + s = -s; + } + j = i; + } + return s * sqrt(d); } -static float s_cuff(float3 p, float zc) +static float2 s_rim_glove(float2 p, int g, float palm) { - float3 q = p - float3(0.185, -0.925, zc); - float2 ab = float2(0.272, 0.12); - float e = (length(q.xz / ab) - 1.0) * min(ab.x, ab.y); - return s_extrude(e, q.y, 0.07, 0.06); + float4 r0 = RIM_GLOVE_R[3 * g]; + float4 r1 = RIM_GLOVE_R[3 * g + 1]; + float4 r2 = RIM_GLOVE_R[3 * g + 2]; + int i = 9 * g; + float f = min(sd_segment(p, RIM_GLOVE[i].xy, RIM_GLOVE[i].zw) - r0.x, + sd_segment(p, RIM_GLOVE[i + 1].xy, RIM_GLOVE[i + 1].zw) - r0.y); + f = min(f, sd_segment(p, RIM_GLOVE[i + 2].xy, RIM_GLOVE[i + 2].zw) - r0.z); + f = min(f, sd_segment(p, RIM_GLOVE[i + 3].xy, RIM_GLOVE[i + 3].zw) - r0.w); + f = min(f, sd_segment(p, RIM_GLOVE[i + 4].xy, RIM_GLOVE[i + 4].zw) - r1.x); + float grooves = min(sd_segment(p, RIM_GLOVE[i + 5].xy, RIM_GLOVE[i + 5].zw) - r1.y, + sd_segment(p, RIM_GLOVE[i + 6].xy, RIM_GLOVE[i + 6].zw) - r1.z); + grooves = min(grooves, sd_segment(p, RIM_GLOVE[i + 7].xy, RIM_GLOVE[i + 7].zw) - r1.w); + float sil = s_smin(palm, f, 0.03); + float4 v = RIM_GLOVE[i + 8]; + float wedge = max(max(-dot(p - v.xy, v.zw), p.x + r2.x), r2.y - p.y); + return float2(sil, max(sil, -min(grooves, wedge))); } -static float2 s_sprout(float3 p, float3 c) +static float2 s_piece(float d, float dc, float sil, float z, float w, float zt, float h, float bump, float mat) { - float3 q = p - c; - float st2 = s_star5(q.xy, 0.125, 0.52) - 0.022; - float2 r = float2(s_extrude(st2, q.z, 0.038, 0.034), 3.0); - float3 e = float3(abs(q.x) - 0.032, q.y + 0.005, q.z - 0.036); - r = s_opu(r, float2(length(e) - 0.0135, 5.0)); - float3 l1 = float3(s_rot(q.xy - float2(-0.04, 0.15), -0.6), q.z); - float3 l2 = float3(s_rot(q.xy - float2(0.045, 0.155), 0.7), q.z); - float leaves = min(s_ellipsoid(l1, float3(0.03, 0.058, 0.02)), s_ellipsoid(l2, float3(0.03, 0.058, 0.02))); - return s_opu(r, float2(leaves, 4.0)); + float tray = s_extrude(sil - w, z - 0.5 * (SCULPT_HOVER + zt), 0.5 * (zt - SCULPT_HOVER), 0.012); + float bead = length(float2(d - 0.5 * w, z - zt)) - 0.5 * w; + float u = saturate(-dc / 0.045); + float cushion = 0.8 * s_extrude(dc, z - zt, h + 0.022 * u * (2.0 - u) + bump, 0.75 * h); + return s_opu(float2(min(tray, bead), 5.0), float2(cushion, mat)); } -// Pixel Candy : une ligne par entier, bit c = colonne c (cf. HLSL et `sculpt.rs`). -constant int SCULPT_ARROWPIX[16] = { 0, 1, 3, 7, 31, 63, 127, 255, 511, 63, 55, 115, 113, 224, 224, 64 }; -constant int SCULPT_ARROWBACK[18] = { - 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 255, 511, 511, 999, 995, 960, 192 -}; -constant int SCULPT_HANDPIX[15] = { 4, 14, 14, 14, 110, 878, 7022, 7022, 8190, 8191, 8191, 8191, 8190, 4092, 4092 }; -constant int SCULPT_HANDBACK[17] = { - 28, 62, 62, 62, 510, 4094, 32766, 32766, 32766, 32767, 32767, 32767, 32767, 32767, 32766, 16380, 16380 -}; -constant float2 SCULPT_HANDSPAN[17] = { - float2(2.0, 4.0), float2(1.0, 5.0), float2(1.0, 5.0), float2(1.0, 5.0), float2(1.0, 8.0), - float2(1.0, 11.0), float2(1.0, 14.0), float2(1.0, 14.0), float2(1.0, 14.0), float2(0.0, 14.0), - float2(0.0, 14.0), float2(0.0, 14.0), float2(0.0, 14.0), float2(0.0, 14.0), float2(1.0, 14.0), - float2(2.0, 13.0), float2(2.0, 13.0) -}; - -inline float2 s_grid_origin(int shape) +static float2 s_paper(float d, float sil, float z, float w) { - return shape == 0 ? float2(0.0, 0.0) : float2(-2.5 * SCULPT_VOX, 0.0); + float navy = s_extrude(sil - w, z - 0.5 * (SCULPT_HOVER + 0.15), 0.5 * (0.15 - SCULPT_HOVER), 0.006); + float sheet = s_extrude(d, z - 0.16, 0.01, 0.004); + return s_opu(float2(navy, 5.0), float2(sheet, 1.0)); } -inline bool s_bit(int row, int r, int c, int rows, int cols) +inline float s_bump(float2 p, float2 c, float r) { - return r >= 0 && r < rows && c >= 0 && c < cols && ((row >> clamp(c, 0, 31)) & 1) == 1; + float k = saturate(1.0 - dot(p - c, p - c) / (r * r)); + return k * k; } -static bool s_occ(float2 id, int shape) +static float s_dash(float2 p, float2 a, float2 b, float ra, float rb) { - int c = int(id.x); - int r = -int(id.y) - 1; - if (shape == 0) + p -= a; + b -= a; + float hb = dot(b, b); + float2 q = float2(abs(dot(p, float2(b.y, -b.x))), dot(p, b)) / hb; + float2 c = float2(sqrt(hb - (ra - rb) * (ra - rb)), ra - rb); + float k = c.x * q.y - c.y * q.x; + if (k < 0.0) { - return s_bit(SCULPT_ARROWPIX[clamp(r, 0, 15)], r, c, 16, 16); + return sqrt(hb * dot(q, q)) - ra; } - return s_bit(SCULPT_HANDPIX[clamp(r, 0, 14)], r, c, 15, 13); -} - -static bool s_occ_back(float2 id, int shape) -{ - int c = int(id.x) + 1; - int r = -int(id.y); - if (shape == 0) + if (k > c.x) { - return s_bit(SCULPT_ARROWBACK[clamp(r, 0, 17)], r, c, 18, 17); + return sqrt(hb * (dot(q, q) + 1.0 - 2.0 * q.y)) - rb; } - return s_bit(SCULPT_HANDBACK[clamp(r, 0, 16)], r, c, 17, 15); + return dot(c, q) - ra; } -static float2 s_voxels(float3 p, int shape) +static float2 s_rimmed(float3 p, int theme, int shape) { - float2 o = s_grid_origin(shape); - float2 cell = floor((p.xy - o) / SCULPT_VOX); - float2 bc = shape == 0 ? float2(0.33, -0.5) : float2(0.25, -0.47); - float2 bh = shape == 0 ? float2(0.44, 0.61) : float2(0.48, 0.55); - float2 bq = max(abs(p.xy - bc) - bh, 0.0); - float bz = max(abs(p.z - (SCULPT_HOVER + 0.07)) - 0.07, 0.0); - float far = max(SCULPT_VOX, sqrt(dot(bq, bq) + bz * bz)); - float dF = far; - float dB = far; - const float3 half_cell = float3(0.5 * SCULPT_VOX, 0.5 * SCULPT_VOX, 0.04); - for (int j = -1; j <= 1; j++) + bool arrow = shape == 0; + int g = theme == 4 ? 0 : (theme == 0 ? 1 : 2); + float poly = s_rim_poly(p.xy, arrow ? 0 : 7 * g + 7); + float2 body = float2(poly, poly); + float w = 0.06, zt = 0.17, h = 0.03; + float bump = 0.022 * s_bump(p.xy, float2(0.15, -0.45), 0.3); + float back = 1e9; + if (!arrow) { - for (int i = -1; i <= 1; i++) + w = RIM_GLOVE_R[3 * g + 2].z; + for (int i = 0; i < 2; i++) { - float2 id = cell + float2(float(i), float(j)); - float3 q = float3(p.xy - (o + (id + 0.5) * SCULPT_VOX), p.z); - if (s_occ(id, shape)) + float2 q = p.xy + (i == 1 ? float2(0.03, 0.04) : float2(0.0, 0.0)); + float2 v = s_rim_glove(q, g, (i == 1 ? s_rim_poly(q, 7 * g + 7) : poly) - 0.05); + if (i == 1) { - dF = min(dF, s_round_box(q - float3(0.0, 0.0, SCULPT_HOVER + 0.1), half_cell, 0.009)); + back = v.x - w; } - if (s_occ_back(id, shape)) + else { - dB = min(dB, s_round_box(q - float3(0.0, 0.0, SCULPT_HOVER + 0.04), half_cell, 0.009)); + body = v; + if (theme != 2 || v.x - w <= 0.0) + { + break; + } } } + zt = 0.185; + h = 0.028; + bump = 0.03 * s_bump(p.xy, float2(0.19, -0.62), 0.28); } - return dF < dB ? float2(dF, 6.0) : float2(dB, 7.0); -} - -static float s_pixel_hand_dist(float2 p) -{ - float d = 1e9; - for (int r = 0; r < 17; r++) + float dc = body.y; + if (arrow && theme == 0) { - float2 x = (SCULPT_HANDSPAN[r] + float2(-3.5, -2.5)) * SCULPT_VOX; - float2 y = float2(-float(r), 1.0 - float(r)) * SCULPT_VOX; - float2 q = max(max(float2(x.x, y.x) - p, p - float2(x.y, y.y)), 0.0); - d = min(d, length(q)); + body -= 0.015; + w = 0.045; + dc = 1.0; } - return d; + float2 r = theme == 2 ? s_paper(body.y, body.x, p.z, w) + : s_piece(body.y, dc, body.x, p.z, w, zt, h, bump, 1.0); + if (theme == 0) + { + if (arrow) + { + float band = s_extrude(abs(poly + 0.0075) - 0.0225, p.z - zt, 0.028, 0.012); + r = s_opu(r, float2(band, 2.0)); + } + return r; + } + if (theme == 2) + { + float dash; + if (arrow) + { + float back = s_rim_poly(p.xy + float2(0.025, 0.05), 0) - 0.06; + r = s_opu(r, float2(s_extrude(back, p.z - 0.08, 0.03, 0.006), 2.0)); + dash = min(s_dash(p.xy, float2(-0.1179, -0.1374), float2(-0.1799, -0.0443), 0.028, 0.045), + s_dash(p.xy, float2(-0.1347, -0.2493), float2(-0.248, -0.2056), 0.026, 0.042)); + } + else + { + r = s_opu(r, float2(s_extrude(back, p.z - 0.08, 0.03, 0.006), 3.0)); + dash = min(s_dash(p.xy, float2(0.216, -0.1695), float2(0.2593, -0.0685), 0.028, 0.042), + s_dash(p.xy, float2(0.3054, -0.2339), float2(0.3949, -0.1652), 0.0275, 0.041)); + } + return s_opu(r, float2(s_extrude(dash, p.z - 0.11, 0.06, 0.006), 3.0)); + } + if (!arrow) + { + float2 cq = p.xy - float2(0.1541, -0.9047); + float2 bq = abs(float2(cq.x, cq.y - 0.2066 * cq.x * cq.x)) - float2(0.214, 0.0361); + float cuff = length(max(bq, 0.0)) + min(max(bq.x, bq.y), 0.0) - 0.03; + r = s_opu(r, s_piece(cuff, cuff, cuff, p.z, 0.0477, 0.225, 0.025, 0.0, 2.0)); + } + float2 c = arrow ? float2(0.6118, -0.8079) : float2(0.15, -0.885); + float rs = arrow ? 0.1678 : 0.128; + float zs = arrow ? 0.23 : 0.285; + float2 q = s_rot(p.xy - c, arrow ? 0.2443 : 0.0) / rs; + float star = (s_star5(q, 0.82, 0.55) - 0.18) * rs; + float leaves = min(s_vesica(s_rot(q - float2(-0.33, 1.38), -0.925), 0.4296, 0.2626), + s_vesica(s_rot(q - float2(0.53, 1.37), 0.873), 0.4296, 0.2626)) * rs; + r = s_opu(r, s_piece(leaves, leaves, leaves, p.z, 0.038, zs - 0.02, 0.018, 0.0, 4.0)); + r = s_opu(r, s_piece(star, star, star, p.z, 0.038, zs, 0.022, 0.018 * s_bump(q, float2(0.0), 1.0), 3.0)); + float3 e = float3(abs(q.x) - 0.25, q.y - 0.08, (p.z - zs - 0.056) / rs); + return s_opu(r, float2(s_ellipsoid(e, float3(0.075, 0.13, 0.1)) * rs, 5.0)); +} + +// Pixel Candy : tables générées par scripts/generate-pixel-candy-voxels.mjs (cf. HLSL). +// +constant int PIX_BODY[32] = { 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 127, 247, 243, 480, 192, 48, 120, 120, 120, 504, 4088, 32760, 65534, 65535, 65535, 65534, 32766, 32764, 16380, 16376, 16376 }; +constant int PIX_LINE[32] = { 1, 3, 5, 9, 17, 33, 65, 129, 257, 513, 1985, 73, 149, 147, 288, 192, 48, 72, 72, 72, 456, 3656, 29256, 37454, 32777, 32769, 32770, 16386, 16388, 8196, 8200, 16376 }; +constant int PIX_HI[32] = { 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 18, 34, 32, 64, 0, 0, 16, 16, 16, 16, 16, 16, 16, 22, 18, 4, 4, 8, 8, 16, 0 }; +constant int PIX_SHADE[32] = { 0, 0, 0, 4, 8, 16, 32, 64, 128, 448, 40, 36, 64, 64, 128, 0, 0, 32, 32, 32, 32, 288, 2336, 18720, 16384, 16384, 16384, 8192, 8192, 4096, 8160, 0 }; +constant int PIX_RECT_N[3] = { 0, 17, 29 }; +constant int4 PIX_GRID[2] = { + int4(11, 16, 0, 0), + int4(16, 16, 16, 0) +}; +constant float2 PIX_ORIGIN[2] = { + float2(0.0, 0.0), + float2(-0.3125, 0.0) +}; +constant float4 PIX_BOX[2] = { + float4(0.3438, -0.5, 0.3438, 0.5), + float4(0.1875, -0.5, 0.5, 0.5) +}; +constant float4 PIX_RECT[29] = { + float4(0.0313, -0.0313, 0.0313, 0.0313), + float4(0.0625, -0.0938, 0.0625, 0.0313), + float4(0.0938, -0.1563, 0.0938, 0.0313), + float4(0.125, -0.2188, 0.125, 0.0313), + float4(0.1563, -0.2813, 0.1563, 0.0313), + float4(0.1875, -0.3438, 0.1875, 0.0313), + float4(0.2188, -0.4063, 0.2188, 0.0313), + float4(0.25, -0.4688, 0.25, 0.0313), + float4(0.2813, -0.5313, 0.2813, 0.0313), + float4(0.3125, -0.5938, 0.3125, 0.0313), + float4(0.3438, -0.6563, 0.3438, 0.0313), + float4(0.2188, -0.7188, 0.2188, 0.0313), + float4(0.0938, -0.7813, 0.0938, 0.0313), + float4(0.375, -0.8125, 0.125, 0.0625), + float4(0.0625, -0.8438, 0.0625, 0.0313), + float4(0.4375, -0.9063, 0.125, 0.0313), + float4(0.4375, -0.9688, 0.0625, 0.0313), + float4(0.0, -0.0313, 0.0625, 0.0313), + float4(0.0, -0.1563, 0.125, 0.0938), + float4(0.0625, -0.2813, 0.1875, 0.0313), + float4(0.1563, -0.3438, 0.2813, 0.0313), + float4(0.25, -0.4063, 0.375, 0.0313), + float4(0.2188, -0.4688, 0.4688, 0.0313), + float4(0.1875, -0.5625, 0.5, 0.0625), + float4(0.2188, -0.6563, 0.4688, 0.0313), + float4(0.1875, -0.7188, 0.4375, 0.0313), + float4(0.2188, -0.7813, 0.4063, 0.0313), + float4(0.1875, -0.8438, 0.375, 0.0313), + float4(0.2188, -0.9375, 0.3438, 0.0625) +}; +// + +inline bool s_pix_bit(int m, int c) +{ + return ((m >> clamp(c, 0, 31)) & 1) == 1; } -static float s_gem_edge(float3 p, float2 a, float2 b, float z0) +static bool s_pix_body(int shape, int c, int r) { - float2 e = b - a; - float len = length(e); - float2 d = e / len; - float2 q = p.xy - a; - float dist = dot(q, float2(-d.y, d.x)); - float along = abs(dot(q, d) - 0.5 * len); - float z = p.z - z0; - float girdle = (z - 0.035 - 2.2 * dist) * 0.4138; - float crown = (z - 0.07 - 0.6 * dist + 0.18 * along) * 0.8438; - return max(-dist, max(girdle, crown)); + int4 g = PIX_GRID[shape]; + return c >= 0 && c < g.x && r >= 0 && r < g.y && s_pix_bit(PIX_BODY[g.z + clamp(r, 0, g.y - 1)], c); } -static float s_gem_arrow(float3 p) +inline int2 s_pix_cell(float2 p, int shape) { - float z0 = SCULPT_HOVER + 0.03; - float slab = max(p.z - z0 - 0.2, z0 - 0.03 - p.z); - float head = max(slab, max(s_gem_edge(p, float2(-0.02, 0.03), float2(-0.02, -0.88), z0), - max(s_gem_edge(p, float2(-0.02, -0.88), float2(0.66, -0.61), z0), - s_gem_edge(p, float2(0.66, -0.61), float2(-0.02, 0.03), z0)))); - float tail = max(slab, max(max(s_gem_edge(p, float2(0.215, -0.665), float2(0.37, -1.0), z0), - s_gem_edge(p, float2(0.37, -1.0), float2(0.53, -0.93), z0)), - max(s_gem_edge(p, float2(0.53, -0.93), float2(0.38, -0.60), z0), - s_gem_edge(p, float2(0.38, -0.60), float2(0.215, -0.665), z0)))); - return min(head, tail); + float2 o = PIX_ORIGIN[shape]; + return int2(int(floor((p.x - o.x) / SCULPT_VOX)), int(floor((o.y - p.y) / SCULPT_VOX))); } -static float s_facet_capsule(float3 p, float3 a, float3 b, float r, float spin) +static float2 s_voxels(float3 p, int shape) { - float3 u = normalize(b - a); - float3 v = normalize(cross(u, float3(0.0, 0.0, 1.0))); - float3 w = cross(u, v); - float3 q = p - b; - float h = dot(q, u); - float2 rad = float2(dot(q, v), dot(q, w)); - float d = max(h - r, -dot(p - a, u) - r); - for (int k = 0; k < 6; k++) + float4 b = PIX_BOX[shape]; + float2 bq = max(abs(p.xy - b.xy) - b.zw, 0.0); + float bz = max(abs(p.z - (SCULPT_HOVER + 0.07)) - 0.07, 0.0); + float d = max(SCULPT_VOX, sqrt(dot(bq, bq) + bz * bz)); + float2 o = PIX_ORIGIN[shape]; + int2 cell = s_pix_cell(p.xy, shape); + const float3 cube = float3(0.5 * SCULPT_VOX, 0.5 * SCULPT_VOX, 0.07); + for (int j = -1; j <= 1; j++) { - float an = spin + float(k) * 1.0471976; - float s = dot(rad, float2(cos(an), sin(an))); - d = max(d, s - r); - d = max(d, dot(rad, float2(cos(an + 0.5236), sin(an + 0.5236))) * 0.8660 + h * 0.5 - r); - d = max(d, s * 0.5 + h * 0.8660 - r); + for (int i = -1; i <= 1; i++) + { + int c = cell.x + i; + int r = cell.y + j; + if (s_pix_body(shape, c, r)) + { + float3 q = float3(p.x - o.x - (float(c) + 0.5) * SCULPT_VOX, p.y - o.y + (float(r) + 0.5) * SCULPT_VOX, + p.z - SCULPT_HOVER - 0.07); + d = min(d, s_round_box(q, cube, 0.006)); + } + } } - return d; + return float2(d, 6.0); } -constant float3 SCULPT_FACETS[10] = { - float3(1.0, 0.0, 0.0), float3(0.0, 1.0, 0.0), float3(0.0, 0.0, 1.0), - float3(0.7071, 0.7071, 0.0), float3(0.7071, 0.0, 0.7071), float3(0.0, 0.7071, 0.7071), - float3(0.5774, 0.5774, 0.5774), float3(0.4472, 0.0, 0.8944), float3(0.0, 0.4472, 0.8944), - float3(0.3015, 0.3015, 0.9045) -}; - -static float s_facet_ellipsoid(float3 p, float3 r) +static float s_pixel_outline(float2 p, int shape) { - float3 q = abs(p); - float d = -1e9; - for (int i = 0; i < 10; i++) + float d = 1e9; + for (int i = PIX_RECT_N[shape]; i < PIX_RECT_N[shape + 1]; i++) { - d = max(d, dot(q, SCULPT_FACETS[i]) - length(r * SCULPT_FACETS[i])); + float2 q = abs(p - PIX_RECT[i].xy) - PIX_RECT[i].zw; + d = min(d, length(max(q, 0.0)) + min(max(q.x, q.y), 0.0)); } return d; } -static float s_crystal_hand(float3 p) -{ - float zc = SCULPT_HAND_ZC; - float d = s_facet_ellipsoid(p - float3(0.185, -0.70, zc), float3(0.3, 0.235, 0.125)); - d = min(d, s_facet_capsule(p, float3(0.0, -0.52, zc), float3(0.0, -0.10, zc), 0.098, 0.3)); - d = min(d, s_facet_capsule(p, float3(0.17, -0.57, zc + 0.012), float3(0.17, -0.41, zc + 0.045), 0.086, 0.1)); - d = min(d, s_facet_capsule(p, float3(0.31, -0.59, zc + 0.01), float3(0.31, -0.45, zc + 0.04), 0.08, 0.5)); - d = min(d, s_facet_capsule(p, float3(0.435, -0.625, zc + 0.005), float3(0.435, -0.52, zc + 0.03), 0.07, 0.2)); - return min(d, s_facet_capsule(p, float3(0.03, -0.77, zc + 0.03), float3(-0.165, -0.60, zc + 0.065), 0.082, 0.7)); -} - static float2 sculpt_proto(float3 p, int theme, int shape) { if (theme == 3) { return s_voxels(p, shape); } - if (theme == 1) - { - return float2(shape == 0 ? s_gem_arrow(p) : s_crystal_hand(p), 8.0); - } - float2 body; - float3 star; - if (shape == 0) - { - float h = 0.075, re = 0.03, dome = 0.0; - if (theme == 2) - { - h = 0.07; - re = 0.06; - dome = 0.035; - } - if (theme == 4) - { - h = 0.07; - re = 0.055; - dome = 0.025; - } - body = float2(s_arrow_solid(p, h, re, dome), 1.0); - if (theme == 0) - { - body = s_opu(body, float2(s_piping(p, SCULPT_HOVER + 2.0 * h - 0.004), 2.0)); - } - star = float3(0.57, -0.86, SCULPT_HOVER + 2.0 * h + dome); - } - else - { - body = s_opu(float2(s_glove(p, SCULPT_HAND_ZC), 1.0), float2(s_cuff(p, SCULPT_HAND_ZC), 2.0)); - star = float3(0.185, -0.93, SCULPT_HAND_ZC + 0.15); - } - return theme == 4 ? s_opu(body, s_sprout(p, star)) : body; + return s_rimmed(p, theme, shape); } inline float3 sculpt_point(float3 q, constant Layer &layer) @@ -965,13 +1001,13 @@ static float2 sculpt_eval(float3 q, bool occ, constant Layer &layer) float2 r; if (occ && theme == 3) { - float d2 = shape == 0 ? s_arrow2(p.xy) - 1.2 * SCULPT_VOX : s_pixel_hand_dist(p.xy); + float d2 = s_pixel_outline(p.xy, shape); float dz = abs(p.z - (SCULPT_HOVER + 0.07)) - 0.07; r = float2(length(max(float2(d2, dz), 0.0)) + min(max(d2, dz), 0.0), 7.0); } else { - r = sculpt_proto(p, occ && theme == 1 ? 2 : theme, shape); + r = sculpt_proto(p, theme, shape); } return float2(r.x * sculpt_units(layer), r.y); } @@ -1076,16 +1112,13 @@ inline SculptMat s_mat(float3 alb, float rough, float spec, float sss, float ref static float3 s_pixel_colour(float3 p, int shape) { - float2 id = floor((p.xy - s_grid_origin(shape)) / SCULPT_VOX); - if (!s_occ(id + float2(-1.0, 0.0), shape) || !s_occ(id + float2(0.0, -1.0), shape)) - { - return s_lin(0.52, 0.91, 0.77); - } - if (!s_occ(id + float2(1.0, 0.0), shape) || !s_occ(id + float2(0.0, 1.0), shape)) - { - return s_lin(1.0, 0.78, 0.87); - } - return s_lin(1.0, 0.50, 0.71); + int2 cell = s_pix_cell(p.xy, shape); + int4 g = PIX_GRID[shape]; + int row = g.z + clamp(cell.y, 0, g.y - 1); + if (s_pix_bit(PIX_LINE[row], cell.x)) return s_lin(0.29, 0.12, 0.36); + if (s_pix_bit(PIX_HI[row], cell.x)) return s_lin(1.0, 0.78, 0.87); + if (s_pix_bit(PIX_SHADE[row], cell.x)) return s_lin(0.87, 0.27, 0.51); + return s_lin(1.0, 0.435, 0.66); } static SculptMat sculpt_material(float mat, float3 p, int theme, int shape) @@ -1093,28 +1126,25 @@ static SculptMat sculpt_material(float mat, float3 p, int theme, int shape) bool primary = mat < 1.5; if (theme == 0) { - if (shape == 0 && primary) return s_mat(s_lin(0.10, 0.10, 0.115), 0.3, 0.2, 0.0, 0.9); - if (shape == 0) return s_mat(s_lin(0.94, 0.91, 0.84), 0.45, 0.4, 0.2, 0.3); - if (primary) return s_mat(s_lin(0.95, 0.92, 0.85), 0.55, 0.35, 0.35, 0.25); - return s_mat(s_lin(0.17, 0.18, 0.22), 0.35, 0.6, 0.0, 0.6); + if (mat < 2.5) return s_mat(s_lin(0.95, 0.92, 0.85), 0.3, 0.6, 0.35, 0.25); + return s_mat(s_lin(0.1, 0.1, 0.11), 0.55, 0.05, 0.0, 0.1); } if (theme == 2) { - if (shape == 0) return s_mat(s_lin(1.0, 0.40, 0.30), 0.5, 0.45, 0.4, 0.25); - if (primary) return s_mat(s_lin(1.0, 0.79, 0.16), 0.5, 0.45, 0.4, 0.25); - return s_mat(s_lin(0.18, 0.20, 0.29), 0.4, 0.5, 0.0, 0.4); + if ((primary && shape == 0) || (mat > 2.5 && mat < 3.5 && shape == 1)) return s_mat(s_lin(1.0, 0.40, 0.30), 0.85, 0.08, 0.15, 0.03); + if (mat < 3.5) return s_mat(s_lin(1.0, 0.80, 0.10), 0.85, 0.08, 0.15, 0.03); + return s_mat(s_lin(0.09, 0.13, 0.45), 0.9, 0.05, 0.05, 0.02); } if (theme == 4) { - if (primary && shape == 0) return s_mat(s_lin(0.62, 0.91, 0.78), 0.22, 0.8, 0.25, 0.6); - if (primary) return s_mat(s_lin(0.96, 0.94, 0.88), 0.5, 0.35, 0.35, 0.25); - if (mat < 2.5) return s_mat(s_lin(0.62, 0.91, 0.78), 0.25, 0.7, 0.25, 0.5); - if (mat < 3.5) return s_mat(s_lin(1.0, 0.80, 0.20), 0.3, 0.6, 0.3, 0.4); - if (mat < 4.5) return s_mat(s_lin(0.38, 0.80, 0.55), 0.35, 0.5, 0.35, 0.3); - return s_mat(s_lin(0.16, 0.12, 0.10), 0.2, 0.8, 0.0, 0.5); + if (primary && shape == 0) return s_mat(s_lin(0.68, 0.93, 0.80), 0.25, 0.7, 0.3, 0.3); + if (primary) return s_mat(s_lin(0.97, 0.95, 0.90), 0.3, 0.6, 0.35, 0.25); + if (mat < 2.5) return s_mat(s_lin(0.52, 0.87, 0.78), 0.25, 0.7, 0.3, 0.3); + if (mat < 3.5) return s_mat(s_lin(1.0, 0.75, 0.25), 0.25, 0.7, 0.3, 0.3); + if (mat < 4.5) return s_mat(s_lin(0.62, 0.92, 0.72), 0.3, 0.6, 0.3, 0.35); + return s_mat(s_lin(0.07, 0.15, 0.33), 0.7, 0.15, 0.05, 0.08); } - if (mat < 6.5) return s_mat(s_pixel_colour(p, shape), 0.45, 0.35, 0.15, 0.2); - return s_mat(s_lin(0.36, 0.18, 0.54), 0.45, 0.35, 0.1, 0.2); + return s_mat(s_pixel_colour(p, shape), 0.65, 0.1, 0.12, 0.04); } static float3 model_env(float3 d, float rough, float3 l, float3 fill) @@ -1127,34 +1157,6 @@ static float3 model_env(float3 d, float rough, float3 l, float3 fill) return col; } -static float3 s_gem(float k) -{ - k = saturate(k) * 3.0; - float3 a = s_lin(0.20, 0.95, 1.0); - float3 b = s_lin(0.15, 0.42, 1.0); - float3 c = s_lin(0.45, 0.25, 0.95); - float3 d = s_lin(0.88, 0.50, 1.0); - if (k < 1.0) return mix(a, b, k); - if (k < 2.0) return mix(b, c, k - 1.0); - return mix(c, d, k - 2.0); -} - -static float3 model_shade_crystal(float3 n, float3 rd, float3 L, float fall, float3 l, float3 fill) -{ - float cosi = saturate(dot(-rd, n)); - float F = 0.04 + 0.96 * pow(1.0 - cosi, 5.0); - float3 t = refract(rd, n, 1.0 / 1.6); - float k = 0.42 + 0.9 * dot(float2(n.x, -n.y), float2(0.7557, -0.6549)) - + 0.6 * dot(float2(t.x, -t.y), float2(0.6, -0.8)); - float3 body = float3(s_gem(k - 0.08).r, s_gem(k).g, s_gem(k + 0.08).b); - float3 col = body * (0.1 + 1.8 * fall * pow(max(dot(n, L), 0.0), 2.5)); - col += body * model_env(reflect(t, float3(0.0, 0.0, 1.0)) * float3(1.0, 1.0, -1.0), 0.15, l, fill) * 0.5; - col += pow(max(dot(reflect(rd, n), L), 0.0), 30.0) * 2.0; - col += model_env(reflect(rd, n), 0.05, l, fill) * F; - col += s_lin(0.5, 0.9, 1.0) * pow(1.0 - cosi, 4.0) * 0.6; - return col; -} - static float3 model_tonemap(float3 c) { const float start = 0.76; @@ -1178,10 +1180,6 @@ static float3 model_shade(float3 q, float3 n, float3 rd, float3 L, float fall, f texture2d texImg) { int id = sculpt_id(layer); - if (id > 0 && (id - 1) / 2 == 1) - { - return model_tonemap(model_shade_crystal(n, rd, L, fall, l, fill)); - } SculptMat m; float gloss = 1.0; if (id > 0) @@ -1217,7 +1215,11 @@ constant int STAGE_NORMAL = 1; constant int STAGE_AO = 2; constant int STAGE_SELF = 3; constant int STAGE_PLANE = 4; -constant int STAGE_DONE = 5; +constant int STAGE_EDGE = 5; +constant int STAGE_SHADE = 6; +constant int STAGE_DONE = 7; + +constant float2 MODEL_SUBPIXEL[3] = { float2(0.35, 0.2), float2(-0.35, 0.2), float2(0.0, -0.4) }; inline float3 model_tetra(int k) { @@ -1253,15 +1255,18 @@ static float4 cursor_model(float2 local, constant Layer &layer, float stride = sculpt_id(layer) > 0 ? 0.85 : 1.0; float3 lamp = float3(layer.color.rg + sprite_size(layer) * 0.5, 0.0) + l * SCULPT_LAMP_DIST; - float2 tb = ray_box(ro, rd, lo - 0.02, hi + 0.02); + float3 ray = rd; + float2 tb = ray_box(ro, ray, lo - 0.02, hi + 0.02); int stage = tb.x < tb.y && tb.y > 0.0 ? STAGE_MARCH : STAGE_DONE; bool plane_next = stage == STAGE_DONE; int k = 0; float t = max(tb.x, 0.0); float best = 1e9; float t_best = t; + float d_best = 0.0; float mat = 0.0; float cov = 0.0; + float cov_s = 0.0; float3 q = float3(0.0); float3 n = float3(0.0); float3 L = float3(0.0); @@ -1273,12 +1278,15 @@ static float4 cursor_model(float2 local, constant Layer &layer, float inside = 0.0; float contact = 0.0; float dropped = 0.0; - for (int it = 0; it < 180; it++) + int sub = 0; + int shaded = 0; + float4 acc = float4(0.0); + for (int it = 0; it < 500; it++) { if (plane_next) { plane_next = false; - stage = STAGE_DONE; + stage = cov > 0.0 ? STAGE_SHADE : STAGE_DONE; float denom = dot(rd, nz); if (cov < 1.0 && denom < -1e-4) { @@ -1293,18 +1301,55 @@ static float4 cursor_model(float2 local, constant Layer &layer, } } } + if (stage == STAGE_SHADE) + { + if (cov_s > 0.0) + { + float3 tl = lamp - q; + float fall = SCULPT_LAMP_DIST * SCULPT_LAMP_DIST / dot(tl, tl); + float3 c = model_shade(q, n, ray, normalize(tl), fall, sh, ao, mat, l, fill, layer, texSdf, texImg); + acc += float4(c * cov_s, cov_s); + } + shaded++; + stage = STAGE_DONE; + if (sub > 0) + { + float3 dws = float3(local + MODEL_SUBPIXEL[3 - sub] + layer.src.xy, -persp); + sub--; + dlen = length(dws); + ray = plane_to_model(world_to_plane(dws / dlen, f), f); + tb = ray_box(ro, ray, lo - 0.02, hi + 0.02); + cov_s = 0.0; + stage = STAGE_SHADE; + if (tb.x < tb.y && tb.y > 0.0) + { + stage = STAGE_MARCH; + k = 0; + t = max(tb.x, 0.0); + best = 1e9; + t_best = t; + } + } + continue; + } float3 pos; if (stage == STAGE_MARCH) { - pos = ro + rd * t; + pos = ro + ray * t; } else if (stage == STAGE_NORMAL) { pos = q + 0.002 * model_tetra(k); } + else if (stage == STAGE_EDGE) + { + float3 side = normalize(cross(n, ray)); + float3 e = (k >= 2 ? cross(n, side) : side) * (k % 2 == 1 ? -1.0 : 1.0); + pos = q + e * 0.5 * t_best / dlen; + } else if (stage == STAGE_AO) { - pos = q + (0.01 + 0.0175 * float(k)) * SCULPT_SCALE * n; + pos = q + (0.01 + 0.0175 * k) * SCULPT_SCALE * n; } else if (stage == STAGE_SELF) { @@ -1328,22 +1373,38 @@ static float4 cursor_model(float2 local, constant Layer &layer, { best = hit ? 0.0 : d / fp; t_best = t; + d_best = d; mat = m.y; } t += d * stride; k++; if (hit || t > tb.y || k == 96) { - cov = saturate(1.0 - best); - if (cov > 0.0) + cov_s = saturate(1.0 - best); + if (shaded > 0) { - q = ro + rd * t_best; - stage = STAGE_NORMAL; - k = 0; + stage = STAGE_SHADE; + if (cov_s > 0.0) + { + q = ro + ray * t_best; + n = float3(0.0); + stage = STAGE_NORMAL; + k = 0; + } } else { - plane_next = true; + cov = cov_s; + if (cov > 0.0) + { + q = ro + ray * t_best; + stage = STAGE_NORMAL; + k = 0; + } + else + { + plane_next = true; + } } } } @@ -1354,6 +1415,19 @@ static float4 cursor_model(float2 local, constant Layer &layer, if (k == 4) { n = normalize(n); + k = 0; + stage = shaded > 0 ? STAGE_SHADE : STAGE_EDGE; + } + } + else if (stage == STAGE_EDGE) + { + if (m.y != mat || abs(d - d_best) > 0.02 * t_best / dlen) + { + sub = 3; + } + k++; + if (k == 4) + { stage = STAGE_AO; k = 0; ao = 0.0; @@ -1361,7 +1435,7 @@ static float4 cursor_model(float2 local, constant Layer &layer, } else if (stage == STAGE_AO) { - ao += ((0.01 + 0.0175 * float(k)) * SCULPT_SCALE - d) * pow(0.85, float(k)); + ao += ((0.01 + 0.0175 * k) * SCULPT_SCALE - d) * pow(0.85, float(k)); k++; if (k == 5) { @@ -1402,7 +1476,7 @@ static float4 cursor_model(float2 local, constant Layer &layer, k = 1; if (!(tbp.x < tbp.y && tbp.y > 0.0)) { - stage = STAGE_DONE; + stage = cov > 0.0 ? STAGE_SHADE : STAGE_DONE; } } else @@ -1414,21 +1488,15 @@ static float4 cursor_model(float2 local, constant Layer &layer, { res = saturate(res); dropped = 1.0 - res * res * (3.0 - 2.0 * res); - stage = STAGE_DONE; + stage = cov > 0.0 ? STAGE_SHADE : STAGE_DONE; } } } - float3 rgb = float3(0.0); - if (cov > 0.0) - { - float3 tl = lamp - q; - float fall = SCULPT_LAMP_DIST * SCULPT_LAMP_DIST / dot(tl, tl); - rgb = model_shade(q, n, rd, normalize(tl), fall, sh, ao, mat, l, fill, layer, texSdf, texImg); - } + float w = 1.0 / max(shaded, 1); float shadow = inside * max(dropped * MODEL_SHADOW_ALPHA, contact * MODEL_CONTACT_ALPHA); - float a = cov * layer.color.a; - return float4(rgb * a, a + (1.0 - a) * shadow * layer.color.a); // prémultiplié, ombre noire + float a = acc.a * w * layer.color.a; + return float4(acc.rgb * w * layer.color.a, a + (1.0 - a) * shadow * layer.color.a); // prémultiplié, ombre noire } // ============ Impact du clic (mode 16) ============ diff --git a/crates/compositor/src/vk_shaders/layer.wgsl b/crates/compositor/src/vk_shaders/layer.wgsl index 2eb46a9b3..a1a84f6ac 100644 --- a/crates/compositor/src/vk_shaders/layer.wgsl +++ b/crates/compositor/src/vk_shaders/layer.wgsl @@ -610,8 +610,6 @@ fn sculpt_id() -> i32 { const SCULPT_SCALE: f32 = 0.85; const SCULPT_HOVER: f32 = 0.05; const SCULPT_VOX: f32 = 0.0625; -const SCULPT_AR_ROUND: f32 = 0.03; -const SCULPT_HAND_ZC: f32 = 0.185; const SCULPT_ZREF_ARROW: f32 = 0.2; const SCULPT_ZREF_HAND: f32 = 0.185; const SCULPT_LAMP_DIST: f32 = 1.9; @@ -630,13 +628,6 @@ fn s_opu(a: vec2, b: vec2) -> vec2 { return select(b, a, a.x < b.x); } -fn s_capsule(p: vec3, a: vec3, b: vec3, r: f32) -> f32 { - let pa = p - a; - let ba = b - a; - let h = saturate(dot(pa, ba) / dot(ba, ba)); - return length(pa - ba * h) - r; -} - fn s_round_box(p: vec3, b: vec3, r: f32) -> f32 { let q = abs(p) - b + vec3(r); return length(max(q, vec3(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0) - r; @@ -660,31 +651,6 @@ fn s_rot(v: vec2, a: f32) -> vec2 { return vec2(c * v.x - s * v.y, s * v.x + c * v.y); } -const SCULPT_ARROW = array, 7>( - vec2(0.0, 0.0), vec2(0.0, -0.86), vec2(0.215, -0.665), vec2(0.37, -1.0), - vec2(0.53, -0.93), vec2(0.38, -0.60), vec2(0.64, -0.60) -); - -fn s_arrow2(p: vec2) -> f32 { - var d = dot(p - SCULPT_ARROW[0], p - SCULPT_ARROW[0]); - var s = 1.0; - var j = 6; - for (var i = 0; i < 7; i++) { - let e = SCULPT_ARROW[j] - SCULPT_ARROW[i]; - let w = p - SCULPT_ARROW[i]; - let b = w - e * saturate(dot(w, e) / dot(e, e)); - d = min(d, dot(b, b)); - let c0 = p.y >= SCULPT_ARROW[i].y; - let c1 = p.y < SCULPT_ARROW[j].y; - let c2 = e.x * w.y > e.y * w.x; - if (c0 && c1 && c2) || (!c0 && !c1 && !c2) { - s = -s; - } - j = i; - } - return s * sqrt(d); -} - fn s_star5(p0: vec2, r: f32, rf: f32) -> f32 { let k1 = vec2(0.809016994375, -0.587785252292); let k2 = vec2(-0.809016994375, -0.587785252292); @@ -698,233 +664,305 @@ fn s_star5(p0: vec2, r: f32, rf: f32) -> f32 { return length(p - ba * h) * sign(p.y * ba.x - p.x * ba.y); } -fn s_arrow_solid(p: vec3, h: f32, re: f32, dome: f32) -> f32 { - let d2 = s_arrow2(p.xy) - SCULPT_AR_ROUND; - let hh = h + dome * smoothstep(0.0, 0.07, -d2); - return s_extrude(d2, p.z - (SCULPT_HOVER + h + dome), hh, re); +fn s_vesica(p0: vec2, r: f32, d: f32) -> f32 { + let p = abs(p0); + let b = sqrt(r * r - d * d); + return select(length(p + vec2(d, 0.0)) - r, length(p - vec2(0.0, b)), (p.y - b) * d > p.x * b); } -fn s_piping(p: vec3, ztop: f32) -> f32 { - let d2 = s_arrow2(p.xy) - SCULPT_AR_ROUND; - return length(vec2(d2 + 0.075, p.z - ztop)) - 0.017; -} +// Curseurs cercles (cf. HLSL) : Studio Ink, Pop Coral, Star Sprout. +const RIM_POLY = array, 28>( + vec2(0.0, -0.06), vec2(0.0, -0.7712), vec2(0.165, -0.6325), vec2(0.313, -0.9318), + vec2(0.4234, -0.8557), vec2(0.2672, -0.5686), vec2(0.456, -0.5393), + vec2(-0.173, -0.5237), vec2(-0.016, -0.75), vec2(0.329, -0.75), vec2(0.411, -0.592), + vec2(0.411, -0.43), vec2(0.19, -0.435), vec2(-0.03, -0.44), + vec2(-0.1825, -0.6215), vec2(0.0037, -0.9195), vec2(0.3462, -0.9195), vec2(0.467, -0.6906), + vec2(0.467, -0.49), vec2(0.2, -0.49), vec2(-0.03, -0.49), + vec2(-0.1807, -0.5534), vec2(0.0194, -0.872), vec2(0.3485, -0.872), vec2(0.47, -0.6795), + vec2(0.47, -0.49), vec2(0.2, -0.49), vec2(-0.03, -0.49) +); -fn s_glove(p: vec3, zc: f32) -> f32 { - let index = s_capsule(p, vec3(0.0, -0.10, zc), vec3(0.0, -0.52, zc), 0.098); - let palm = s_round_box(p - vec3(0.185, -0.70, zc), vec3(0.255, 0.19, 0.105), 0.1); - let f1 = s_capsule(p, vec3(0.17, -0.57, zc + 0.012), vec3(0.17, -0.41, zc + 0.045), 0.086); - let f2 = s_capsule(p, vec3(0.31, -0.59, zc + 0.01), vec3(0.31, -0.45, zc + 0.04), 0.08); - let f3 = s_capsule(p, vec3(0.435, -0.625, zc + 0.005), vec3(0.435, -0.52, zc + 0.03), 0.07); - let thumb = s_capsule(p, vec3(0.03, -0.77, zc + 0.03), vec3(-0.165, -0.60, zc + 0.065), 0.082); - var d = s_smin(palm, min(f1, min(f2, f3)), 0.035); - d = s_smin(d, index, 0.05); - return s_smin(d, thumb, 0.05); -} +const RIM_GLOVE = array, 27>( + vec4(0.0, -0.1077, 0.0, -0.6), vec4(0.1493, -0.3279, 0.1493, -0.6), + vec4(0.2863, -0.3654, 0.2863, -0.6), vec4(0.4193, -0.4043, 0.4193, -0.6), + vec4(-0.2, -0.478, -0.075, -0.625), vec4(0.0788, -0.25, 0.0788, -0.3992), + vec4(0.2188, -0.3, 0.2188, -0.4234), vec4(0.3552, -0.33, 0.3552, -0.4534), + vec4(-0.0845, -0.5386, 0.762, 0.648), + vec4(0.0, -0.1186, 0.0, -0.65), vec4(0.1614, -0.3838, 0.1614, -0.65), + vec4(0.3177, -0.427, 0.3177, -0.65), vec4(0.4645, -0.4848, 0.4645, -0.65), + vec4(-0.215, -0.535, -0.1208, -0.6864), vec4(0.0832, -0.3, 0.0832, -0.48), + vec4(0.2414, -0.35, 0.2414, -0.482), vec4(0.3959, -0.41, 0.3959, -0.533), + vec4(-0.122, -0.5718, 0.867, 0.498), + vec4(0.0, -0.122, 0.0, -0.65), vec4(0.172, -0.387, 0.172, -0.65), + vec4(0.3267, -0.427, 0.3267, -0.65), vec4(0.4727, -0.4867, 0.4727, -0.65), + vec4(-0.199, -0.483, -0.102, -0.645), vec4(0.0927, -0.3, 0.0927, -0.476), + vec4(0.2493, -0.35, 0.2493, -0.478), vec4(0.402, -0.41, 0.402, -0.52), + vec4(-0.0993, -0.5267, 0.858, 0.514) +); -fn s_cuff(p: vec3, zc: f32) -> f32 { - let q = p - vec3(0.185, -0.925, zc); - let ab = vec2(0.272, 0.12); - let e = (length(q.xz / ab) - 1.0) * min(ab.x, ab.y); - return s_extrude(e, q.y, 0.07, 0.06); -} +const RIM_GLOVE_R = array, 9>( + vec4(0.0552, 0.047, 0.045, 0.0375), vec4(0.052, 0.0235, 0.0225, 0.0225), vec4(0.0552, -0.548, 0.0525, 0.0), + vec4(0.064, 0.0585, 0.0585, 0.0515), vec4(0.06, 0.0197, 0.0178, 0.0172), vec4(0.0648, -0.6, 0.058, 0.0), + vec4(0.066, 0.0553, 0.0553, 0.0507), vec4(0.063, 0.024, 0.022, 0.02), vec4(0.0647, -0.555, 0.056, 0.0) +); -fn s_sprout(p: vec3, c: vec3) -> vec2 { - let q = p - c; - let st2 = s_star5(q.xy, 0.125, 0.52) - 0.022; - var r = vec2(s_extrude(st2, q.z, 0.038, 0.034), 3.0); - let e = vec3(abs(q.x) - 0.032, q.y + 0.005, q.z - 0.036); - r = s_opu(r, vec2(length(e) - 0.0135, 5.0)); - let l1 = vec3(s_rot(q.xy - vec2(-0.04, 0.15), -0.6), q.z); - let l2 = vec3(s_rot(q.xy - vec2(0.045, 0.155), 0.7), q.z); - let leaves = min(s_ellipsoid(l1, vec3(0.03, 0.058, 0.02)), s_ellipsoid(l2, vec3(0.03, 0.058, 0.02))); - return s_opu(r, vec2(leaves, 4.0)); +fn s_rim_poly(p: vec2, base: i32) -> f32 { + var d = dot(p - RIM_POLY[base], p - RIM_POLY[base]); + var s = 1.0; + var j = base + 6; + for (var i = base; i < base + 7; i++) { + let e = RIM_POLY[j] - RIM_POLY[i]; + let w = p - RIM_POLY[i]; + let b = w - e * saturate(dot(w, e) / dot(e, e)); + d = min(d, dot(b, b)); + let c0 = p.y >= RIM_POLY[i].y; + let c1 = p.y < RIM_POLY[j].y; + let c2 = e.x * w.y > e.y * w.x; + if (c0 && c1 && c2) || (!c0 && !c1 && !c2) { + s = -s; + } + j = i; + } + return s * sqrt(d); } -// Pixel Candy : une ligne par entier, bit c = colonne c (cf. HLSL et `sculpt.rs`). -const SCULPT_ARROWPIX = array(0, 1, 3, 7, 31, 63, 127, 255, 511, 63, 55, 115, 113, 224, 224, 64); -const SCULPT_ARROWBACK = array( - 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 255, 511, 511, 999, 995, 960, 192 +fn s_rim_glove(p: vec2, g: i32, palm: f32) -> vec2 { + let r0 = RIM_GLOVE_R[3 * g]; + let r1 = RIM_GLOVE_R[3 * g + 1]; + let r2 = RIM_GLOVE_R[3 * g + 2]; + let i = 9 * g; + var f = min(sd_segment(p, RIM_GLOVE[i].xy, RIM_GLOVE[i].zw) - r0.x, + sd_segment(p, RIM_GLOVE[i + 1].xy, RIM_GLOVE[i + 1].zw) - r0.y); + f = min(f, sd_segment(p, RIM_GLOVE[i + 2].xy, RIM_GLOVE[i + 2].zw) - r0.z); + f = min(f, sd_segment(p, RIM_GLOVE[i + 3].xy, RIM_GLOVE[i + 3].zw) - r0.w); + f = min(f, sd_segment(p, RIM_GLOVE[i + 4].xy, RIM_GLOVE[i + 4].zw) - r1.x); + var grooves = min(sd_segment(p, RIM_GLOVE[i + 5].xy, RIM_GLOVE[i + 5].zw) - r1.y, + sd_segment(p, RIM_GLOVE[i + 6].xy, RIM_GLOVE[i + 6].zw) - r1.z); + grooves = min(grooves, sd_segment(p, RIM_GLOVE[i + 7].xy, RIM_GLOVE[i + 7].zw) - r1.w); + let sil = s_smin(palm, f, 0.03); + let v = RIM_GLOVE[i + 8]; + let wedge = max(max(-dot(p - v.xy, v.zw), p.x + r2.x), r2.y - p.y); + return vec2(sil, max(sil, -min(grooves, wedge))); +} + +fn s_piece(d: f32, dc: f32, sil: f32, z: f32, w: f32, zt: f32, h: f32, bump: f32, mat: f32) -> vec2 { + let tray = s_extrude(sil - w, z - 0.5 * (SCULPT_HOVER + zt), 0.5 * (zt - SCULPT_HOVER), 0.012); + let bead = length(vec2(d - 0.5 * w, z - zt)) - 0.5 * w; + let u = saturate(-dc / 0.045); + let cushion = 0.8 * s_extrude(dc, z - zt, h + 0.022 * u * (2.0 - u) + bump, 0.75 * h); + return s_opu(vec2(min(tray, bead), 5.0), vec2(cushion, mat)); +} + +fn s_paper(d: f32, sil: f32, z: f32, w: f32) -> vec2 { + let navy = s_extrude(sil - w, z - 0.5 * (SCULPT_HOVER + 0.15), 0.5 * (0.15 - SCULPT_HOVER), 0.006); + let sheet = s_extrude(d, z - 0.16, 0.01, 0.004); + return s_opu(vec2(navy, 5.0), vec2(sheet, 1.0)); +} + +fn s_bump(p: vec2, c: vec2, r: f32) -> f32 { + let k = saturate(1.0 - dot(p - c, p - c) / (r * r)); + return k * k; +} + +fn s_dash(p: vec2, a: vec2, b0: vec2, ra: f32, rb: f32) -> f32 { + let q0 = p - a; + let b = b0 - a; + let hb = dot(b, b); + let q = vec2(abs(dot(q0, vec2(b.y, -b.x))), dot(q0, b)) / hb; + let c = vec2(sqrt(hb - (ra - rb) * (ra - rb)), ra - rb); + let k = c.x * q.y - c.y * q.x; + if k < 0.0 { + return sqrt(hb * dot(q, q)) - ra; + } + if k > c.x { + return sqrt(hb * (dot(q, q) + 1.0 - 2.0 * q.y)) - rb; + } + return dot(c, q) - ra; +} + +fn s_rimmed(p: vec3, theme: i32, shape: i32) -> vec2 { + let arrow = shape == 0; + let g = select(select(2, 1, theme == 0), 0, theme == 4); + let poly = s_rim_poly(p.xy, select(7 * g + 7, 0, arrow)); + var body = vec2(poly, poly); + var w = 0.06; + var zt = 0.17; + var h = 0.03; + var bump = 0.022 * s_bump(p.xy, vec2(0.15, -0.45), 0.3); + var back = 1e9; + if !arrow { + w = RIM_GLOVE_R[3 * g + 2].z; + for (var i = 0; i < 2; i++) { + let q = p.xy + select(vec2(0.0), vec2(0.03, 0.04), i == 1); + let v = s_rim_glove(q, g, select(poly, s_rim_poly(q, 7 * g + 7), i == 1) - 0.05); + if i == 1 { + back = v.x - w; + } else { + body = v; + if theme != 2 || v.x - w <= 0.0 { + break; + } + } + } + zt = 0.185; + h = 0.028; + bump = 0.03 * s_bump(p.xy, vec2(0.19, -0.62), 0.28); + } + var dc = body.y; + if arrow && theme == 0 { + body = body - vec2(0.015); + w = 0.045; + dc = 1.0; + } + var r: vec2; + if theme == 2 { + r = s_paper(body.y, body.x, p.z, w); + } else { + r = s_piece(body.y, dc, body.x, p.z, w, zt, h, bump, 1.0); + } + if theme == 0 { + if arrow { + let band = s_extrude(abs(poly + 0.0075) - 0.0225, p.z - zt, 0.028, 0.012); + r = s_opu(r, vec2(band, 2.0)); + } + return r; + } + if theme == 2 { + var dash: f32; + if arrow { + let back = s_rim_poly(p.xy + vec2(0.025, 0.05), 0) - 0.06; + r = s_opu(r, vec2(s_extrude(back, p.z - 0.08, 0.03, 0.006), 2.0)); + dash = min(s_dash(p.xy, vec2(-0.1179, -0.1374), vec2(-0.1799, -0.0443), 0.028, 0.045), + s_dash(p.xy, vec2(-0.1347, -0.2493), vec2(-0.248, -0.2056), 0.026, 0.042)); + } else { + r = s_opu(r, vec2(s_extrude(back, p.z - 0.08, 0.03, 0.006), 3.0)); + dash = min(s_dash(p.xy, vec2(0.216, -0.1695), vec2(0.2593, -0.0685), 0.028, 0.042), + s_dash(p.xy, vec2(0.3054, -0.2339), vec2(0.3949, -0.1652), 0.0275, 0.041)); + } + return s_opu(r, vec2(s_extrude(dash, p.z - 0.11, 0.06, 0.006), 3.0)); + } + if !arrow { + let cq = p.xy - vec2(0.1541, -0.9047); + let bq = abs(vec2(cq.x, cq.y - 0.2066 * cq.x * cq.x)) - vec2(0.214, 0.0361); + let cuff = length(max(bq, vec2(0.0))) + min(max(bq.x, bq.y), 0.0) - 0.03; + r = s_opu(r, s_piece(cuff, cuff, cuff, p.z, 0.0477, 0.225, 0.025, 0.0, 2.0)); + } + let c = select(vec2(0.15, -0.885), vec2(0.6118, -0.8079), arrow); + let rs = select(0.128, 0.1678, arrow); + let zs = select(0.285, 0.23, arrow); + let q = s_rot(p.xy - c, select(0.0, 0.2443, arrow)) / rs; + let star = (s_star5(q, 0.82, 0.55) - 0.18) * rs; + let l1 = s_rot(q - vec2(-0.33, 1.38), -0.925); + let l2 = s_rot(q - vec2(0.53, 1.37), 0.873); + let leaves = min(s_vesica(l1, 0.4296, 0.2626), s_vesica(l2, 0.4296, 0.2626)) * rs; + r = s_opu(r, s_piece(leaves, leaves, leaves, p.z, 0.038, zs - 0.02, 0.018, 0.0, 4.0)); + r = s_opu(r, s_piece(star, star, star, p.z, 0.038, zs, 0.022, 0.018 * s_bump(q, vec2(0.0), 1.0), 3.0)); + let e = vec3(abs(q.x) - 0.25, q.y - 0.08, (p.z - zs - 0.056) / rs); + return s_opu(r, vec2(s_ellipsoid(e, vec3(0.075, 0.13, 0.1)) * rs, 5.0)); +} + +// Pixel Candy : tables générées par scripts/generate-pixel-candy-voxels.mjs (cf. HLSL). +// +const PIX_BODY = array(1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 127, 247, 243, 480, 192, 48, 120, 120, 120, 504, 4088, 32760, 65534, 65535, 65535, 65534, 32766, 32764, 16380, 16376, 16376); +const PIX_LINE = array(1, 3, 5, 9, 17, 33, 65, 129, 257, 513, 1985, 73, 149, 147, 288, 192, 48, 72, 72, 72, 456, 3656, 29256, 37454, 32777, 32769, 32770, 16386, 16388, 8196, 8200, 16376); +const PIX_HI = array(0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 18, 34, 32, 64, 0, 0, 16, 16, 16, 16, 16, 16, 16, 22, 18, 4, 4, 8, 8, 16, 0); +const PIX_SHADE = array(0, 0, 0, 4, 8, 16, 32, 64, 128, 448, 40, 36, 64, 64, 128, 0, 0, 32, 32, 32, 32, 288, 2336, 18720, 16384, 16384, 16384, 8192, 8192, 4096, 8160, 0); +const PIX_RECT_N = array(0, 17, 29); +const PIX_GRID = array, 2>( + vec4(11, 16, 0, 0), + vec4(16, 16, 16, 0) ); -const SCULPT_HANDPIX = array(4, 14, 14, 14, 110, 878, 7022, 7022, 8190, 8191, 8191, 8191, 8190, 4092, 4092); -const SCULPT_HANDBACK = array( - 28, 62, 62, 62, 510, 4094, 32766, 32766, 32766, 32767, 32767, 32767, 32767, 32767, 32766, 16380, 16380 +const PIX_ORIGIN = array, 2>( + vec2(0.0, 0.0), + vec2(-0.3125, 0.0) ); -const SCULPT_HANDSPAN = array, 17>( - vec2(2.0, 4.0), vec2(1.0, 5.0), vec2(1.0, 5.0), vec2(1.0, 5.0), vec2(1.0, 8.0), - vec2(1.0, 11.0), vec2(1.0, 14.0), vec2(1.0, 14.0), vec2(1.0, 14.0), vec2(0.0, 14.0), - vec2(0.0, 14.0), vec2(0.0, 14.0), vec2(0.0, 14.0), vec2(0.0, 14.0), vec2(1.0, 14.0), - vec2(2.0, 13.0), vec2(2.0, 13.0) +const PIX_BOX = array, 2>( + vec4(0.3438, -0.5, 0.3438, 0.5), + vec4(0.1875, -0.5, 0.5, 0.5) ); +const PIX_RECT = array, 29>( + vec4(0.0313, -0.0313, 0.0313, 0.0313), + vec4(0.0625, -0.0938, 0.0625, 0.0313), + vec4(0.0938, -0.1563, 0.0938, 0.0313), + vec4(0.125, -0.2188, 0.125, 0.0313), + vec4(0.1563, -0.2813, 0.1563, 0.0313), + vec4(0.1875, -0.3438, 0.1875, 0.0313), + vec4(0.2188, -0.4063, 0.2188, 0.0313), + vec4(0.25, -0.4688, 0.25, 0.0313), + vec4(0.2813, -0.5313, 0.2813, 0.0313), + vec4(0.3125, -0.5938, 0.3125, 0.0313), + vec4(0.3438, -0.6563, 0.3438, 0.0313), + vec4(0.2188, -0.7188, 0.2188, 0.0313), + vec4(0.0938, -0.7813, 0.0938, 0.0313), + vec4(0.375, -0.8125, 0.125, 0.0625), + vec4(0.0625, -0.8438, 0.0625, 0.0313), + vec4(0.4375, -0.9063, 0.125, 0.0313), + vec4(0.4375, -0.9688, 0.0625, 0.0313), + vec4(0.0, -0.0313, 0.0625, 0.0313), + vec4(0.0, -0.1563, 0.125, 0.0938), + vec4(0.0625, -0.2813, 0.1875, 0.0313), + vec4(0.1563, -0.3438, 0.2813, 0.0313), + vec4(0.25, -0.4063, 0.375, 0.0313), + vec4(0.2188, -0.4688, 0.4688, 0.0313), + vec4(0.1875, -0.5625, 0.5, 0.0625), + vec4(0.2188, -0.6563, 0.4688, 0.0313), + vec4(0.1875, -0.7188, 0.4375, 0.0313), + vec4(0.2188, -0.7813, 0.4063, 0.0313), + vec4(0.1875, -0.8438, 0.375, 0.0313), + vec4(0.2188, -0.9375, 0.3438, 0.0625) +); +// -fn s_grid_origin(shape: i32) -> vec2 { - return select(vec2(-2.5 * SCULPT_VOX, 0.0), vec2(0.0), shape == 0); -} - -fn s_bit(row: i32, r: i32, c: i32, rows: i32, cols: i32) -> bool { - return r >= 0 && r < rows && c >= 0 && c < cols && ((row >> u32(clamp(c, 0, 31))) & 1) == 1; +fn s_pix_bit(m: i32, c: i32) -> bool { + return ((m >> u32(clamp(c, 0, 31))) & 1) == 1; } -fn s_occ(id: vec2, shape: i32) -> bool { - let c = i32(id.x); - let r = -i32(id.y) - 1; - if shape == 0 { - return s_bit(SCULPT_ARROWPIX[clamp(r, 0, 15)], r, c, 16, 16); - } - return s_bit(SCULPT_HANDPIX[clamp(r, 0, 14)], r, c, 15, 13); +fn s_pix_body(shape: i32, c: i32, r: i32) -> bool { + let g = PIX_GRID[shape]; + return c >= 0 && c < g.x && r >= 0 && r < g.y && s_pix_bit(PIX_BODY[g.z + clamp(r, 0, g.y - 1)], c); } -fn s_occ_back(id: vec2, shape: i32) -> bool { - let c = i32(id.x) + 1; - let r = -i32(id.y); - if shape == 0 { - return s_bit(SCULPT_ARROWBACK[clamp(r, 0, 17)], r, c, 18, 17); - } - return s_bit(SCULPT_HANDBACK[clamp(r, 0, 16)], r, c, 17, 15); +fn s_pix_cell(p: vec2, shape: i32) -> vec2 { + let o = PIX_ORIGIN[shape]; + return vec2(i32(floor((p.x - o.x) / SCULPT_VOX)), i32(floor((o.y - p.y) / SCULPT_VOX))); } fn s_voxels(p: vec3, shape: i32) -> vec2 { - let o = s_grid_origin(shape); - let cell = floor((p.xy - o) / SCULPT_VOX); - let bc = select(vec2(0.25, -0.47), vec2(0.33, -0.5), shape == 0); - let bh = select(vec2(0.48, 0.55), vec2(0.44, 0.61), shape == 0); - let bq = max(abs(p.xy - bc) - bh, vec2(0.0)); + let b = PIX_BOX[shape]; + let bq = max(abs(p.xy - b.xy) - b.zw, vec2(0.0)); let bz = max(abs(p.z - (SCULPT_HOVER + 0.07)) - 0.07, 0.0); - let far = max(SCULPT_VOX, sqrt(dot(bq, bq) + bz * bz)); - var dF = far; - var dB = far; - let half_cell = vec3(0.5 * SCULPT_VOX, 0.5 * SCULPT_VOX, 0.04); + var d = max(SCULPT_VOX, sqrt(dot(bq, bq) + bz * bz)); + let o = PIX_ORIGIN[shape]; + let cell = s_pix_cell(p.xy, shape); + let cube = vec3(0.5 * SCULPT_VOX, 0.5 * SCULPT_VOX, 0.07); for (var j = -1; j <= 1; j++) { for (var i = -1; i <= 1; i++) { - let id = cell + vec2(f32(i), f32(j)); - let q = vec3(p.xy - (o + (id + vec2(0.5)) * SCULPT_VOX), p.z); - if s_occ(id, shape) { - dF = min(dF, s_round_box(q - vec3(0.0, 0.0, SCULPT_HOVER + 0.1), half_cell, 0.009)); - } - if s_occ_back(id, shape) { - dB = min(dB, s_round_box(q - vec3(0.0, 0.0, SCULPT_HOVER + 0.04), half_cell, 0.009)); + let c = cell.x + i; + let r = cell.y + j; + if s_pix_body(shape, c, r) { + let q = vec3(p.x - o.x - (f32(c) + 0.5) * SCULPT_VOX, p.y - o.y + (f32(r) + 0.5) * SCULPT_VOX, + p.z - SCULPT_HOVER - 0.07); + d = min(d, s_round_box(q, cube, 0.006)); } } } - return select(vec2(dB, 7.0), vec2(dF, 6.0), dF < dB); + return vec2(d, 6.0); } -fn s_pixel_hand_dist(p: vec2) -> f32 { +fn s_pixel_outline(p: vec2, shape: i32) -> f32 { var d = 1e9; - for (var r = 0; r < 17; r++) { - let x = (SCULPT_HANDSPAN[r] + vec2(-3.5, -2.5)) * SCULPT_VOX; - let y = vec2(-f32(r), 1.0 - f32(r)) * SCULPT_VOX; - let q = max(max(vec2(x.x, y.x) - p, p - vec2(x.y, y.y)), vec2(0.0)); - d = min(d, length(q)); - } - return d; -} - -fn s_gem_edge(p: vec3, a: vec2, b: vec2, z0: f32) -> f32 { - let e = b - a; - let len = length(e); - let d = e / len; - let q = p.xy - a; - let dist = dot(q, vec2(-d.y, d.x)); - let along = abs(dot(q, d) - 0.5 * len); - let z = p.z - z0; - let girdle = (z - 0.035 - 2.2 * dist) * 0.4138; - let crown = (z - 0.07 - 0.6 * dist + 0.18 * along) * 0.8438; - return max(-dist, max(girdle, crown)); -} - -fn s_gem_arrow(p: vec3) -> f32 { - let z0 = SCULPT_HOVER + 0.03; - let slab = max(p.z - z0 - 0.2, z0 - 0.03 - p.z); - let head = max(slab, max(s_gem_edge(p, vec2(-0.02, 0.03), vec2(-0.02, -0.88), z0), - max(s_gem_edge(p, vec2(-0.02, -0.88), vec2(0.66, -0.61), z0), - s_gem_edge(p, vec2(0.66, -0.61), vec2(-0.02, 0.03), z0)))); - let tail = max(slab, max(max(s_gem_edge(p, vec2(0.215, -0.665), vec2(0.37, -1.0), z0), - s_gem_edge(p, vec2(0.37, -1.0), vec2(0.53, -0.93), z0)), - max(s_gem_edge(p, vec2(0.53, -0.93), vec2(0.38, -0.60), z0), - s_gem_edge(p, vec2(0.38, -0.60), vec2(0.215, -0.665), z0)))); - return min(head, tail); -} - -fn s_facet_capsule(p: vec3, a: vec3, b: vec3, r: f32, spin: f32) -> f32 { - let u = normalize(b - a); - let v = normalize(cross(u, vec3(0.0, 0.0, 1.0))); - let w = cross(u, v); - let q = p - b; - let h = dot(q, u); - let rad = vec2(dot(q, v), dot(q, w)); - var d = max(h - r, -dot(p - a, u) - r); - for (var k = 0; k < 6; k++) { - let an = spin + f32(k) * 1.0471976; - let s = dot(rad, vec2(cos(an), sin(an))); - d = max(d, s - r); - d = max(d, dot(rad, vec2(cos(an + 0.5236), sin(an + 0.5236))) * 0.8660 + h * 0.5 - r); - d = max(d, s * 0.5 + h * 0.8660 - r); - } - return d; -} - -const SCULPT_FACETS = array, 10>( - vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), vec3(0.0, 0.0, 1.0), - vec3(0.7071, 0.7071, 0.0), vec3(0.7071, 0.0, 0.7071), vec3(0.0, 0.7071, 0.7071), - vec3(0.5774, 0.5774, 0.5774), vec3(0.4472, 0.0, 0.8944), vec3(0.0, 0.4472, 0.8944), - vec3(0.3015, 0.3015, 0.9045) -); - -fn s_facet_ellipsoid(p: vec3, r: vec3) -> f32 { - let q = abs(p); - var d = -1e9; - for (var i = 0; i < 10; i++) { - d = max(d, dot(q, SCULPT_FACETS[i]) - length(r * SCULPT_FACETS[i])); + for (var i = PIX_RECT_N[shape]; i < PIX_RECT_N[shape + 1]; i++) { + let q = abs(p - PIX_RECT[i].xy) - PIX_RECT[i].zw; + d = min(d, length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0)); } return d; } -fn s_crystal_hand(p: vec3) -> f32 { - let zc = SCULPT_HAND_ZC; - var d = s_facet_ellipsoid(p - vec3(0.185, -0.70, zc), vec3(0.3, 0.235, 0.125)); - d = min(d, s_facet_capsule(p, vec3(0.0, -0.52, zc), vec3(0.0, -0.10, zc), 0.098, 0.3)); - d = min(d, s_facet_capsule(p, vec3(0.17, -0.57, zc + 0.012), vec3(0.17, -0.41, zc + 0.045), 0.086, 0.1)); - d = min(d, s_facet_capsule(p, vec3(0.31, -0.59, zc + 0.01), vec3(0.31, -0.45, zc + 0.04), 0.08, 0.5)); - d = min(d, s_facet_capsule(p, vec3(0.435, -0.625, zc + 0.005), vec3(0.435, -0.52, zc + 0.03), 0.07, 0.2)); - return min(d, s_facet_capsule(p, vec3(0.03, -0.77, zc + 0.03), vec3(-0.165, -0.60, zc + 0.065), 0.082, 0.7)); -} - fn sculpt_proto(p: vec3, theme: i32, shape: i32) -> vec2 { if theme == 3 { return s_voxels(p, shape); } - if theme == 1 { - return vec2(select(s_crystal_hand(p), s_gem_arrow(p), shape == 0), 8.0); - } - var body: vec2; - var star: vec3; - if shape == 0 { - var h = 0.075; - var re = 0.03; - var dome = 0.0; - if theme == 2 { - h = 0.07; - re = 0.06; - dome = 0.035; - } - if theme == 4 { - h = 0.07; - re = 0.055; - dome = 0.025; - } - body = vec2(s_arrow_solid(p, h, re, dome), 1.0); - if theme == 0 { - body = s_opu(body, vec2(s_piping(p, SCULPT_HOVER + 2.0 * h - 0.004), 2.0)); - } - star = vec3(0.57, -0.86, SCULPT_HOVER + 2.0 * h + dome); - } else { - body = s_opu(vec2(s_glove(p, SCULPT_HAND_ZC), 1.0), vec2(s_cuff(p, SCULPT_HAND_ZC), 2.0)); - star = vec3(0.185, -0.93, SCULPT_HAND_ZC + 0.15); - } - if theme == 4 { - return s_opu(body, s_sprout(p, star)); - } - return body; + return s_rimmed(p, theme, shape); } fn sculpt_point(q: vec3) -> vec3 { @@ -943,11 +981,11 @@ fn sculpt_eval(q: vec3, occ: bool) -> vec2 { let p = sculpt_point(q); var r: vec2; if occ && theme == 3 { - let d2 = select(s_pixel_hand_dist(p.xy), s_arrow2(p.xy) - 1.2 * SCULPT_VOX, shape == 0); + let d2 = s_pixel_outline(p.xy, shape); let dz = abs(p.z - (SCULPT_HOVER + 0.07)) - 0.07; r = vec2(length(max(vec2(d2, dz), vec2(0.0))) + min(max(d2, dz), 0.0), 7.0); } else { - r = sculpt_proto(p, select(theme, 2, occ && theme == 1), shape); + r = sculpt_proto(p, theme, shape); } return vec2(r.x * sculpt_units(), r.y); } @@ -1028,61 +1066,57 @@ struct SculptMat { } fn s_pixel_colour(p: vec3, shape: i32) -> vec3 { - let id = floor((p.xy - s_grid_origin(shape)) / SCULPT_VOX); - if !s_occ(id + vec2(-1.0, 0.0), shape) || !s_occ(id + vec2(0.0, -1.0), shape) { - return s_lin(0.52, 0.91, 0.77); + let cell = s_pix_cell(p.xy, shape); + let g = PIX_GRID[shape]; + let row = g.z + clamp(cell.y, 0, g.y - 1); + if s_pix_bit(PIX_LINE[row], cell.x) { + return s_lin(0.29, 0.12, 0.36); } - if !s_occ(id + vec2(1.0, 0.0), shape) || !s_occ(id + vec2(0.0, 1.0), shape) { + if s_pix_bit(PIX_HI[row], cell.x) { return s_lin(1.0, 0.78, 0.87); } - return s_lin(1.0, 0.50, 0.71); + if s_pix_bit(PIX_SHADE[row], cell.x) { + return s_lin(0.87, 0.27, 0.51); + } + return s_lin(1.0, 0.435, 0.66); } fn sculpt_material(mat: f32, p: vec3, theme: i32, shape: i32) -> SculptMat { let primary = mat < 1.5; if theme == 0 { - if shape == 0 && primary { - return SculptMat(s_lin(0.10, 0.10, 0.115), 0.3, 0.2, 0.0, 0.9); - } - if shape == 0 { - return SculptMat(s_lin(0.94, 0.91, 0.84), 0.45, 0.4, 0.2, 0.3); - } - if primary { - return SculptMat(s_lin(0.95, 0.92, 0.85), 0.55, 0.35, 0.35, 0.25); + if mat < 2.5 { + return SculptMat(s_lin(0.95, 0.92, 0.85), 0.3, 0.6, 0.35, 0.25); } - return SculptMat(s_lin(0.17, 0.18, 0.22), 0.35, 0.6, 0.0, 0.6); + return SculptMat(s_lin(0.1, 0.1, 0.11), 0.55, 0.05, 0.0, 0.1); } if theme == 2 { - if shape == 0 { - return SculptMat(s_lin(1.0, 0.40, 0.30), 0.5, 0.45, 0.4, 0.25); + if (primary && shape == 0) || (mat > 2.5 && mat < 3.5 && shape == 1) { + return SculptMat(s_lin(1.0, 0.40, 0.30), 0.85, 0.08, 0.15, 0.03); } - if primary { - return SculptMat(s_lin(1.0, 0.79, 0.16), 0.5, 0.45, 0.4, 0.25); + if mat < 3.5 { + return SculptMat(s_lin(1.0, 0.80, 0.10), 0.85, 0.08, 0.15, 0.03); } - return SculptMat(s_lin(0.18, 0.20, 0.29), 0.4, 0.5, 0.0, 0.4); + return SculptMat(s_lin(0.09, 0.13, 0.45), 0.9, 0.05, 0.05, 0.02); } if theme == 4 { if primary && shape == 0 { - return SculptMat(s_lin(0.62, 0.91, 0.78), 0.22, 0.8, 0.25, 0.6); + return SculptMat(s_lin(0.68, 0.93, 0.80), 0.25, 0.7, 0.3, 0.3); } if primary { - return SculptMat(s_lin(0.96, 0.94, 0.88), 0.5, 0.35, 0.35, 0.25); + return SculptMat(s_lin(0.97, 0.95, 0.90), 0.3, 0.6, 0.35, 0.25); } if mat < 2.5 { - return SculptMat(s_lin(0.62, 0.91, 0.78), 0.25, 0.7, 0.25, 0.5); + return SculptMat(s_lin(0.52, 0.87, 0.78), 0.25, 0.7, 0.3, 0.3); } if mat < 3.5 { - return SculptMat(s_lin(1.0, 0.80, 0.20), 0.3, 0.6, 0.3, 0.4); + return SculptMat(s_lin(1.0, 0.75, 0.25), 0.25, 0.7, 0.3, 0.3); } if mat < 4.5 { - return SculptMat(s_lin(0.38, 0.80, 0.55), 0.35, 0.5, 0.35, 0.3); + return SculptMat(s_lin(0.62, 0.92, 0.72), 0.3, 0.6, 0.3, 0.35); } - return SculptMat(s_lin(0.16, 0.12, 0.10), 0.2, 0.8, 0.0, 0.5); - } - if mat < 6.5 { - return SculptMat(s_pixel_colour(p, shape), 0.45, 0.35, 0.15, 0.2); + return SculptMat(s_lin(0.07, 0.15, 0.33), 0.7, 0.15, 0.05, 0.08); } - return SculptMat(s_lin(0.36, 0.18, 0.54), 0.45, 0.35, 0.1, 0.2); + return SculptMat(s_pixel_colour(p, shape), 0.65, 0.1, 0.12, 0.04); } fn model_env(d: vec3, rough: f32, l: vec3, fill: vec3) -> vec3 { @@ -1094,36 +1128,6 @@ fn model_env(d: vec3, rough: f32, l: vec3, fill: vec3) -> vec3 vec3 { - let k = saturate(k0) * 3.0; - let a = s_lin(0.20, 0.95, 1.0); - let b = s_lin(0.15, 0.42, 1.0); - let c = s_lin(0.45, 0.25, 0.95); - let d = s_lin(0.88, 0.50, 1.0); - if k < 1.0 { - return mix(a, b, k); - } - if k < 2.0 { - return mix(b, c, k - 1.0); - } - return mix(c, d, k - 2.0); -} - -fn model_shade_crystal(n: vec3, rd: vec3, L: vec3, fall: f32, l: vec3, fill: vec3) -> vec3 { - let cosi = saturate(dot(-rd, n)); - let F = 0.04 + 0.96 * pow(1.0 - cosi, 5.0); - let t = refract(rd, n, 1.0 / 1.6); - let k = 0.42 + 0.9 * dot(vec2(n.x, -n.y), vec2(0.7557, -0.6549)) - + 0.6 * dot(vec2(t.x, -t.y), vec2(0.6, -0.8)); - let body = vec3(s_gem(k - 0.08).r, s_gem(k).g, s_gem(k + 0.08).b); - var col = body * (0.1 + 1.8 * fall * pow(max(dot(n, L), 0.0), 2.5)); - col = col + body * model_env(reflect(t, vec3(0.0, 0.0, 1.0)) * vec3(1.0, 1.0, -1.0), 0.15, l, fill) * 0.5; - col = col + vec3(pow(max(dot(reflect(rd, n), L), 0.0), 30.0) * 2.0); - col = col + model_env(reflect(rd, n), 0.05, l, fill) * F; - col = col + s_lin(0.5, 0.9, 1.0) * pow(1.0 - cosi, 4.0) * 0.6; - return col; -} - fn model_tonemap(c0: vec3) -> vec3 { let start = 0.76; let x = min(c0.r, min(c0.g, c0.b)); @@ -1142,9 +1146,6 @@ fn model_tonemap(c0: vec3) -> vec3 { fn model_shade(q: vec3, n: vec3, rd: vec3, L: vec3, fall: f32, sh: f32, ao: f32, mat: f32, l: vec3, fill: vec3) -> vec3 { let id = sculpt_id(); - if id > 0 && (id - 1) / 2 == 1 { - return model_tonemap(model_shade_crystal(n, rd, L, fall, l, fill)); - } var m: SculptMat; var gloss = 1.0; if id > 0 { @@ -1177,7 +1178,11 @@ const STAGE_NORMAL: i32 = 1; const STAGE_AO: i32 = 2; const STAGE_SELF: i32 = 3; const STAGE_PLANE: i32 = 4; -const STAGE_DONE: i32 = 5; +const STAGE_EDGE: i32 = 5; +const STAGE_SHADE: i32 = 6; +const STAGE_DONE: i32 = 7; + +const MODEL_SUBPIXEL = array, 3>(vec2(0.35, 0.2), vec2(-0.35, 0.2), vec2(0.0, -0.4)); fn model_tetra(k: i32) -> vec3 { let b = vec3(f32(((k + 3) >> 1u) & 1), f32((k >> 1u) & 1), f32(k & 1)); @@ -1199,7 +1204,7 @@ fn cursor_model(local: vec2) -> vec4 { let hi = vec3(layer.color.rg + sprite_size(), layer.trail_a.z); let dw = vec3(local + layer.src.xy, -persp); - let dlen = length(dw); + var dlen = length(dw); // Plan translate de mb.zw dans le repere camera (camera reelle ; 0 sous un angle fixe). let ro = plane_to_model((world_to_plane(vec3(-layer.mb.z, -layer.mb.w, persp), f) - tip) / unit, f); let rd = plane_to_model(world_to_plane(dw / dlen, f), f); @@ -1210,15 +1215,18 @@ fn cursor_model(local: vec2) -> vec4 { let stride = select(1.0, 0.85, sculpt_id() > 0); let lamp = vec3(layer.color.rg + sprite_size() * 0.5, 0.0) + l * SCULPT_LAMP_DIST; - let tb = ray_box(ro, rd, lo - vec3(0.02), hi + vec3(0.02)); + var ray = rd; + var tb = ray_box(ro, ray, lo - vec3(0.02), hi + vec3(0.02)); var stage = select(STAGE_DONE, STAGE_MARCH, tb.x < tb.y && tb.y > 0.0); var plane_next = stage == STAGE_DONE; var k = 0; var t = max(tb.x, 0.0); var best = 1e9; var t_best = t; + var d_best = 0.0; var mat = 0.0; var cov = 0.0; + var cov_s = 0.0; var q = vec3(0.0); var n = vec3(0.0); var L = vec3(0.0); @@ -1230,10 +1238,13 @@ fn cursor_model(local: vec2) -> vec4 { var inside = 0.0; var contact = 0.0; var dropped = 0.0; - for (var it = 0; it < 180; it++) { + var sub = 0; + var shaded = 0; + var acc = vec4(0.0); + for (var it = 0; it < 500; it++) { if plane_next { plane_next = false; - stage = STAGE_DONE; + stage = select(STAGE_DONE, STAGE_SHADE, cov > 0.0); let denom = dot(rd, nz); if cov < 1.0 && denom < -1e-4 { g = ro + rd * ((hz - dot(ro, nz)) / denom); @@ -1246,11 +1257,42 @@ fn cursor_model(local: vec2) -> vec4 { } } } + if stage == STAGE_SHADE { + if cov_s > 0.0 { + let tl = lamp - q; + let fall = SCULPT_LAMP_DIST * SCULPT_LAMP_DIST / dot(tl, tl); + let c = model_shade(q, n, ray, normalize(tl), fall, sh, ao, mat, l, fill); + acc = acc + vec4(c * cov_s, cov_s); + } + shaded++; + stage = STAGE_DONE; + if sub > 0 { + let dws = vec3(local + MODEL_SUBPIXEL[3 - sub] + layer.src.xy, -persp); + sub--; + dlen = length(dws); + ray = plane_to_model(world_to_plane(dws / dlen, f), f); + tb = ray_box(ro, ray, lo - vec3(0.02), hi + vec3(0.02)); + cov_s = 0.0; + stage = STAGE_SHADE; + if tb.x < tb.y && tb.y > 0.0 { + stage = STAGE_MARCH; + k = 0; + t = max(tb.x, 0.0); + best = 1e9; + t_best = t; + } + } + continue; + } var pos: vec3; if stage == STAGE_MARCH { - pos = ro + rd * t; + pos = ro + ray * t; } else if stage == STAGE_NORMAL { pos = q + 0.002 * model_tetra(k); + } else if stage == STAGE_EDGE { + let side = normalize(cross(n, ray)); + let e = select(side, cross(n, side), k >= 2) * select(1.0, -1.0, k % 2 == 1); + pos = q + e * 0.5 * t_best / dlen; } else if stage == STAGE_AO { pos = q + (0.01 + 0.0175 * f32(k)) * SCULPT_SCALE * n; } else if stage == STAGE_SELF { @@ -1268,18 +1310,30 @@ fn cursor_model(local: vec2) -> vec4 { if hit || d / fp < best { best = select(d / fp, 0.0, hit); t_best = t; + d_best = d; mat = m.y; } t = t + d * stride; k++; if hit || t > tb.y || k == 96 { - cov = saturate(1.0 - best); - if cov > 0.0 { - q = ro + rd * t_best; - stage = STAGE_NORMAL; - k = 0; + cov_s = saturate(1.0 - best); + if shaded > 0 { + stage = STAGE_SHADE; + if cov_s > 0.0 { + q = ro + ray * t_best; + n = vec3(0.0); + stage = STAGE_NORMAL; + k = 0; + } } else { - plane_next = true; + cov = cov_s; + if cov > 0.0 { + q = ro + ray * t_best; + stage = STAGE_NORMAL; + k = 0; + } else { + plane_next = true; + } } } } else if stage == STAGE_NORMAL { @@ -1287,6 +1341,15 @@ fn cursor_model(local: vec2) -> vec4 { k++; if k == 4 { n = normalize(n); + k = 0; + stage = select(STAGE_EDGE, STAGE_SHADE, shaded > 0); + } + } else if stage == STAGE_EDGE { + if m.y != mat || abs(d - d_best) > 0.02 * t_best / dlen { + sub = 3; + } + k++; + if k == 4 { stage = STAGE_AO; k = 0; ao = 0.0; @@ -1323,7 +1386,7 @@ fn cursor_model(local: vec2) -> vec4 { res = 1.0; k = 1; if !(tbp.x < tbp.y && tbp.y > 0.0) { - stage = STAGE_DONE; + stage = select(STAGE_DONE, STAGE_SHADE, cov > 0.0); } } else { res = min(res, MODEL_SOFTNESS * d / ts.x); @@ -1332,20 +1395,15 @@ fn cursor_model(local: vec2) -> vec4 { if res < 0.002 || ts.x > ts.y || k == 33 { res = saturate(res); dropped = 1.0 - res * res * (3.0 - 2.0 * res); - stage = STAGE_DONE; + stage = select(STAGE_DONE, STAGE_SHADE, cov > 0.0); } } } - var rgb = vec3(0.0); - if cov > 0.0 { - let tl = lamp - q; - let fall = SCULPT_LAMP_DIST * SCULPT_LAMP_DIST / dot(tl, tl); - rgb = model_shade(q, n, rd, normalize(tl), fall, sh, ao, mat, l, fill); - } + let w = 1.0 / f32(max(shaded, 1)); let shadow = inside * max(dropped * MODEL_SHADOW_ALPHA, contact * MODEL_CONTACT_ALPHA); - let a = cov * layer.color.a; - return vec4(rgb * a, a + (1.0 - a) * shadow * layer.color.a); // premultiplie, ombre noire + let a = acc.a * w * layer.color.a; + return vec4(acc.rgb * w * layer.color.a, a + (1.0 - a) * shadow * layer.color.a); // premultiplie, ombre noire } // ---- Impact du clic (mode 16) ---- diff --git a/crates/compositor/tests/cursor_model_render.rs b/crates/compositor/tests/cursor_model_render.rs index 86d28bf70..a48105252 100644 --- a/crates/compositor/tests/cursor_model_render.rs +++ b/crates/compositor/tests/cursor_model_render.rs @@ -161,13 +161,13 @@ fn cursors_dir() -> String { .replace('\\', "/") } -/// Les cinq thèmes d'origine et les hotspots de leur flèche et de leur main plates -/// (`CURSOR_THEMES`, `src/lib/cursor/cursorThemes.ts`, sur 32). -const SCULPTED: [(&str, [f32; 2], [f32; 2]); 5] = [ +/// Les thèmes d'origine qui ont un modèle (Prism Glow n'en a pas : son dessin est extrudé) et les +/// hotspots de leur flèche et de leur main plates (`CURSOR_THEMES`, +/// `src/lib/cursor/cursorThemes.ts`, sur 32). +const SCULPTED: [(&str, [f32; 2], [f32; 2]); 4] = [ ("studio-ink", [6.2304, 2.0992], [12.848, 2.0704]), - ("prism-glow", [6.3456, 2.0672], [11.968, 2.0352]), ("pop-coral", [10.4768, 2.1792], [12.3456, 2.0]), - ("pixel-candy", [7.3664, 2.0], [13.376, 1.9264]), + ("pixel-candy", [6.5, 2.0], [10.75, 2.0]), ("star-sprout", [4.7232, 2.1152], [13.1712, 2.0384]), ]; @@ -765,10 +765,10 @@ fn the_motion_blur_trail_draws_modelled_copies() { assert!(rgba != flat, "la traînée 3D est celle du sprite plat"); } -/// Les dix curseurs sculptés (cinq thèmes, flèche et main), dessinés par le shader et non -/// extrudés d'un PNG : chacun est là, sa pointe sur le hotspot, son corps en bas à droite de -/// celle-ci (y vers le bas : un modèle retourné finirait au-dessus), et il porte son ombre en -/// l'air. +/// Les curseurs sculptés (flèche et main de chaque thème qui a un modèle), dessinés par le +/// shader et non extrudés d'un PNG : chacun est là, sa pointe sur le hotspot, son corps en bas à +/// droite de celle-ci (y vers le bas : un modèle retourné finirait au-dessus), et il porte son +/// ombre en l'air. #[test] fn the_sculpted_cursors_stand_at_the_hotspot() { let Some(gpu) = gpu() else { return }; @@ -865,7 +865,7 @@ fn contact_sheets() { } tilted.save(format!("{dir}/states-touch-flat-and-iso.png")).expect("planche"); - // Les dix curseurs sculptés : un thème par ligne, flèche et main en l'air sur l'écran à plat, + // Les curseurs sculptés : un thème par ligne, flèche et main en l'air sur l'écran à plat, // puis posées sur l'écran incliné. let cases = [("arrow", "null", false), ("pointer", "null", false), ("arrow", r#""iso""#, true), ("pointer", r#""iso""#, true)]; let mut sculpted = image::RgbaImage::new(4 * CELL, SCULPTED.len() as u32 * CELL); diff --git a/design/cursors/3d-concept.png b/design/cursors/3d-concept.png index fc828101b..4b7a01c98 100644 Binary files a/design/cursors/3d-concept.png and b/design/cursors/3d-concept.png differ diff --git a/design/cursors/3d-direction.md b/design/cursors/3d-direction.md index 6333c23d0..33dd399ae 100644 --- a/design/cursors/3d-direction.md +++ b/design/cursors/3d-direction.md @@ -2,21 +2,32 @@ `3d-concept.png` is a visual reference, not a render from OpenScreen. -With the 3D option on, mode 15 ray-marches each original theme's arrow and hand -as a sculpted model: signed distance functions written in the compositor's three -shaders (HLSL, Metal, WGSL), not a PNG face or a height map. -`crates/compositor/src/sculpt.rs` names the ten models and holds their bounding -boxes and hotspots. The scene names the model (`"/"`); text, -resize, move and the other OS states keep their built-in art and the -compositor's beveled extrusion. +With the 3D option on, mode 15 ray-marches the arrow and hand of Studio Ink, Pop +Coral, Pixel Candy and Star Sprout as sculpted models: signed distance functions +written in the compositor's three shaders (HLSL, Metal, WGSL), not a PNG face or +a height map. `crates/compositor/src/sculpt.rs` names the eight models and holds +their bounding boxes and hotspots; Prism Glow has none. The scene names the +model (`"/"`); text, resize, move and the other OS states keep +their built-in art and the compositor's beveled extrusion. | Theme | Arrow model | Hand model | | --- | --- | --- | -| Studio Ink | Matte black body with an ivory piping set in from the edge. | Ivory glove with a dark cuff. | -| Prism Glow | Cut crystal: flat facets, a polygonal silhouette, cyan to violet dispersion. | The same glove cut into faceted fingers and palm. | -| Pop Coral | Soft coral rubber with rounded edges and a gentle dome. | Plump yellow rubber glove with a navy cuff. | -| Pixel Candy | Beveled voxels: a pink face with mint and pale pink edges, purple blocks one step behind. | Voxel glove with the same colours and purple back layer. | -| Star Sprout | Glossy mint ceramic carrying a star with a face and two leaves. | Ivory glove with a mint cuff and the same star. | +| Studio Ink | Black rim, a raised ivory band, a recessed satin black field. | Ivory glove cushion in a black rim, black ridges between the fingers. | +| Prism Glow | No sculpted model: its faceted drawing is extruded as drawn, with a beveled edge, like the default cursors. | The same. | +| Pop Coral | Cut paper: flat-topped sheets, no rounded bead or gloss. A coral sheet on a navy sheet, a yellow sheet behind them offset down-left, two yellow click dashes. | A yellow sheet on a navy sheet, the navy showing between the fingers, a coral sheet behind offset down-left, two coral click dashes. | +| Pixel Candy | Simplified pixel art, one cube per pixel: a plum outline, a pink body with a pale highlight down the lit edge and a darker shade along the outline. The 3D model and the 2D sprite come from the same grid. | The same, a pixel hand: the index up, three knuckles apart, the thumb. | +| Star Sprout | Glossy mint cushion framed by the navy outline of its drawing, carrying a star with a face and two leaves. | Ivory glove cushion in the same navy frame, navy ridges between the fingers, a mint cuff and the same star. | + +Studio Ink, Pop Coral and Star Sprout keep their drawn outline, as in +`3d-concept.png` (and `star-sprout/3d-reference.png` for the Star Sprout hand). +Each piece is a tray in the outline colour under its silhouette, a rounded bead +along the stroke and a puffy colour cushion inside it; the grooves between the +fingers become ridges of the outline colour. The three arrows share one polygon +and the three gloves one construction, each glove measured on its own drawing +(`RIM_POLY`, `RIM_GLOVE`). Pop Coral turns the same shapes into flat paper +sheets instead (`s_paper`). What stands in front is theme-specific: Studio +Ink's ivory band, Pop Coral's yellow and coral layers and click dashes, Star +Sprout's cuff, star, leaves and eyes. All models share one close lamp: a gradient and a highlight even on flat faces, soft self shadows, ambient occlusion and a studio environment in the @@ -27,4 +38,6 @@ click and size changes. To change a model, edit it in all three shaders (`sculpt_proto`, `sculpt_material`) and keep `sculpt.rs` in step: its boxes must contain the shapes, and a test checks that the shaders mirror its constants. Pixel Candy's -arrow voxels are sampled from the arrow polygon; `sculpt.rs` says how. +voxel grids are tables written by `scripts/generate-pixel-candy-voxels.mjs`: +change a grid there and run the script; it rewrites all three shaders, and a +test checks the tables agree. diff --git a/design/cursors/README.md b/design/cursors/README.md index b3a21cd3c..8b58dc6dd 100644 --- a/design/cursors/README.md +++ b/design/cursors/README.md @@ -18,14 +18,16 @@ fringe pixels, and resize them into transparent 128 × 128 PNGs under `public/cursors//`. It prints the normalized tip hotspots; copy them into `src/lib/cursor/cursorThemes.ts` after changing a master. -With the 3D option on, the compositor draws each theme's arrow and hand as a -model sculpted in its shaders (`crates/compositor/src/sculpt.rs`), not from these -PNGs. The theme picker and the flat cursor keep using the PNGs. Text, resize, -move and other states remain state-accurate through the built-in art and -receive the shared 3D extrusion. +With the 3D option on, the compositor draws the arrow and hand of Studio Ink, +Pop Coral, Pixel Candy and Star Sprout as models sculpted in its shaders +(`crates/compositor/src/sculpt.rs`), not from these PNGs. Prism Glow's arrow and +hand keep their PNGs and receive the shared 3D extrusion. The theme picker and +the flat cursor keep using the PNGs. Text, resize, move and other states remain +state-accurate through the built-in art and receive the shared 3D extrusion. `contact-sheet.png` shows the sprites enlarged on a light background; `dark-32px.png` shows them at their 32-pixel reference size on a dark background. -The 3D modeling direction and concept image are in `3d-direction.md` and -`3d-concept.png`. +The 3D modeling direction and the reference sheet the models follow are in +`3d-direction.md` and `3d-concept.png`; `star-sprout/3d-reference.png` is a closer +view of the Star Sprout hand. The consolidated acceptance list is in `requirements.md`. diff --git a/design/cursors/pixel-candy/source.png b/design/cursors/pixel-candy/source.png index 79b5c859d..daeed19be 100644 Binary files a/design/cursors/pixel-candy/source.png and b/design/cursors/pixel-candy/source.png differ diff --git a/design/cursors/requirements.md b/design/cursors/requirements.md index 3274014c7..5495e39c7 100644 --- a/design/cursors/requirements.md +++ b/design/cursors/requirements.md @@ -21,16 +21,17 @@ their 3D models. - Every replacement theme and supported cursor state must work with the existing 3D cursor option. -- Every replacement arrow and hand is a real volume, sculpted in the - compositor's shaders (`crates/compositor/src/sculpt.rs`): lighting, normals, - self shadows and contact shadows respond to cursor tilt and rotation. A PNG - face or a height map is not a model. +- Every replacement arrow and hand except Prism Glow's is a real volume, + sculpted in the compositor's shaders (`crates/compositor/src/sculpt.rs`): + lighting, normals, self shadows and contact shadows respond to cursor tilt + and rotation. A PNG face or a height map is not a model. - A rounded extrusion of the PNG silhouette remains the fallback for cursor - states that do not have a dedicated theme model. The five replacement packs - add style-specific volume: a glove with separate fingers, cut crystal facets, - puffy rubber, beveled voxels, and ceramic accents. -- Model the arrow and hand separately for every theme. Do not apply one - contour, material, or roundness rule to all designs. + states that do not have a dedicated theme model, and for Prism Glow, whose + faceted drawing is extruded as drawn. The four sculpted packs add + style-specific volume: gloves with separate fingers, beveled voxels, flat + cut-paper sheets, and cushions set in the rim of their drawn outline. +- Model the arrow and hand separately for every sculpted theme. Do not apply + one contour, material, or roundness rule to all designs. - Preserve the cursor hotspot through hover, tilt, yaw, click, and size changes. - Reuse the compositor's 3D lighting and contact shadows. The surface must change with the compositor camera; a static image of a 3D render is not a @@ -40,11 +41,11 @@ their 3D models. | Theme | Arrow | Hand | | --- | --- | --- | -| Studio Ink | Matte black sculpted body; ivory mark becomes a piping set in from the edge. | Ivory glove with a convex palm and separate fingers; remove the dark outline. | -| Prism Glow | Translucent faceted crystal; no navy ink contour. | Crystal palm and distinct faceted fingers; no navy ink contour. | -| Pop Coral | Rounded coral rubber; omit the yellow offset and navy rim. | Plump yellow rubber glove; no navy outline, accents become physical details. | -| Pixel Candy | Keep the stepped voxel form; purple is side/back blocks, not a flat outline. | Stepped voxel fingers and palm; preserve purple as physical side/back blocks. | -| Star Sprout | Soft mint ceramic; no heavy blue outline, raised star and leaves. | Rounded ivory glove with no blue outline; separate mint cuff, star, and leaves. | +| Studio Ink | Black rim, raised ivory band, recessed satin black field. | Ivory glove cushion; the black outline stays as a physical rim, with black ridges between the fingers. | +| Prism Glow | The faceted drawing extruded as drawn, beveled edge; no sculpted model. | The same. | +| Pop Coral | Cut paper: a coral sheet on a navy sheet, flat tops, matte; the yellow offset becomes a flat sheet behind; the click dashes become flat pieces. | A yellow sheet on a navy sheet, flat tops, matte; the coral offset becomes a flat sheet behind; the coral click dashes become flat pieces. | +| Pixel Candy | Simplified pixel art that reads at 20 px as at full size; the 3D model is one cube per pixel of the same grid as the sprite. | A pixel hand from the same kind of grid: index, three knuckles, thumb. | +| Star Sprout | Puffy mint cushion; the navy outline stays as a physical rim (a navy tray and a rounded bead) around it, the star and its leaves in front, each in its own navy rim. | Puffy ivory glove in the same navy rim, with navy ridges between the fingers; separate mint cuff framed in navy; the star and leaves in front, as in `star-sprout/3d-reference.png`. | The 2D PNG may keep a drawn contour where it helps readability. Its contour must not be copied blindly onto the 3D material: decide per model whether it @@ -53,7 +54,10 @@ becomes an inlay, side material, separate piece, or disappears. ## Review references - `contact-sheet.png`: 2D PNG themes. -- `3d-concept.png`: visual direction only, not rendered by the app. +- `3d-concept.png`: the 3D reference sheet the eight sculpted models follow + (Prism Glow is extruded), not rendered by the app. +- `star-sprout/3d-reference.png`: a closer view of the Star Sprout hand; also + not rendered by the app. - `3d-direction.md`: per-theme modeling notes. - `README.md`: source PNG and preparation workflow. diff --git a/design/cursors/star-sprout/3d-reference.png b/design/cursors/star-sprout/3d-reference.png new file mode 100644 index 000000000..760ed6b62 Binary files /dev/null and b/design/cursors/star-sprout/3d-reference.png differ diff --git a/docs/3d-effects-v2.md b/docs/3d-effects-v2.md index 978cd1a80..41528c542 100644 --- a/docs/3d-effects-v2.md +++ b/docs/3d-effects-v2.md @@ -233,12 +233,12 @@ inconnues sont ignorées). ### B.2 Le réglage **Un seul interrupteur**, `cursor.model3d` (« 3D cursor », **éteint par défaut**) : il passe -**chaque état** du curseur en 3D. La flèche et la main des cinq thèmes d'origine deviennent leur -modèle sculpté (B.3 bis) ; les autres états, et tous ceux du thème par défaut (les seize de -`DEFAULT_CURSOR_SPRITES` : flèche, I, main, croix, mains ouverte et fermée, redimensionnements, -déplacement, interdit, attente…), leur sprite extrudé. Curseur masqué, l'interrupteur est grisé et -son info-bulle dit pourquoi. Éteint, la frame est celle d'avant **à l'octet** (vérifié à plat et -incliné contre le commit de base). +**chaque état** du curseur en 3D. La flèche et la main de quatre des cinq thèmes d'origine +deviennent leur modèle sculpté (B.3 bis) ; celles de Prism Glow, les autres états, et tous ceux +du thème par défaut (les seize de `DEFAULT_CURSOR_SPRITES` : flèche, I, main, croix, mains +ouverte et fermée, redimensionnements, déplacement, interdit, attente…), leur sprite extrudé. +Curseur masqué, l'interrupteur est grisé et son info-bulle dit pourquoi. Éteint, la frame est +celle d'avant **à l'octet** (vérifié à plat et incliné contre le commit de base). Tuyauterie : `CursorVisualSettings.model3d`, clé legacy `cursorModel3d`, préréglages (absent → éteint), `SceneCursor.model3d` (`serde(default)`), `LiveParams.cursor_model3d`, paramètre live @@ -287,24 +287,36 @@ du bord (0,48 au pire), sous 1 texel au-delà (le flou arrondit les crêtes). ### B.3 bis Les curseurs sculptés -La flèche et la main des cinq thèmes d'origine sont **modélisées à la main**, en fonctions de -distance signée écrites dans les trois shaders (`sculpt_proto`, `sculpt_material`) : capsules et -unions lissées pour les gants, extrusion arrondie et bombée pour les flèches, voxels biseautés -pour Pixel Candy, polyèdres taillés pour le cristal de Prism Glow. Le PNG du thème reste l'art en -2D. En 3D, la scène nomme le modèle ; `sculpt.rs` en tient la boîte, qui pose le hotspot (pointe -de la flèche, bout de l'index), règle la garde au sol et borne la boîte de dessin. Emplacement du -cbuffer : `trail_a` = [modèle, épaisseur sous z = 0, hauteur au-dessus, 0] (`cursor_model_cb`). +La flèche et la main des thèmes d'origine sont **modélisées à la main**, en fonctions de +distance signée écrites dans les trois shaders (`sculpt_proto`, `sculpt_material`) : voxels +pour Pixel Candy ; pour Studio Ink et Star Sprout des pièces qui gardent le trait de +leur dessin : un plateau et un jonc de la couleur du trait, un coussin de couleur dedans +(`s_rimmed`) ; pour Pop Coral les mêmes formes en papier découpé, des feuilles au dessus plat +(`s_paper`). Prism Glow n'a pas de modèle : son dessin à facettes est extrudé comme tout sprite +(B.3). Le PNG du thème reste l'art en 2D. En 3D, la scène nomme le modèle ; `sculpt.rs` en tient +la boîte, qui pose le hotspot (pointe de la flèche, bout de l'index), règle la garde au sol et +borne la boîte de dessin. Emplacement du cbuffer : `trail_a` = [modèle, épaisseur sous z = 0, +hauteur au-dessus, 0] (`cursor_model_cb`). +Pixel Candy est un pixel art dessiné une fois, en grille, dans +`scripts/generate-pixel-candy-voxels.mjs` : le script en tire ses PNG 2D et les tables des trois +shaders, un cube par pixel plein, tous de même hauteur, colorés comme leur pixel (contour prune, +rose, reflet rose pâle, ombre rose foncé). - **Éclairage**, sprites extrudés compris : une lampe proche en haut à gauche, qui met un dégradé et un reflet même sur une face plane ; une lumière d'appoint faible ; le côté ombré teinté par la matière ; occlusion ambiante, ombre propre douce vers la lampe, studio dans les reflets, liseré de Fresnel ; tone map Khronos PBR Neutral. -- **Ombre sur l'écran** : celle de B.3. Le cristal et les voxels donnent de mauvaises distances - loin de leur surface (bornes de plans, champ de grille) : l'ombre prend la forme lisse de Pop - Coral pour le cristal et le contour extrudé pour les voxels. +- **Ombre sur l'écran** : celle de B.3. Les voxels donnent de mauvaises distances loin de leur + surface (champ de grille) : l'ombre de Pixel Candy vient de sa forme extrudée. +- **Antialiasing** : la silhouette l'est par la marche (la distance minimale frôlée, en pixels, + donne la couverture). Les bords intérieurs (deux matières, une arête, un arrondi serré, un + joint de voxels) sont suréchantillonnés : quatre sondes à un demi-pixel du point touché, dans + son plan tangent, et si la matière change ou que la surface s'en écarte, trois rayons de plus + dans le pixel, ombrés avec l'occlusion et les ombres du premier (`STAGE_EDGE`, `STAGE_SHADE`). + Sur un grand curseur, ~+50 % du coût du curseur (rendu logiciel), rien ailleurs. - **Coût de compilation** : FXC recopie chaque appel. La marche, les normales, l'occlusion et les - ombres forment donc une seule boucle à étapes avec un seul appel au modèle, et l'étoile de Star - Sprout un seul appel pour les deux formes. + ombres forment donc une seule boucle à étapes avec un seul appel au modèle, et les thèmes + cerclés un seul appel pour leurs deux formes. ### B.4 La caméra et l'ancrage diff --git a/public/cursors/pixel-candy/arrow.png b/public/cursors/pixel-candy/arrow.png index 1efe79656..f80ba85e5 100644 Binary files a/public/cursors/pixel-candy/arrow.png and b/public/cursors/pixel-candy/arrow.png differ diff --git a/public/cursors/pixel-candy/pointer.png b/public/cursors/pixel-candy/pointer.png index 13e181de9..432b6e285 100644 Binary files a/public/cursors/pixel-candy/pointer.png and b/public/cursors/pixel-candy/pointer.png differ diff --git a/scripts/generate-original-cursor-themes.mjs b/scripts/generate-original-cursor-themes.mjs index 678cae5ff..f9d925c35 100644 --- a/scripts/generate-original-cursor-themes.mjs +++ b/scripts/generate-original-cursor-themes.mjs @@ -1,6 +1,7 @@ // Prepare transparent cursor PNGs from the original raster artwork in design/cursors. // Run with `node scripts/generate-original-cursor-themes.mjs`. -// Each source PNG contains the arrow on the left and the hand on the right. +// Each source PNG contains the arrow on the left and the hand on the right. Pixel Candy is not +// here: its pixel art is a grid, and scripts/generate-pixel-candy-voxels.mjs writes its PNGs. import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; @@ -27,10 +28,6 @@ const themes = [ id: "pop-coral", hotspots: { arrow: [265, 58], pointer: [1245, 65] }, }, - { - id: "pixel-candy", - hotspots: { arrow: [273, 76], pointer: [1218, 78] }, - }, { id: "star-sprout", hotspots: { arrow: [210, 65], pointer: [1218, 65] }, diff --git a/scripts/generate-pixel-candy-voxels.mjs b/scripts/generate-pixel-candy-voxels.mjs new file mode 100644 index 000000000..a6470c9c8 --- /dev/null +++ b/scripts/generate-pixel-candy-voxels.mjs @@ -0,0 +1,300 @@ +// Pixel Candy, drawn once as pixel art: this grid is both the 2D cursor (its PNGs) and the 3D +// model (one cube per pixel, mode 15, theme 3). +// Run with `node scripts/generate-pixel-candy-voxels.mjs` after changing a grid below: it writes +// public/cursors/pixel-candy/{arrow,pointer}.png, design/cursors/pixel-candy/source.png, and the +// PIX_* tables of the three compositor shaders between their `` markers, then +// prints the hotspots for src/lib/cursor/cursorThemes.ts. +// +// `O` is the plum outline, `P` the pink body, `H` its pale highlight down the lit edge, `.` +// nothing. The shade is not drawn: a body pixel with the outline right of it or under it turns +// darker pink by itself. The hotspot is the top-left corner of the arrow's first pixel and the top +// middle of the index; in the 3D prototype frame (crates/compositor/src/sculpt.rs) it is the +// origin. + +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { deflateSync } from "node:zlib"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const SHADERS = path.join(ROOT, "crates", "compositor", "src"); +// Mirrors SCULPT_VOX in the shaders and VOX in sculpt.rs: sixteen rows make a cursor one unit tall. +const VOX = 0.0625; +// Sprite pixels per art pixel, in the 128px sprite: sixteen rows fill the 112px the other themes +// use. +const CELL = 7; +const SPRITE = 128; + +const SHAPES = [ + { + name: "arrow", + // Hotspot column: the left edge of the first pixel. + hotspotColumn: 0, + grid: [ + "O..........", + "OO.........", + "OHO........", + "OHPO.......", + "OHPPO......", + "OHPPPO.....", + "OHPPPPO....", + "OHPPPPPO...", + "OHPPPPPPO..", + "OHPPPPPPPO.", + "OHPPPPOOOOO", + "OHPOHPO....", + "OHO.OHPO...", + "OO..OHPO...", + ".....OHPO..", + "......OO...", + ], + }, + { + name: "pointer", + // Between the index's two columns. + hotspotColumn: 5, + grid: [ + "....OO..........", + "...OHPO.........", + "...OHPO.........", + "...OHPO.........", + "...OHPOOO.......", + "...OHPOPPOOO....", + "...OHPOPPOPPOOO.", + ".OOOHPOPPOPPOPPO", + "OHHOHPPPPPPPPPPO", + "OHPPHPPPPPPPPPPO", + ".OHPPPPPPPPPPPPO", + ".OHPPPPPPPPPPPO.", + "..OHPPPPPPPPPPO.", + "..OHPPPPPPPPPO..", + "...OHPPPPPPPPO..", + "...OOOOOOOOOOO..", + ], + }, +]; + +// sRGB, in the order of the shader's colour classes. +const COLOURS = { O: [74, 31, 92], P: [255, 111, 168], H: [255, 200, 222], S: [222, 70, 130] }; + +// The grid with its shade pixels. +function shaded(grid) { + const at = (c, r) => grid[r]?.[c] ?? "."; + return grid.map((row, r) => + [...row] + .map((ch, c) => (ch === "P" && (at(c + 1, r) === "O" || at(c, r + 1) === "O") ? "S" : ch)) + .join(""), + ); +} + +const shapes = SHAPES.map((shape) => { + const grid = shaded(shape.grid); + const cols = Math.max(...grid.map((row) => row.length)); + const rows = grid.length; + const mask = (test) => + grid.map((row) => [...row].reduce((m, ch, c) => (test(ch) ? m | (1 << c) : m), 0)); + return { + ...shape, + grid, + cols, + rows, + origin: [-shape.hotspotColumn * VOX, 0], + body: mask((ch) => ch !== "."), + line: mask((ch) => ch === "O"), + hi: mask((ch) => ch === "H"), + shade: mask((ch) => ch === "S"), + }; +}); + +// The body as rectangles (centre, half size, prototype units): each row's runs, merged with the +// rows below while the run stays the same. The shadow uses their exact outline. +function rects(shape) { + const open = []; + const done = []; + shape.body.forEach((m, r) => { + const runs = []; + for (let c = 0; c < shape.cols; c++) { + if (!(m & (1 << c))) continue; + const start = c; + while (c + 1 < shape.cols && m & (1 << (c + 1))) c++; + runs.push([start, c]); + } + for (const box of open.splice(0)) { + const k = runs.findIndex(([a, b]) => a === box.a && b === box.b); + if (k >= 0) { + box.r1 = r; + runs.splice(k, 1); + open.push(box); + } else done.push(box); + } + for (const [a, b] of runs) open.push({ a, b, r0: r, r1: r }); + }); + done.push(...open); + return done.map(({ a, b, r0, r1 }) => { + const [ox, oy] = shape.origin; + const [x0, x1] = [ox + a * VOX, ox + (b + 1) * VOX]; + const [y0, y1] = [oy - (r1 + 1) * VOX, oy - r0 * VOX]; + return [(x0 + x1) / 2, (y0 + y1) / 2, (x1 - x0) / 2, (y1 - y0) / 2]; + }); +} + +const boxes = shapes.map(rects); +const rectStarts = boxes.reduce((s, b) => [...s, s[s.length - 1] + b.length], [0]); +const grids = []; +let at = 0; +for (const s of shapes) { + grids.push([s.cols, s.rows, at, 0]); + at += s.rows; +} +const bounds = shapes.map(({ origin: [ox, oy], cols, rows }) => [ + ox + (cols * VOX) / 2, + oy - (rows * VOX) / 2, + (cols * VOX) / 2, + (rows * VOX) / 2, +]); + +// ---- PNG ---- + +const CRC = new Uint32Array(256).map((_, n) => { + let c = n; + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + return c >>> 0; +}); +function crc32(bytes) { + let c = 0xffffffff; + for (const b of bytes) c = CRC[(c ^ b) & 0xff] ^ (c >>> 8); + return (c ^ 0xffffffff) >>> 0; +} +function chunk(type, data) { + const out = Buffer.alloc(12 + data.length); + out.writeUInt32BE(data.length, 0); + out.write(type, 4, "ascii"); + data.copy(out, 8); + out.writeUInt32BE(crc32(out.subarray(4, 8 + data.length)), 8 + data.length); + return out; +} +function png(width, height, rgba) { + const header = Buffer.alloc(13); + header.writeUInt32BE(width, 0); + header.writeUInt32BE(height, 4); + header.set([8, 6, 0, 0, 0], 8); + const raw = Buffer.alloc((width * 4 + 1) * height); + for (let y = 0; y < height; y++) + rgba.copy(raw, y * (width * 4 + 1) + 1, y * width * 4, (y + 1) * width * 4); + return Buffer.concat([ + Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), + chunk("IHDR", header), + chunk("IDAT", deflateSync(raw, { level: 9 })), + chunk("IEND", Buffer.alloc(0)), + ]); +} +// The grid at `cell` pixels per art pixel, its top-left at (x0, y0) of a `w` × `h` canvas. +function paint(rgba, w, shape, cell, x0, y0) { + shape.grid.forEach((row, r) => { + [...row].forEach((ch, c) => { + if (ch === ".") return; + for (let y = 0; y < cell; y++) { + for (let x = 0; x < cell; x++) { + const i = ((y0 + r * cell + y) * w + x0 + c * cell + x) * 4; + rgba.set([...COLOURS[ch], 255], i); + } + } + }); + }); +} + +const cursors = path.join(ROOT, "public", "cursors", "pixel-candy"); +await mkdir(cursors, { recursive: true }); +const hotspots = []; +for (const shape of shapes) { + const x0 = Math.round((SPRITE - shape.cols * CELL) / 2); + const y0 = Math.round((SPRITE - shape.rows * CELL) / 2); + const rgba = Buffer.alloc(SPRITE * SPRITE * 4); + paint(rgba, SPRITE, shape, CELL, x0, y0); + await writeFile(path.join(cursors, `${shape.name}.png`), png(SPRITE, SPRITE, rgba)); + // On the 32-logical reference of cursorThemes.ts. + hotspots.push([ + shape.name, + ((x0 + shape.hotspotColumn * CELL) * 32) / SPRITE, + (y0 * 32) / SPRITE, + ]); +} + +// The design master, arrow on the left and hand on the right, like the other themes' sources. +{ + const [w, h, cell] = [1774, 887, 40]; + const rgba = Buffer.alloc(w * h * 4); + shapes.forEach((shape, i) => { + const x0 = Math.round(i * (w / 2) + (w / 2 - shape.cols * cell) / 2); + paint(rgba, w, shape, cell, x0, Math.round((h - shape.rows * cell) / 2)); + }); + await writeFile( + path.join(ROOT, "design", "cursors", "pixel-candy", "source.png"), + png(w, h, rgba), + ); +} + +// ---- Shader tables ---- + +const num = (v) => { + const s = v.toFixed(4); + return Number(s) === 0 ? "0.0" : s.replace(/0+$/, "").replace(/\.$/, ".0"); +}; + +function block(lang) { + const t = { + wgsl: { i4: "vec4", f2: "vec2", f4: "vec4" }, + hlsl: { i4: "int4", f2: "float2", f4: "float4" }, + metal: { i4: "int4", f2: "float2", f4: "float4" }, + }[lang]; + const vec = (type, v) => + `${type}(${v.map((x) => (type === t.i4 ? String(x) : num(x))).join(", ")})`; + const tables = [ + ["PIX_BODY", shapes.flatMap((s) => s.body)], + ["PIX_LINE", shapes.flatMap((s) => s.line)], + ["PIX_HI", shapes.flatMap((s) => s.hi)], + ["PIX_SHADE", shapes.flatMap((s) => s.shade)], + ["PIX_RECT_N", rectStarts], + ]; + const vectors = [ + ["PIX_GRID", t.i4, grids], + ["PIX_ORIGIN", t.f2, shapes.map((s) => s.origin)], + ["PIX_BOX", t.f4, bounds], + ["PIX_RECT", t.f4, boxes.flat()], + ]; + if (lang === "wgsl") { + return [ + ...tables.map(([n, v]) => `const ${n} = array(${v.join(", ")});`), + ...vectors.map( + ([n, ty, v]) => + `const ${n} = array<${ty}, ${v.length}>(\n ${v.map((x) => vec(ty, x)).join(",\n ")}\n);`, + ), + ].join("\n"); + } + const decl = lang === "hlsl" ? "static const" : "constant"; + return [ + ...tables.map(([n, v]) => `${decl} int ${n}[${v.length}] = { ${v.join(", ")} };`), + ...vectors.map( + ([n, ty, v]) => + `${decl} ${ty} ${n}[${v.length}] = {\n ${v.map((x) => vec(ty, x)).join(",\n ")}\n};`, + ), + ].join("\n"); +} + +const targets = [ + ["vk_shaders/layer.wgsl", "wgsl"], + ["shaders.hlsl", "hlsl"], + ["shaders.metal", "metal"], +]; +const marked = /(\/\/ \n)[\s\S]*?(\/\/ <\/pixel-candy-voxels>)/; +const sources = await Promise.all( + targets.map(([file]) => readFile(path.join(SHADERS, file), "utf8")), +); +targets.forEach(([file], i) => { + if (!marked.test(sources[i])) throw new Error(`${file}: no markers`); +}); +for (const [i, [file, lang]] of targets.entries()) { + const src = sources[i].replace(marked, (_, open, close) => `${open}${block(lang)}\n${close}`); + await writeFile(path.join(SHADERS, file), src); +} +for (const [name, x, y] of hotspots) console.log(`${name}: hotspotX ${x}, hotspotY ${y}`); diff --git a/src/lib/cursor/cursorThemes.test.ts b/src/lib/cursor/cursorThemes.test.ts index e712e8ad4..c6dceb37a 100644 --- a/src/lib/cursor/cursorThemes.test.ts +++ b/src/lib/cursor/cursorThemes.test.ts @@ -67,7 +67,9 @@ describe("normalizeCursorThemeId", () => { // The compositor models these two states itself (`crates/compositor/src/sculpt.rs`, which // knows the same names): in 3D the scene names the model, and the sprite stays the flat art. - it.each(CURSOR_THEMES)("names the sculpted 3D arrow and hand of $name", (theme) => { + // Prism Glow is the exception: its drawing is extruded, like the default art. + const sculptedThemes = CURSOR_THEMES.filter((theme) => theme.id !== "prism-glow"); + it.each(sculptedThemes)("names the sculpted 3D arrow and hand of $name", (theme) => { const flat = resolveCursorSprites(theme.id); const sprites = resolveCursorSprites(theme.id, [], true); for (const state of ["arrow", "pointer"] as const) { @@ -78,8 +80,11 @@ describe("normalizeCursorThemeId", () => { expect(sprites.text).toBe(DEFAULT_CURSOR_SPRITES.text); }); - it("leaves the default art to the extruded sprite in 3D", () => { - for (const sprite of Object.values(resolveCursorSprites(DEFAULT_CURSOR_THEME_ID, [], true))) { + it.each([ + DEFAULT_CURSOR_THEME_ID, + "prism-glow", + ])("leaves %s to the extruded sprite in 3D", (id) => { + for (const sprite of Object.values(resolveCursorSprites(id, [], true))) { expect(sprite.sculpt).toBeUndefined(); } }); diff --git a/src/lib/cursor/cursorThemes.ts b/src/lib/cursor/cursorThemes.ts index ada3ee416..fb7cf2e11 100644 --- a/src/lib/cursor/cursorThemes.ts +++ b/src/lib/cursor/cursorThemes.ts @@ -129,14 +129,16 @@ export function readCursorAsArrow( /** * Bundled cursor themes. These five packs are original OpenScreen artwork. Their raster - * masters live in design/cursors/ and are prepared by scripts/generate-original-cursor-themes.mjs. + * masters live in design/cursors/ and are prepared by scripts/generate-original-cursor-themes.mjs, + * except Pixel Candy's pixel art, drawn as a grid in scripts/generate-pixel-candy-voxels.mjs. * The former Sweezy packs were removed * because their terms forbid redistribution without written permission. * * To add one: drop arrow.png/pointer.png into public/cursors// and add an entry here * with hotspots normalized to the 32-logical reference (divide a 128px-pack hotspot by 4). * Mark an asset `sculpted` when the compositor has a 3D model for it (`sculpt.rs`); other states - * are extruded from their sprite in 3D. An id that leaves this list reads back as the default art + * are extruded from their sprite in 3D, and so is all of Prism Glow, whose faceted art reads + * best as it is drawn. An id that leaves this list reads back as the default art * through `normalizeCursorThemeId`, so a project saved with it still opens. */ export const CURSOR_THEMES: readonly CursorTheme[] = [ @@ -172,7 +174,6 @@ export const CURSOR_THEMES: readonly CursorTheme[] = [ height: 32, hotspotX: 6.3456, hotspotY: 2.0672, - sculpted: true, }, pointer: { assetPath: "cursors/prism-glow/pointer.png", @@ -180,7 +181,6 @@ export const CURSOR_THEMES: readonly CursorTheme[] = [ height: 32, hotspotX: 11.968, hotspotY: 2.0352, - sculpted: true, }, }, }, @@ -214,7 +214,7 @@ export const CURSOR_THEMES: readonly CursorTheme[] = [ assetPath: "cursors/pixel-candy/arrow.png", width: 32, height: 32, - hotspotX: 7.3664, + hotspotX: 6.5, hotspotY: 2, sculpted: true, }, @@ -222,8 +222,8 @@ export const CURSOR_THEMES: readonly CursorTheme[] = [ assetPath: "cursors/pixel-candy/pointer.png", width: 32, height: 32, - hotspotX: 13.376, - hotspotY: 1.9264, + hotspotX: 10.75, + hotspotY: 2, sculpted: true, }, },