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 |