diff --git a/crates/compositor/src/compositor_linux.rs b/crates/compositor/src/compositor_linux.rs index d6f14670f..3b9a97a31 100644 --- a/crates/compositor/src/compositor_linux.rs +++ b/crates/compositor/src/compositor_linux.rs @@ -71,6 +71,14 @@ fn layer_bytes(cb: &LayerCB) -> &[u8] { unsafe { std::slice::from_raw_parts(cb as *const LayerCB as *const u8, LAYER_BYTES as usize) } } +/// Le maillage de Prism Glow tel que le lit le WGSL (`PrismMesh`) : les triangles, les +/// polygones, un point par vec4 (le pas d'un tableau uniform), puis les boîtes. +fn prism_mesh_bytes() -> Vec { + use crate::prism_mesh::{BOXES, POLYS, TRIS}; + let points = POLYS.iter().map(|&[x, y]| [x, y, 0.0, 0.0]); + TRIS.iter().copied().chain(points).chain(BOXES).flatten().flat_map(f32::to_ne_bytes).collect() +} + /// Un calque de fond deja lie, en attente de son `draw`. `_buf`/`_tex`/`_view` /// ne sont jamais relus : ils gardent en vie ce que le bind group reference /// jusqu'au submit. Ce backend encode toute la frame avant de la soumettre, la @@ -272,6 +280,8 @@ pub struct Compositor { pipeline_copy: wgpu::RenderPipeline, bind_group_layout: wgpu::BindGroupLayout, sampler: wgpu::Sampler, + /// Le maillage de Prism Glow (`prism_mesh.rs`) tel que le lit le WGSL (`PrismMesh`, binding 7). + prism_mesh: wgpu::Buffer, // Chaine de blur Kawase du fond (`blur.wgsl`) : layout dedie (uniform + 1 // tex + sampler), 2 pipelines (down/up), 3 textures de pyramide (1/2, 1/4, @@ -474,8 +484,26 @@ impl Compositor { // Pyramide de profondeur de champ (modes 8 et 18), `dummy` ailleurs : le // mode 18 lit a la fois le metrage, son rendu isole (binding 4) et elle. tex_entry(6), + // Le maillage de Prism Glow (mode 15). Un uniform et non des tables dans le + // shader : lavapipe les recopiait a chaque pixel de chaque calque, et une + // frame sans curseur se rendait 3,4 fois plus lentement. + wgpu::BindGroupLayoutEntry { + binding: 7, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, ], }); + let prism_mesh = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("prism-mesh"), + contents: &prism_mesh_bytes(), + usage: wgpu::BufferUsages::UNIFORM, + }); let pipeline_layout = gpu.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("layer"), bind_group_layouts: &[&bind_group_layout], @@ -663,6 +691,7 @@ impl Compositor { pipeline_copy, bind_group_layout, sampler, + prism_mesh, blur_bgl, blur_down, blur_up, @@ -1283,6 +1312,7 @@ impl Compositor { /// `make_bind`, binding 4 impose (le rendu isole du mode 18, a la place du masque webcam /// qu'il ne lit pas), et la pyramide de profondeur de champ en binding 6 (modes 8 et 18). + /// Le mode 15 y met le champ de son sprite et le sprite. fn make_bind_b4( &self, cb: &LayerCB, @@ -1338,6 +1368,7 @@ impl Compositor { binding: 6, resource: wgpu::BindingResource::TextureView(dof.unwrap_or(dummy)), }, + wgpu::BindGroupEntry { binding: 7, resource: self.prism_mesh.as_entire_binding() }, ], }); (uniform, bind) @@ -2502,6 +2533,8 @@ impl Compositor { .as_ref() .is_some_and(|s| s.annotations.iter().any(|a| a.kind == "blur" && visible(a))); let mut ann_draws: Vec = Vec::new(); + // Les flous de confidentialite, dessines sur le metrage avant le curseur. + let mut privacy_draws: Vec = Vec::new(); if let Some(scene) = scene_ref.as_ref() { // La liste arrive deja triee par zIndex cote app : l'ordre d'iteration // EST l'ordre de peinture. @@ -2592,7 +2625,7 @@ impl Compositor { Some((&self.ann_copy_view, &self.ann_copy_view, &self.ann_copy_view)), &dummy, ); - ann_draws.push(AnnDraw::plain(buf, bind)); + privacy_draws.push(AnnDraw::plain(buf, bind)); } "image" => { let Some(src) = a.image_path.as_ref().filter(|s| !s.is_empty()) else { @@ -2828,6 +2861,8 @@ impl Compositor { binds: Vec, /// L'impact des clics (mode 16), dessine SOUS le curseur, sur le RT. impacts: Vec, + /// Le cristal de Prism Glow : il lit la copie de l'image composee (`CursorPlan::glass`). + glass: bool, } let cursor_draw: Option = (|| { let track = cursor_ref.as_ref()?; @@ -2906,7 +2941,9 @@ impl Compositor { let view = tex.create_view(&wgpu::TextureViewDescriptor::default()); // Curseur modelise (mode 15) : ce meme sprite extrude, `plan_cursor` en a tire la - // pose. Sprite au binding 1 (texY), champ au binding 2 (texU). Parite Windows/macOS. + // pose. La copie de l'image composee au binding 1 (texY), que refracte le cristal de + // Prism Glow, le champ au binding 4 (texMask), le sprite au binding 6 (texDof). + // Parite Windows/macOS. if let Some(pose) = plan.model { match self.cursor_sdf(&sprite.path) { Ok((sdf, shape)) => { @@ -2923,8 +2960,13 @@ impl Compositor { ) else { continue; }; - let (buf, bind) = - self.make_bind(&cb, Some((&view, &sdf_view, &view)), &dummy); + let (buf, bind) = self.make_bind_b4( + &cb, + Some((&self.ann_copy_view, &dummy, &dummy)), + &dummy, + Some(&sdf_view), + Some(&view), + ); bufs.push(buf); binds.push(bind); } @@ -2933,6 +2975,7 @@ impl Compositor { _tex: vec![(tex, view), (sdf, sdf_view)], binds, impacts, + glass: plan.glass, }); } Err(e) => eprintln!("[curseur] champ de \"{}\" : {e:#}", sprite.path), @@ -2956,7 +2999,7 @@ impl Compositor { bufs.push(buf); binds.push(bind); } - Some(CursorDraw { _bufs: bufs, _tex: vec![(tex, view)], binds, impacts }) + Some(CursorDraw { _bufs: bufs, _tex: vec![(tex, view)], binds, impacts, glass: false }) })(); // Bind group de la passe de composition d'`accum` (layout du blur : // uniform + texture + sampler). Construit hors de la pass, comme les @@ -3068,9 +3111,9 @@ impl Compositor { } // Blur du fond (avant l'ecran), si active par la scene/l'inspector. self.blur_bg(&mut encoder, cfg.bg_blur); - // Passe 2 : avant-plan (ecran + webcam), compose par-dessus le fond - // (eventuellement floute) avec `LoadOp::Load`. Les annotations sont dans - // une passe a part, cf. plus bas. + // Passe 2 : l'ecran, compose par-dessus le fond (eventuellement floute) avec + // `LoadOp::Load`. Puis les flous de confidentialite, le curseur, la camera et les autres + // annotations, dans cet ordre : celui de Windows et macOS. { let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("fg-pass"), @@ -3103,40 +3146,22 @@ impl Compositor { &device_frame, ), } - if let Some((_buf, bind)) = &webcam_shadow { - rpass.set_bind_group(0, bind, &[]); - rpass.draw(0..4, 0..1); - } - // Fond personnalise : ENTRE l'ombre et la camera. C'est ce sandwich qui - // remplace la branche « mode 3 » du shader — la camera, decoupee, se - // fond dessus par alpha ; l'ombre reste dessous, elle appartient a la - // bulle et non a son contenu. - if let Some(bg) = &webcam_bg { - rpass.set_bind_group(0, &bg.bind, &[]); - rpass.draw(0..4, 0..1); - } - if let Some((_buf, bind)) = &webcam_draw { - rpass.set_bind_group(0, bind, &[]); - rpass.draw(0..4, 0..1); - } } // Fige la frame composee pour les annotations « flou ». ICI et nulle part - // ailleurs : apres l'ecran et la camera (sinon un flou masquerait du vide) - // et avant la premiere annotation (sinon deux flous qui se recouvrent + // ailleurs : apres l'ecran (sinon un flou masquerait du vide) + // et avant le premier flou (sinon deux flous qui se recouvrent // s'echantillonnent l'un l'autre). Une passe de rendu ne peut pas lire sa // propre cible, d'ou la copie -- et d'ou le fait que les annotations // doivent avoir leur propre passe. if needs_ann_copy { self.generate_ann_mips(&mut encoder); } - // Passe 3 : annotations puis curseur net, par-dessus tout le reste. Elle - // existe meme sans flou : deux passes consecutives sur la MEME cible avec - // `LoadOp::Load` ne coutent rien de plus qu'une seule sur un GPU - // desktop, et un seul chemin de code vaut mieux qu'un branchement qui ne - // serait exerce que dans un projet sur dix. - { + // Passe 2 bis : les flous de confidentialite, sur le metrage, AVANT le curseur (parite + // Windows et macOS). Le curseur reste net par-dessus, et le cristal de Prism Glow, qui + // refracte l'image composee, n'y voit que des pixels deja floutes. + if !privacy_draws.is_empty() { let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("ann-pass"), + label: Some("privacy-pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &self.rt_view, resolve_target: None, @@ -3150,18 +3175,44 @@ impl Compositor { occlusion_query_set: None, }); rpass.set_pipeline(&self.pipeline); - for a in &ann_draws { + for a in &privacy_draws { rpass.set_bind_group(0, &a.bind, &[]); rpass.draw(0..4, 0..1); } + } + // Le cristal de Prism Glow refracte l'image telle qu'elle est composee a cet instant, + // flous compris : sa copie dans `ann_copy` (libre, les flous l'ont deja lue ; le mip 0 + // suffit), une fois pour toutes les copies de la trainee. + if cursor_draw.as_ref().is_some_and(|c| c.glass) { + encoder.copy_texture_to_texture( + self.rt.as_image_copy(), + self.ann_copy.as_image_copy(), + wgpu::Extent3d { width: self.render_w, height: self.render_h, depth_or_array_layers: 1 }, + ); + } + // Le curseur net, et l'impact des clics sous lui. La trainee, elle, a besoin de sa + // propre cible : elle suit. + { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("cursor-pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &self.rt_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rpass.set_pipeline(&self.pipeline); // L'impact des clics, sous le curseur et sa trainee (dessinee plus bas). for bind in cursor_draw.iter().flat_map(|c| &c.impacts) { rpass.set_bind_group(0, bind, &[]); rpass.draw(0..4, 0..1); } - // Curseur en dernier : au-dessus de l'ecran et des annotations. - // Une seule copie = curseur net, il tient dans cette pass. La - // trainee, elle, a besoin de sa propre cible (voir plus bas). if let Some(c) = cursor_draw.as_ref().filter(|c| c.binds.len() == 1) { rpass.set_bind_group(0, &c.binds[0], &[]); rpass.draw(0..4, 0..1); @@ -3225,6 +3276,66 @@ impl Compositor { rpass.set_bind_group(0, abind, &[]); rpass.draw(0..3, 0..1); } + // La camera, au-dessus de l'ecran et du curseur. + { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("webcam-pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &self.rt_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rpass.set_pipeline(&self.pipeline); + if let Some((_buf, bind)) = &webcam_shadow { + rpass.set_bind_group(0, bind, &[]); + rpass.draw(0..4, 0..1); + } + // Fond personnalise : ENTRE l'ombre et la camera. C'est ce sandwich qui + // remplace la branche « mode 3 » du shader — la camera, decoupee, se + // fond dessus par alpha ; l'ombre reste dessous, elle appartient a la + // bulle et non a son contenu. + if let Some(bg) = &webcam_bg { + rpass.set_bind_group(0, &bg.bind, &[]); + rpass.draw(0..4, 0..1); + } + if let Some((_buf, bind)) = &webcam_draw { + rpass.set_bind_group(0, bind, &[]); + rpass.draw(0..4, 0..1); + } + } + // Passe 3 : les autres annotations, par-dessus tout le reste (les flous sont passes avant + // le curseur). Elle existe meme sans annotation : deux passes consecutives sur la MEME + // cible avec `LoadOp::Load` ne coutent rien de plus qu'une seule sur un GPU desktop, et un + // seul chemin de code vaut mieux qu'un branchement qui ne serait exerce que dans un projet + // sur dix. + { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("ann-pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &self.rt_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rpass.set_pipeline(&self.pipeline); + for a in &ann_draws { + rpass.set_bind_group(0, &a.bind, &[]); + rpass.draw(0..4, 0..1); + } + } self.gpu.context.submit(std::iter::once(encoder.finish())); Ok(()) } @@ -5523,6 +5634,22 @@ mod tests { .collect() } + /// Les pixels que couvre un curseur de VERRE (le cristal de Prism Glow, qui laisse voir le + /// contenu et le réfracte) : opaques, ou changés sur les deux teintes sans y être plus sombres + /// (une ombre ne fait qu'assombrir ; le verre réfracte, reflète et luit). + fn model_covered(on_blue: &[u8], on_orange: &[u8], bare: &[u8], bare_orange: &[u8]) -> Vec { + let opaque = model_opaque(on_blue, on_orange, bare); + (0..1280 * 720) + .map(|i| { + let lit = |on: &[u8], under: &[u8]| { + let (a, b) = (&on[i * 4..i * 4 + 3], &under[i * 4..i * 4 + 3]); + a != b && model_luma(a) >= model_luma(b) - 2.0 + }; + opaque[i] || (lit(on_blue, bare) && lit(on_orange, bare_orange)) + }) + .collect() + } + fn model_iou(a: &[bool], b: &[bool]) -> f32 { let inter = a.iter().zip(b).filter(|(x, y)| **x && **y).count(); let union = a.iter().zip(b).filter(|(x, y)| **x || **y).count(); @@ -5681,7 +5808,8 @@ mod tests { /// Pendant de `the_sculpted_cursors_stand_at_the_hotspot` (Windows) : la flèche et la main /// sculptées des thèmes d'origine (`sculpt.rs`) passent par le WGSL, tiennent au hotspot, en - /// bas à droite de lui, et portent leur ombre en l'air. + /// bas à droite de lui, et portent leur ombre en l'air. Le cristal de Prism Glow est de verre : + /// son corps compte ce qu'il couvre, et une bonne part de lui change avec la teinte de l'écran. #[test] fn the_sculpted_cursors_stand_at_the_hotspot() { let Some(gpu) = gpu() else { return }; @@ -5693,11 +5821,12 @@ mod tests { 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 bare_orange = compose_model(&comp, &orange, &hidden, &model_track("arrow", false, 0.5)); let mut failures = Vec::new(); 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", "pop-coral", "pixel-candy", "star-sprout"] { + for theme in ["studio-ink", "prism-glow", "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}","#)); @@ -5707,7 +5836,12 @@ mod tests { failures.push(format!("{theme}/{state}: le sprite extrude au lieu du modele")); } let (tip, u) = model_tip(&json, &still); - let mask = model_opaque(&hover, &hover_b, &bare); + let glass = theme == "prism-glow"; + let mask = if glass { + model_covered(&hover, &hover_b, &bare, &bare_orange) + } else { + model_opaque(&hover, &hover_b, &bare) + }; let (mut body, mut c, mut near) = (0usize, [0.0f32; 2], f32::MAX); for (i, _) in mask.iter().enumerate().filter(|(_, m)| **m) { let (x, y) = ((i % 1280) as f32, (i / 1280) as f32); @@ -5735,6 +5869,61 @@ mod tests { if shadow * 10 < body * 3 { failures.push(format!("{theme}/{state}: pas d'ombre en l'air ({shadow} px)")); } + if glass { + let through = (0..1280 * 720).filter(|&i| mask[i] && hover[i * 4..i * 4 + 3] != hover_b[i * 4..i * 4 + 3]).count(); + println!("{theme}/{state} : {through} px du corps laissent voir l'ecran"); + if through * 10 < body * 3 { + failures.push(format!("{theme}/{state}: le cristal ne laisse voir l'ecran que sur {through} px")); + } + } + } + } + assert!(failures.is_empty(), "{failures:#?}"); + } + + /// Le métrage, puis ses flous de confidentialité, puis le curseur, net par-dessus (parité + /// Windows et macOS) : le cristal de Prism Glow réfracte l'image déjà floutée. Sous le flou, + /// des rayures fines décalées d'une colonne donnent la même image floutée ; si le verre lisait + /// la vidéo brute, il montrerait les rayures, et les deux rendus différeraient à travers lui. + #[test] + fn the_crystal_refracts_the_blurred_picture() { + let Some(gpu) = gpu() else { return }; + let comp = Compositor::new_sized(&gpu, 1280, 720).expect("Compositor::new_sized"); + // Moitié gauche grise, moitié droite (la zone floutée) rayée une colonne sur deux. + let (w, h) = (640u32, 360u32); + let stripes = |phase: u32| { + let y: Vec = (0..w * h) + .map(|i| if i % w < w / 2 { 150 } else if (i % w + phase) % 2 == 0 { 40 } else { 220 }) + .collect(); + FakeFrame::from_planes(&gpu, w, h, &y, &vec![128; (w * (h / 2)) as usize]) + }; + let (a, b) = (stripes(0), stripes(1)); + let blur = r#""annotations":[{"id":"b","startSec":0,"endSec":10,"kind":"blur","x":0.5,"y":0.0,"w":0.5,"h":1.0,"blur":{"style":"blur","shape":"rectangle","color":"white","intensity":24,"blockSize":16}}]"#; + let json = |show: bool| { + model_scene_json("null", Some(true), "default", show, 5.0) + .replace(r#"/arrow.png","#, r#"/arrow.png","sculpt":"prism-glow/arrow","#) + .replace(r#""annotations":[]"#, blur) + }; + let mut failures = Vec::new(); + // Tout entier dans la zone, puis à cheval sur son bord. + for x in [0.7f32, 0.48] { + let track = crate::cursor::CursorTrack::new(vec![(0.0, x, 0.45), (9.0, x, 0.45)], vec![], vec![(0.0, "arrow".to_string())]); + let (ra, rb) = (compose_model(&comp, &a, &json(true), &track), compose_model(&comp, &b, &json(true), &track)); + let bare = compose_model(&comp, &a, &json(false), &track); + model_save(&format!("glass-over-blur-{x}"), &ra); + let px = |img: &[u8], i: usize, c: usize| img[i * 4 + c] as i32; + let hidden = (0..1280 * 720).filter(|&i| (0..3).any(|c| (px(&ra, i, c) - px(&rb, i, c)).abs() > 24)).count(); + // Le serti net du curseur : des sauts francs, entre voisins, de ce qu'il change. + let d = |i: usize, c: usize| px(&ra, i, c) - px(&bare, i, c); + let sharp = (0..1280 * 720 - 1) + .filter(|&i| i % 1280 < 1279 && (0..3).any(|c| (d(i, c) - d(i + 1, c)).abs() > 60)) + .count(); + println!("cristal en x = {x} : {sharp} bords francs, {hidden} px qui suivent les rayures cachées"); + if sharp < 50 { + failures.push(format!("x = {x} : {sharp} bords francs, le curseur n'est pas net par-dessus le flou")); + } + if hidden > 0 { + failures.push(format!("x = {x} : {hidden} px montrent les rayures cachées sous le flou")); } } assert!(failures.is_empty(), "{failures:#?}"); diff --git a/crates/compositor/src/compositor_macos.rs b/crates/compositor/src/compositor_macos.rs index b00a288ee..ad4bbf28e 100644 --- a/crates/compositor/src/compositor_macos.rs +++ b/crates/compositor/src/compositor_macos.rs @@ -1417,6 +1417,23 @@ impl Compositor { } + /// Fige l'image composée dans `ann_copy` (mip 0), qu'y lisent les flous (mips dérivés, + /// `mips`) et le cristal de Prism Glow. + unsafe fn snapshot_rt(&self, cmd: &metal::CommandBufferRef, mips: bool) { + let blit = cmd.new_blit_command_encoder(); + blit.copy_from_texture( + &self.rt, 0, 0, + metal::MTLOrigin { x: 0, y: 0, z: 0 }, + metal::MTLSize { width: self.render_w as u64, height: self.render_h as u64, depth: 1 }, + &self.ann_copy, 0, 0, + metal::MTLOrigin { x: 0, y: 0, z: 0 }, + ); + if mips { + blit.generate_mipmaps(&self.ann_copy); + } + blit.end_encoding(); + } + /// Annotations : calque le plus haut, ancré sur `s_ann` — le rect écran SANS ZOOM, le /// conteneur que reçoit l'overlay web. Port de `compositor_windows::draw_annotations`. /// @@ -1425,6 +1442,9 @@ impl Compositor { /// grossir les sous-titres sous un zoom (issue #179, puis #397 sur Linux). L'arithmétique /// elle-même vit dans `frame_geometry::annotation_dst_in`, partagée par les trois backends. /// Le flou, lui, se place par `g.privacy_mask` : un masque doit rester sur ce qu'il cache. + /// + /// `privacy` : les flous de confidentialité seuls, dessinés sur le métrage avant le curseur ; + /// sinon toutes les autres annotations, le calque le plus haut. unsafe fn draw_annotations( &self, cmd: &metal::CommandBufferRef, @@ -1432,30 +1452,21 @@ impl Compositor { t: f32, s_ann: [f32; 4], g: &crate::frame_geometry::FrameGeometry, + privacy: bool, ) -> Result<()> { let Some(scene) = scene else { return Ok(()) }; - if scene.annotations.is_empty() { - return Ok(()); - } let (rw, rh) = (self.render_w as f32, self.render_h as f32); let visible = |a: &crate::scene::SceneAnnotation| { - t >= a.start_sec as f32 && t < a.end_sec as f32 + t >= a.start_sec as f32 && t < a.end_sec as f32 && (a.kind == "blur") == privacy }; + if !scene.annotations.iter().any(visible) { + return Ok(()); + } // UNE seule recopie pour toutes les annotations flou de la frame : leur lecture doit // voir l'image composée SANS les flous eux-mêmes, sinon deux zones qui se recouvrent // s'échantillonneraient l'une l'autre selon l'ordre de dessin. - if scene.annotations.iter().any(|a| a.kind == "blur" && visible(a)) { - let blit = cmd.new_blit_command_encoder(); - blit.copy_from_texture( - &self.rt, 0, 0, - metal::MTLOrigin { x: 0, y: 0, z: 0 }, - metal::MTLSize { width: rw as u64, height: rh as u64, depth: 1 }, - &self.ann_copy, 0, 0, - metal::MTLOrigin { x: 0, y: 0, z: 0 }, - ); - // Seul le mip 0 est rempli ; le GPU dérive le reste. - blit.generate_mipmaps(&self.ann_copy); - blit.end_encoding(); + if privacy { + self.snapshot_rt(cmd, true); } let enc = self.begin_pass(cmd, &self.rt, None, &self.pipeline_main)?; @@ -2066,7 +2077,8 @@ impl Compositor { /// `frame_geometry::cursor_sprite_cb`, partagée avec Windows et Linux. /// /// Avec `model`, le même sprite extrudé (mode 15, `cursor_model_cb`) : le sprite en - /// texture(2), son champ de distance en texture(4). + /// texture(2), son champ de distance en texture(4), l'enregistrement que l'appelant a lié en + /// texture(0) et (1). #[allow(clippy::too_many_arguments)] unsafe fn draw_cursor_sprite( &self, @@ -2400,6 +2412,11 @@ impl Compositor { enc.end_encoding(); + // --- flous de confidentialité : sur le métrage, AVANT le curseur (parité Windows). Le + // curseur reste net par-dessus, et le cristal de Prism Glow, qui réfracte l'image + // composée, n'y voit que des pixels déjà floutés. + self.draw_annotations(cmd_buf, scene_ref.as_ref(), g.source_t, g.s_ann, &g, true)?; + // --- curseur --- (parité `compositor_windows.rs`, section « curseur custom ») if let Some(track) = cursor_ref.as_ref() { let plan = crate::frame_geometry::plan_cursor( @@ -2422,6 +2439,12 @@ impl Compositor { .map(|s| s.cursor.cursor_sprites.clone()) .unwrap_or_default(); let kind = plan.cursor_type.as_deref(); + // Le cristal de Prism Glow réfracte l'image telle qu'elle est composée à cet + // instant, flous compris : sa copie en texture(5), une fois pour toutes les copies + // de la traînée (`ann_copy` est libre, les flous l'ont déjà lue). + if plan.glass { + self.snapshot_rt(cmd_buf, false); + } // L'impact des clics (mode 16, sans texture), posé sur l'écran SOUS le curseur. if !plan.impacts.is_empty() { let e = self.begin_pass(cmd_buf, &self.rt, None, &self.pipeline_main)?; @@ -2432,6 +2455,9 @@ impl Compositor { } if plan.taps <= 1 { let e = self.begin_pass(cmd_buf, &self.rt, None, &self.pipeline_main)?; + if plan.glass { + e.set_fragment_texture(5, Some(&self.ann_copy)); + } self.draw_cur_themed( e, &sprites, @@ -2454,6 +2480,9 @@ impl Compositor { Some(metal::MTLClearColor::new(0.0, 0.0, 0.0, 0.0)), &self.pipeline_add, )?; + if plan.glass { + e.set_fragment_texture(5, Some(&self.ann_copy)); + } for k in 0..plan.taps { let f = k as f32 / (plan.taps - 1) as f32; let w = crate::frame_geometry::cursor_tap_weight(k, plan.taps); @@ -2578,8 +2607,9 @@ impl Compositor { // --- annotations : calque le plus haut, ancré sur le rect ÉCRAN SANS ZOOM --- // `s_ann`, pas `s_dst` : le zoom vit dans la boîte depuis l'issue #179, donc `s_dst` // grandit avec lui et emmenait annotations et sous-titres dans le mouvement. Le flou de - // confidentialité est l'exception : il suit le contenu, d'où `&g`. - self.draw_annotations(cmd_buf, scene_ref.as_ref(), g.source_t, g.s_ann, &g)?; + // confidentialité est l'exception : il suit le contenu, d'où `&g` ; il est passé avant le + // curseur (plus haut). + self.draw_annotations(cmd_buf, scene_ref.as_ref(), g.source_t, g.s_ann, &g, false)?; // Ni miroir RGBA ni attente ici : le miroir ne sert qu'à `readback_direct` (la // preview), et l'export ne lit jamais le RGBA — le blit pleine résolution était payé diff --git a/crates/compositor/src/compositor_windows.rs b/crates/compositor/src/compositor_windows.rs index 59431e383..c99a0ac82 100644 --- a/crates/compositor/src/compositor_windows.rs +++ b/crates/compositor/src/compositor_windows.rs @@ -2138,6 +2138,11 @@ impl Compositor { self.ctx.PSSetShaderResources(5, Some(&[None])); } + // --- flous de confidentialité : sur le métrage, AVANT le curseur. Le curseur reste net + // par-dessus, et le cristal de Prism Glow, qui réfracte l'image composée, n'y voit que des + // pixels déjà floutés. Les autres annotations restent le calque le plus haut (plus bas). + self.draw_annotations(scene_ref.as_ref(), source_t, s_ann, &g, true); + // --- curseur custom : suit le mapping src/dst (zoom+layout), click bounce, // et flou de mouvement (parité `compositor_macos.rs` et `compositor_linux.rs`) --- if let Some(track) = cursor_ref.as_ref() { @@ -2161,6 +2166,13 @@ impl Compositor { .map(|s| s.cursor.cursor_sprites.clone()) .unwrap_or_default(); let cursor_type = plan.cursor_type.as_deref(); + // Le cristal de Prism Glow réfracte l'image telle qu'elle est composée à cet + // instant, flous compris : sa copie en t5, une fois pour toutes les copies de la + // traînée (`ann_copy` est libre, les flous l'ont déjà lue). + if plan.glass { + self.ctx.CopySubresourceRegion(&self.ann_copy, 0, 0, 0, 0, &self.rt, 0, None); + self.ctx.PSSetShaderResources(5, Some(&[Some(self.ann_copy_srv.clone())])); + } // L'impact des clics (mode 16, sans texture), posé sur l'écran SOUS le curseur. for cb in &plan.impacts { self.draw_solid(cb); @@ -2216,6 +2228,9 @@ impl Compositor { // le dessin de la webcam qui suit juste après. self.bind_compose_state(); } + if plan.glass { + self.ctx.PSSetShaderResources(5, Some(&[None])); + } } } @@ -2334,12 +2349,13 @@ impl Compositor { // la transform, donc les annotations restent en place pendant que le contenu zoome dessous. // Ce fut `s_dst` tant que le zoom vivait dans la coupe source ; depuis l'issue #179 il vit // dans la BOÎTE, et `s_dst` emmenait annotations et sous-titres avec lui. - // Exception : le flou de confidentialité suit le contenu (`FrameGeometry::privacy_mask`), - // d'où la géométrie entière passée en plus de `s_ann`. + // Les flous de confidentialité, eux, sont passés avant le curseur (plus haut) : ils suivent + // le contenu (`FrameGeometry::privacy_mask`), d'où la géométrie entière passée en plus de + // `s_ann`. // `source_t`, la même base de temps que les zoom/speed regions : le temps SOURCE du clip, // pas le compteur de frames. C'est ce qui garde une annotation alignée sur l'image quand // une speed region répète ou saute des frames. - self.draw_annotations(scene_ref.as_ref(), source_t, s_ann, &g); + self.draw_annotations(scene_ref.as_ref(), source_t, s_ann, &g, false); Ok(()) } @@ -2351,19 +2367,23 @@ impl Compositor { /// sous-titres sous un zoom (issue #179, puis #397 sur Linux). L'arithmétique elle-même vit /// dans `frame_geometry::annotation_dst_in`, partagée par les trois backends. Le flou, lui, /// se place par `g.privacy_mask` : un masque doit rester sur ce qu'il cache. + /// + /// `privacy` : les flous de confidentialité seuls, dessinés sur le métrage avant le curseur ; + /// sinon toutes les autres annotations, le calque le plus haut. unsafe fn draw_annotations( &self, scene: Option<&Scene>, t: f32, s_ann: [f32; 4], g: &crate::frame_geometry::FrameGeometry, + privacy: bool, ) { let Some(scene) = scene else { return }; if scene.annotations.is_empty() { return; } let visible = |a: &crate::scene::SceneAnnotation| { - t >= a.start_sec as f32 && t < a.end_sec as f32 + t >= a.start_sec as f32 && t < a.end_sec as f32 && (a.kind == "blur") == privacy }; // Une seule recopie du render target pour TOUTES les annotations flou de la frame — leur // lecture doit voir l'image composée sans les flous eux-mêmes, sinon deux zones qui se diff --git a/crates/compositor/src/frame_geometry.rs b/crates/compositor/src/frame_geometry.rs index 7c367a9a1..4aee32a9d 100644 --- a/crates/compositor/src/frame_geometry.rs +++ b/crates/compositor/src/frame_geometry.rs @@ -3426,6 +3426,10 @@ pub struct CursorPlan { /// L'impact des clics récents sur l'écran (mode 16), à dessiner SOUS le curseur. Vide sans /// curseur modélisé, sans clic récent ou à `clickBounce` nul. pub impacts: Vec, + /// Le curseur modélisé est le cristal de Prism Glow : il réfracte l'image déjà composée, le + /// métrage et ses flous de confidentialité, que le backend lui copie juste avant de le + /// dessiner. Seuls des pixels floutés passent ainsi à travers le verre. + pub glass: bool, } impl CursorPlan { @@ -3727,6 +3731,7 @@ pub fn plan_cursor(g: &FrameGeometry, input: &CursorPlanInput) -> Option Option [f32; 3] { @@ -62,17 +62,34 @@ fn model_box(theme: usize, arrow: bool) -> [f32; 3] { } } +/// Le curseur sculpté que nomme la scène est de verre (Prism Glow) : il réfracte ce qui est dessous. +pub fn refracts(name: &str) -> bool { + sculpted_shape(name).is_some_and(|s| THEMES[((s.sculpt - 1) / 2) as usize] == "prism-glow") +} + /// Le curseur sculpté que nomme la scène (`"/<état>"`, cf. `resolveCursorSprites`), sous -/// 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. +/// la forme que le mode 15 attend. `None` pour un nom inconnu : 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 && *t != EXTRUDED)?; + let theme = THEMES.iter().position(|t| *t == theme)?; let arrow = match state { "arrow" => true, "pointer" => false, _ => return None, }; + let sculpt = 1 + 2 * theme as u32 + u32::from(!arrow); + if THEMES[theme] == "prism-glow" { + // Le maillage est déjà dans le repère du modèle : sa boîte, son dessus et son dessous. + let m = &crate::prism_mesh::MODELS[usize::from(!arrow)]; + return Some(SpriteShape { + size: m.size, + hotspot: m.hotspot, + top: m.top, + max_height: m.height, + thick: m.thick, + sculpt, + }); + } 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; @@ -84,7 +101,7 @@ pub fn sculpted_shape(name: &str) -> Option { top: y1 * s, max_height: (z_high - zref) * s, thick: (zref - Z_LOW) * s, - sculpt: 1 + 2 * theme as u32 + u32::from(!arrow), + sculpt, }) } @@ -108,29 +125,28 @@ mod tests { #[test] fn each_theme_arrow_and_hand_has_its_own_id_in_shader_order() { for (k, name) in NAMES.iter().enumerate() { - 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"), - } + let shape = sculpted_shape(name).expect(name); + assert_eq!(shape.sculpt, k as u32 + 1, "{name}"); } } #[test] fn other_states_and_themes_keep_the_extruded_sprite() { - for name in ["default/arrow", "pop-coral/text", "pop-coral", "", "pop-coral/arrow/x", "prism-glow/arrow"] { + for name in ["default/arrow", "pop-coral/text", "pop-coral", "", "pop-coral/arrow/x", "prism-glow/text"] { assert!(sculpted_shape(name).is_none(), "{name}"); } } /// Le hotspot est dans la boîte, sous le haut de la silhouette ; le modèle a une épaisseur et /// une hauteur ; la boîte a la hauteur du sprite, son plus grand côté, comme le veut le mode 15. + /// Le serti de Prism Glow est le plus mince (0,043) : c'est son épaisseur dans le dessin. #[test] fn the_box_holds_the_hotspot_and_the_silhouette_top() { - for name in NAMES.into_iter().filter(|n| !n.starts_with(EXTRUDED)) { + for name in NAMES { 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"); - assert!(s.thick > 0.05 && s.max_height > 0.0, "{name}"); + assert!(s.thick > 0.04 && s.max_height > 0.0, "{name}"); assert!(s.size[0] < 1.0 && s.size[1] == 1.0, "{name} : le plus grand côté n'est pas 1"); } } @@ -150,6 +166,7 @@ mod tests { ("SCULPT_ZREF_HAND", ZREF_HAND), ("SCULPT_HOVER", Z_LOW), ("SCULPT_VOX", VOX), + ("PRISM_BEVEL", crate::prism_mesh::BEVEL), ] { let line = src .lines() @@ -212,4 +229,110 @@ mod tests { } } } + + /// Seul Prism Glow est de verre, flèche et main. + #[test] + fn only_prism_glow_refracts() { + for name in NAMES { + assert_eq!(refracts(name), name.starts_with("prism-glow/"), "{name}"); + } + assert!(!refracts("prism-glow/text")); + } + + /// Le maillage de Prism Glow tient dans sa boîte (le shader n'y lance de rayons que là), son + /// dessous est à l'épaisseur annoncée (la garde au sol en dépend) et son sommet à la hauteur. + #[test] + fn the_prism_boxes_hold_their_mesh() { + use crate::prism_mesh::{MODELS, POLYS, TRIS}; + for (k, name) in ["prism-glow/arrow", "prism-glow/pointer"].iter().enumerate() { + let s = sculpted_shape(name).unwrap(); + let m = &MODELS[k]; + let [x0, y0] = [-s.hotspot[0] * s.size[0], -s.hotspot[1] * s.size[1]]; + let inside = |x: f32, y: f32| x >= x0 && x <= x0 + s.size[0] && y >= y0 && y <= y0 + s.size[1]; + let (mut zlo, mut zhi) = (f32::MAX, f32::MIN); + for t in TRIS[4 * m.tri_start..4 * (m.tri_start + m.tri_count)].chunks(4) { + for e in [[0.0; 3], [t[1][0], t[1][1], t[1][2]], [t[2][0], t[2][1], t[2][2]]] { + let [x, y, z] = [t[0][0] + e[0], t[0][1] + e[1], t[0][2] + e[2]]; + assert!(inside(x, y), "{name} : sommet ({x}, {y}) hors de la boîte"); + zlo = zlo.min(z); + zhi = zhi.max(z); + } + } + let sil = &POLYS[m.sil_start..m.sil_start + m.sil_count]; + let out = &POLYS[m.out_start..m.out_start + m.out_count]; + for p in sil.iter().chain(out) { + assert!(inside(p[0], p[1]), "{name} : contour ({}, {}) hors de la boîte", p[0], p[1]); + } + assert!((zlo + s.thick).abs() < 1e-5, "{name} : dessous {zlo}, épaisseur {}", s.thick); + assert!((zhi - s.max_height).abs() < 1e-5, "{name} : sommet {zhi}, hauteur {}", s.max_height); + } + } + + /// Les trois shaders portent le maillage de `prism_mesh.rs` tel quel : triangles, boîtes, + /// polygones, et les débuts et longueurs de chaque modèle dans ces tables. Le WGSL lit + /// triangles, polygones et boîtes dans un uniform rempli de `prism_mesh.rs` + /// (`compositor_linux.rs`) : il n'en déclare que la taille. + #[test] + fn the_shaders_carry_the_prism_mesh() { + use crate::prism_mesh::{PrismModel, BOXES, MODELS, POLYS, TRIS}; + let per_model = |f: fn(&PrismModel) -> usize| MODELS.iter().map(|m| f(m) as f32).collect::>(); + let want: [(&str, Vec); 11] = [ + ("PRISM_TRIS", TRIS.iter().flatten().copied().collect()), + ("PRISM_POLY", POLYS.iter().flatten().copied().collect()), + ("PRISM_BOXES", BOXES.iter().flatten().copied().collect()), + ("PRISM_BOX_START", per_model(|m| m.box_start)), + ("PRISM_BOX_COUNT", per_model(|m| m.box_count)), + ("PRISM_TRI_START", per_model(|m| m.tri_start)), + ("PRISM_TRI_COUNT", per_model(|m| m.tri_count)), + ("PRISM_SIL_START", per_model(|m| m.sil_start)), + ("PRISM_SIL_COUNT", per_model(|m| m.sil_count)), + ("PRISM_OUT_START", per_model(|m| m.out_start)), + ("PRISM_OUT_COUNT", per_model(|m| m.out_count)), + ]; + let wgsl = include_str!("vk_shaders/layer.wgsl"); + for field in [ + format!("tris: array, {}>", TRIS.len()), + format!("poly: array, {}>", POLYS.len()), + format!("boxes: array, {}>", BOXES.len()), + ] { + assert!(wgsl.contains(&field), "vk_shaders/layer.wgsl : `{field}` attendu dans PrismMesh"); + } + for (file, src, tables) in [ + ("shaders.hlsl", include_str!("shaders.hlsl"), &want[..]), + ("shaders.metal", include_str!("shaders.metal"), &want[..]), + ("vk_shaders/layer.wgsl", wgsl, &want[3..]), + ] { + for (name, values) in tables { + let got = table(src, name); + assert_eq!(got.len(), values.len(), "{file} : {name}, nombre de valeurs"); + for (i, (g, w)) in got.iter().zip(values).enumerate() { + assert!((g - w).abs() <= 1e-6 + 1e-6 * w.abs(), "{file} : {name}[{i}] = {g} au lieu de {w}"); + } + } + } + } + + /// Chaque triangle du cristal est dans une boîte et une seule, qui le contient : le shader ne + /// teste les triangles d'une boîte que si le rayon la touche, un triangle hors de sa boîte + /// disparaîtrait par endroits. + #[test] + fn each_crystal_triangle_sits_in_its_box() { + use crate::prism_mesh::{BOXES, MODELS, TRIS}; + for (k, m) in MODELS.iter().enumerate() { + let mut next = m.tri_start; + for b in m.box_start..m.box_start + m.box_count { + let (lo, hi) = (BOXES[2 * b], BOXES[2 * b + 1]); + let (first, count) = (lo[3] as usize, hi[3] as usize); + assert_eq!(first, next, "modèle {k}, boîte {b} : triangles non contigus"); + next += count; + for t in TRIS[4 * first..4 * (first + count)].chunks(4) { + for e in [[0.0; 3], [t[1][0], t[1][1], t[1][2]], [t[2][0], t[2][1], t[2][2]]] { + let v = [t[0][0] + e[0], t[0][1] + e[1], t[0][2] + e[2]]; + assert!((0..3).all(|a| v[a] >= lo[a] && v[a] <= hi[a]), "modèle {k}, boîte {b} : sommet {v:?} dehors"); + } + } + } + assert_eq!(next, m.tri_start + m.tri_count, "modèle {k} : triangles hors des boîtes"); + } + } } diff --git a/crates/compositor/src/shaders.hlsl b/crates/compositor/src/shaders.hlsl index 16c62192d..37614e23d 100644 --- a/crates/compositor/src/shaders.hlsl +++ b/crates/compositor/src/shaders.hlsl @@ -81,6 +81,7 @@ float3 sample_yuv(float2 uv) return yuv709_limited(y, cbcr); } +// La même, au niveau 0 : lisible dans une boucle ou une branche (le cristal du mode 15). // SDF segment à bouts ronds — la primitive des flèches d'annotation, dont les tracés SVG sont // trois segments `stroke-linecap="round"` (cf. ArrowSvgs.tsx). float sd_segment(float2 p, float2 a, float2 b) @@ -589,8 +590,7 @@ static const float MODEL_CONTACT_ALPHA = 0.5; // 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 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). +// yeux), 6 cubes de Pixel Candy, 8 serti de Prism Glow, 9 son cristal (plus bas, `prism_*`). static const float SCULPT_SCALE = 0.85; static const float SCULPT_HOVER = 0.05; static const float SCULPT_VOX = 0.0625; @@ -1043,6 +1043,950 @@ float s_pixel_outline(float2 p, int shape) return d; } +// ---- Prism Glow : un cristal en MAILLAGE ---- +// Les facettes tracées sur l'art 2D du thème (`design/cursors/prism-glow/model`), en triangles dans +// le repère du MODÈLE lui-même (unité = côté du sprite 128 px, origine au hotspot, y vers le bas, +// dessus du serti en z = 0). Le serti marine est la silhouette de l'art extrudée sous z = 0, un +// champ de distance comme les autres sculptés : il donne la couverture, l'ombre portée et l'ombre +// de contact. Le cristal est lancé de rayons, triangles rangés par boîtes : réfraction par canal +// (dispersion), réflexions totales internes, sortie par son fond plat vers l'image composée sous +// le curseur, flous de confidentialité compris (sa copie en t5). Tables : miroir de `prism_mesh.rs` ; +// un triangle = quatre +// float4 (v0, drapeaux), (e1, r), (e2, g), (normale sortante, b) ; drapeaux = paroi (1) + 2 × +// arêtes réelles (bit k : l'arête opposée au sommet k) ; une boîte = deux float4 (coin bas, +// premier triangle), (coin haut, nombre de triangles). +// prism mesh: generated by design/cursors/prism-glow/model/export_compositor.py +static const int PRISM_TRI_START[2] = { 0, 31 }; +static const int PRISM_TRI_COUNT[2] = { 31, 130 }; +static const int PRISM_SIL_START[2] = { 0, 25 }; +static const int PRISM_SIL_COUNT[2] = { 25, 42 }; +static const int PRISM_OUT_START[2] = { 67, 76 }; +static const int PRISM_OUT_COUNT[2] = { 9, 34 }; +static const int PRISM_BOX_START[2] = { 0, 4 }; +static const int PRISM_BOX_COUNT[2] = { 4, 18 }; +static const float4 PRISM_TRIS[644] = { + float4(0.252301, 0.266258, -0.042945, 11.0), + float4(-0.221166, -0.203988, 0.0, 0.01033), + float4(-0.221166, -0.203988, 0.042945, 0.045182), + float4(0.677984, -0.735077, 0.0, 0.577584), + float4(0.252301, 0.266258, -0.042945, 3.0), + float4(-0.221166, -0.203988, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(0.677984, -0.735077, 0.0, 0.577584), + float4(0.031135, 0.06227, -0.042945, 7.0), + float4(0.004294, 0.419785, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(-0.999948, 0.01023, 0.0, 0.577584), + float4(0.169632, 0.356442, 0.115951, 14.0), + float4(0.082669, -0.090184, -0.115951, 0.590616), + float4(-0.138497, -0.294172, -0.115951, 0.737911), + float4(0.466385, -0.505659, 0.725806, 0.973449), + float4(0.035429, 0.482055, 0.0, 14.0), + float4(0.134202, -0.125614, 0.115951, 0.000305), + float4(-0.004294, -0.419785, 0.0, 0.938689), + float4(-0.650191, 0.006652, 0.759742, 0.854994), + float4(0.031135, 0.06227, -0.042945, 9.0), + float4(0.004294, 0.419785, 0.0, 0.01033), + float4(0.004294, 0.419785, 0.042945, 0.045182), + float4(-0.999948, 0.01023, 0.0, 0.577584), + float4(0.169632, 0.356442, 0.115951, 14.0), + float4(0.159969, 0.080521, -0.045092, 0.381323), + float4(0.082669, -0.090184, -0.115951, 0.056123), + float4(0.461407, -0.510214, 0.725799, 0.973449), + float4(0.541104, 0.544325, -0.042945, 9.0), + float4(-0.288804, -0.278068, 0.0, 0.01033), + float4(-0.288804, -0.278068, 0.042945, 0.045182), + float4(0.69359, -0.72037, 0.0, 0.577584), + float4(0.329601, 0.436963, 0.070859, 14.0), + float4(0.211503, 0.107362, -0.070859, 0.545724), + float4(-0.077301, -0.170706, -0.070859, 0.064805), + float4(0.49567, -0.514808, 0.699488, 0.973449), + float4(0.268405, 0.533589, 0.081595, 14.0), + float4(0.061196, -0.096626, -0.010736, 0.000916), + float4(-0.098773, -0.177147, 0.034356, 0.046662), + float4(0.247834, 0.049458, 0.967539, 0.904662), + float4(0.541104, 0.544325, -0.042945, 7.0), + float4(-0.288804, -0.278068, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(0.69359, -0.72037, 0.0, 0.577584), + float4(0.199693, 0.595859, 0.0, 14.0), + float4(0.068712, -0.06227, 0.081595, 0.0), + float4(-0.030061, -0.239417, 0.115951, 0.01445), + float4(-0.504461, 0.426837, 0.750553, 0.254154), + float4(0.268405, 0.533589, 0.081595, 14.0), + float4(0.272699, 0.010736, -0.081595, 0.0), + float4(0.061196, -0.096626, -0.010736, 0.025193), + float4(0.283244, 0.073137, 0.956255, 0.527108), + float4(0.035429, 0.482055, 0.0, 14.0), + float4(0.0, 0.258742, 0.0, 0.0), + float4(0.134202, -0.125614, 0.115951, 0.473533), + float4(-0.653777, 0.0, 0.756687, 0.964691), + float4(0.359663, 0.559356, 0.0, 14.0), + float4(0.181442, -0.015031, 0.0, 0.006043), + float4(-0.091258, -0.025767, 0.081595, 0.076188), + float4(0.076465, 0.923048, 0.37701, 0.964691), + float4(0.035429, 0.482055, -0.042945, 3.0), + float4(0.0, 0.258742, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(-1.0, 0.0, 0.0, 0.577584), + float4(0.035429, 0.482055, -0.042945, 11.0), + float4(0.0, 0.258742, 0.0, 0.01033), + float4(0.0, 0.258742, 0.042945, 0.045182), + float4(-1.0, 0.0, 0.0, 0.577584), + float4(0.035429, 0.740798, -0.042945, 7.0), + float4(0.164264, -0.144939, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(0.661622, 0.749838, 0.0, 0.577584), + float4(0.035429, 0.740798, 0.0, 14.0), + float4(0.164264, -0.144939, 0.0, 0.0), + float4(0.134202, -0.384356, 0.115951, 0.010956), + float4(0.332572, 0.376915, 0.864483, 0.215854), + float4(0.035429, 0.740798, -0.042945, 11.0), + float4(0.164264, -0.144939, 0.0, 0.01033), + float4(0.164264, -0.144939, 0.042945, 0.045182), + float4(0.661622, 0.749838, 0.0, 0.577584), + float4(0.199693, 0.595859, -0.042945, 7.0), + float4(0.105215, 0.22546, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(-0.906183, 0.422886, 0.0, 0.577584), + float4(0.199693, 0.595859, 0.0, 14.0), + float4(0.105215, 0.22546, 0.0, 0.0), + float4(0.068712, -0.06227, 0.081595, 0.015991), + float4(-0.613882, 0.286478, 0.735581, 0.473533), + float4(0.199693, 0.595859, -0.042945, 11.0), + float4(0.105215, 0.22546, 0.0, 0.01033), + float4(0.105215, 0.22546, 0.042945, 0.045182), + float4(-0.906183, 0.422886, 0.0, 0.577584), + float4(0.304908, 0.821319, 0.0, 14.0), + float4(0.128834, -0.067638, 0.0, 0.0), + float4(-0.036503, -0.28773, 0.081595, 0.254154), + float4(0.133686, 0.254641, 0.957751, 0.973449), + float4(0.304908, 0.821319, -0.042945, 7.0), + float4(0.128834, -0.067638, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(0.464834, 0.885398, 0.0, 0.577584), + float4(0.433742, 0.753681, 0.0, 14.0), + float4(-0.07408, -0.194325, 0.0, 0.0), + float4(-0.165337, -0.220092, 0.081595, 0.964691), + float4(0.683362, -0.260508, 0.682021, 0.964691), + float4(0.433742, 0.753681, -0.042945, 11.0), + float4(-0.07408, -0.194325, 0.0, 0.01033), + float4(-0.07408, -0.194325, 0.042945, 0.045182), + float4(0.934406, -0.35621, 0.0, 0.577584), + float4(0.304908, 0.821319, -0.042945, 11.0), + float4(0.128834, -0.067638, 0.0, 0.01033), + float4(0.128834, -0.067638, 0.042945, 0.045182), + float4(0.464834, 0.885398, 0.0, 0.577584), + float4(0.433742, 0.753681, -0.042945, 7.0), + float4(-0.07408, -0.194325, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(0.934406, -0.35621, 0.0, 0.577584), + float4(0.359663, 0.559356, -0.042945, 7.0), + float4(0.181442, -0.015031, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(0.082557, 0.996586, 0.0, 0.577584), + float4(0.359663, 0.559356, -0.042945, 11.0), + float4(0.181442, -0.015031, 0.0, 0.01033), + float4(0.181442, -0.015031, 0.042945, 0.045182), + float4(0.082557, 0.996586, 0.0, 0.577584), + float4(0.044345, 0.043263, -0.043263, 11.0), + float4(-0.043263, -0.005408, 0.0, 0.01033), + float4(-0.043263, -0.005408, 0.043263, 0.045182), + float4(0.124035, -0.992278, 0.0, 0.577584), + float4(0.044345, 0.043263, -0.043263, 7.0), + float4(-0.043263, -0.005408, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.124035, -0.992278, 0.0, 0.577584), + float4(0.001082, 0.037855, -0.043263, 7.0), + float4(-0.044345, 0.040019, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.669964, -0.742393, 0.0, 0.577584), + float4(0.044345, 0.043263, 0.0, 10.0), + float4(-0.043263, -0.005408, 0.0, 0.000305), + float4(-0.040019, 0.030284, 0.049753, 0.964691), + float4(0.101434, -0.811469, 0.575525, 0.973449), + float4(0.004326, 0.073548, 0.049753, 14.0), + float4(-0.003245, -0.035692, -0.049753, 0.0), + float4(-0.04759, 0.004326, -0.049753, 0.955978), + float4(-0.580475, -0.643229, 0.499304, 0.896269), + float4(0.001082, 0.037855, -0.043263, 11.0), + float4(-0.044345, 0.040019, 0.0, 0.01033), + float4(-0.044345, 0.040019, 0.043263, 0.045182), + float4(-0.669964, -0.742393, 0.0, 0.577584), + float4(-0.031366, 0.229295, 0.058405, 14.0), + float4(0.035692, -0.155748, -0.008653, 0.000305), + float4(-0.011897, -0.151422, -0.058405, 0.955978), + float4(-0.716533, -0.20131, 0.667874, 0.904662), + float4(-0.043263, 0.077874, -0.043263, 7.0), + float4(-0.001082, 0.154666, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.999976, -0.006993, 0.0, 0.577584), + float4(-0.031366, 0.229295, 0.058405, 14.0), + float4(0.089771, -0.137361, -0.02055, 0.445197), + float4(0.035692, -0.155748, -0.008653, 0.973449), + float4(0.216369, -0.004654, 0.976301, 0.973449), + float4(-0.031366, 0.229295, 0.058405, 14.0), + float4(-0.011897, -0.151422, -0.058405, 0.000305), + float4(-0.012979, 0.003245, -0.058405, 0.955978), + float4(-0.976245, -0.006827, 0.216564, 0.904662), + float4(-0.043263, 0.077874, -0.043263, 9.0), + float4(-0.001082, 0.154666, 0.0, 0.01033), + float4(-0.001082, 0.154666, 0.043263, 0.045182), + float4(-0.999976, -0.006993, 0.0, 0.577584), + float4(-0.045426, 0.564586, 0.052998, 14.0), + float4(0.014061, -0.335291, 0.005408, 0.520989), + float4(0.001082, -0.332046, -0.052998, 0.088655), + float4(-0.975941, -0.037462, 0.214794, 0.973449), + float4(-0.044345, 0.23254, 0.0, 3.0), + float4(0.0, 0.0, -0.043263, 0.01033), + float4(-0.001082, 0.332046, -0.043263, 0.045182), + float4(-0.999995, -0.003257, 0.0, 0.577584), + float4(-0.163319, 0.380717, -0.043263, 7.0), + float4(-0.060569, 0.02704, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.407651, -0.913138, 0.0, 0.577584), + float4(-0.094098, 0.431551, -0.043263, 11.0), + float4(-0.069221, -0.050834, 0.0, 0.01033), + float4(-0.069221, -0.050834, 0.043263, 0.045182), + float4(0.591909, -0.806004, 0.0, 0.577584), + float4(-0.163319, 0.380717, -0.043263, 11.0), + float4(-0.060569, 0.02704, 0.0, 0.01033), + float4(-0.060569, 0.02704, 0.043263, 0.045182), + float4(-0.407651, -0.913138, 0.0, 0.577584), + float4(0.075711, 0.08869, -0.043263, 11.0), + float4(-0.031366, -0.045426, 0.0, 0.01033), + float4(-0.031366, -0.045426, 0.043263, 0.045182), + float4(0.822897, -0.568191, 0.0, 0.577584), + float4(0.044345, 0.043263, 0.0, 6.0), + float4(-0.040019, 0.030284, 0.049753, 0.000305), + float4(0.014061, 0.048671, 0.037855, 0.964691), + float4(0.365607, -0.634949, 0.680566, 0.973449), + float4(0.075711, 0.08869, -0.043263, 7.0), + float4(-0.031366, -0.045426, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.822897, -0.568191, 0.0, 0.577584), + float4(0.058405, 0.091934, 0.037855, 14.0), + float4(0.017305, -0.003245, -0.037855, 0.0), + float4(-0.014061, -0.048671, -0.037855, 0.473533), + float4(0.757369, -0.522945, 0.39105, 0.904662), + float4(-0.031366, 0.229295, 0.058405, 14.0), + float4(0.107077, -0.140606, -0.058405, 0.0), + float4(0.089771, -0.137361, -0.02055, 0.376257), + float4(0.812052, 0.481346, 0.329966, 0.830777), + float4(0.077874, 0.407756, -0.043263, 11.0), + float4(-0.002163, -0.319067, 0.0, 0.01033), + float4(-0.002163, -0.319067, 0.043263, 0.045182), + float4(0.999977, -0.006779, 0.0, 0.577584), + float4(-0.031366, 0.229295, 0.058405, 14.0), + float4(0.10924, 0.178461, -0.058405, 0.0), + float4(0.107077, -0.140606, -0.058405, 0.376257), + float4(0.475583, -0.003224, 0.879665, 0.830777), + float4(0.147095, 0.272559, -0.043263, 7.0), + float4(-0.035692, 0.034611, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.696146, -0.7179, 0.0, 0.577584), + float4(0.147095, 0.272559, -0.043263, 11.0), + float4(-0.035692, 0.034611, 0.0, 0.01033), + float4(-0.035692, 0.034611, 0.043263, 0.045182), + float4(-0.696146, -0.7179, 0.0, 0.577584), + float4(0.144932, 0.307169, 0.049753, 14.0), + float4(0.002163, -0.034611, -0.049753, 0.0), + float4(-0.033529, 0.0, -0.049753, 0.9131), + float4(-0.630237, -0.649931, 0.424725, 0.904662), + float4(0.183869, 0.317985, 0.037855, 6.0), + float4(-0.036774, -0.045426, -0.037855, 0.0), + float4(-0.038937, -0.010816, 0.011897, 0.806958), + float4(0.374433, -0.753477, 0.540438, 0.854994), + float4(0.077874, 0.407756, -0.043263, 7.0), + float4(-0.002163, -0.319067, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.999977, -0.006779, 0.0, 0.577584), + float4(0.111403, 0.307169, -0.043263, 7.0), + float4(-0.002163, 0.131953, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.999866, -0.016391, 0.0, 0.577584), + float4(0.10924, 0.439122, 0.0, 6.0), + float4(0.035692, -0.131953, 0.049753, 0.18117), + float4(0.002163, -0.131953, 0.0, 0.955978), + float4(-0.82919, -0.013593, 0.558802, 0.964691), + float4(0.128708, 0.463999, 0.063813, 14.0), + float4(0.055161, -0.146014, -0.025958, 0.246204), + float4(0.016224, -0.156829, -0.014061, 0.964691), + float4(0.305397, -0.053644, 0.950713, 0.973449), + float4(0.111403, 0.307169, -0.043263, 11.0), + float4(-0.002163, 0.131953, 0.0, 0.01033), + float4(-0.002163, 0.131953, 0.043263, 0.045182), + float4(-0.999866, -0.016391, 0.0, 0.577584), + float4(0.189277, 0.28013, -0.043263, 11.0), + float4(-0.042182, -0.007571, 0.0, 0.01033), + float4(-0.042182, -0.007571, 0.043263, 0.045182), + float4(0.176664, -0.984271, 0.0, 0.577584), + float4(0.183869, 0.317985, 0.037855, 10.0), + float4(0.005408, -0.037855, -0.037855, 0.0), + float4(-0.036774, -0.045426, -0.037855, 0.806958), + float4(0.124328, -0.692684, 0.710445, 0.854994), + float4(0.189277, 0.28013, -0.043263, 7.0), + float4(-0.042182, -0.007571, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.176664, -0.984271, 0.0, 0.577584), + float4(0.183869, 0.317985, 0.037855, 14.0), + float4(0.02704, -0.004326, -0.037855, 0.0), + float4(0.005408, -0.037855, -0.037855, 0.514916), + float4(0.70062, -0.452013, 0.552101, 0.846876), + float4(0.210908, 0.313659, -0.043263, 11.0), + float4(-0.021632, -0.033529, 0.0, 0.01033), + float4(-0.021632, -0.033529, 0.043263, 0.045182), + float4(0.840297, -0.542127, 0.0, 0.577584), + float4(0.210908, 0.445612, 0.0, 10.0), + float4(0.0, -0.131953, 0.0, 0.0), + float4(-0.02704, -0.127627, 0.037855, 0.439658), + float4(0.813734, 0.0, 0.581238, 0.871366), + float4(0.210908, 0.313659, -0.043263, 7.0), + float4(-0.021632, -0.033529, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.840297, -0.542127, 0.0, 0.577584), + float4(0.210908, 0.445612, -0.043263, 11.0), + float4(0.0, -0.131953, 0.0, 0.01033), + float4(0.0, -0.131953, 0.043263, 0.045182), + float4(1.0, 0.0, 0.0, 0.577584), + float4(0.241193, 0.343943, -0.043263, 7.0), + float4(-0.002163, 0.107077, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.999796, -0.020198, 0.0, 0.577584), + float4(0.276885, 0.311496, -0.043263, 11.0), + float4(-0.035692, 0.032447, 0.0, 0.01033), + float4(-0.035692, 0.032447, 0.043263, 0.045182), + float4(-0.672673, -0.73994, 0.0, 0.577584), + float4(0.23903, 0.45102, 0.0, 14.0), + float4(0.042182, -0.108158, 0.046508, 0.000305), + float4(0.002163, -0.107077, 0.0, 0.846876), + float4(-0.758098, -0.015315, 0.651961, 0.896269), + float4(0.276885, 0.311496, -0.043263, 7.0), + float4(-0.035692, 0.032447, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.672673, -0.73994, 0.0, 0.577584), + float4(0.281211, 0.342862, 0.046508, 14.0), + float4(-0.004326, -0.031366, -0.046508, 0.0), + float4(-0.040019, 0.001082, -0.046508, 0.752941), + float4(-0.58651, -0.645161, 0.489667, 0.846876), + float4(0.246601, 0.459672, 0.06165, 14.0), + float4(0.070303, -0.101669, -0.024876, 0.262257), + float4(0.034611, -0.116811, -0.015142, 0.973449), + float4(0.27928, -0.041605, 0.959308, 0.973449), + float4(0.319067, 0.319067, -0.043263, 11.0), + float4(-0.042182, -0.007571, 0.0, 0.01033), + float4(-0.042182, -0.007571, 0.043263, 0.045182), + float4(0.176664, -0.984271, 0.0, 0.577584), + float4(0.319067, 0.319067, 0.0, 10.0), + float4(-0.042182, -0.007571, 0.0, 0.0), + float4(-0.037855, 0.023795, 0.046508, 0.896269), + float4(0.1483, -0.826245, 0.54344, 0.896269), + float4(0.319067, 0.319067, -0.043263, 7.0), + float4(-0.042182, -0.007571, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.176664, -0.984271, 0.0, 0.577584), + float4(0.319067, 0.319067, 0.0, 6.0), + float4(-0.037855, 0.023795, 0.046508, 0.0), + float4(-0.002163, 0.038937, 0.036774, 0.896269), + float4(0.437906, -0.604311, 0.665618, 0.896269), + float4(0.316904, 0.358004, 0.036774, 14.0), + float4(0.024876, -0.004326, -0.036774, 0.0), + float4(0.002163, -0.038937, -0.036774, 0.617212), + float4(0.707338, -0.464191, 0.533104, 0.921584), + float4(0.34178, 0.353677, -0.043263, 11.0), + float4(-0.022713, -0.034611, 0.0, 0.01033), + float4(-0.022713, -0.034611, 0.043263, 0.045182), + float4(0.836048, -0.548657, 0.0, 0.577584), + float4(0.340698, 0.474815, 0.0, 14.0), + float4(0.001082, -0.121137, 0.0, 0.0), + float4(-0.023795, -0.116811, 0.036774, 0.473533), + float4(0.828664, 0.007399, 0.559697, 0.930114), + float4(0.34178, 0.353677, -0.043263, 7.0), + float4(-0.022713, -0.034611, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.836048, -0.548657, 0.0, 0.577584), + float4(0.340698, 0.474815, -0.043263, 11.0), + float4(0.001082, -0.121137, 0.0, 0.01033), + float4(0.001082, -0.121137, 0.043263, 0.045182), + float4(0.99996, 0.008928, 0.0, 0.577584), + float4(0.411001, 0.351514, -0.043263, 11.0), + float4(-0.034611, 0.030284, 0.0, 0.01033), + float4(-0.034611, 0.030284, 0.043263, 0.045182), + float4(-0.658505, -0.752577, 0.0, 0.577584), + float4(0.411001, 0.351514, -0.043263, 7.0), + float4(-0.034611, 0.030284, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.658505, -0.752577, 0.0, 0.577584), + float4(0.412083, 0.38288, 0.043263, 14.0), + float4(-0.001082, -0.031366, -0.043263, 0.0), + float4(-0.035692, -0.001082, -0.043263, 0.745403), + float4(-0.574039, -0.656045, 0.489984, 0.854994), + float4(0.45102, 0.359085, -0.043263, 11.0), + float4(-0.040019, -0.007571, 0.0, 0.01033), + float4(-0.040019, -0.007571, 0.043263, 0.045182), + float4(0.185892, -0.98257, 0.0, 0.577584), + float4(0.45102, 0.359085, 0.0, 10.0), + float4(-0.040019, -0.007571, 0.0, 0.000305), + float4(-0.038937, 0.023795, 0.043263, 0.879622), + float4(0.151736, -0.802035, 0.577682, 0.9131), + float4(0.45102, 0.359085, -0.043263, 7.0), + float4(-0.040019, -0.007571, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.185892, -0.98257, 0.0, 0.577584), + float4(0.45102, 0.359085, 0.0, 6.0), + float4(-0.038937, 0.023795, 0.043263, 0.000305), + float4(0.003245, 0.0411, 0.032447, 0.879622), + float4(0.417852, -0.583049, 0.696744, 0.9131), + float4(0.473733, 0.391533, -0.043263, 11.0), + float4(-0.022713, -0.032447, 0.0, 0.01033), + float4(-0.022713, -0.032447, 0.043263, 0.045182), + float4(0.819232, -0.573462, 0.0, 0.577584), + float4(0.454264, 0.400185, 0.032447, 14.0), + float4(0.019468, -0.008653, -0.032447, 0.0), + float4(-0.003245, -0.0411, -0.032447, 0.527108), + float4(0.688617, -0.482032, 0.541712, 0.887922), + float4(0.473733, 0.391533, -0.043263, 7.0), + float4(-0.022713, -0.032447, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.819232, -0.573462, 0.0, 0.577584), + float4(-0.223888, 0.407756, -0.043263, 7.0), + float4(0.002163, 0.06814, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.999496, 0.03173, 0.0, 0.577584), + float4(-0.223888, 0.407756, -0.043263, 11.0), + float4(0.002163, 0.06814, 0.0, 0.01033), + float4(0.002163, 0.06814, 0.043263, 0.045182), + float4(-0.999496, 0.03173, 0.0, 0.577584), + float4(-0.167645, 0.416409, 0.046508, 14.0), + float4(-0.056242, -0.008653, -0.046508, 0.000305), + float4(-0.054079, 0.059487, -0.046508, 0.955978), + float4(-0.638985, 0.020285, 0.768952, 0.973449), + float4(-0.167645, 0.416409, 0.046508, 14.0), + float4(0.004326, -0.035692, -0.046508, 0.000305), + float4(-0.056242, -0.008653, -0.046508, 0.955978), + float4(-0.339782, -0.761112, 0.552501, 0.973449), + float4(-0.221724, 0.475896, 0.0, 14.0), + float4(0.139524, 0.055161, 0.043263, 0.520989), + float4(0.054079, -0.059487, 0.046508, 0.973449), + float4(-0.39307, 0.317373, 0.863001, 0.973449), + float4(-0.167645, 0.416409, 0.046508, 6.0), + float4(0.073548, 0.015142, -0.046508, 0.0), + float4(0.004326, -0.035692, -0.046508, 0.768154), + float4(0.490917, -0.668482, 0.558688, 0.871366), + float4(-0.094098, 0.431551, -0.043263, 7.0), + float4(-0.069221, -0.050834, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.591909, -0.806004, 0.0, 0.577584), + float4(-0.167645, 0.416409, 0.046508, 10.0), + float4(0.085445, 0.114648, -0.003245, 0.0), + float4(0.073548, 0.015142, -0.046508, 0.768154), + float4(0.548356, -0.38771, 0.740936, 0.871366), + float4(-0.0822, 0.531057, -0.043263, 11.0), + float4(-0.011897, -0.099506, 0.0, 0.01033), + float4(-0.011897, -0.099506, 0.043263, 0.045182), + float4(0.992928, -0.11872, 0.0, 0.577584), + float4(-0.044345, 0.23254, 0.0, 7.0), + float4(-0.001082, 0.332046, -0.043263, 0.01033), + float4(-0.001082, 0.332046, 0.052998, 0.045182), + float4(-0.999995, -0.003257, 0.0, 0.577584), + float4(-0.045426, 0.564586, 0.052998, 14.0), + float4(0.1233, -0.156829, -0.052998, 0.520989), + float4(0.014061, -0.335291, 0.005408, 0.088655), + float4(0.429355, 0.032562, 0.902549, 0.973449), + float4(0.10924, 0.439122, 0.0, 14.0), + float4(-0.031366, -0.031366, 0.0, 0.132876), + float4(-0.154666, 0.125464, 0.052998, 0.028428), + float4(0.182761, -0.182761, 0.966021, 0.955978), + float4(0.10924, 0.439122, -0.043263, 11.0), + float4(-0.031366, -0.031366, 0.0, 0.01033), + float4(-0.031366, -0.031366, 0.043263, 0.045182), + float4(0.707107, -0.707107, 0.0, 0.577584), + float4(0.10924, 0.439122, -0.043263, 7.0), + float4(-0.031366, -0.031366, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.707107, -0.707107, 0.0, 0.577584), + float4(0.10924, 0.439122, 0.0, 10.0), + float4(0.019468, 0.024876, 0.063813, 0.18117), + float4(0.035692, -0.131953, 0.049753, 0.955978), + float4(-0.933936, -0.126584, 0.334276, 0.964691), + float4(0.128708, 0.463999, 0.063813, 14.0), + float4(0.050834, 0.002163, -0.005408, 0.0), + float4(0.0822, -0.018387, -0.063813, 0.439658), + float4(0.07859, -0.92643, 0.368172, 0.871366), + float4(-0.221724, 0.475896, -0.043263, 7.0), + float4(0.122219, 0.207664, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.861819, 0.507216, 0.0, 0.577584), + float4(-0.221724, 0.475896, -0.043263, 9.0), + float4(0.122219, 0.207664, 0.0, 0.01033), + float4(0.122219, 0.207664, 0.043263, 0.045182), + float4(-0.861819, 0.507216, 0.0, 0.577584), + float4(-0.099506, 0.68356, 0.0, 14.0), + float4(0.017305, -0.152503, 0.043263, 0.0), + float4(-0.122219, -0.207664, 0.0, 0.723049), + float4(-0.365879, 0.215335, 0.905408, 0.854994), + float4(-0.0822, 0.531057, -0.043263, 7.0), + float4(-0.011897, -0.099506, 0.043263, 0.01033), + float4(0.0, 0.0, 0.086527, 0.045182), + float4(0.992928, -0.11872, 0.0, 0.577584), + float4(-0.099506, 0.68356, 0.0, 14.0), + float4(0.054079, -0.118974, 0.052998, 0.0), + float4(0.017305, -0.152503, 0.043263, 0.514916), + float4(-0.41958, 0.203352, 0.884647, 0.863157), + float4(-0.099506, 0.68356, -0.043263, 3.0), + float4(0.086527, 0.149258, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.86514, 0.501531, 0.0, 0.577584), + float4(-0.045426, 0.564586, -0.043263, 11.0), + float4(-0.036774, -0.033529, 0.0, 0.01033), + float4(-0.036774, -0.033529, 0.086527, 0.045182), + float4(0.673754, -0.738956, 0.0, 0.577584), + float4(-0.045426, 0.564586, -0.043263, 7.0), + float4(-0.036774, -0.033529, 0.086527, 0.01033), + float4(0.0, 0.0, 0.096261, 0.045182), + float4(0.673754, -0.738956, 0.0, 0.577584), + float4(-0.099506, 0.68356, 0.0, 14.0), + float4(0.086527, 0.149258, 0.0, 0.0), + float4(0.054079, -0.118974, 0.052998, 0.262257), + float4(-0.385563, 0.223515, 0.8952, 0.9131), + float4(-0.099506, 0.68356, -0.043263, 11.0), + float4(0.086527, 0.149258, 0.0, 0.01033), + float4(0.086527, 0.149258, 0.043263, 0.045182), + float4(-0.86514, 0.501531, 0.0, 0.577584), + float4(-0.012979, 0.832818, 0.0, 14.0), + float4(0.214153, -0.194685, 0.0822, 0.000305), + float4(-0.032447, -0.268232, 0.052998, 0.930114), + float4(-0.176863, 0.211325, 0.961281, 0.973449), + float4(0.128708, 0.463999, 0.063813, 14.0), + float4(-0.019468, -0.024876, -0.063813, 0.168276), + float4(-0.174135, 0.100587, -0.010816, 0.038209), + float4(-0.469238, -0.764881, 0.441331, 0.955978), + float4(0.201174, 0.638133, 0.0822, 14.0), + float4(-0.072466, -0.174135, -0.018387, 0.18117), + float4(-0.246601, -0.073548, -0.029203, 0.082277), + float4(-0.098561, -0.063844, 0.993081, 0.973449), + float4(-0.012979, 0.832818, -0.043263, 7.0), + float4(0.339617, -0.002163, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.006369, 0.99998, 0.0, 0.577584), + float4(0.179543, 0.466162, 0.058405, 10.0), + float4(-0.050834, -0.002163, 0.005408, 0.000305), + float4(0.021632, 0.171972, 0.023795, 0.022171), + float4(0.110896, -0.149888, 0.982464, 0.381323), + float4(-0.012979, 0.832818, 0.0, 14.0), + float4(0.339617, -0.002163, 0.0, 0.0), + float4(0.214153, -0.194685, 0.0822, 0.027314), + float4(0.002492, 0.391296, 0.920261, 0.49102), + float4(0.183869, 0.317985, 0.037855, 10.0), + float4(-0.055161, 0.146014, 0.025958, 0.0), + float4(0.02704, 0.127627, -0.037855, 0.439658), + float4(0.623841, 0.097823, 0.775405, 0.871366), + float4(0.210908, 0.445612, -0.043263, 7.0), + float4(0.0, -0.131953, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(1.0, 0.0, 0.0, 0.577584), + float4(0.210908, 0.445612, 0.0, 10.0), + float4(-0.031366, 0.02055, 0.058405, 0.000305), + float4(0.035692, 0.014061, 0.06165, 0.022171), + float4(-0.105864, -0.954447, 0.278971, 0.381323), + float4(0.23903, 0.45102, -0.043263, 11.0), + float4(-0.028121, -0.005408, 0.0, 0.01033), + float4(-0.028121, -0.005408, 0.043263, 0.045182), + float4(0.188847, -0.982006, 0.0, 0.577584), + float4(0.23903, 0.45102, -0.043263, 7.0), + float4(-0.028121, -0.005408, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.188847, -0.982006, 0.0, 0.577584), + float4(0.246601, 0.459672, 0.06165, 11.0), + float4(-0.007571, -0.008653, -0.06165, 0.000305), + float4(-0.035692, -0.014061, -0.06165, 0.022171), + float4(0.187618, -0.975617, 0.113888, 0.381323), + float4(0.241193, 0.343943, -0.043263, 11.0), + float4(-0.002163, 0.107077, 0.0, 0.01033), + float4(-0.002163, 0.107077, 0.043263, 0.045182), + float4(-0.999796, -0.020198, 0.0, 0.577584), + float4(0.23903, 0.45102, 0.0, 15.0), + float4(0.007571, 0.008653, 0.06165, 0.000305), + float4(0.042182, -0.108158, 0.046508, 0.846876), + float4(-0.94107, -0.299262, 0.157572, 0.896269), + float4(0.316904, 0.358004, 0.036774, 14.0), + float4(-0.070303, 0.101669, 0.024876, 0.0), + float4(0.023795, 0.116811, -0.036774, 0.473533), + float4(0.523424, 0.157027, 0.837478, 0.930114), + float4(0.340698, 0.474815, -0.043263, 7.0), + float4(0.001082, -0.121137, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.99996, 0.008928, 0.0, 0.577584), + float4(0.376391, 0.381799, -0.043263, 11.0), + float4(-0.001082, 0.107077, 0.0, 0.01033), + float4(-0.001082, 0.107077, 0.043263, 0.045182), + float4(-0.999949, -0.0101, 0.0, 0.577584), + float4(0.376391, 0.381799, -0.043263, 7.0), + float4(-0.001082, 0.107077, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.999949, -0.0101, 0.0, 0.577584), + float4(0.375309, 0.488875, 0.0, 14.0), + float4(0.036774, -0.105995, 0.043263, 0.000305), + float4(0.001082, -0.107077, 0.0, 0.879622), + float4(-0.771254, -0.00779, 0.63648, 0.896269), + float4(0.375309, 0.488875, 0.0, 14.0), + float4(0.078955, -0.08869, 0.032447, 0.223224), + float4(0.036774, -0.105995, 0.043263, 0.964691), + float4(0.071225, 0.39802, 0.914608, 0.973449), + float4(0.473733, 0.603523, 0.0, 14.0), + float4(0.0, -0.21199, 0.0, 0.158953), + float4(-0.019468, -0.203337, 0.032447, 0.030716), + float4(0.857493, 0.0, 0.514496, 0.964691), + float4(0.473733, 0.603523, -0.043263, 11.0), + float4(0.0, -0.21199, 0.0, 0.01033), + float4(0.0, -0.21199, 0.043263, 0.045182), + float4(1.0, 0.0, 0.0, 0.577584), + float4(0.276885, 0.484549, 0.058405, 10.0), + float4(-0.030284, -0.024876, 0.003245, 0.000305), + float4(-0.097342, -0.018387, 0.0, 0.022171), + float4(-0.03153, 0.166923, 0.985466, 0.381323), + float4(0.246601, 0.459672, 0.06165, 14.0), + float4(0.030284, 0.024876, -0.003245, 0.0), + float4(0.094098, 0.015142, -0.06165, 0.473533), + float4(0.518869, -0.545855, 0.657888, 0.930114), + float4(0.375309, 0.488875, -0.043263, 11.0), + float4(-0.034611, -0.014061, 0.0, 0.01033), + float4(-0.034611, -0.014061, 0.043263, 0.045182), + float4(0.376377, -0.926467, 0.0, 0.577584), + float4(0.340698, 0.474815, 0.0, 14.0), + float4(-0.063813, 0.009734, 0.058405, 0.000305), + float4(0.034611, 0.014061, 0.0, 0.022171), + float4(0.3276, -0.806402, 0.492334, 0.381323), + float4(0.375309, 0.488875, -0.043263, 7.0), + float4(-0.034611, -0.014061, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.376377, -0.926467, 0.0, 0.577584), + float4(0.454264, 0.400185, 0.032447, 12.0), + float4(-0.078955, 0.08869, -0.032447, 0.158953), + float4(0.019468, 0.203337, -0.032447, 0.030716), + float4(-0.201684, 0.173144, 0.964025, 0.964691), + float4(0.201174, 0.638133, 0.0822, 8.0), + float4(0.075711, -0.153585, -0.023795, 0.000305), + float4(-0.021632, -0.171972, -0.023795, 0.022171), + float4(0.026497, -0.140281, 0.989757, 0.381323), + float4(0.473733, 0.603523, -0.043263, 7.0), + float4(0.0, -0.21199, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(1.0, 0.0, 0.0, 0.577584), + float4(0.276885, 0.484549, 0.058405, 14.0), + float4(-0.075711, 0.153585, 0.023795, 0.000305), + float4(0.098424, 0.004326, -0.058405, 0.022171), + float4(0.50316, 0.115345, 0.856461, 0.381323), + float4(0.375309, 0.488875, 0.0, 10.0), + float4(0.090853, 0.12979, 0.0, 0.158953), + float4(0.098424, 0.114648, 0.0, 0.030716), + float4(0.0, 0.0, 1.0, 0.964691), + float4(0.466162, 0.618665, -0.043263, 11.0), + float4(0.007571, -0.015142, 0.0, 0.01033), + float4(0.007571, -0.015142, 0.043263, 0.045182), + float4(0.894427, 0.447214, 0.0, 0.577584), + float4(0.466162, 0.618665, -0.043263, 3.0), + float4(0.007571, -0.015142, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.894427, 0.447214, 0.0, 0.577584), + float4(0.326638, 0.830655, 0.0, 14.0), + float4(0.139524, -0.21199, 0.0, 0.0), + float4(0.048671, -0.34178, 0.0, 0.015213), + float4(0.0, 0.0, 1.0, 0.428687), + float4(0.326638, 0.830655, 0.0, 14.0), + float4(0.048671, -0.34178, 0.0, 0.007492), + float4(-0.125464, -0.192522, 0.0822, 0.054475), + float4(0.472493, 0.067285, 0.878762, 0.964691), + float4(0.326638, 0.830655, -0.043263, 9.0), + float4(0.139524, -0.21199, 0.0, 0.01033), + float4(0.139524, -0.21199, 0.043263, 0.045182), + float4(0.835314, 0.549773, 0.0, 0.577584), + float4(0.326638, 0.830655, -0.043263, 7.0), + float4(0.139524, -0.21199, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.835314, 0.549773, 0.0, 0.577584), + float4(-0.012979, 0.832818, -0.043263, 11.0), + float4(0.339617, -0.002163, 0.0, 0.01033), + float4(0.339617, -0.002163, 0.043263, 0.045182), + float4(0.006369, 0.99998, 0.0, 0.577584) +}; +static const float4 PRISM_BOXES[44] = { + float4(0.030135, 0.06127, -0.043945, 0.0), + float4(0.330601, 0.483055, 0.116951, 7.0), + float4(0.034429, 0.265258, -0.043945, 7.0), + float4(0.542104, 0.741798, 0.116951, 8.0), + float4(0.034429, 0.355442, -0.043945, 15.0), + float4(0.305908, 0.822319, 0.116951, 8.0), + float4(0.267405, 0.532589, -0.043945, 23.0), + float4(0.542104, 0.822319, 0.082595, 8.0), + float4(-0.045345, 0.036855, -0.044263, 31.0), + float4(0.045345, 0.23354, 0.059405, 8.0), + float4(-0.224888, 0.072548, -0.044263, 39.0), + float4(0.059405, 0.565586, 0.059405, 8.0), + float4(-0.032366, 0.042263, -0.044263, 47.0), + float4(0.148095, 0.408756, 0.059405, 8.0), + float4(0.074711, 0.08769, -0.044263, 55.0), + float4(0.184869, 0.464999, 0.064813, 8.0), + float4(0.146095, 0.271559, -0.044263, 63.0), + float4(0.211908, 0.446612, 0.038855, 8.0), + float4(0.23803, 0.310496, -0.044263, 71.0), + float4(0.320067, 0.460672, 0.06265, 8.0), + float4(0.275885, 0.310496, -0.044263, 79.0), + float4(0.412001, 0.475815, 0.047508, 8.0), + float4(0.375391, 0.350514, -0.044263, 87.0), + float4(0.45202, 0.38388, 0.044263, 4.0), + float4(0.410001, 0.350514, -0.044263, 91.0), + float4(0.474733, 0.401185, 0.044263, 5.0), + float4(-0.224888, 0.379717, -0.044263, 96.0), + float4(-0.0812, 0.532057, 0.047508, 8.0), + float4(-0.095098, 0.228295, -0.044263, 104.0), + float4(0.211908, 0.565586, 0.064813, 8.0), + float4(-0.222724, 0.430551, -0.044263, 112.0), + float4(-0.011979, 0.833818, 0.053998, 8.0), + float4(-0.100506, 0.438122, -0.044263, 120.0), + float4(0.327638, 0.833818, 0.0832, 8.0), + float4(0.127708, 0.312659, -0.044263, 128.0), + float4(0.282211, 0.467162, 0.064813, 8.0), + float4(0.245601, 0.352677, -0.044263, 136.0), + float4(0.474733, 0.604523, 0.06265, 8.0), + float4(0.178543, 0.390533, -0.044263, 144.0), + float4(0.474733, 0.639133, 0.0832, 8.0), + float4(0.200174, 0.483549, -0.044263, 152.0), + float4(0.474733, 0.639133, 0.0832, 4.0), + float4(-0.013979, 0.487875, -0.044263, 156.0), + float4(0.467162, 0.833818, 0.0832, 5.0) +}; +static const float2 PRISM_POLY[110] = { + float2(0.006442, -0.002147), + float2(-0.001074, 0.001074), + float2(-0.004294, 0.007515), + float2(-0.004294, 0.806288), + float2(-0.001074, 0.809509), + float2(0.004294, 0.809509), + float2(0.022546, 0.796626), + float2(0.185736, 0.655982), + float2(0.281288, 0.86319), + float2(0.286656, 0.870706), + float2(0.293098, 0.871779), + float2(0.465951, 0.781595), + float2(0.478834, 0.773006), + float2(0.478834, 0.763344), + float2(0.407975, 0.592638), + float2(0.45092, 0.586196), + float2(0.601227, 0.573313), + float2(0.606595, 0.569018), + float2(0.606595, 0.564724), + float2(0.601227, 0.556135), + float2(0.50138, 0.453067), + float2(0.425153, 0.37684), + float2(0.289877, 0.246933), + float2(0.081595, 0.057975), + float2(0.013957, 0.0), + float2(-0.011897, 0.0), + float2(-0.055161, 0.037855), + float2(-0.081119, 0.063813), + float2(-0.083282, 0.06814), + float2(-0.084363, 0.38288), + float2(-0.089771, 0.38288), + float2(-0.158993, 0.33529), + float2(-0.166564, 0.336372), + float2(-0.250927, 0.379635), + float2(-0.261743, 0.388288), + float2(-0.263906, 0.461836), + float2(-0.262824, 0.486712), + float2(-0.173053, 0.638133), + float2(-0.044345, 0.860939), + float2(-0.036774, 0.867429), + float2(-0.024876, 0.871755), + float2(0.33529, 0.871755), + float2(0.349351, 0.867429), + float2(0.363412, 0.853368), + float2(0.513752, 0.619747), + float2(0.513752, 0.379635), + float2(0.466162, 0.322311), + float2(0.461836, 0.320148), + float2(0.399104, 0.311496), + float2(0.393696, 0.313659), + float2(0.374227, 0.332046), + float2(0.343943, 0.288782), + float2(0.336372, 0.282293), + float2(0.269314, 0.271477), + float2(0.263906, 0.27364), + float2(0.243356, 0.292027), + float2(0.21199, 0.244438), + float2(0.207664, 0.241193), + float2(0.149258, 0.231459), + float2(0.138443, 0.231459), + float2(0.118974, 0.248764), + float2(0.116811, 0.222806), + float2(0.117892, 0.162237), + float2(0.115729, 0.076792), + float2(0.06814, 0.011897), + float2(0.062732, 0.007571), + float2(0.005408, 0.0), + float2(0.031135, 0.06227), + float2(0.252301, 0.266258), + float2(0.541104, 0.544325), + float2(0.359663, 0.559356), + float2(0.433742, 0.753681), + float2(0.304908, 0.821319), + float2(0.199693, 0.595859), + float2(0.035429, 0.740798), + float2(0.035429, 0.482055), + float2(-0.043263, 0.077874), + float2(0.001082, 0.037855), + float2(0.044345, 0.043263), + float2(0.075711, 0.08869), + float2(0.077874, 0.407756), + float2(0.10924, 0.439122), + float2(0.111403, 0.307169), + float2(0.147095, 0.272559), + float2(0.189277, 0.28013), + float2(0.210909, 0.313659), + float2(0.210909, 0.445612), + float2(0.23903, 0.45102), + float2(0.241193, 0.343943), + float2(0.276885, 0.311496), + float2(0.319067, 0.319067), + float2(0.34178, 0.353677), + float2(0.340698, 0.474815), + float2(0.375309, 0.488875), + float2(0.376391, 0.381799), + float2(0.411001, 0.351514), + float2(0.45102, 0.359085), + float2(0.473733, 0.391533), + float2(0.473733, 0.603523), + float2(0.466162, 0.618665), + float2(0.326638, 0.830655), + float2(-0.012979, 0.832818), + float2(-0.099506, 0.68356), + float2(-0.221724, 0.475896), + float2(-0.223888, 0.407756), + float2(-0.163319, 0.380717), + float2(-0.094098, 0.431551), + float2(-0.0822, 0.531057), + float2(-0.045426, 0.564586), + float2(-0.044345, 0.23254) +}; +static const float PRISM_BEVEL = 0.012883; +// end of the prism mesh + +// Le verre validé sur le prototype (30/09/2026). +static const float PRISM_IOR = 1.61; +static const float PRISM_DISPERSION = 0.035; +static const float PRISM_GLOW = 0.45; +static const float PRISM_FOLD = 0.35; +// Rebonds au plus dans le cristal, par canal ; l'id de matière du cristal. +static const int PRISM_BOUNCES = 6; +static const float PRISM_MAT = 9.0; + +// Le modèle de Prism Glow de ce dessin : 0 flèche, 1 main, -1 un autre curseur. +int prism_shape() +{ + int id = sculpt_id(); + return id == 3 || id == 4 ? id - 3 : -1; +} + +// Distance signée au polygone `n` sommets de PRISM_POLY à partir de `i0` (plan xy du modèle), +// négative dedans : la silhouette du serti, ou le contour du cristal. +float prism_poly_dist(float2 p, int i0, int n) +{ + float2 vj = PRISM_POLY[i0 + n - 1]; + float d = 1e9; + float sgn = 1.0; + [loop] for (int i = 0; i < n; i++) + { + float2 vi = PRISM_POLY[i0 + i]; + float2 e = vj - vi; + float2 w = p - vi; + float2 b = w - e * saturate(dot(w, e) / dot(e, e)); + d = min(d, dot(b, b)); + bool c0 = p.y >= vi.y; + bool c1 = p.y < vj.y; + bool c2 = e.x * w.y > e.y * w.x; + if ((c0 && c1 && c2) || (!c0 && !c1 && !c2)) + { + sgn = -sgn; + } + vj = vi; + } + return sgn * sqrt(d); +} + +// Le serti : la silhouette extrudée de z = -épaisseur à 0, arêtes arrondies de PRISM_BEVEL. +float prism_rim(float3 p, int shape) +{ + float half_t = model_thick() * 0.5; + float sil = prism_poly_dist(p.xy, PRISM_SIL_START[shape], PRISM_SIL_COUNT[shape]); + float2 w = float2(sil + PRISM_BEVEL, abs(p.z + half_t) - (half_t - PRISM_BEVEL)); + return min(max(w.x, w.y), 0.0) + length(max(w, 0.0)) - PRISM_BEVEL; +} + +// Distance le long de (ro, rd) au triangle `k` (Möller–Trumbore), -1 s'il le rate. Un rien de jeu +// sur les bords : un rayon ne passe pas entre deux facettes voisines. +float prism_tri(float3 ro, float3 rd, int k) +{ + float3 v0 = PRISM_TRIS[4 * k].xyz; + float3 e1 = PRISM_TRIS[4 * k + 1].xyz; + float3 e2 = PRISM_TRIS[4 * k + 2].xyz; + float3 pv = cross(rd, e2); + float det = dot(e1, pv); + if (abs(det) < 1e-10) + { + return -1.0; + } + float inv = 1.0 / det; + float3 sv = ro - v0; + float u = dot(sv, pv) * inv; + float3 qv = cross(sv, e1); + float v = dot(rd, qv) * inv; + if (u < -2e-4 || v < -2e-4 || u + v > 1.0004) + { + return -1.0; + } + return dot(e2, qv) * inv; +} + +// Le triangle du cristal `shape` le plus proche devant (ro, rd) : sa distance (1e9 si aucun), son +// indice dans `hit` (-1 si aucun). Les triangles sont rangés par voisinage, une boîte englobante +// par groupe (PRISM_BOXES) : un rayon qui rate une boîte, ou n'y entre qu'au-delà du meilleur +// point, saute tous ses triangles. L'inverse de la direction sert à toutes les boîtes. +float prism_trace(float3 ro, float3 rd, int shape, out int hit) +{ + float3 inv = 1.0 / (abs(rd) > 1e-6 ? rd : 1e-6); + int b0 = PRISM_BOX_START[shape]; + int nb = PRISM_BOX_COUNT[shape]; + float best = 1e9; + hit = -1; + [loop] for (int j = b0; j < b0 + nb; j++) + { + float4 lo = PRISM_BOXES[2 * j]; + float4 hi = PRISM_BOXES[2 * j + 1]; + float3 t0 = (lo.xyz - ro) * inv; + float3 t1 = (hi.xyz - ro) * inv; + float3 tn = min(t0, t1); + float3 tf = max(t0, t1); + float tmin = max(max(tn.x, tn.y), tn.z); + float tmax = min(min(tf.x, tf.y), tf.z); + if (tmax < max(tmin, 1e-5) || tmin > best) + { + continue; + } + int k0 = (int)lo.w; + int n = (int)hi.w; + [loop] for (int i = k0; i < k0 + n; i++) + { + float t = prism_tri(ro, rd, i); + if (t > 1e-5 && t < best) + { + best = t; + hit = i; + } + } + } + return best; +} + // 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) { @@ -1071,11 +2015,17 @@ 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. Pixel Candy porte donc l'ombre de sa forme, extrudée sur la hauteur des cubes. +// voxels. Pixel Candy porte donc l'ombre de sa forme, extrudée sur la hauteur des cubes. Prism +// Glow n'a ici que son serti, dans le repère du modèle : son cristal est tracé à part +// (`prism_primary`), et l'ombre est celle de toute sa silhouette. float2 sculpt_eval(float3 q, bool occ) { int id = sculpt_id() - 1; int theme = id / 2, shape = id % 2; + if (theme == 1) + { + return float2(prism_rim(q, shape), 8.0); + } float3 p = sculpt_point(q); float2 r; if (occ && theme == 3) @@ -1226,6 +2176,11 @@ SculptMat sculpt_material(float mat, float3 p, int theme, int shape) 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 == 1) + { + // Le serti de Prism Glow : un émail marine, brillant sur son arrondi seulement (`model_shade`). + return s_mat(s_lin(0.02, 0.05, 0.33), 0.35, 0.5, 0.05, 0.3); + } if (theme == 2) { // Corail : la flèche, le calque et les tirets de la main ; jaune : le gant, le calque et les @@ -1300,6 +2255,12 @@ float3 model_shade(float3 q, float3 n, float3 rd, float3 L, float fall, float sh // La matière d'un voxel se lit juste sous la surface : son flanc lit sa propre cellule. float3 p = sculpt_point(q) - float3(n.x, -n.y, n.z) * 0.01; m = sculpt_material(mat, p, (id - 1) / 2, (id - 1) % 2); + if (id == 3 || id == 4) + { + // Le serti de Prism Glow, comme un sprite : son dessus plat, face à la lampe, prendrait + // tout son reflet et virerait au gris. + gloss = 1.0 - smoothstep(0.97, 0.995, abs(n.z)); + } } else { @@ -1325,6 +2286,177 @@ float3 model_shade(float3 q, float3 n, float3 rd, float3 L, float fall, float sh return model_tonemap(col); } +// ---- Le cristal de Prism Glow ---- + +float prism_fresnel(float c, float ior) +{ + float f0 = (ior - 1.0) / (ior + 1.0); + f0 *= f0; + return f0 + (1.0 - f0) * pow(1.0 - c, 5.0); +} + +// Le cristal sur le rayon (ro, rd) : distance au point touché (1e9 si raté ou hors de Prism Glow), +// facette dans `hc`. Une paroi sous le dessus du serti ne compte pas : le rayon a traversé le +// serti avant, c'est lui qu'on voit. +float prism_primary(float3 ro, float3 rd, out int hc) +{ + hc = -1; + int shape = prism_shape(); + if (shape < 0) + { + return 1e9; + } + float t = prism_trace(ro, rd, shape, hc); + if (hc >= 0 && fmod(PRISM_TRIS[4 * hc].w, 2.0) > 0.5 && ro.z + rd.z * t < 0.0) + { + hc = -1; + return 1e9; + } + return t; +} + +// La normale de la facette `h`, tournée vers le rayon qui arrive le long de `rd`. +float3 prism_normal(int h, float3 rd) +{ + float3 n = PRISM_TRIS[4 * h + 3].xyz; + return dot(n, rd) > 0.0 ? -n : n; +} + +// Distance de `p` au segment [a, b]. +float prism_seg(float3 p, float3 a, float3 b) +{ + float3 pa = p - a, ba = b - a; + return length(pa - ba * saturate(dot(pa, ba) / dot(ba, ba))); +} + +// Distance, en pixels (`px` = unités du modèle par pixel au point), de `p` aux arêtes réelles de +// la facette `h` : les plis de l'art et le bord du serti. +float prism_edge_px(float3 p, int h, float px) +{ + float4 r0 = PRISM_TRIS[4 * h]; + float3 a = r0.xyz; + float3 b = a + PRISM_TRIS[4 * h + 1].xyz; + float3 c = a + PRISM_TRIS[4 * h + 2].xyz; + int bits = (int)(r0.w * 0.5); + float d = 1e9; + if ((bits & 1) != 0) d = min(d, prism_seg(p, b, c)); + if ((bits & 2) != 0) d = min(d, prism_seg(p, c, a)); + if ((bits & 4) != 0) d = min(d, prism_seg(p, a, b)); + return d / px; +} + +// La rotation inverse de `world_to_plane` : du repère du plan à celui de la caméra. +float3 plane_to_world(float3 v, ModelFrame f) +{ + float x = v.x * f.c.z - v.y * f.s.z; + float y = v.x * f.s.z + v.y * f.c.z; + float z = -x * f.s.y + v.z * f.c.y; + return float3(x * f.c.y + v.z * f.s.y, y * f.c.x - z * f.s.x, y * f.s.x + z * f.c.x); +} + +// Ce que montre l'image là où le rayon (q, d) du modèle retombe sur le plan : la copie de l'image +// composée faite juste avant le curseur (t5), flous de confidentialité compris, lue au pixel de +// sortie de ce point, en linéaire. Seuls des pixels déjà floutés passent donc à travers le verre. +// L'œil est en (-mb.zw, persp) du repère caméra, le rayon du pixel `local` y va vers +// (local + src.xy, -persp) : on remonte de ce point du plan à son `local`, puis à son uv de sortie. +// Hors de l'image, l'écran sombre du studio. +float3 prism_screen(float3 q, float3 d, float3 nz, float hz, float3 tip, float unit, ModelFrame f) +{ + float denom = dot(d, nz); + if (denom > -1e-4) + { + return SCULPT_SCREEN * 0.2; + } + float3 g = q + d * ((hz - dot(q, nz)) / denom); + float3 w = plane_to_world(tip + unit * model_to_plane(g, f), f); + float k = 1.0 - w.z / src.z; + if (k < 1e-3) + { + return SCULPT_SCREEN * 0.2; + } + float2 uv = dst.xy + ((w.xy + mb.zw) / k - src.xy) / quad_px * dst.zw; + if (any(uv < 0.0) || any(uv > 1.0)) + { + return SCULPT_SCREEN * 0.2; + } + return pow(texDof.SampleLevel(samp, uv, 0.0).rgb, 2.2); +} + +// La couleur (sRGB, comme `model_shade`) du cristal au point `p` de la facette `h`, vu le long de +// `rd` : reflet du studio, l'enregistrement réfracté par canal (la dispersion) à travers les +// facettes et le fond plat, la lueur de la couleur de la facette dans l'art, ses plis en liseré. +float3 prism_shade(float3 p, float3 n, float3 rd, int h, float px, float3 l, float3 fill, float3 nz, + float hz, float3 tip, float unit, ModelFrame f) +{ + int shape = prism_shape(); + float4 r3 = PRISM_TRIS[4 * h + 3]; + float3 facet = float3(PRISM_TRIS[4 * h + 1].w, PRISM_TRIS[4 * h + 2].w, r3.w); + float F = prism_fresnel(saturate(-dot(rd, n)), PRISM_IOR); + float zb = -model_thick(); + float3 trans = 0.0; + [loop] for (int ch = 0; ch < 3; ch++) + { + // Le rouge plie le moins, le bleu le plus. FXC n'indexe pas un vecteur par une variable en + // écriture (il déroulerait la boucle) : le canal passe par un masque. + float3 mask = float3(ch == 0, ch == 1, ch == 2); + float ior = PRISM_IOR + PRISM_DISPERSION * (ch - 1); + float3 d = refract(rd, n, 1.0 / ior); + float3 pos = p; + float thr = 1.0; + float acc = 0.0; + [loop] for (int b = 0; b < PRISM_BOUNCES; b++) + { + int hh; + float th = prism_trace(pos, d, shape, hh); + float tb = d.z < -1e-6 ? (zb - pos.z) / d.z : 1e9; + if (tb <= th) + { + // Le fond plat : le rayon sort vers l'écran, ou s'y réfléchit tout entier. + float3 qb = pos + d * tb; + float3 d2 = refract(d, float3(0.0, 0.0, 1.0), ior); + if (dot(d2, d2) < 1e-8) + { + pos = qb; + d = reflect(d, float3(0.0, 0.0, 1.0)); + continue; + } + acc += thr * dot(prism_screen(qb, d2, nz, hz, tip, unit, f), mask); + thr = 0.0; + break; + } + if (hh < 0) + { + break; + } + pos += d * th; + float4 rn = PRISM_TRIS[4 * hh + 3]; + if (fmod(PRISM_TRIS[4 * hh].w, 2.0) > 0.5 && pos.z < 0.0) + { + // Une paroi sous le dessus du serti : le marine de son flanc intérieur. + acc += thr * dot(s_lin(0.03, 0.06, 0.37) * 0.35, mask); + thr = 0.0; + break; + } + float3 dout = refract(d, -rn.xyz, ior); + if (dot(dout, dout) < 1e-8) + { + d = reflect(d, -rn.xyz); + continue; + } + float fi = prism_fresnel(saturate(dot(dout, rn.xyz)), ior); + acc += thr * (1.0 - fi) * dot(model_env(dout, 0.05, l, fill), mask); + thr *= fi; + d = reflect(d, -rn.xyz); + } + trans += acc * mask; + } + float3 col = F * model_env(reflect(rd, n), 0.05, l, fill) + (1.0 - F) * trans + facet * PRISM_GLOW; + // Les plis de l'art : un liseré clair sur chaque arête réelle, fin d'un pixel. + float fold = 1.0 - smoothstep(0.2, 1.0, prism_edge_px(p, h, px)); + col = lerp(col, s_lin(0.96, 0.99, 1.0) * 1.3, PRISM_FOLD * fold * (0.35 + 0.65 * saturate(dot(n, l)))); + return model_tonemap(col); +} + // Les passes de la boucle unique de `cursor_model`, dans l'ordre. Chaque tour n'évalue le modèle // QU'UNE fois (`model_eval`) : FXC recopie une fonction à chaque appel, et un curseur sculpté est // assez gros pour qu'un appel par passe multiplie par neuf le temps de compilation de ps_main. @@ -1388,6 +2520,11 @@ float4 cursor_model(float2 local) 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; + // Prism Glow : le cristal est tracé au départ de chaque rayon ; `tc` est + // son point touché (1e9 : raté), `hc` sa facette. La marche du serti s'arrête à lui. + bool need_tc = stage == STAGE_MARCH; + float tc = 1e9; + int hc = -1; // Le point du plan derrière le pixel est à régler avant le prochain tour. bool plane_next = stage == STAGE_DONE; int k = 0; @@ -1418,6 +2555,11 @@ float4 cursor_model(float2 local) float4 acc = 0.0; [loop] for (int it = 0; it < 500; it++) { + if (need_tc) + { + need_tc = false; + tc = prism_primary(ro, ray, hc); + } if (plane_next) { // Le plan, là où le modèle ne couvre pas tout le pixel : ombre portée et ombre de @@ -1442,9 +2584,17 @@ float4 cursor_model(float2 local) { 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); + float3 c; + if (mat > PRISM_MAT - 0.5) + { + c = prism_shade(q, n, ray, hc, t_best / dlen, l, fill, nz, hz, tip, unit, f); + } + else + { + float3 tl = lamp - q; + float fall = SCULPT_LAMP_DIST * SCULPT_LAMP_DIST / dot(tl, tl); + c = model_shade(q, n, ray, normalize(tl), fall, sh, ao, mat, l, fill); + } acc += float4(c * cov_s, cov_s); } shaded++; @@ -1467,6 +2617,7 @@ float4 cursor_model(float2 local) t = max(tb.x, 0.0); best = 1e9; t_best = t; + need_tc = true; } } continue; @@ -1517,6 +2668,15 @@ float4 cursor_model(float2 local) } t += d * stride; k++; + if (!hit && t >= tc) + { + // Le pas dépasse le cristal : c'est lui que le rayon touche, avant le serti. + hit = true; + best = 0.0; + t_best = tc; + d_best = 0.0; + mat = PRISM_MAT; + } if (hit || t > tb.y || k == 96) { cov_s = saturate(1.0 - best); @@ -1545,6 +2705,27 @@ float4 cursor_model(float2 local) plane_next = true; } } + if (stage == STAGE_NORMAL && mat > PRISM_MAT - 0.5) + { + // Le cristal : la normale de sa facette, sans sondes ; ni occlusion ni ombre + // propre. Un bord réel à moins d'un pixel (le serti, un pli) appelle les trois + // rayons de plus, comme les bords intérieurs des autres modèles. + n = prism_normal(hc, ray); + k = 0; + if (shaded > 0) + { + stage = STAGE_SHADE; + } + else + { + if (prism_edge_px(q, hc, t_best / dlen) < 0.75) + { + sub = 3; + } + stage = STAGE_DONE; + plane_next = true; + } + } } } else if (stage == STAGE_NORMAL) @@ -1566,6 +2747,19 @@ float4 cursor_model(float2 local) { sub = 3; } + if (k == 0 && mat > 7.5) + { + // Le serti de Prism Glow au bord du cristal : son contour à moins d'un pixel. + int ps = prism_shape(); + if (ps >= 0) + { + float dout = prism_poly_dist(q.xy, PRISM_OUT_START[ps], PRISM_OUT_COUNT[ps]); + if (abs(dout) < 0.75 * t_best / dlen) + { + sub = 3; + } + } + } k++; if (k == 4) { diff --git a/crates/compositor/src/shaders.metal b/crates/compositor/src/shaders.metal index 01cce9358..6456c3ff7 100644 --- a/crates/compositor/src/shaders.metal +++ b/crates/compositor/src/shaders.metal @@ -545,7 +545,9 @@ inline float3 image_motion(float2 q, float time, float motion, float aspect) // ============ Curseur MODÉLISÉ (mode 15) ============ // Port ligne pour ligne de `cursor_model` (HLSL), dont les commentaires font foi ; seules // différences : `layer` et les textures arrivent en paramètres (le sprite en texture(2), son -// champ R16F en texture(4)), `lerp` s'écrit `mix`, `SampleLevel` s'écrit `sample(…, level(0.0))`, +// champ R16F en texture(4), la copie de l'image composée en texture(5) pour le cristal de Prism +// Glow), +// `lerp` s'écrit `mix`, `SampleLevel` s'écrit `sample(…, level(0.0))`, un `out` s'écrit `thread &`, // et `pow` veut un exposant du type de sa base. // Constantes : miroir exact de `frame_geometry.rs` (MODEL_*) et de `sculpt.rs` (SCULPT_*). constant float MODEL_BEVEL = 0.045; @@ -973,6 +975,927 @@ static float s_pixel_outline(float2 p, int shape) return d; } +// ---- Prism Glow : un cristal en MAILLAGE ---- (cf. HLSL) +// prism mesh: generated by design/cursors/prism-glow/model/export_compositor.py +constant int PRISM_TRI_START[2] = { 0, 31 }; +constant int PRISM_TRI_COUNT[2] = { 31, 130 }; +constant int PRISM_SIL_START[2] = { 0, 25 }; +constant int PRISM_SIL_COUNT[2] = { 25, 42 }; +constant int PRISM_OUT_START[2] = { 67, 76 }; +constant int PRISM_OUT_COUNT[2] = { 9, 34 }; +constant int PRISM_BOX_START[2] = { 0, 4 }; +constant int PRISM_BOX_COUNT[2] = { 4, 18 }; +constant float4 PRISM_TRIS[644] = { + float4(0.252301, 0.266258, -0.042945, 11.0), + float4(-0.221166, -0.203988, 0.0, 0.01033), + float4(-0.221166, -0.203988, 0.042945, 0.045182), + float4(0.677984, -0.735077, 0.0, 0.577584), + float4(0.252301, 0.266258, -0.042945, 3.0), + float4(-0.221166, -0.203988, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(0.677984, -0.735077, 0.0, 0.577584), + float4(0.031135, 0.06227, -0.042945, 7.0), + float4(0.004294, 0.419785, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(-0.999948, 0.01023, 0.0, 0.577584), + float4(0.169632, 0.356442, 0.115951, 14.0), + float4(0.082669, -0.090184, -0.115951, 0.590616), + float4(-0.138497, -0.294172, -0.115951, 0.737911), + float4(0.466385, -0.505659, 0.725806, 0.973449), + float4(0.035429, 0.482055, 0.0, 14.0), + float4(0.134202, -0.125614, 0.115951, 0.000305), + float4(-0.004294, -0.419785, 0.0, 0.938689), + float4(-0.650191, 0.006652, 0.759742, 0.854994), + float4(0.031135, 0.06227, -0.042945, 9.0), + float4(0.004294, 0.419785, 0.0, 0.01033), + float4(0.004294, 0.419785, 0.042945, 0.045182), + float4(-0.999948, 0.01023, 0.0, 0.577584), + float4(0.169632, 0.356442, 0.115951, 14.0), + float4(0.159969, 0.080521, -0.045092, 0.381323), + float4(0.082669, -0.090184, -0.115951, 0.056123), + float4(0.461407, -0.510214, 0.725799, 0.973449), + float4(0.541104, 0.544325, -0.042945, 9.0), + float4(-0.288804, -0.278068, 0.0, 0.01033), + float4(-0.288804, -0.278068, 0.042945, 0.045182), + float4(0.69359, -0.72037, 0.0, 0.577584), + float4(0.329601, 0.436963, 0.070859, 14.0), + float4(0.211503, 0.107362, -0.070859, 0.545724), + float4(-0.077301, -0.170706, -0.070859, 0.064805), + float4(0.49567, -0.514808, 0.699488, 0.973449), + float4(0.268405, 0.533589, 0.081595, 14.0), + float4(0.061196, -0.096626, -0.010736, 0.000916), + float4(-0.098773, -0.177147, 0.034356, 0.046662), + float4(0.247834, 0.049458, 0.967539, 0.904662), + float4(0.541104, 0.544325, -0.042945, 7.0), + float4(-0.288804, -0.278068, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(0.69359, -0.72037, 0.0, 0.577584), + float4(0.199693, 0.595859, 0.0, 14.0), + float4(0.068712, -0.06227, 0.081595, 0.0), + float4(-0.030061, -0.239417, 0.115951, 0.01445), + float4(-0.504461, 0.426837, 0.750553, 0.254154), + float4(0.268405, 0.533589, 0.081595, 14.0), + float4(0.272699, 0.010736, -0.081595, 0.0), + float4(0.061196, -0.096626, -0.010736, 0.025193), + float4(0.283244, 0.073137, 0.956255, 0.527108), + float4(0.035429, 0.482055, 0.0, 14.0), + float4(0.0, 0.258742, 0.0, 0.0), + float4(0.134202, -0.125614, 0.115951, 0.473533), + float4(-0.653777, 0.0, 0.756687, 0.964691), + float4(0.359663, 0.559356, 0.0, 14.0), + float4(0.181442, -0.015031, 0.0, 0.006043), + float4(-0.091258, -0.025767, 0.081595, 0.076188), + float4(0.076465, 0.923048, 0.37701, 0.964691), + float4(0.035429, 0.482055, -0.042945, 3.0), + float4(0.0, 0.258742, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(-1.0, 0.0, 0.0, 0.577584), + float4(0.035429, 0.482055, -0.042945, 11.0), + float4(0.0, 0.258742, 0.0, 0.01033), + float4(0.0, 0.258742, 0.042945, 0.045182), + float4(-1.0, 0.0, 0.0, 0.577584), + float4(0.035429, 0.740798, -0.042945, 7.0), + float4(0.164264, -0.144939, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(0.661622, 0.749838, 0.0, 0.577584), + float4(0.035429, 0.740798, 0.0, 14.0), + float4(0.164264, -0.144939, 0.0, 0.0), + float4(0.134202, -0.384356, 0.115951, 0.010956), + float4(0.332572, 0.376915, 0.864483, 0.215854), + float4(0.035429, 0.740798, -0.042945, 11.0), + float4(0.164264, -0.144939, 0.0, 0.01033), + float4(0.164264, -0.144939, 0.042945, 0.045182), + float4(0.661622, 0.749838, 0.0, 0.577584), + float4(0.199693, 0.595859, -0.042945, 7.0), + float4(0.105215, 0.22546, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(-0.906183, 0.422886, 0.0, 0.577584), + float4(0.199693, 0.595859, 0.0, 14.0), + float4(0.105215, 0.22546, 0.0, 0.0), + float4(0.068712, -0.06227, 0.081595, 0.015991), + float4(-0.613882, 0.286478, 0.735581, 0.473533), + float4(0.199693, 0.595859, -0.042945, 11.0), + float4(0.105215, 0.22546, 0.0, 0.01033), + float4(0.105215, 0.22546, 0.042945, 0.045182), + float4(-0.906183, 0.422886, 0.0, 0.577584), + float4(0.304908, 0.821319, 0.0, 14.0), + float4(0.128834, -0.067638, 0.0, 0.0), + float4(-0.036503, -0.28773, 0.081595, 0.254154), + float4(0.133686, 0.254641, 0.957751, 0.973449), + float4(0.304908, 0.821319, -0.042945, 7.0), + float4(0.128834, -0.067638, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(0.464834, 0.885398, 0.0, 0.577584), + float4(0.433742, 0.753681, 0.0, 14.0), + float4(-0.07408, -0.194325, 0.0, 0.0), + float4(-0.165337, -0.220092, 0.081595, 0.964691), + float4(0.683362, -0.260508, 0.682021, 0.964691), + float4(0.433742, 0.753681, -0.042945, 11.0), + float4(-0.07408, -0.194325, 0.0, 0.01033), + float4(-0.07408, -0.194325, 0.042945, 0.045182), + float4(0.934406, -0.35621, 0.0, 0.577584), + float4(0.304908, 0.821319, -0.042945, 11.0), + float4(0.128834, -0.067638, 0.0, 0.01033), + float4(0.128834, -0.067638, 0.042945, 0.045182), + float4(0.464834, 0.885398, 0.0, 0.577584), + float4(0.433742, 0.753681, -0.042945, 7.0), + float4(-0.07408, -0.194325, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(0.934406, -0.35621, 0.0, 0.577584), + float4(0.359663, 0.559356, -0.042945, 7.0), + float4(0.181442, -0.015031, 0.042945, 0.01033), + float4(0.0, 0.0, 0.042945, 0.045182), + float4(0.082557, 0.996586, 0.0, 0.577584), + float4(0.359663, 0.559356, -0.042945, 11.0), + float4(0.181442, -0.015031, 0.0, 0.01033), + float4(0.181442, -0.015031, 0.042945, 0.045182), + float4(0.082557, 0.996586, 0.0, 0.577584), + float4(0.044345, 0.043263, -0.043263, 11.0), + float4(-0.043263, -0.005408, 0.0, 0.01033), + float4(-0.043263, -0.005408, 0.043263, 0.045182), + float4(0.124035, -0.992278, 0.0, 0.577584), + float4(0.044345, 0.043263, -0.043263, 7.0), + float4(-0.043263, -0.005408, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.124035, -0.992278, 0.0, 0.577584), + float4(0.001082, 0.037855, -0.043263, 7.0), + float4(-0.044345, 0.040019, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.669964, -0.742393, 0.0, 0.577584), + float4(0.044345, 0.043263, 0.0, 10.0), + float4(-0.043263, -0.005408, 0.0, 0.000305), + float4(-0.040019, 0.030284, 0.049753, 0.964691), + float4(0.101434, -0.811469, 0.575525, 0.973449), + float4(0.004326, 0.073548, 0.049753, 14.0), + float4(-0.003245, -0.035692, -0.049753, 0.0), + float4(-0.04759, 0.004326, -0.049753, 0.955978), + float4(-0.580475, -0.643229, 0.499304, 0.896269), + float4(0.001082, 0.037855, -0.043263, 11.0), + float4(-0.044345, 0.040019, 0.0, 0.01033), + float4(-0.044345, 0.040019, 0.043263, 0.045182), + float4(-0.669964, -0.742393, 0.0, 0.577584), + float4(-0.031366, 0.229295, 0.058405, 14.0), + float4(0.035692, -0.155748, -0.008653, 0.000305), + float4(-0.011897, -0.151422, -0.058405, 0.955978), + float4(-0.716533, -0.20131, 0.667874, 0.904662), + float4(-0.043263, 0.077874, -0.043263, 7.0), + float4(-0.001082, 0.154666, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.999976, -0.006993, 0.0, 0.577584), + float4(-0.031366, 0.229295, 0.058405, 14.0), + float4(0.089771, -0.137361, -0.02055, 0.445197), + float4(0.035692, -0.155748, -0.008653, 0.973449), + float4(0.216369, -0.004654, 0.976301, 0.973449), + float4(-0.031366, 0.229295, 0.058405, 14.0), + float4(-0.011897, -0.151422, -0.058405, 0.000305), + float4(-0.012979, 0.003245, -0.058405, 0.955978), + float4(-0.976245, -0.006827, 0.216564, 0.904662), + float4(-0.043263, 0.077874, -0.043263, 9.0), + float4(-0.001082, 0.154666, 0.0, 0.01033), + float4(-0.001082, 0.154666, 0.043263, 0.045182), + float4(-0.999976, -0.006993, 0.0, 0.577584), + float4(-0.045426, 0.564586, 0.052998, 14.0), + float4(0.014061, -0.335291, 0.005408, 0.520989), + float4(0.001082, -0.332046, -0.052998, 0.088655), + float4(-0.975941, -0.037462, 0.214794, 0.973449), + float4(-0.044345, 0.23254, 0.0, 3.0), + float4(0.0, 0.0, -0.043263, 0.01033), + float4(-0.001082, 0.332046, -0.043263, 0.045182), + float4(-0.999995, -0.003257, 0.0, 0.577584), + float4(-0.163319, 0.380717, -0.043263, 7.0), + float4(-0.060569, 0.02704, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.407651, -0.913138, 0.0, 0.577584), + float4(-0.094098, 0.431551, -0.043263, 11.0), + float4(-0.069221, -0.050834, 0.0, 0.01033), + float4(-0.069221, -0.050834, 0.043263, 0.045182), + float4(0.591909, -0.806004, 0.0, 0.577584), + float4(-0.163319, 0.380717, -0.043263, 11.0), + float4(-0.060569, 0.02704, 0.0, 0.01033), + float4(-0.060569, 0.02704, 0.043263, 0.045182), + float4(-0.407651, -0.913138, 0.0, 0.577584), + float4(0.075711, 0.08869, -0.043263, 11.0), + float4(-0.031366, -0.045426, 0.0, 0.01033), + float4(-0.031366, -0.045426, 0.043263, 0.045182), + float4(0.822897, -0.568191, 0.0, 0.577584), + float4(0.044345, 0.043263, 0.0, 6.0), + float4(-0.040019, 0.030284, 0.049753, 0.000305), + float4(0.014061, 0.048671, 0.037855, 0.964691), + float4(0.365607, -0.634949, 0.680566, 0.973449), + float4(0.075711, 0.08869, -0.043263, 7.0), + float4(-0.031366, -0.045426, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.822897, -0.568191, 0.0, 0.577584), + float4(0.058405, 0.091934, 0.037855, 14.0), + float4(0.017305, -0.003245, -0.037855, 0.0), + float4(-0.014061, -0.048671, -0.037855, 0.473533), + float4(0.757369, -0.522945, 0.39105, 0.904662), + float4(-0.031366, 0.229295, 0.058405, 14.0), + float4(0.107077, -0.140606, -0.058405, 0.0), + float4(0.089771, -0.137361, -0.02055, 0.376257), + float4(0.812052, 0.481346, 0.329966, 0.830777), + float4(0.077874, 0.407756, -0.043263, 11.0), + float4(-0.002163, -0.319067, 0.0, 0.01033), + float4(-0.002163, -0.319067, 0.043263, 0.045182), + float4(0.999977, -0.006779, 0.0, 0.577584), + float4(-0.031366, 0.229295, 0.058405, 14.0), + float4(0.10924, 0.178461, -0.058405, 0.0), + float4(0.107077, -0.140606, -0.058405, 0.376257), + float4(0.475583, -0.003224, 0.879665, 0.830777), + float4(0.147095, 0.272559, -0.043263, 7.0), + float4(-0.035692, 0.034611, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.696146, -0.7179, 0.0, 0.577584), + float4(0.147095, 0.272559, -0.043263, 11.0), + float4(-0.035692, 0.034611, 0.0, 0.01033), + float4(-0.035692, 0.034611, 0.043263, 0.045182), + float4(-0.696146, -0.7179, 0.0, 0.577584), + float4(0.144932, 0.307169, 0.049753, 14.0), + float4(0.002163, -0.034611, -0.049753, 0.0), + float4(-0.033529, 0.0, -0.049753, 0.9131), + float4(-0.630237, -0.649931, 0.424725, 0.904662), + float4(0.183869, 0.317985, 0.037855, 6.0), + float4(-0.036774, -0.045426, -0.037855, 0.0), + float4(-0.038937, -0.010816, 0.011897, 0.806958), + float4(0.374433, -0.753477, 0.540438, 0.854994), + float4(0.077874, 0.407756, -0.043263, 7.0), + float4(-0.002163, -0.319067, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.999977, -0.006779, 0.0, 0.577584), + float4(0.111403, 0.307169, -0.043263, 7.0), + float4(-0.002163, 0.131953, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.999866, -0.016391, 0.0, 0.577584), + float4(0.10924, 0.439122, 0.0, 6.0), + float4(0.035692, -0.131953, 0.049753, 0.18117), + float4(0.002163, -0.131953, 0.0, 0.955978), + float4(-0.82919, -0.013593, 0.558802, 0.964691), + float4(0.128708, 0.463999, 0.063813, 14.0), + float4(0.055161, -0.146014, -0.025958, 0.246204), + float4(0.016224, -0.156829, -0.014061, 0.964691), + float4(0.305397, -0.053644, 0.950713, 0.973449), + float4(0.111403, 0.307169, -0.043263, 11.0), + float4(-0.002163, 0.131953, 0.0, 0.01033), + float4(-0.002163, 0.131953, 0.043263, 0.045182), + float4(-0.999866, -0.016391, 0.0, 0.577584), + float4(0.189277, 0.28013, -0.043263, 11.0), + float4(-0.042182, -0.007571, 0.0, 0.01033), + float4(-0.042182, -0.007571, 0.043263, 0.045182), + float4(0.176664, -0.984271, 0.0, 0.577584), + float4(0.183869, 0.317985, 0.037855, 10.0), + float4(0.005408, -0.037855, -0.037855, 0.0), + float4(-0.036774, -0.045426, -0.037855, 0.806958), + float4(0.124328, -0.692684, 0.710445, 0.854994), + float4(0.189277, 0.28013, -0.043263, 7.0), + float4(-0.042182, -0.007571, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.176664, -0.984271, 0.0, 0.577584), + float4(0.183869, 0.317985, 0.037855, 14.0), + float4(0.02704, -0.004326, -0.037855, 0.0), + float4(0.005408, -0.037855, -0.037855, 0.514916), + float4(0.70062, -0.452013, 0.552101, 0.846876), + float4(0.210908, 0.313659, -0.043263, 11.0), + float4(-0.021632, -0.033529, 0.0, 0.01033), + float4(-0.021632, -0.033529, 0.043263, 0.045182), + float4(0.840297, -0.542127, 0.0, 0.577584), + float4(0.210908, 0.445612, 0.0, 10.0), + float4(0.0, -0.131953, 0.0, 0.0), + float4(-0.02704, -0.127627, 0.037855, 0.439658), + float4(0.813734, 0.0, 0.581238, 0.871366), + float4(0.210908, 0.313659, -0.043263, 7.0), + float4(-0.021632, -0.033529, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.840297, -0.542127, 0.0, 0.577584), + float4(0.210908, 0.445612, -0.043263, 11.0), + float4(0.0, -0.131953, 0.0, 0.01033), + float4(0.0, -0.131953, 0.043263, 0.045182), + float4(1.0, 0.0, 0.0, 0.577584), + float4(0.241193, 0.343943, -0.043263, 7.0), + float4(-0.002163, 0.107077, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.999796, -0.020198, 0.0, 0.577584), + float4(0.276885, 0.311496, -0.043263, 11.0), + float4(-0.035692, 0.032447, 0.0, 0.01033), + float4(-0.035692, 0.032447, 0.043263, 0.045182), + float4(-0.672673, -0.73994, 0.0, 0.577584), + float4(0.23903, 0.45102, 0.0, 14.0), + float4(0.042182, -0.108158, 0.046508, 0.000305), + float4(0.002163, -0.107077, 0.0, 0.846876), + float4(-0.758098, -0.015315, 0.651961, 0.896269), + float4(0.276885, 0.311496, -0.043263, 7.0), + float4(-0.035692, 0.032447, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.672673, -0.73994, 0.0, 0.577584), + float4(0.281211, 0.342862, 0.046508, 14.0), + float4(-0.004326, -0.031366, -0.046508, 0.0), + float4(-0.040019, 0.001082, -0.046508, 0.752941), + float4(-0.58651, -0.645161, 0.489667, 0.846876), + float4(0.246601, 0.459672, 0.06165, 14.0), + float4(0.070303, -0.101669, -0.024876, 0.262257), + float4(0.034611, -0.116811, -0.015142, 0.973449), + float4(0.27928, -0.041605, 0.959308, 0.973449), + float4(0.319067, 0.319067, -0.043263, 11.0), + float4(-0.042182, -0.007571, 0.0, 0.01033), + float4(-0.042182, -0.007571, 0.043263, 0.045182), + float4(0.176664, -0.984271, 0.0, 0.577584), + float4(0.319067, 0.319067, 0.0, 10.0), + float4(-0.042182, -0.007571, 0.0, 0.0), + float4(-0.037855, 0.023795, 0.046508, 0.896269), + float4(0.1483, -0.826245, 0.54344, 0.896269), + float4(0.319067, 0.319067, -0.043263, 7.0), + float4(-0.042182, -0.007571, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.176664, -0.984271, 0.0, 0.577584), + float4(0.319067, 0.319067, 0.0, 6.0), + float4(-0.037855, 0.023795, 0.046508, 0.0), + float4(-0.002163, 0.038937, 0.036774, 0.896269), + float4(0.437906, -0.604311, 0.665618, 0.896269), + float4(0.316904, 0.358004, 0.036774, 14.0), + float4(0.024876, -0.004326, -0.036774, 0.0), + float4(0.002163, -0.038937, -0.036774, 0.617212), + float4(0.707338, -0.464191, 0.533104, 0.921584), + float4(0.34178, 0.353677, -0.043263, 11.0), + float4(-0.022713, -0.034611, 0.0, 0.01033), + float4(-0.022713, -0.034611, 0.043263, 0.045182), + float4(0.836048, -0.548657, 0.0, 0.577584), + float4(0.340698, 0.474815, 0.0, 14.0), + float4(0.001082, -0.121137, 0.0, 0.0), + float4(-0.023795, -0.116811, 0.036774, 0.473533), + float4(0.828664, 0.007399, 0.559697, 0.930114), + float4(0.34178, 0.353677, -0.043263, 7.0), + float4(-0.022713, -0.034611, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.836048, -0.548657, 0.0, 0.577584), + float4(0.340698, 0.474815, -0.043263, 11.0), + float4(0.001082, -0.121137, 0.0, 0.01033), + float4(0.001082, -0.121137, 0.043263, 0.045182), + float4(0.99996, 0.008928, 0.0, 0.577584), + float4(0.411001, 0.351514, -0.043263, 11.0), + float4(-0.034611, 0.030284, 0.0, 0.01033), + float4(-0.034611, 0.030284, 0.043263, 0.045182), + float4(-0.658505, -0.752577, 0.0, 0.577584), + float4(0.411001, 0.351514, -0.043263, 7.0), + float4(-0.034611, 0.030284, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.658505, -0.752577, 0.0, 0.577584), + float4(0.412083, 0.38288, 0.043263, 14.0), + float4(-0.001082, -0.031366, -0.043263, 0.0), + float4(-0.035692, -0.001082, -0.043263, 0.745403), + float4(-0.574039, -0.656045, 0.489984, 0.854994), + float4(0.45102, 0.359085, -0.043263, 11.0), + float4(-0.040019, -0.007571, 0.0, 0.01033), + float4(-0.040019, -0.007571, 0.043263, 0.045182), + float4(0.185892, -0.98257, 0.0, 0.577584), + float4(0.45102, 0.359085, 0.0, 10.0), + float4(-0.040019, -0.007571, 0.0, 0.000305), + float4(-0.038937, 0.023795, 0.043263, 0.879622), + float4(0.151736, -0.802035, 0.577682, 0.9131), + float4(0.45102, 0.359085, -0.043263, 7.0), + float4(-0.040019, -0.007571, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.185892, -0.98257, 0.0, 0.577584), + float4(0.45102, 0.359085, 0.0, 6.0), + float4(-0.038937, 0.023795, 0.043263, 0.000305), + float4(0.003245, 0.0411, 0.032447, 0.879622), + float4(0.417852, -0.583049, 0.696744, 0.9131), + float4(0.473733, 0.391533, -0.043263, 11.0), + float4(-0.022713, -0.032447, 0.0, 0.01033), + float4(-0.022713, -0.032447, 0.043263, 0.045182), + float4(0.819232, -0.573462, 0.0, 0.577584), + float4(0.454264, 0.400185, 0.032447, 14.0), + float4(0.019468, -0.008653, -0.032447, 0.0), + float4(-0.003245, -0.0411, -0.032447, 0.527108), + float4(0.688617, -0.482032, 0.541712, 0.887922), + float4(0.473733, 0.391533, -0.043263, 7.0), + float4(-0.022713, -0.032447, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.819232, -0.573462, 0.0, 0.577584), + float4(-0.223888, 0.407756, -0.043263, 7.0), + float4(0.002163, 0.06814, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.999496, 0.03173, 0.0, 0.577584), + float4(-0.223888, 0.407756, -0.043263, 11.0), + float4(0.002163, 0.06814, 0.0, 0.01033), + float4(0.002163, 0.06814, 0.043263, 0.045182), + float4(-0.999496, 0.03173, 0.0, 0.577584), + float4(-0.167645, 0.416409, 0.046508, 14.0), + float4(-0.056242, -0.008653, -0.046508, 0.000305), + float4(-0.054079, 0.059487, -0.046508, 0.955978), + float4(-0.638985, 0.020285, 0.768952, 0.973449), + float4(-0.167645, 0.416409, 0.046508, 14.0), + float4(0.004326, -0.035692, -0.046508, 0.000305), + float4(-0.056242, -0.008653, -0.046508, 0.955978), + float4(-0.339782, -0.761112, 0.552501, 0.973449), + float4(-0.221724, 0.475896, 0.0, 14.0), + float4(0.139524, 0.055161, 0.043263, 0.520989), + float4(0.054079, -0.059487, 0.046508, 0.973449), + float4(-0.39307, 0.317373, 0.863001, 0.973449), + float4(-0.167645, 0.416409, 0.046508, 6.0), + float4(0.073548, 0.015142, -0.046508, 0.0), + float4(0.004326, -0.035692, -0.046508, 0.768154), + float4(0.490917, -0.668482, 0.558688, 0.871366), + float4(-0.094098, 0.431551, -0.043263, 7.0), + float4(-0.069221, -0.050834, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.591909, -0.806004, 0.0, 0.577584), + float4(-0.167645, 0.416409, 0.046508, 10.0), + float4(0.085445, 0.114648, -0.003245, 0.0), + float4(0.073548, 0.015142, -0.046508, 0.768154), + float4(0.548356, -0.38771, 0.740936, 0.871366), + float4(-0.0822, 0.531057, -0.043263, 11.0), + float4(-0.011897, -0.099506, 0.0, 0.01033), + float4(-0.011897, -0.099506, 0.043263, 0.045182), + float4(0.992928, -0.11872, 0.0, 0.577584), + float4(-0.044345, 0.23254, 0.0, 7.0), + float4(-0.001082, 0.332046, -0.043263, 0.01033), + float4(-0.001082, 0.332046, 0.052998, 0.045182), + float4(-0.999995, -0.003257, 0.0, 0.577584), + float4(-0.045426, 0.564586, 0.052998, 14.0), + float4(0.1233, -0.156829, -0.052998, 0.520989), + float4(0.014061, -0.335291, 0.005408, 0.088655), + float4(0.429355, 0.032562, 0.902549, 0.973449), + float4(0.10924, 0.439122, 0.0, 14.0), + float4(-0.031366, -0.031366, 0.0, 0.132876), + float4(-0.154666, 0.125464, 0.052998, 0.028428), + float4(0.182761, -0.182761, 0.966021, 0.955978), + float4(0.10924, 0.439122, -0.043263, 11.0), + float4(-0.031366, -0.031366, 0.0, 0.01033), + float4(-0.031366, -0.031366, 0.043263, 0.045182), + float4(0.707107, -0.707107, 0.0, 0.577584), + float4(0.10924, 0.439122, -0.043263, 7.0), + float4(-0.031366, -0.031366, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.707107, -0.707107, 0.0, 0.577584), + float4(0.10924, 0.439122, 0.0, 10.0), + float4(0.019468, 0.024876, 0.063813, 0.18117), + float4(0.035692, -0.131953, 0.049753, 0.955978), + float4(-0.933936, -0.126584, 0.334276, 0.964691), + float4(0.128708, 0.463999, 0.063813, 14.0), + float4(0.050834, 0.002163, -0.005408, 0.0), + float4(0.0822, -0.018387, -0.063813, 0.439658), + float4(0.07859, -0.92643, 0.368172, 0.871366), + float4(-0.221724, 0.475896, -0.043263, 7.0), + float4(0.122219, 0.207664, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.861819, 0.507216, 0.0, 0.577584), + float4(-0.221724, 0.475896, -0.043263, 9.0), + float4(0.122219, 0.207664, 0.0, 0.01033), + float4(0.122219, 0.207664, 0.043263, 0.045182), + float4(-0.861819, 0.507216, 0.0, 0.577584), + float4(-0.099506, 0.68356, 0.0, 14.0), + float4(0.017305, -0.152503, 0.043263, 0.0), + float4(-0.122219, -0.207664, 0.0, 0.723049), + float4(-0.365879, 0.215335, 0.905408, 0.854994), + float4(-0.0822, 0.531057, -0.043263, 7.0), + float4(-0.011897, -0.099506, 0.043263, 0.01033), + float4(0.0, 0.0, 0.086527, 0.045182), + float4(0.992928, -0.11872, 0.0, 0.577584), + float4(-0.099506, 0.68356, 0.0, 14.0), + float4(0.054079, -0.118974, 0.052998, 0.0), + float4(0.017305, -0.152503, 0.043263, 0.514916), + float4(-0.41958, 0.203352, 0.884647, 0.863157), + float4(-0.099506, 0.68356, -0.043263, 3.0), + float4(0.086527, 0.149258, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.86514, 0.501531, 0.0, 0.577584), + float4(-0.045426, 0.564586, -0.043263, 11.0), + float4(-0.036774, -0.033529, 0.0, 0.01033), + float4(-0.036774, -0.033529, 0.086527, 0.045182), + float4(0.673754, -0.738956, 0.0, 0.577584), + float4(-0.045426, 0.564586, -0.043263, 7.0), + float4(-0.036774, -0.033529, 0.086527, 0.01033), + float4(0.0, 0.0, 0.096261, 0.045182), + float4(0.673754, -0.738956, 0.0, 0.577584), + float4(-0.099506, 0.68356, 0.0, 14.0), + float4(0.086527, 0.149258, 0.0, 0.0), + float4(0.054079, -0.118974, 0.052998, 0.262257), + float4(-0.385563, 0.223515, 0.8952, 0.9131), + float4(-0.099506, 0.68356, -0.043263, 11.0), + float4(0.086527, 0.149258, 0.0, 0.01033), + float4(0.086527, 0.149258, 0.043263, 0.045182), + float4(-0.86514, 0.501531, 0.0, 0.577584), + float4(-0.012979, 0.832818, 0.0, 14.0), + float4(0.214153, -0.194685, 0.0822, 0.000305), + float4(-0.032447, -0.268232, 0.052998, 0.930114), + float4(-0.176863, 0.211325, 0.961281, 0.973449), + float4(0.128708, 0.463999, 0.063813, 14.0), + float4(-0.019468, -0.024876, -0.063813, 0.168276), + float4(-0.174135, 0.100587, -0.010816, 0.038209), + float4(-0.469238, -0.764881, 0.441331, 0.955978), + float4(0.201174, 0.638133, 0.0822, 14.0), + float4(-0.072466, -0.174135, -0.018387, 0.18117), + float4(-0.246601, -0.073548, -0.029203, 0.082277), + float4(-0.098561, -0.063844, 0.993081, 0.973449), + float4(-0.012979, 0.832818, -0.043263, 7.0), + float4(0.339617, -0.002163, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.006369, 0.99998, 0.0, 0.577584), + float4(0.179543, 0.466162, 0.058405, 10.0), + float4(-0.050834, -0.002163, 0.005408, 0.000305), + float4(0.021632, 0.171972, 0.023795, 0.022171), + float4(0.110896, -0.149888, 0.982464, 0.381323), + float4(-0.012979, 0.832818, 0.0, 14.0), + float4(0.339617, -0.002163, 0.0, 0.0), + float4(0.214153, -0.194685, 0.0822, 0.027314), + float4(0.002492, 0.391296, 0.920261, 0.49102), + float4(0.183869, 0.317985, 0.037855, 10.0), + float4(-0.055161, 0.146014, 0.025958, 0.0), + float4(0.02704, 0.127627, -0.037855, 0.439658), + float4(0.623841, 0.097823, 0.775405, 0.871366), + float4(0.210908, 0.445612, -0.043263, 7.0), + float4(0.0, -0.131953, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(1.0, 0.0, 0.0, 0.577584), + float4(0.210908, 0.445612, 0.0, 10.0), + float4(-0.031366, 0.02055, 0.058405, 0.000305), + float4(0.035692, 0.014061, 0.06165, 0.022171), + float4(-0.105864, -0.954447, 0.278971, 0.381323), + float4(0.23903, 0.45102, -0.043263, 11.0), + float4(-0.028121, -0.005408, 0.0, 0.01033), + float4(-0.028121, -0.005408, 0.043263, 0.045182), + float4(0.188847, -0.982006, 0.0, 0.577584), + float4(0.23903, 0.45102, -0.043263, 7.0), + float4(-0.028121, -0.005408, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.188847, -0.982006, 0.0, 0.577584), + float4(0.246601, 0.459672, 0.06165, 11.0), + float4(-0.007571, -0.008653, -0.06165, 0.000305), + float4(-0.035692, -0.014061, -0.06165, 0.022171), + float4(0.187618, -0.975617, 0.113888, 0.381323), + float4(0.241193, 0.343943, -0.043263, 11.0), + float4(-0.002163, 0.107077, 0.0, 0.01033), + float4(-0.002163, 0.107077, 0.043263, 0.045182), + float4(-0.999796, -0.020198, 0.0, 0.577584), + float4(0.23903, 0.45102, 0.0, 15.0), + float4(0.007571, 0.008653, 0.06165, 0.000305), + float4(0.042182, -0.108158, 0.046508, 0.846876), + float4(-0.94107, -0.299262, 0.157572, 0.896269), + float4(0.316904, 0.358004, 0.036774, 14.0), + float4(-0.070303, 0.101669, 0.024876, 0.0), + float4(0.023795, 0.116811, -0.036774, 0.473533), + float4(0.523424, 0.157027, 0.837478, 0.930114), + float4(0.340698, 0.474815, -0.043263, 7.0), + float4(0.001082, -0.121137, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.99996, 0.008928, 0.0, 0.577584), + float4(0.376391, 0.381799, -0.043263, 11.0), + float4(-0.001082, 0.107077, 0.0, 0.01033), + float4(-0.001082, 0.107077, 0.043263, 0.045182), + float4(-0.999949, -0.0101, 0.0, 0.577584), + float4(0.376391, 0.381799, -0.043263, 7.0), + float4(-0.001082, 0.107077, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(-0.999949, -0.0101, 0.0, 0.577584), + float4(0.375309, 0.488875, 0.0, 14.0), + float4(0.036774, -0.105995, 0.043263, 0.000305), + float4(0.001082, -0.107077, 0.0, 0.879622), + float4(-0.771254, -0.00779, 0.63648, 0.896269), + float4(0.375309, 0.488875, 0.0, 14.0), + float4(0.078955, -0.08869, 0.032447, 0.223224), + float4(0.036774, -0.105995, 0.043263, 0.964691), + float4(0.071225, 0.39802, 0.914608, 0.973449), + float4(0.473733, 0.603523, 0.0, 14.0), + float4(0.0, -0.21199, 0.0, 0.158953), + float4(-0.019468, -0.203337, 0.032447, 0.030716), + float4(0.857493, 0.0, 0.514496, 0.964691), + float4(0.473733, 0.603523, -0.043263, 11.0), + float4(0.0, -0.21199, 0.0, 0.01033), + float4(0.0, -0.21199, 0.043263, 0.045182), + float4(1.0, 0.0, 0.0, 0.577584), + float4(0.276885, 0.484549, 0.058405, 10.0), + float4(-0.030284, -0.024876, 0.003245, 0.000305), + float4(-0.097342, -0.018387, 0.0, 0.022171), + float4(-0.03153, 0.166923, 0.985466, 0.381323), + float4(0.246601, 0.459672, 0.06165, 14.0), + float4(0.030284, 0.024876, -0.003245, 0.0), + float4(0.094098, 0.015142, -0.06165, 0.473533), + float4(0.518869, -0.545855, 0.657888, 0.930114), + float4(0.375309, 0.488875, -0.043263, 11.0), + float4(-0.034611, -0.014061, 0.0, 0.01033), + float4(-0.034611, -0.014061, 0.043263, 0.045182), + float4(0.376377, -0.926467, 0.0, 0.577584), + float4(0.340698, 0.474815, 0.0, 14.0), + float4(-0.063813, 0.009734, 0.058405, 0.000305), + float4(0.034611, 0.014061, 0.0, 0.022171), + float4(0.3276, -0.806402, 0.492334, 0.381323), + float4(0.375309, 0.488875, -0.043263, 7.0), + float4(-0.034611, -0.014061, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.376377, -0.926467, 0.0, 0.577584), + float4(0.454264, 0.400185, 0.032447, 12.0), + float4(-0.078955, 0.08869, -0.032447, 0.158953), + float4(0.019468, 0.203337, -0.032447, 0.030716), + float4(-0.201684, 0.173144, 0.964025, 0.964691), + float4(0.201174, 0.638133, 0.0822, 8.0), + float4(0.075711, -0.153585, -0.023795, 0.000305), + float4(-0.021632, -0.171972, -0.023795, 0.022171), + float4(0.026497, -0.140281, 0.989757, 0.381323), + float4(0.473733, 0.603523, -0.043263, 7.0), + float4(0.0, -0.21199, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(1.0, 0.0, 0.0, 0.577584), + float4(0.276885, 0.484549, 0.058405, 14.0), + float4(-0.075711, 0.153585, 0.023795, 0.000305), + float4(0.098424, 0.004326, -0.058405, 0.022171), + float4(0.50316, 0.115345, 0.856461, 0.381323), + float4(0.375309, 0.488875, 0.0, 10.0), + float4(0.090853, 0.12979, 0.0, 0.158953), + float4(0.098424, 0.114648, 0.0, 0.030716), + float4(0.0, 0.0, 1.0, 0.964691), + float4(0.466162, 0.618665, -0.043263, 11.0), + float4(0.007571, -0.015142, 0.0, 0.01033), + float4(0.007571, -0.015142, 0.043263, 0.045182), + float4(0.894427, 0.447214, 0.0, 0.577584), + float4(0.466162, 0.618665, -0.043263, 3.0), + float4(0.007571, -0.015142, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.894427, 0.447214, 0.0, 0.577584), + float4(0.326638, 0.830655, 0.0, 14.0), + float4(0.139524, -0.21199, 0.0, 0.0), + float4(0.048671, -0.34178, 0.0, 0.015213), + float4(0.0, 0.0, 1.0, 0.428687), + float4(0.326638, 0.830655, 0.0, 14.0), + float4(0.048671, -0.34178, 0.0, 0.007492), + float4(-0.125464, -0.192522, 0.0822, 0.054475), + float4(0.472493, 0.067285, 0.878762, 0.964691), + float4(0.326638, 0.830655, -0.043263, 9.0), + float4(0.139524, -0.21199, 0.0, 0.01033), + float4(0.139524, -0.21199, 0.043263, 0.045182), + float4(0.835314, 0.549773, 0.0, 0.577584), + float4(0.326638, 0.830655, -0.043263, 7.0), + float4(0.139524, -0.21199, 0.043263, 0.01033), + float4(0.0, 0.0, 0.043263, 0.045182), + float4(0.835314, 0.549773, 0.0, 0.577584), + float4(-0.012979, 0.832818, -0.043263, 11.0), + float4(0.339617, -0.002163, 0.0, 0.01033), + float4(0.339617, -0.002163, 0.043263, 0.045182), + float4(0.006369, 0.99998, 0.0, 0.577584) +}; +constant float4 PRISM_BOXES[44] = { + float4(0.030135, 0.06127, -0.043945, 0.0), + float4(0.330601, 0.483055, 0.116951, 7.0), + float4(0.034429, 0.265258, -0.043945, 7.0), + float4(0.542104, 0.741798, 0.116951, 8.0), + float4(0.034429, 0.355442, -0.043945, 15.0), + float4(0.305908, 0.822319, 0.116951, 8.0), + float4(0.267405, 0.532589, -0.043945, 23.0), + float4(0.542104, 0.822319, 0.082595, 8.0), + float4(-0.045345, 0.036855, -0.044263, 31.0), + float4(0.045345, 0.23354, 0.059405, 8.0), + float4(-0.224888, 0.072548, -0.044263, 39.0), + float4(0.059405, 0.565586, 0.059405, 8.0), + float4(-0.032366, 0.042263, -0.044263, 47.0), + float4(0.148095, 0.408756, 0.059405, 8.0), + float4(0.074711, 0.08769, -0.044263, 55.0), + float4(0.184869, 0.464999, 0.064813, 8.0), + float4(0.146095, 0.271559, -0.044263, 63.0), + float4(0.211908, 0.446612, 0.038855, 8.0), + float4(0.23803, 0.310496, -0.044263, 71.0), + float4(0.320067, 0.460672, 0.06265, 8.0), + float4(0.275885, 0.310496, -0.044263, 79.0), + float4(0.412001, 0.475815, 0.047508, 8.0), + float4(0.375391, 0.350514, -0.044263, 87.0), + float4(0.45202, 0.38388, 0.044263, 4.0), + float4(0.410001, 0.350514, -0.044263, 91.0), + float4(0.474733, 0.401185, 0.044263, 5.0), + float4(-0.224888, 0.379717, -0.044263, 96.0), + float4(-0.0812, 0.532057, 0.047508, 8.0), + float4(-0.095098, 0.228295, -0.044263, 104.0), + float4(0.211908, 0.565586, 0.064813, 8.0), + float4(-0.222724, 0.430551, -0.044263, 112.0), + float4(-0.011979, 0.833818, 0.053998, 8.0), + float4(-0.100506, 0.438122, -0.044263, 120.0), + float4(0.327638, 0.833818, 0.0832, 8.0), + float4(0.127708, 0.312659, -0.044263, 128.0), + float4(0.282211, 0.467162, 0.064813, 8.0), + float4(0.245601, 0.352677, -0.044263, 136.0), + float4(0.474733, 0.604523, 0.06265, 8.0), + float4(0.178543, 0.390533, -0.044263, 144.0), + float4(0.474733, 0.639133, 0.0832, 8.0), + float4(0.200174, 0.483549, -0.044263, 152.0), + float4(0.474733, 0.639133, 0.0832, 4.0), + float4(-0.013979, 0.487875, -0.044263, 156.0), + float4(0.467162, 0.833818, 0.0832, 5.0) +}; +constant float2 PRISM_POLY[110] = { + float2(0.006442, -0.002147), + float2(-0.001074, 0.001074), + float2(-0.004294, 0.007515), + float2(-0.004294, 0.806288), + float2(-0.001074, 0.809509), + float2(0.004294, 0.809509), + float2(0.022546, 0.796626), + float2(0.185736, 0.655982), + float2(0.281288, 0.86319), + float2(0.286656, 0.870706), + float2(0.293098, 0.871779), + float2(0.465951, 0.781595), + float2(0.478834, 0.773006), + float2(0.478834, 0.763344), + float2(0.407975, 0.592638), + float2(0.45092, 0.586196), + float2(0.601227, 0.573313), + float2(0.606595, 0.569018), + float2(0.606595, 0.564724), + float2(0.601227, 0.556135), + float2(0.50138, 0.453067), + float2(0.425153, 0.37684), + float2(0.289877, 0.246933), + float2(0.081595, 0.057975), + float2(0.013957, 0.0), + float2(-0.011897, 0.0), + float2(-0.055161, 0.037855), + float2(-0.081119, 0.063813), + float2(-0.083282, 0.06814), + float2(-0.084363, 0.38288), + float2(-0.089771, 0.38288), + float2(-0.158993, 0.33529), + float2(-0.166564, 0.336372), + float2(-0.250927, 0.379635), + float2(-0.261743, 0.388288), + float2(-0.263906, 0.461836), + float2(-0.262824, 0.486712), + float2(-0.173053, 0.638133), + float2(-0.044345, 0.860939), + float2(-0.036774, 0.867429), + float2(-0.024876, 0.871755), + float2(0.33529, 0.871755), + float2(0.349351, 0.867429), + float2(0.363412, 0.853368), + float2(0.513752, 0.619747), + float2(0.513752, 0.379635), + float2(0.466162, 0.322311), + float2(0.461836, 0.320148), + float2(0.399104, 0.311496), + float2(0.393696, 0.313659), + float2(0.374227, 0.332046), + float2(0.343943, 0.288782), + float2(0.336372, 0.282293), + float2(0.269314, 0.271477), + float2(0.263906, 0.27364), + float2(0.243356, 0.292027), + float2(0.21199, 0.244438), + float2(0.207664, 0.241193), + float2(0.149258, 0.231459), + float2(0.138443, 0.231459), + float2(0.118974, 0.248764), + float2(0.116811, 0.222806), + float2(0.117892, 0.162237), + float2(0.115729, 0.076792), + float2(0.06814, 0.011897), + float2(0.062732, 0.007571), + float2(0.005408, 0.0), + float2(0.031135, 0.06227), + float2(0.252301, 0.266258), + float2(0.541104, 0.544325), + float2(0.359663, 0.559356), + float2(0.433742, 0.753681), + float2(0.304908, 0.821319), + float2(0.199693, 0.595859), + float2(0.035429, 0.740798), + float2(0.035429, 0.482055), + float2(-0.043263, 0.077874), + float2(0.001082, 0.037855), + float2(0.044345, 0.043263), + float2(0.075711, 0.08869), + float2(0.077874, 0.407756), + float2(0.10924, 0.439122), + float2(0.111403, 0.307169), + float2(0.147095, 0.272559), + float2(0.189277, 0.28013), + float2(0.210909, 0.313659), + float2(0.210909, 0.445612), + float2(0.23903, 0.45102), + float2(0.241193, 0.343943), + float2(0.276885, 0.311496), + float2(0.319067, 0.319067), + float2(0.34178, 0.353677), + float2(0.340698, 0.474815), + float2(0.375309, 0.488875), + float2(0.376391, 0.381799), + float2(0.411001, 0.351514), + float2(0.45102, 0.359085), + float2(0.473733, 0.391533), + float2(0.473733, 0.603523), + float2(0.466162, 0.618665), + float2(0.326638, 0.830655), + float2(-0.012979, 0.832818), + float2(-0.099506, 0.68356), + float2(-0.221724, 0.475896), + float2(-0.223888, 0.407756), + float2(-0.163319, 0.380717), + float2(-0.094098, 0.431551), + float2(-0.0822, 0.531057), + float2(-0.045426, 0.564586), + float2(-0.044345, 0.23254) +}; +constant float PRISM_BEVEL = 0.012883; +// end of the prism mesh + +constant float PRISM_IOR = 1.61; +constant float PRISM_DISPERSION = 0.035; +constant float PRISM_GLOW = 0.45; +constant float PRISM_FOLD = 0.35; +constant int PRISM_BOUNCES = 6; +constant float PRISM_MAT = 9.0; + +inline int prism_shape(constant Layer &layer) +{ + int id = sculpt_id(layer); + return id == 3 || id == 4 ? id - 3 : -1; +} + +static float prism_poly_dist(float2 p, int i0, int n) +{ + float2 vj = PRISM_POLY[i0 + n - 1]; + float d = 1e9; + float sgn = 1.0; + for (int i = 0; i < n; i++) + { + float2 vi = PRISM_POLY[i0 + i]; + float2 e = vj - vi; + float2 w = p - vi; + float2 b = w - e * saturate(dot(w, e) / dot(e, e)); + d = min(d, dot(b, b)); + bool c0 = p.y >= vi.y; + bool c1 = p.y < vj.y; + bool c2 = e.x * w.y > e.y * w.x; + if ((c0 && c1 && c2) || (!c0 && !c1 && !c2)) + { + sgn = -sgn; + } + vj = vi; + } + return sgn * sqrt(d); +} + +static float prism_rim(float3 p, int shape, constant Layer &layer) +{ + float half_t = model_thick(layer) * 0.5; + float sil = prism_poly_dist(p.xy, PRISM_SIL_START[shape], PRISM_SIL_COUNT[shape]); + float2 w = float2(sil + PRISM_BEVEL, abs(p.z + half_t) - (half_t - PRISM_BEVEL)); + return min(max(w.x, w.y), 0.0) + length(max(w, 0.0)) - PRISM_BEVEL; +} + +static float prism_tri(float3 ro, float3 rd, int k) +{ + float3 v0 = PRISM_TRIS[4 * k].xyz; + float3 e1 = PRISM_TRIS[4 * k + 1].xyz; + float3 e2 = PRISM_TRIS[4 * k + 2].xyz; + float3 pv = cross(rd, e2); + float det = dot(e1, pv); + if (abs(det) < 1e-10) + { + return -1.0; + } + float inv = 1.0 / det; + float3 sv = ro - v0; + float u = dot(sv, pv) * inv; + float3 qv = cross(sv, e1); + float v = dot(rd, qv) * inv; + if (u < -2e-4 || v < -2e-4 || u + v > 1.0004) + { + return -1.0; + } + return dot(e2, qv) * inv; +} + +static float prism_trace(float3 ro, float3 rd, int shape, thread int &hit) +{ + float3 inv = 1.0 / select(float3(1e-6), rd, abs(rd) > 1e-6); + int b0 = PRISM_BOX_START[shape]; + int nb = PRISM_BOX_COUNT[shape]; + float best = 1e9; + hit = -1; + for (int j = b0; j < b0 + nb; j++) + { + float4 lo = PRISM_BOXES[2 * j]; + float4 hi = PRISM_BOXES[2 * j + 1]; + float3 t0 = (lo.xyz - ro) * inv; + float3 t1 = (hi.xyz - ro) * inv; + float3 tn = min(t0, t1); + float3 tf = max(t0, t1); + float tmin = max(max(tn.x, tn.y), tn.z); + float tmax = min(min(tf.x, tf.y), tf.z); + if (tmax < max(tmin, 1e-5) || tmin > best) + { + continue; + } + int k0 = int(lo.w); + int n = int(hi.w); + for (int i = k0; i < k0 + n; i++) + { + float t = prism_tri(ro, rd, i); + if (t > 1e-5 && t < best) + { + best = t; + hit = i; + } + } + } + return best; +} + static float2 sculpt_proto(float3 p, int theme, int shape) { if (theme == 3) @@ -997,6 +1920,10 @@ static float2 sculpt_eval(float3 q, bool occ, constant Layer &layer) { int id = sculpt_id(layer) - 1; int theme = id / 2, shape = id % 2; + if (theme == 1) + { + return float2(prism_rim(q, shape, layer), 8.0); + } float3 p = sculpt_point(q, layer); float2 r; if (occ && theme == 3) @@ -1129,6 +2056,7 @@ static SculptMat sculpt_material(float mat, float3 p, int theme, int shape) 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 == 1) return s_mat(s_lin(0.02, 0.05, 0.33), 0.35, 0.5, 0.05, 0.3); if (theme == 2) { 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); @@ -1186,6 +2114,10 @@ static float3 model_shade(float3 q, float3 n, float3 rd, float3 L, float fall, f { float3 p = sculpt_point(q, layer) - float3(n.x, -n.y, n.z) * 0.01; m = sculpt_material(mat, p, (id - 1) / 2, (id - 1) % 2); + if (id == 3 || id == 4) + { + gloss = 1.0 - smoothstep(0.97, 0.995, abs(n.z)); + } } else { @@ -1209,6 +2141,157 @@ static float3 model_shade(float3 q, float3 n, float3 rd, float3 L, float fall, f return model_tonemap(col); } +// ---- Le cristal de Prism Glow ---- (cf. HLSL) + +inline float prism_fresnel(float c, float ior) +{ + float f0 = (ior - 1.0) / (ior + 1.0); + f0 *= f0; + return f0 + (1.0 - f0) * pow(1.0 - c, 5.0); +} + +static float prism_primary(float3 ro, float3 rd, thread int &hc, constant Layer &layer) +{ + hc = -1; + int shape = prism_shape(layer); + if (shape < 0) + { + return 1e9; + } + float t = prism_trace(ro, rd, shape, hc); + if (hc >= 0 && fmod(PRISM_TRIS[4 * hc].w, 2.0) > 0.5 && ro.z + rd.z * t < 0.0) + { + hc = -1; + return 1e9; + } + return t; +} + +inline float3 prism_normal(int h, float3 rd) +{ + float3 n = PRISM_TRIS[4 * h + 3].xyz; + return dot(n, rd) > 0.0 ? -n : n; +} + +inline float prism_seg(float3 p, float3 a, float3 b) +{ + float3 pa = p - a, ba = b - a; + return length(pa - ba * saturate(dot(pa, ba) / dot(ba, ba))); +} + +static float prism_edge_px(float3 p, int h, float px) +{ + float4 r0 = PRISM_TRIS[4 * h]; + float3 a = r0.xyz; + float3 b = a + PRISM_TRIS[4 * h + 1].xyz; + float3 c = a + PRISM_TRIS[4 * h + 2].xyz; + int bits = int(r0.w * 0.5); + float d = 1e9; + if ((bits & 1) != 0) d = min(d, prism_seg(p, b, c)); + if ((bits & 2) != 0) d = min(d, prism_seg(p, c, a)); + if ((bits & 4) != 0) d = min(d, prism_seg(p, a, b)); + return d / px; +} + +static float3 plane_to_world(float3 v, ModelFrame f) +{ + float x = v.x * f.c.z - v.y * f.s.z; + float y = v.x * f.s.z + v.y * f.c.z; + float z = -x * f.s.y + v.z * f.c.y; + return float3(x * f.c.y + v.z * f.s.y, y * f.c.x - z * f.s.x, y * f.s.x + z * f.c.x); +} + +static float3 prism_screen(float3 q, float3 d, float3 nz, float hz, float3 tip, float unit, ModelFrame f, + constant Layer &layer, texture2d texFrame) +{ + float denom = dot(d, nz); + if (denom > -1e-4) + { + return SCULPT_SCREEN * 0.2; + } + float3 g = q + d * ((hz - dot(q, nz)) / denom); + float3 w = plane_to_world(tip + unit * model_to_plane(g, f), f); + float k = 1.0 - w.z / layer.src.z; + if (k < 1e-3) + { + return SCULPT_SCREEN * 0.2; + } + float2 uv = layer.dst.xy + ((w.xy + layer.mb.zw) / k - layer.src.xy) / layer.quad_px * layer.dst.zw; + if (any(uv < 0.0) || any(uv > 1.0)) + { + return SCULPT_SCREEN * 0.2; + } + return pow(texFrame.sample(samp, uv, level(0.0)).rgb, float3(2.2)); +} + +static float3 prism_shade(float3 p, float3 n, float3 rd, int h, float px, float3 l, float3 fill, float3 nz, + float hz, float3 tip, float unit, ModelFrame f, constant Layer &layer, + texture2d texFrame) +{ + int shape = prism_shape(layer); + float4 r3 = PRISM_TRIS[4 * h + 3]; + float3 facet = float3(PRISM_TRIS[4 * h + 1].w, PRISM_TRIS[4 * h + 2].w, r3.w); + float F = prism_fresnel(saturate(-dot(rd, n)), PRISM_IOR); + float zb = -model_thick(layer); + float3 trans = float3(0.0); + for (int ch = 0; ch < 3; ch++) + { + float3 mask = float3(float(ch == 0), float(ch == 1), float(ch == 2)); + float ior = PRISM_IOR + PRISM_DISPERSION * float(ch - 1); + float3 d = refract(rd, n, 1.0 / ior); + float3 pos = p; + float thr = 1.0; + float acc = 0.0; + for (int b = 0; b < PRISM_BOUNCES; b++) + { + int hh; + float th = prism_trace(pos, d, shape, hh); + float tb = d.z < -1e-6 ? (zb - pos.z) / d.z : 1e9; + if (tb <= th) + { + float3 qb = pos + d * tb; + float3 d2 = refract(d, float3(0.0, 0.0, 1.0), ior); + if (dot(d2, d2) < 1e-8) + { + pos = qb; + d = reflect(d, float3(0.0, 0.0, 1.0)); + continue; + } + acc += thr * dot(prism_screen(qb, d2, nz, hz, tip, unit, f, layer, texFrame), mask); + thr = 0.0; + break; + } + if (hh < 0) + { + break; + } + pos += d * th; + float4 rn = PRISM_TRIS[4 * hh + 3]; + if (fmod(PRISM_TRIS[4 * hh].w, 2.0) > 0.5 && pos.z < 0.0) + { + acc += thr * dot(s_lin(0.03, 0.06, 0.37) * 0.35, mask); + thr = 0.0; + break; + } + float3 dout = refract(d, -rn.xyz, ior); + if (dot(dout, dout) < 1e-8) + { + d = reflect(d, -rn.xyz); + continue; + } + float fi = prism_fresnel(saturate(dot(dout, rn.xyz)), ior); + acc += thr * (1.0 - fi) * dot(model_env(dout, 0.05, l, fill), mask); + thr *= fi; + d = reflect(d, -rn.xyz); + } + trans += acc * mask; + } + float3 col = F * model_env(reflect(rd, n), 0.05, l, fill) + (1.0 - F) * trans + facet * PRISM_GLOW; + float fold = 1.0 - smoothstep(0.2, 1.0, prism_edge_px(p, h, px)); + col = mix(col, s_lin(0.96, 0.99, 1.0) * 1.3, PRISM_FOLD * fold * (0.35 + 0.65 * saturate(dot(n, l)))); + return model_tonemap(col); +} + // Les passes de la boucle unique de `cursor_model` (cf. HLSL : un seul appel de `model_eval`). constant int STAGE_MARCH = 0; constant int STAGE_NORMAL = 1; @@ -1228,7 +2311,8 @@ inline float3 model_tetra(int k) static float4 cursor_model(float2 local, constant Layer &layer, texture2d texSdf, - texture2d texImg) + texture2d texImg, + texture2d texFrame) { ModelFrame f; f.c = cos(layer.fx.xyz); @@ -1259,6 +2343,9 @@ static float4 cursor_model(float2 local, constant Layer &layer, 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; + bool need_tc = stage == STAGE_MARCH; + float tc = 1e9; + int hc = -1; int k = 0; float t = max(tb.x, 0.0); float best = 1e9; @@ -1283,6 +2370,11 @@ static float4 cursor_model(float2 local, constant Layer &layer, float4 acc = float4(0.0); for (int it = 0; it < 500; it++) { + if (need_tc) + { + need_tc = false; + tc = prism_primary(ro, ray, hc, layer); + } if (plane_next) { plane_next = false; @@ -1305,9 +2397,17 @@ static float4 cursor_model(float2 local, constant Layer &layer, { 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); + float3 c; + if (mat > PRISM_MAT - 0.5) + { + c = prism_shade(q, n, ray, hc, t_best / dlen, l, fill, nz, hz, tip, unit, f, layer, texFrame); + } + else + { + float3 tl = lamp - q; + float fall = SCULPT_LAMP_DIST * SCULPT_LAMP_DIST / dot(tl, tl); + 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++; @@ -1328,6 +2428,7 @@ static float4 cursor_model(float2 local, constant Layer &layer, t = max(tb.x, 0.0); best = 1e9; t_best = t; + need_tc = true; } } continue; @@ -1378,6 +2479,14 @@ static float4 cursor_model(float2 local, constant Layer &layer, } t += d * stride; k++; + if (!hit && t >= tc) + { + hit = true; + best = 0.0; + t_best = tc; + d_best = 0.0; + mat = PRISM_MAT; + } if (hit || t > tb.y || k == 96) { cov_s = saturate(1.0 - best); @@ -1406,6 +2515,24 @@ static float4 cursor_model(float2 local, constant Layer &layer, plane_next = true; } } + if (stage == STAGE_NORMAL && mat > PRISM_MAT - 0.5) + { + n = prism_normal(hc, ray); + k = 0; + if (shaded > 0) + { + stage = STAGE_SHADE; + } + else + { + if (prism_edge_px(q, hc, t_best / dlen) < 0.75) + { + sub = 3; + } + stage = STAGE_DONE; + plane_next = true; + } + } } } else if (stage == STAGE_NORMAL) @@ -1425,6 +2552,18 @@ static float4 cursor_model(float2 local, constant Layer &layer, { sub = 3; } + if (k == 0 && mat > 7.5) + { + int ps = prism_shape(layer); + if (ps >= 0) + { + float dout = prism_poly_dist(q.xy, PRISM_OUT_START[ps], PRISM_OUT_COUNT[ps]); + if (abs(dout) < 0.75 * t_best / dlen) + { + sub = 3; + } + } + } k++; if (k == 4) { @@ -2171,7 +3310,7 @@ fragment float4 ps_main(VSOut i [[stage_in]], { return float4(0.0, 0.0, 0.0, 0.0); } - return cursor_model(i.local, layer, texSdf, texImg); + return cursor_model(i.local, layer, texSdf, texImg, texDof); } // mode 14 : CADRE DE FENÊTRE autour de l'écran, dessiné SOUS lui. Cf. commentaires HLSL. diff --git a/crates/compositor/src/vk_shaders/layer.wgsl b/crates/compositor/src/vk_shaders/layer.wgsl index a1a84f6ac..96532b6ba 100644 --- a/crates/compositor/src/vk_shaders/layer.wgsl +++ b/crates/compositor/src/vk_shaders/layer.wgsl @@ -34,16 +34,21 @@ struct Layer { } @group(0) @binding(0) var layer: Layer; -@group(0) @binding(1) var texY: texture_2d; // R8Unorm, sample .r ; modes 7, 13 et 15 : le sprite RGBA -@group(0) @binding(2) var texU: texture_2d; // R8Unorm, sample .r ; mode 15 : le champ R16F du sprite +@group(0) @binding(1) var texY: texture_2d; // R8Unorm, sample .r ; modes 7 et 13 : le sprite RGBA ; mode 15 : la copie de l'image composee +@group(0) @binding(2) var texU: texture_2d; // R8Unorm, sample .r @group(0) @binding(3) var samp: sampler; // Masque de segmentation du sujet webcam, R8. Une vue 1x1 est liee quand aucun masque // n'existe : la branche n'est de toute facon prise que si layer.fx.z > 0.5. -// Mode 18 : ce binding porte a la place le rendu isole de l'ecran cadre. +// Mode 18 : ce binding porte a la place le rendu isole de l'ecran cadre ; mode 15, le champ R16F +// du sprite. @group(0) @binding(4) var texMask: texture_2d; // Pyramide RGBA de profondeur de champ (`tilted_sample`), lue par le mode 8 et par le repli du -// mode 18, qui garde le binding 4 pour son rendu isole : d'ou un binding a elle. Vue 1x1 ailleurs. +// mode 18, qui garde le binding 4 pour son rendu isole : d'ou un binding a elle. Mode 15 : le +// sprite RGBA. Vue 1x1 ailleurs. @group(0) @binding(6) var texDof: texture_2d; +// Le maillage de Prism Glow (mode 15), rempli de `prism_mesh.rs` : un uniform et non des tables +// dans le shader, que lavapipe recopierait a chaque pixel de chaque calque (cf. `PrismMesh`). +@group(0) @binding(7) var prism: PrismMesh; // Plafond de la profondeur de champ du mode 8, en niveau de la pyramide demi-resolution. // Meme valeur que `DOF_MAX_LOD` du HLSL. @@ -568,9 +573,10 @@ fn blur_webcam_bg(uv: vec2, intensity: f32, qpx: vec2, local_px: vec2< // propres et occlusion, et qui porte une ombre douce et une ombre de contact sur le plan de // l'ecran. Constantes : miroir exact de `frame_geometry.rs` (MODEL_*) et de `sculpt.rs` // (SCULPT_*), emplacements du cbuffer : `cursor_model_cb`. -// Textures : le sprite RGBA (alpha droit) au binding 1 (`texY`, comme aux modes 7 et 13), son -// champ R16F au binding 2 (`texU`), sur le meme rect ; `color.rg` = coin du sprite dans le repere -// du modele, `sprite_size()` = sa taille (w/h dans `radius_px`), `color.b` = l'ecrasement au clic. +// Textures : la copie de l'image composee au binding 1 (`texY`), que refracte le cristal de Prism +// Glow ; le sprite RGBA (alpha droit) est au binding 6 (`texDof`), son champ R16F au +// binding 4 (`texMask`), sur le meme rect ; `color.rg` = coin du sprite dans le repere du modele, +// `sprite_size()` = sa taille (w/h dans `radius_px`), `color.b` = l'ecrasement au clic. const MODEL_BEVEL: f32 = 0.045; const MODEL_LIGHT = vec3(-0.4194, -0.5792, 0.6990); const MODEL_FILL = vec3(0.7557, 0.2519, 0.6046); @@ -592,7 +598,7 @@ fn sprite_size() -> vec2 { // Un texel du sprite, en unites du modele : le champ est le sprite surechantillonne x4. const CURSOR_SDF_UPSAMPLE: f32 = 4.0; fn sprite_texel() -> f32 { - let d = vec2(textureDimensions(texU)); + let d = vec2(textureDimensions(texMask)); return CURSOR_SDF_UPSAMPLE / max(d.x, d.y); } @@ -958,6 +964,127 @@ fn s_pixel_outline(p: vec2, shape: i32) -> f32 { return d; } +// ---- Prism Glow : un cristal en MAILLAGE ---- (port du HLSL, dont les commentaires font foi) +// Les facettes tracees sur son dessin, en triangles dans le repere du modele ; le serti est la +// silhouette extrudee (champ de distance), le cristal est lance de rayons boite par boite. +// prism mesh: generated by design/cursors/prism-glow/model/export_compositor.py +var PRISM_TRI_START = array(0, 31); +var PRISM_TRI_COUNT = array(31, 130); +var PRISM_SIL_START = array(0, 25); +var PRISM_SIL_COUNT = array(25, 42); +var PRISM_OUT_START = array(67, 76); +var PRISM_OUT_COUNT = array(9, 34); +var PRISM_BOX_START = array(0, 4); +var PRISM_BOX_COUNT = array(4, 18); +struct PrismMesh { + tris: array, 644>, + poly: array, 110>, + boxes: array, 44>, +} +const PRISM_BEVEL: f32 = 0.012883; +// end of the prism mesh + +const PRISM_IOR: f32 = 1.61; +const PRISM_DISPERSION: f32 = 0.035; +const PRISM_GLOW: f32 = 0.45; +const PRISM_FOLD: f32 = 0.35; +const PRISM_BOUNCES: i32 = 6; +const PRISM_MAT: f32 = 9.0; + +fn prism_shape() -> i32 { + let id = sculpt_id(); + if id == 3 || id == 4 { + return id - 3; + } + return -1; +} + +// Drapeaux d'un triangle : paroi (bit 0), aretes reelles (bits 1 a 3). +fn prism_flags(k: i32) -> i32 { + return i32(prism.tris[4 * k].w + 0.5); +} + +fn prism_poly_dist(p: vec2, i0: i32, n: i32) -> f32 { + var vj = prism.poly[i0 + n - 1].xy; + var d = 1e9; + var sgn = 1.0; + for (var i = 0; i < n; i++) { + let vi = prism.poly[i0 + i].xy; + let e = vj - vi; + let w = p - vi; + let b = w - e * saturate(dot(w, e) / dot(e, e)); + d = min(d, dot(b, b)); + let c0 = p.y >= vi.y; + let c1 = p.y < vj.y; + let c2 = e.x * w.y > e.y * w.x; + if (c0 && c1 && c2) || (!c0 && !c1 && !c2) { + sgn = -sgn; + } + vj = vi; + } + return sgn * sqrt(d); +} + +fn prism_rim(p: vec3, shape: i32) -> f32 { + let half_t = model_thick() * 0.5; + let sil = prism_poly_dist(p.xy, PRISM_SIL_START[shape], PRISM_SIL_COUNT[shape]); + let w = vec2(sil + PRISM_BEVEL, abs(p.z + half_t) - (half_t - PRISM_BEVEL)); + return min(max(w.x, w.y), 0.0) + length(max(w, vec2(0.0))) - PRISM_BEVEL; +} + +fn prism_tri(ro: vec3, rd: vec3, k: i32) -> f32 { + let v0 = prism.tris[4 * k].xyz; + let e1 = prism.tris[4 * k + 1].xyz; + let e2 = prism.tris[4 * k + 2].xyz; + let pv = cross(rd, e2); + let det = dot(e1, pv); + if abs(det) < 1e-10 { + return -1.0; + } + let inv = 1.0 / det; + let sv = ro - v0; + let u = dot(sv, pv) * inv; + let qv = cross(sv, e1); + let v = dot(rd, qv) * inv; + if u < -2e-4 || v < -2e-4 || u + v > 1.0004 { + return -1.0; + } + return dot(e2, qv) * inv; +} + +// (distance, indice) du triangle du cristal `shape` le plus proche devant (ro, rd) ; (1e9, -1) +// s'il n'y en a pas. Les triangles sont parcourus boîte par boîte (cf. HLSL). +fn prism_trace(ro: vec3, rd: vec3, shape: i32) -> vec2 { + let inv = vec3(1.0) / select(vec3(1e-6), rd, abs(rd) > vec3(1e-6)); + let b0 = PRISM_BOX_START[shape]; + let nb = PRISM_BOX_COUNT[shape]; + var best = 1e9; + var hit = -1; + for (var j = b0; j < b0 + nb; j++) { + let lo = prism.boxes[2 * j]; + let hi = prism.boxes[2 * j + 1]; + let t0 = (lo.xyz - ro) * inv; + let t1 = (hi.xyz - ro) * inv; + let tn = min(t0, t1); + let tf = max(t0, t1); + let tmin = max(max(tn.x, tn.y), tn.z); + let tmax = min(min(tf.x, tf.y), tf.z); + if tmax < max(tmin, 1e-5) || tmin > best { + continue; + } + let k0 = i32(lo.w + 0.5); + let n = i32(hi.w + 0.5); + for (var i = k0; i < k0 + n; i++) { + let t = prism_tri(ro, rd, i); + if t > 1e-5 && t < best { + best = t; + hit = i; + } + } + } + return vec2(best, f32(hit)); +} + fn sculpt_proto(p: vec3, theme: i32, shape: i32) -> vec2 { if theme == 3 { return s_voxels(p, shape); @@ -978,6 +1105,10 @@ fn sculpt_eval(q: vec3, occ: bool) -> vec2 { let id = sculpt_id() - 1; let theme = id / 2; let shape = id % 2; + if theme == 1 { + // Prism Glow : son serti seul, dans le repere du modele (le cristal est trace a part). + return vec2(prism_rim(q, shape), 8.0); + } let p = sculpt_point(q); var r: vec2; if occ && theme == 3 { @@ -994,7 +1125,7 @@ fn sd_sprite2(p: vec2) -> f32 { let lo = layer.color.rg; let c = clamp(p, lo, lo + sprite_size()); // Niveau 0 explicite : la marche est une boucle a sortie anticipee (pas de derivees). - let d = textureSampleLevel(texU, samp, (c - lo) / sprite_size(), 0.0).r; + let d = textureSampleLevel(texMask, samp, (c - lo) / sprite_size(), 0.0).r; let o = p - c; let out2 = dot(o, o); let e = max(d, 0.0); @@ -1054,7 +1185,7 @@ fn model_albedo(p: vec2) -> vec3 { let g = vec2(sd_sprite2(p + vec2(e, 0.0)) - sd_sprite2(p - vec2(e, 0.0)), sd_sprite2(p + vec2(0.0, e)) - sd_sprite2(p - vec2(0.0, e))); let q = p - g / max(length(g), 1e-6) * max(sd_sprite2(p) + MODEL_RIM_INSET * texel, 0.0); - return textureSampleLevel(texY, samp, (q - layer.color.rg) / sprite_size(), 0.0).rgb; + return textureSampleLevel(texDof, samp, (q - layer.color.rg) / sprite_size(), 0.0).rgb; } struct SculptMat { @@ -1089,6 +1220,9 @@ fn sculpt_material(mat: f32, p: vec3, theme: i32, shape: i32) -> SculptMat } return SculptMat(s_lin(0.1, 0.1, 0.11), 0.55, 0.05, 0.0, 0.1); } + if theme == 1 { + return SculptMat(s_lin(0.02, 0.05, 0.33), 0.35, 0.5, 0.05, 0.3); + } if theme == 2 { if (primary && shape == 0) || (mat > 2.5 && mat < 3.5 && shape == 1) { return SculptMat(s_lin(1.0, 0.40, 0.30), 0.85, 0.08, 0.15, 0.03); @@ -1151,6 +1285,10 @@ fn model_shade(q: vec3, n: vec3, rd: vec3, L: vec3, fall: f3 if id > 0 { let p = sculpt_point(q) - vec3(n.x, -n.y, n.z) * 0.01; m = sculpt_material(mat, p, (id - 1) / 2, (id - 1) % 2); + if id == 3 || id == 4 { + // Le serti de Prism Glow, comme un sprite : brillant sur son arrondi seulement. + gloss = 1.0 - smoothstep(0.97, 0.995, abs(n.z)); + } } else { m = SculptMat(pow(model_albedo(q.xy), vec3(2.2)), 0.45, 0.35, 0.2, 0.3); gloss = 1.0 - smoothstep(0.97, 0.995, abs(n.z)); @@ -1172,6 +1310,149 @@ fn model_shade(q: vec3, n: vec3, rd: vec3, L: vec3, fall: f3 return model_tonemap(col); } +// ---- Le cristal de Prism Glow ---- + +fn prism_fresnel(c: f32, ior: f32) -> f32 { + var f0 = (ior - 1.0) / (ior + 1.0); + f0 = f0 * f0; + return f0 + (1.0 - f0) * pow(1.0 - c, 5.0); +} + +// (distance, facette) du cristal sur le rayon ; (1e9, -1) si rate, hors de Prism Glow, ou sur une +// paroi sous le dessus du serti (le rayon a traverse le serti avant). +fn prism_primary(ro: vec3, rd: vec3) -> vec2 { + let shape = prism_shape(); + if shape < 0 { + return vec2(1e9, -1.0); + } + let r = prism_trace(ro, rd, shape); + let h = i32(r.y); + if h >= 0 && (prism_flags(h) & 1) == 1 && ro.z + rd.z * r.x < 0.0 { + return vec2(1e9, -1.0); + } + return r; +} + +fn prism_normal(h: i32, rd: vec3) -> vec3 { + let n = prism.tris[4 * h + 3].xyz; + return select(n, -n, dot(n, rd) > 0.0); +} + +fn prism_seg(p: vec3, a: vec3, b: vec3) -> f32 { + let pa = p - a; + let ba = b - a; + return length(pa - ba * saturate(dot(pa, ba) / dot(ba, ba))); +} + +// Distance, en pixels, de `p` aux aretes reelles de la facette `h` (plis et bord du serti). +fn prism_edge_px(p: vec3, h: i32, px: f32) -> f32 { + let a = prism.tris[4 * h].xyz; + let b = a + prism.tris[4 * h + 1].xyz; + let c = a + prism.tris[4 * h + 2].xyz; + let bits = prism_flags(h) >> 1u; + var d = 1e9; + if (bits & 1) != 0 { + d = min(d, prism_seg(p, b, c)); + } + if (bits & 2) != 0 { + d = min(d, prism_seg(p, c, a)); + } + if (bits & 4) != 0 { + d = min(d, prism_seg(p, a, b)); + } + return d / px; +} + +// La rotation inverse de `world_to_plane` : du repere du plan a celui de la camera. +fn plane_to_world(v: vec3, f: ModelFrame) -> vec3 { + let x = v.x * f.c.z - v.y * f.s.z; + let y = v.x * f.s.z + v.y * f.c.z; + let z = -x * f.s.y + v.z * f.c.y; + return vec3(x * f.c.y + v.z * f.s.y, y * f.c.x - z * f.s.x, y * f.s.x + z * f.c.x); +} + +// L'image composee la ou le rayon retombe sur le plan (cf. HLSL) : sa copie au binding 1. +fn prism_screen(q: vec3, d: vec3, nz: vec3, hz: f32, tip: vec3, unit: f32, f: ModelFrame) -> vec3 { + let denom = dot(d, nz); + if denom > -1e-4 { + return SCULPT_SCREEN * 0.2; + } + let g = q + d * ((hz - dot(q, nz)) / denom); + let w = plane_to_world(tip + unit * model_to_plane(g, f), f); + let k = 1.0 - w.z / layer.src.z; + if k < 1e-3 { + return SCULPT_SCREEN * 0.2; + } + let uv = layer.dst.xy + ((w.xy + layer.mb.zw) / k - layer.src.xy) / layer.quad_px * layer.dst.zw; + if any(uv < vec2(0.0)) || any(uv > vec2(1.0)) { + return SCULPT_SCREEN * 0.2; + } + return pow(textureSampleLevel(texY, samp, uv, 0.0).rgb, vec3(2.2)); +} + +fn prism_shade(p: vec3, n: vec3, rd: vec3, h: i32, px: f32, l: vec3, fill: vec3, + nz: vec3, hz: f32, tip: vec3, unit: f32, f: ModelFrame) -> vec3 { + let shape = prism_shape(); + let facet = vec3(prism.tris[4 * h + 1].w, prism.tris[4 * h + 2].w, prism.tris[4 * h + 3].w); + let F = prism_fresnel(saturate(-dot(rd, n)), PRISM_IOR); + let zb = -model_thick(); + var trans = vec3(0.0); + for (var ch = 0; ch < 3; ch++) { + // Le rouge plie le moins, le bleu le plus ; le canal passe par un masque (comme au HLSL). + let mask = vec3(f32(ch == 0), f32(ch == 1), f32(ch == 2)); + let ior = PRISM_IOR + PRISM_DISPERSION * f32(ch - 1); + var d = refract(rd, n, 1.0 / ior); + var pos = p; + var thr = 1.0; + var acc = 0.0; + for (var b = 0; b < PRISM_BOUNCES; b++) { + let hit = prism_trace(pos, d, shape); + let hh = i32(hit.y); + let th = hit.x; + var tb = 1e9; + if d.z < -1e-6 { + tb = (zb - pos.z) / d.z; + } + if tb <= th { + let qb = pos + d * tb; + let d2 = refract(d, vec3(0.0, 0.0, 1.0), ior); + if dot(d2, d2) < 1e-8 { + pos = qb; + d = reflect(d, vec3(0.0, 0.0, 1.0)); + continue; + } + acc = acc + thr * dot(prism_screen(qb, d2, nz, hz, tip, unit, f), mask); + thr = 0.0; + break; + } + if hh < 0 { + break; + } + pos = pos + d * th; + let rn = prism.tris[4 * hh + 3].xyz; + if (prism_flags(hh) & 1) == 1 && pos.z < 0.0 { + acc = acc + thr * dot(s_lin(0.03, 0.06, 0.37) * 0.35, mask); + thr = 0.0; + break; + } + let dout = refract(d, -rn, ior); + if dot(dout, dout) < 1e-8 { + d = reflect(d, -rn); + continue; + } + let fi = prism_fresnel(saturate(dot(dout, rn)), ior); + acc = acc + thr * (1.0 - fi) * dot(model_env(dout, 0.05, l, fill), mask); + thr = thr * fi; + d = reflect(d, -rn); + } + trans = trans + acc * mask; + } + var col = F * model_env(reflect(rd, n), 0.05, l, fill) + (1.0 - F) * trans + facet * PRISM_GLOW; + let fold = 1.0 - smoothstep(0.2, 1.0, prism_edge_px(p, h, px)); + col = mix(col, s_lin(0.96, 0.99, 1.0) * 1.3, PRISM_FOLD * fold * (0.35 + 0.65 * saturate(dot(n, l)))); + return model_tonemap(col); +} + // Les passes de la boucle unique de `cursor_model` (cf. HLSL : un seul appel de `model_eval`). const STAGE_MARCH: i32 = 0; const STAGE_NORMAL: i32 = 1; @@ -1219,6 +1500,10 @@ fn cursor_model(local: vec2) -> vec4 { var tb = ray_box(ro, ray, lo - vec3(0.02), hi + vec3(0.02)); var stage = select(STAGE_DONE, STAGE_MARCH, tb.x < tb.y && tb.y > 0.0); var plane_next = stage == STAGE_DONE; + // Prism Glow : le cristal, trace au depart de chaque rayon (`tc` son point, `hc` sa facette). + var need_tc = stage == STAGE_MARCH; + var tc = 1e9; + var hc = -1; var k = 0; var t = max(tb.x, 0.0); var best = 1e9; @@ -1242,6 +1527,12 @@ fn cursor_model(local: vec2) -> vec4 { var shaded = 0; var acc = vec4(0.0); for (var it = 0; it < 500; it++) { + if need_tc { + need_tc = false; + let pr = prism_primary(ro, ray); + tc = pr.x; + hc = i32(pr.y); + } if plane_next { plane_next = false; stage = select(STAGE_DONE, STAGE_SHADE, cov > 0.0); @@ -1259,9 +1550,14 @@ fn cursor_model(local: vec2) -> vec4 { } if stage == STAGE_SHADE { if cov_s > 0.0 { - let tl = lamp - q; - let fall = SCULPT_LAMP_DIST * SCULPT_LAMP_DIST / dot(tl, tl); - let c = model_shade(q, n, ray, normalize(tl), fall, sh, ao, mat, l, fill); + var c: vec3; + if mat > PRISM_MAT - 0.5 { + c = prism_shade(q, n, ray, hc, t_best / dlen, l, fill, nz, hz, tip, unit, f); + } else { + let tl = lamp - q; + let fall = SCULPT_LAMP_DIST * SCULPT_LAMP_DIST / dot(tl, tl); + c = model_shade(q, n, ray, normalize(tl), fall, sh, ao, mat, l, fill); + } acc = acc + vec4(c * cov_s, cov_s); } shaded++; @@ -1280,6 +1576,7 @@ fn cursor_model(local: vec2) -> vec4 { t = max(tb.x, 0.0); best = 1e9; t_best = t; + need_tc = true; } } continue; @@ -1306,7 +1603,7 @@ fn cursor_model(local: vec2) -> vec4 { let d = m.x; if stage == STAGE_MARCH { let fp = t / dlen; - let hit = d < 0.1 * fp; + var hit = d < 0.1 * fp; if hit || d / fp < best { best = select(d / fp, 0.0, hit); t_best = t; @@ -1315,6 +1612,14 @@ fn cursor_model(local: vec2) -> vec4 { } t = t + d * stride; k++; + if !hit && t >= tc { + // Le pas depasse le cristal : c'est lui que le rayon touche, avant le serti. + hit = true; + best = 0.0; + t_best = tc; + d_best = 0.0; + mat = PRISM_MAT; + } if hit || t > tb.y || k == 96 { cov_s = saturate(1.0 - best); if shaded > 0 { @@ -1335,6 +1640,21 @@ fn cursor_model(local: vec2) -> vec4 { plane_next = true; } } + if stage == STAGE_NORMAL && mat > PRISM_MAT - 0.5 { + // Le cristal : la normale de sa facette, ni occlusion ni ombre propre ; un bord + // reel a moins d'un pixel appelle les trois rayons de plus. + n = prism_normal(hc, ray); + k = 0; + if shaded > 0 { + stage = STAGE_SHADE; + } else { + if prism_edge_px(q, hc, t_best / dlen) < 0.75 { + sub = 3; + } + stage = STAGE_DONE; + plane_next = true; + } + } } } else if stage == STAGE_NORMAL { n = n + model_tetra(k) * d; @@ -1348,6 +1668,16 @@ fn cursor_model(local: vec2) -> vec4 { if m.y != mat || abs(d - d_best) > 0.02 * t_best / dlen { sub = 3; } + if k == 0 && mat > 7.5 { + // Le serti de Prism Glow au bord du cristal : son contour a moins d'un pixel. + let ps = prism_shape(); + if ps >= 0 { + let dout = prism_poly_dist(q.xy, PRISM_OUT_START[ps], PRISM_OUT_COUNT[ps]); + if abs(dout) < 0.75 * t_best / dlen { + sub = 3; + } + } + } k++; if k == 4 { stage = STAGE_AO; diff --git a/crates/compositor/tests/cursor_model_render.rs b/crates/compositor/tests/cursor_model_render.rs index a48105252..70151158f 100644 --- a/crates/compositor/tests/cursor_model_render.rs +++ b/crates/compositor/tests/cursor_model_render.rs @@ -161,11 +161,11 @@ fn cursors_dir() -> String { .replace('\\', "/") } -/// Les thèmes d'origine qui ont un modèle (Prism Glow n'en a pas : son dessin est extrudé) et les -/// hotspots de leur flèche et de leur main plates (`CURSOR_THEMES`, +/// Les thèmes d'origine et les hotspots de leur flèche et de leur main plates (`CURSOR_THEMES`, /// `src/lib/cursor/cursorThemes.ts`, sur 32). -const SCULPTED: [(&str, [f32; 2], [f32; 2]); 4] = [ +const SCULPTED: [(&str, [f32; 2], [f32; 2]); 5] = [ ("studio-ink", [6.2304, 2.0992], [12.848, 2.0704]), + ("prism-glow", [6.3456, 2.0672], [11.968, 2.0352]), ("pop-coral", [10.4768, 2.1792], [12.3456, 2.0]), ("pixel-candy", [6.5, 2.0], [10.75, 2.0]), ("star-sprout", [4.7232, 2.1152], [13.1712, 2.0384]), @@ -447,6 +447,20 @@ fn opaque_mask(on_blue: &[u8], on_orange: &[u8], bare_blue: &[u8]) -> Vec .collect() } +/// Les pixels que couvre un curseur de VERRE (le cristal de Prism Glow, qui laisse voir le contenu +/// et le réfracte) : opaques, ou changés sur les deux teintes sans y être plus sombres (une ombre +/// ne fait qu'assombrir ; le verre réfracte, reflète et luit). +fn covered_mask(on_blue: &[u8], on_orange: &[u8], bare_blue: &[u8], bare_orange: &[u8]) -> Vec { + let opaque = opaque_mask(on_blue, on_orange, bare_blue); + let px = |img: &[u8], i: usize| [img[i * 4], img[i * 4 + 1], img[i * 4 + 2], 255]; + (0..1280 * 720) + .map(|i| { + let lit = |on: &[u8], bare: &[u8]| px(on, i) != px(bare, i) && luma(px(on, i)) >= luma(px(bare, i)) - 2.0; + opaque[i] || (lit(on_blue, bare_blue) && lit(on_orange, bare_orange)) + }) + .collect() +} + fn iou(a: &[bool], b: &[bool]) -> f32 { let inter = a.iter().zip(b).filter(|(x, y)| **x && **y).count(); let union = a.iter().zip(b).filter(|(x, y)| **x || **y).count(); @@ -765,16 +779,18 @@ fn the_motion_blur_trail_draws_modelled_copies() { assert!(rgba != flat, "la traînée 3D est celle du sprite plat"); } -/// Les curseurs sculptés (flèche et main de chaque thème qui a un modèle), dessinés par le -/// shader et non extrudés d'un PNG : chacun est là, sa pointe sur le hotspot, son corps en bas à -/// droite de celle-ci (y vers le bas : un modèle retourné finirait au-dessus), et il porte son -/// ombre en l'air. +/// Les curseurs sculptés (flèche et main de chaque thème), dessinés par le shader et non extrudés +/// d'un PNG : chacun est là, sa pointe sur le hotspot, son corps en bas à droite de celle-ci (y +/// vers le bas : un modèle retourné finirait au-dessus), et il porte son ombre en l'air. Le +/// cristal de Prism Glow est de verre : son corps compte ce qu'il couvre, et il doit laisser voir +/// le contenu (une bonne part de lui change avec la teinte de l'écran). #[test] fn the_sculpted_cursors_stand_at_the_hotspot() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor"); let (blue, orange) = (FakeFrame::new(&gpu, Tint::Blue), FakeFrame::new(&gpu, Tint::Orange)); let bare = render_any(&comp, &blue, &hidden_json("null"), &resting("sculpt-bare", false)).0; + let bare_orange = render_any(&comp, &orange, &hidden_json("null"), &resting("sculpt-bare", false)).0; let mut failures = Vec::new(); for (theme, ..) in SCULPTED { for state in ["arrow", "pointer"] { @@ -783,7 +799,8 @@ fn the_sculpted_cursors_stand_at_the_hotspot() { let (hover, p) = render(&comp, &blue, &json, &still); let hover_b = render(&comp, &orange, &json, &still).0; save(&format!("sculpt-{theme}-{state}"), &hover); - let mask = opaque_mask(&hover, &hover_b, &bare); + let glass = theme == "prism-glow"; + let mask = if glass { covered_mask(&hover, &hover_b, &bare, &bare_orange) } else { opaque_mask(&hover, &hover_b, &bare) }; let (mut body, mut c, mut near) = (0usize, [0.0f32; 2], f32::MAX); for y in 0..720 { for x in 0..1280 { @@ -819,6 +836,13 @@ fn the_sculpted_cursors_stand_at_the_hotspot() { if shadow * 10 < body * 3 { failures.push(format!("{theme}/{state} : pas d'ombre en l'air ({shadow} px)")); } + if glass { + let through = (0..1280 * 720).filter(|&i| mask[i] && hover[i * 4..i * 4 + 3] != hover_b[i * 4..i * 4 + 3]).count(); + println!("{theme}/{state} : {through} px du corps laissent voir l'écran"); + if through * 10 < body * 3 { + failures.push(format!("{theme}/{state} : le cristal ne laisse voir l'écran que sur {through} px")); + } + } } } assert!(failures.is_empty(), "{failures:#?}"); diff --git a/design/cursors/3d-direction.md b/design/cursors/3d-direction.md index 33dd399ae..1205d3174 100644 --- a/design/cursors/3d-direction.md +++ b/design/cursors/3d-direction.md @@ -5,15 +5,16 @@ With the 3D option on, mode 15 ray-marches the arrow and hand of Studio Ink, Pop Coral, Pixel Candy and Star Sprout as sculpted models: signed distance functions written in the compositor's three shaders (HLSL, Metal, WGSL), not a PNG face or -a height map. `crates/compositor/src/sculpt.rs` names the eight models and holds -their bounding boxes and hotspots; Prism Glow has none. The scene names the -model (`"/"`); text, resize, move and the other OS states keep +a height map. Prism Glow's arrow and hand are glass crystals: a triangle mesh +traced on its drawing, ray-traced in a navy rim. `crates/compositor/src/sculpt.rs` +names the ten models and holds their bounding boxes and hotspots. The scene names +the model (`"/"`); text, resize, move and the other OS states keep their built-in art and the compositor's beveled extrusion. | Theme | Arrow model | Hand model | | --- | --- | --- | | Studio Ink | Black rim, a raised ivory band, a recessed satin black field. | Ivory glove cushion in a black rim, black ridges between the fingers. | -| Prism Glow | No sculpted model: its faceted drawing is extruded as drawn, with a beveled edge, like the default cursors. | The same. | +| Prism Glow | A glass crystal cut along the facets of its drawing, set in a thin navy rim. The picture under it shows through, refracted with a slight dispersion; each facet glows faintly in its drawn colour, and a light line marks its folds. | The same crystal: a facet for each plane of the drawn hand, the fingers and thumb included. | | Pop Coral | Cut paper: flat-topped sheets, no rounded bead or gloss. A coral sheet on a navy sheet, a yellow sheet behind them offset down-left, two yellow click dashes. | A yellow sheet on a navy sheet, the navy showing between the fingers, a coral sheet behind offset down-left, two coral click dashes. | | Pixel Candy | Simplified pixel art, one cube per pixel: a plum outline, a pink body with a pale highlight down the lit edge and a darker shade along the outline. The 3D model and the 2D sprite come from the same grid. | The same, a pixel hand: the index up, three knuckles apart, the thumb. | | Star Sprout | Glossy mint cushion framed by the navy outline of its drawing, carrying a star with a face and two leaves. | Ivory glove cushion in the same navy frame, navy ridges between the fingers, a mint cuff and the same star. | @@ -29,6 +30,20 @@ sheets instead (`s_paper`). What stands in front is theme-specific: Studio Ink's ivory band, Pop Coral's yellow and coral layers and click dashes, Star Sprout's cuff, star, leaves and eyes. +Prism Glow is built from its 2D drawing alone (`prism-glow/source.png`), not from +`3d-concept.png`. `prism-glow/model/` holds the mesh: `prism_geo.py` lists its +vertices, heights and facets and writes `prism.json`; `build_blend.py` builds +`prism-glow.blend` and `prism-glow.glb` from it in Blender; `export_compositor.py` +writes the mesh into `crates/compositor/src/prism_mesh.rs` and the three shaders. +The compositor ray-traces the crystal, its triangles grouped in bounding boxes that +a ray skips when it misses them: refraction per colour +channel, total internal reflection, and an exit through its flat base onto the +picture under the cursor. That picture is a copy of the composed frame taken just +before the cursor is drawn: the recording, then its privacy blurs, then the cursor, +sharp on top, so only blurred pixels show through the glass. The rim is its +silhouette extruded, a distance field +like the other models: it gives the coverage and the shadows. + All models share one close lamp: a gradient and a highlight even on flat faces, soft self shadows, ambient occlusion and a studio environment in the reflections, plus the compositor's cast and contact shadows on the screen. The @@ -40,4 +55,6 @@ To change a model, edit it in all three shaders (`sculpt_proto`, shapes, and a test checks that the shaders mirror its constants. Pixel Candy's voxel grids are tables written by `scripts/generate-pixel-candy-voxels.mjs`: change a grid there and run the script; it rewrites all three shaders, and a -test checks the tables agree. +test checks the tables agree. To change Prism Glow, edit `prism_geo.py`, run it, +run `build_blend.py` in Blender, then `export_compositor.py`; a test checks that +the three shaders carry the mesh of `prism_mesh.rs`. diff --git a/design/cursors/README.md b/design/cursors/README.md index 8b58dc6dd..c6cd471c4 100644 --- a/design/cursors/README.md +++ b/design/cursors/README.md @@ -21,9 +21,10 @@ fringe pixels, and resize them into transparent 128 × 128 PNGs under With the 3D option on, the compositor draws the arrow and hand of Studio Ink, Pop Coral, Pixel Candy and Star Sprout as models sculpted in its shaders (`crates/compositor/src/sculpt.rs`), not from these PNGs. Prism Glow's arrow and -hand keep their PNGs and receive the shared 3D extrusion. The theme picker and -the flat cursor keep using the PNGs. Text, resize, move and other states remain -state-accurate through the built-in art and receive the shared 3D extrusion. +hand are glass crystals meshed on its drawing (`prism-glow/model/`). The theme +picker and the flat cursor keep using the PNGs. Text, resize, move and other +states remain state-accurate through the built-in art and receive the shared 3D +extrusion. `contact-sheet.png` shows the sprites enlarged on a light background; `dark-32px.png` shows them at their 32-pixel reference size on a dark background. diff --git a/design/cursors/prism-glow/model/build_blend.py b/design/cursors/prism-glow/model/build_blend.py new file mode 100644 index 000000000..4a941e04c --- /dev/null +++ b/design/cursors/prism-glow/model/build_blend.py @@ -0,0 +1,265 @@ +"""Build the Prism Glow cursor in Blender from prism.json, then save prism-glow.blend and export +prism-glow.glb next to this script. + + blender --background --python build_blend.py + +Per cursor, two objects: the faceted crystal (a closed mesh, one flat face per facet of the 2D art, +with its colour in the face attribute `concept`) and the navy rim (a filled 2D curve with a hole, +extruded and rounded). Units: 1 = 1000 px of source.png, origin on the hotspot, y up, z toward the +viewer, rim top at z = 0, everything's underside at z = -0.04. +""" + +import json +import math +import os + +import bmesh +import bpy +from mathutils import Matrix, Vector + +HERE = globals().get("HERE") or os.path.dirname(os.path.abspath(__file__)) +S = 0.001 +T_RIM = 40.0 # rim and crystal-body thickness, px +BEVEL = 5.0 # rim edge rounding, px +HAND_AT = ((1220 - 208) * S, -(50 - 42) * S) # the hand stands where it is in source.png + +D = json.load(open(os.path.join(HERE, "prism.json"))) + + +def srgb_to_linear(c): + return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 + + +def hex_rgba(h): + return tuple(srgb_to_linear(int(h[i : i + 2], 16) / 255) for i in (1, 3, 5)) + (1.0,) + + +def signed_area(pts): + return 0.5 * sum(a[0] * b[1] - b[0] * a[1] for a, b in zip(pts, pts[1:] + pts[:1])) + + +def model_xy(sh, x, y): + hx, hy = sh["hotspot"] + return ((x - hx) * S, -(y - hy) * S) + + +def fresh_collection(name): + col = bpy.data.collections.get(name) or bpy.data.collections.new(name) + if col.name not in bpy.context.scene.collection.children: + bpy.context.scene.collection.children.link(col) + for ob in list(col.objects): + bpy.data.objects.remove(ob) + return col + + +def build_crystal(name, sh): + V = sh["verts"] + bm = bmesh.new() + concept = bm.faces.layers.float_color.new("concept") + top = {k: bm.verts.new((*model_xy(sh, x, y), z * S)) for k, (x, y, z) in V.items()} + outline = list(sh["outline"]) + if signed_area([model_xy(sh, *V[k][:2]) for k in outline]) < 0: + outline.reverse() + bot = {k: bm.verts.new((*model_xy(sh, *V[k][:2]), -T_RIM * S)) for k in outline} + for f, hexcol in zip(sh["facets"], sh["colors"]): + vs = [top[k] for k in f] + if signed_area([v.co.xy[:] for v in vs]) < 0: + vs.reverse() + bm.faces.new(vs)[concept] = hex_rgba(hexcol) + side = hex_rgba("#1a3cc8") + for a, b in zip(outline, outline[1:] + outline[:1]): + bm.faces.new((top[a], bot[a], bot[b], top[b]))[concept] = side + bm.faces.new([bot[k] for k in reversed(outline)])[concept] = side + bmesh.ops.triangulate(bm, faces=[f for f in bm.faces if len(f.verts) > 3], + quad_method="BEAUTY", ngon_method="BEAUTY") + bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:]) + me = bpy.data.meshes.get(name) or bpy.data.meshes.new(name) + bm.to_mesh(me) + bm.free() + # glTF carries colours per corner: mirror the face colour there + corner = me.attributes.get("Col") or me.attributes.new("Col", "FLOAT_COLOR", "CORNER") + for p in me.polygons: + p.use_smooth = False + for li in p.loop_indices: + corner.data[li].color = me.attributes["concept"].data[p.index].color + me.color_attributes.active_color = corner + return bpy.data.objects.new(name, me) + + +def inset(loop, d): + """Offset a closed counter-clockwise loop inward by d (mitred).""" + out = [] + for i in range(len(loop)): + p0, p1, p2 = Vector(loop[i - 1]), Vector(loop[i]), Vector(loop[(i + 1) % len(loop)]) + e0, e1 = (p1 - p0).normalized(), (p2 - p1).normalized() + n0, n1 = Vector((-e0.y, e0.x)), Vector((-e1.y, e1.x)) + m = (n0 + n1).normalized() if (n0 + n1).length > 1e-6 else n0 + out.append(tuple(p1 + m * (d / max(m.dot(n0), 0.25)))) + return out + + +def build_rim(name, sh): + outer = [model_xy(sh, x, y) for x, y in sh["silhouette"]] + if signed_area(outer) < 0: + outer.reverse() + inner = [model_xy(sh, *sh["verts"][k][:2]) for k in sh["outline"]] + # the bevel rounds outward from the spline: pull the outer loop in so the silhouette stays exact + outer = inset(outer, BEVEL * S) + cu = bpy.data.curves.get(name) or bpy.data.curves.new(name, "CURVE") + cu.splines.clear() + cu.dimensions = "2D" + cu.fill_mode = "BOTH" + cu.extrude = (T_RIM / 2 - BEVEL) * S + cu.bevel_depth = BEVEL * S + cu.bevel_resolution = 3 + for loop in (outer, inner): + sp = cu.splines.new("POLY") + sp.points.add(len(loop) - 1) + for p, (x, y) in zip(sp.points, loop): + p.co = (x, y, 0.0, 1.0) + sp.use_cyclic_u = True + ob = bpy.data.objects.new(name, cu) + ob.location.z = -T_RIM / 2 * S + return ob + + +def nodes_of(name): + m = bpy.data.materials.get(name) or bpy.data.materials.new(name) + m.use_nodes = True + m.node_tree.nodes.clear() + return m, m.node_tree, m.node_tree.nodes.new("ShaderNodeOutputMaterial") + + +def material_crystal(): + """Clear glass whose facets glow with their colour in the art (the look chosen on 30/09/2026).""" + m, nt, out = nodes_of("Prism Crystal") + bsdf = nt.nodes.new("ShaderNodeBsdfPrincipled") + attr = nt.nodes.new("ShaderNodeAttribute") + attr.attribute_name = "concept" + nt.links.new(attr.outputs["Color"], bsdf.inputs["Emission Color"]) + bsdf.inputs["Base Color"].default_value = (1, 1, 1, 1) + bsdf.inputs["Emission Strength"].default_value = 0.45 + bsdf.inputs["Transmission Weight"].default_value = 1.0 + bsdf.inputs["IOR"].default_value = 1.61 + bsdf.inputs["Roughness"].default_value = 0.0 + nt.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"]) + return m + + +def material_rim(): + m, nt, out = nodes_of("Prism Rim") + bsdf = nt.nodes.new("ShaderNodeBsdfPrincipled") + bsdf.inputs["Base Color"].default_value = hex_rgba("#040c5a") + bsdf.inputs["Roughness"].default_value = 0.22 + bsdf.inputs["Coat Weight"].default_value = 1.0 + bsdf.inputs["Coat Roughness"].default_value = 0.05 + nt.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"]) + return m + + +def build(): + col = fresh_collection("Prism Glow") + crystal, rim = material_crystal(), material_rim() + obs = [] + for key, at in (("arrow", (0.0, 0.0)), ("hand", HAND_AT)): + c = build_crystal(f"prism_{key}_crystal", D[key]) + r = build_rim(f"prism_{key}_rim", D[key]) + c.data.materials.clear() + c.data.materials.append(crystal) + r.data.materials.clear() + r.data.materials.append(rim) + for ob in (c, r): + ob.location.x, ob.location.y = at + col.objects.link(ob) + obs += [c, r] + return obs + + +def studio(): + """Studio HDRI for light and reflections, the editor screenshot as the screen, a 3/4 camera.""" + sc = bpy.context.scene + w = sc.world or bpy.data.worlds.new("World") + sc.world = w + w.use_nodes = True + nt = w.node_tree + nt.nodes.clear() + env = nt.nodes.new("ShaderNodeTexEnvironment") + env.image = bpy.data.images.load(os.path.join(bpy.utils.system_resource("DATAFILES"), "studiolights", "world", "studio.exr"), check_existing=True) + env.image.pack() # the install path differs per machine; 100 KB inside the .blend does not + bg, flat = nt.nodes.new("ShaderNodeBackground"), nt.nodes.new("ShaderNodeBackground") + flat.inputs["Color"].default_value = (0.06, 0.06, 0.07, 1) + lp, mix, out = nt.nodes.new("ShaderNodeLightPath"), nt.nodes.new("ShaderNodeMixShader"), nt.nodes.new("ShaderNodeOutputWorld") + nt.links.new(env.outputs["Color"], bg.inputs["Color"]) + nt.links.new(lp.outputs["Is Camera Ray"], mix.inputs["Fac"]) + nt.links.new(bg.outputs["Background"], mix.inputs[1]) + nt.links.new(flat.outputs["Background"], mix.inputs[2]) + nt.links.new(mix.outputs["Shader"], out.inputs["Surface"]) + + shot = os.path.normpath(os.path.join(HERE, "..", "..", "..", "..", "public", "preview4.png")) + screen = bpy.data.objects.get("screen") + if screen is None and os.path.exists(shot): + me = bpy.data.meshes.new("screen") + me.from_pydata([(-3, -1.66, 0), (3, -1.66, 0), (3, 1.66, 0), (-3, 1.66, 0)], [], [(0, 1, 2, 3)]) + uv = me.uv_layers.new() + for li, st in enumerate(((0, 0), (1, 0), (1, 1), (0, 1))): + uv.data[li].uv = st + m, mnt, mout = nodes_of("screen") + em, tex = mnt.nodes.new("ShaderNodeEmission"), mnt.nodes.new("ShaderNodeTexImage") + tex.image = bpy.data.images.load(shot, check_existing=True) + mnt.links.new(tex.outputs["Color"], em.inputs["Color"]) + mnt.links.new(em.outputs["Emission"], mout.inputs["Surface"]) + me.materials.append(m) + screen = bpy.data.objects.new("screen", me) + screen.location = (1.9, -1.0, -T_RIM * S - 0.0005) + sc.collection.objects.link(screen) + + target = Vector((0.679, -0.43, 0.0)) + cam = bpy.data.objects.get("cam_34") or bpy.data.objects.new("cam_34", bpy.data.cameras.new("cam_34")) + if cam.name not in sc.collection.objects: + sc.collection.objects.link(cam) + eye = target + Matrix.Rotation(math.radians(-28), 3, "Y") @ Matrix.Rotation(math.radians(-38), 3, "X") @ Vector((0, 0, 4.4)) + f = (target - eye).normalized() + r = f.cross(Vector((0, 1, 0))).normalized() + cam.location = eye + cam.rotation_euler = Matrix((r, r.cross(f), -f)).transposed().to_euler() + cam.data.lens = 85 + sc.camera = cam + sc.render.engine = "CYCLES" + sc.cycles.device = "GPU" + sc.cycles.samples = 128 + sc.cycles.transmission_bounces = sc.cycles.max_bounces = 24 + sc.render.resolution_x, sc.render.resolution_y = 1600, 1000 + sc.view_settings.view_transform = "AgX" + sc.view_settings.look = "AgX - Medium High Contrast" + + +def export_glb(obs, path): + """The rims are curves; glTF wants meshes: bake them into temporary twins for the export.""" + dg = bpy.context.evaluated_depsgraph_get() + twins = [] + for ob in obs: + if ob.type == "CURVE": + name = ob.name + me = bpy.data.meshes.new_from_object(ob.evaluated_get(dg)) + ob.name = name + "_curve" # the twin takes the name the glTF node should carry + tw = bpy.data.objects.new(name, me) + tw.matrix_world = ob.matrix_world + me.materials.append(ob.data.materials[0]) + bpy.context.scene.collection.objects.link(tw) + twins.append((tw, ob, name)) + bpy.ops.object.select_all(action="DESELECT") + for ob in [o for o in obs if o.type == "MESH"] + [tw for tw, _, _ in twins]: + ob.select_set(True) + bpy.ops.export_scene.gltf(filepath=path, export_format="GLB", use_selection=True, + export_vertex_color="ACTIVE", export_apply=True) + for tw, ob, name in twins: + me = tw.data + bpy.data.objects.remove(tw) + bpy.data.meshes.remove(me) + ob.name = name + + +obs = build() +studio() +export_glb(obs, os.path.join(HERE, "prism-glow.glb")) +bpy.ops.wm.save_as_mainfile(filepath=os.path.join(HERE, "prism-glow.blend"), relative_remap=True) diff --git a/design/cursors/prism-glow/model/export_compositor.py b/design/cursors/prism-glow/model/export_compositor.py new file mode 100644 index 000000000..15cd261af --- /dev/null +++ b/design/cursors/prism-glow/model/export_compositor.py @@ -0,0 +1,363 @@ +"""Write the Prism Glow meshes into the compositor: `crates/compositor/src/prism_mesh.rs` and the +matching tables in the three shaders, between their `prism mesh` markers. + + python export_compositor.py # after build_blend.py has written prism-glow.glb + +Everything is expressed in the mode 15 model frame: the unit is the side of the theme's 128 px +sprite (the flat cursor's size), origin on the hotspot, x right, y DOWN, z toward the camera, +rim top at z = 0. The source-to-sprite transform is the one scripts/generate-original-cursor-themes.mjs +applies, so the crystal lands exactly on the flat cursor. +""" + +import json +import math +import struct +from pathlib import Path + +import cv2 +import numpy as np + +HERE = Path(__file__).resolve().parent +REPO = HERE.parents[3] +SRC = REPO / "crates" / "compositor" / "src" +SPRITE, INNER, ALPHA_MIN = 128, 112, 28 # generate-original-cursor-themes.mjs +BEVEL_PX = 12.0 # rounding of the rim's edges, source px +EDGE_FOLD_DEG = 25.0 +LEAF = 8 # triangles per bounding box at most +BOX_PAD = 1e-3 # model units: past prism_tri's edge slack + + +def glb_meshes(path): + """{node name: (positions (n,3), triangle indices (m,3), colours (n,3))}, glTF frame (y up).""" + b = path.read_bytes() + n = struct.unpack_from("= ALPHA_MIN + ys, xs = np.nonzero(a) + w, h = xs.max() - xs.min() + 1, ys.max() - ys.min() + 1 + scale = INNER / max(w, h) + tx, ty = (SPRITE - w * scale) / 2, (SPRITE - h * scale) / 2 + hx = (tx + (hotspot[0] - x0 - xs.min()) * scale) / SPRITE + hy = (ty + (hotspot[1] - ys.min()) * scale) / SPRITE + return scale / SPRITE, (hx, hy), ty / SPRITE + + +def crystal_triangles(pos, tri, col, sheet, s): + """Triangles of the crystal (facets and walls; the flat bottom is left to the shader), each + with its outward normal, its colour in the art and which of its edges are real fold lines.""" + # glTF (x, y up, z) -> Blender (x, y, z up), hotspot at the origin, then the model frame + blender = np.c_[pos[:, 0], -pos[:, 2], pos[:, 1]] - np.array([sheet[0], sheet[1], 0.0]) + model = np.c_[blender[:, 0], -blender[:, 1], blender[:, 2]] * (1000.0 * s) + zmin = model[:, 2].min() + tris = [] + for t in tri: + a, b, c = model[t] + nb = np.cross(blender[t[1]] - blender[t[0]], blender[t[2]] - blender[t[0]]) + nb /= np.linalg.norm(nb) + n = np.array([nb[0], -nb[1], nb[2]]) # the y mirror keeps the normal outward + if n[2] < -0.9 and abs(a[2] - zmin) < 1e-6: + continue + tris.append({"v": (a, b, c), "n": n, "col": col[t[0]], "key": tuple(map(tuple, np.round(model[t], 6)))}) + # real edges: the mesh's boundary, a change of facet colour, or a sharp fold + owners = {} + for i, t in enumerate(tris): + for e in range(3): + k = tuple(sorted((t["key"][(e + 1) % 3], t["key"][(e + 2) % 3]))) + owners.setdefault(k, []).append(i) + for i, t in enumerate(tris): + bits = 0 + for e in range(3): + k = tuple(sorted((t["key"][(e + 1) % 3], t["key"][(e + 2) % 3]))) + other = [j for j in owners[k] if j != i] + real = not other or np.abs(tris[other[0]]["col"] - t["col"]).sum() > 1e-3 or ( + tris[other[0]]["n"] @ t["n"] < math.cos(math.radians(EDGE_FOLD_DEG))) + bits |= int(real) << e + t["flags"] = (1 if abs(t["n"][2]) < 0.2 else 0) + 2 * bits + return tris, float(zmin), float(model[:, 2].max()), model + + +def cluster(tris): + """The triangles regrouped by neighbourhood, and one bounding box per group: a ray that + misses a box skips all its triangles. Groups are halves of the centroids' longest extent, + split until they hold LEAF triangles at most; the triangles come back in group order.""" + groups = [] + + def split(idx): + if len(idx) <= LEAF: + groups.append(idx) + return + cent = np.array([np.mean(tris[i]["v"], axis=0) for i in idx]) + axis = int(np.argmax(cent.max(0) - cent.min(0))) + order = [idx[j] for j in np.argsort(cent[:, axis], kind="stable")] + split(order[: len(order) // 2]) + split(order[len(order) // 2 :]) + + split(list(range(len(tris)))) + ordered, leaves = [], [] + for g in groups: + pts = np.array([v for i in g for v in tris[i]["v"]]) + leaves.append((pts.min(0) - BOX_PAD, pts.max(0) + BOX_PAD, len(ordered), len(g))) + ordered += [tris[i] for i in g] + return ordered, leaves + + +def main(): + img = cv2.imread(str(HERE.parent / "source.png"), cv2.IMREAD_UNCHANGED) + geo = json.loads((HERE / "prism.json").read_text()) + meshes = glb_meshes(HERE / "prism-glow.glb") + shapes = [] + for kind, node, (x0, x1), sheet in ( + ("arrow", "prism_arrow_crystal", (0, 887), (0.0, 0.0)), + ("hand", "prism_hand_crystal", (887, 1774), ((1220 - 208) / 1000, -(50 - 42) / 1000)), + ): + hot = geo[kind]["hotspot"] + s, (hx, hy), top = sprite_transform(img, x0, x1, hot) + tris, zmin, zmax, model = crystal_triangles(*meshes[node], sheet, s) + tris, leaves = cluster(tris) + sil = [((x - hot[0]) * s, (y - hot[1]) * s) for x, y in geo[kind]["silhouette"]] + out = [((geo[kind]["verts"][k][0] - hot[0]) * s, (geo[kind]["verts"][k][1] - hot[1]) * s) + for k in geo[kind]["outline"]] + sx = [p[0] for p in sil] + # the draw box: the sprite's full height (1 unit), the silhouette's width + bx0, bx1 = min(sx) - 0.01, max(sx) + 0.01 + shapes.append({ + "kind": kind, "tris": tris, "leaves": leaves, "sil": sil, "out": out, "thick": -zmin, + "height": zmax, + "size": (bx1 - bx0, 1.0), "hotspot": (-bx0 / (bx1 - bx0), hy), "top": top, + "per_px": s, + }) + print(f"{kind}: {len(tris)} triangles in {len(leaves)} boxes, {len(sil)} silhouette points, " + f"box {bx1 - bx0:.4f} x 1, hotspot ({-bx0 / (bx1 - bx0):.4f}, {hy:.4f}), " + f"height {zmax:.4f}, thick {-zmin:.4f}") + write_rust(shapes) + write_shaders(shapes) + + +def f(x): + return f"{x:.6f}".rstrip("0").rstrip(".") if abs(x) > 5e-7 else "0.0" + + +def fl(x): # a float literal every language reads as a float + s = f(x) + return s if "." in s else s + ".0" + + +def bevel(shapes): + return BEVEL_PX * shapes[0]["per_px"] + + +def polygons(shapes): + """Silhouettes, then outlines, in one table; their starts.""" + poly, sil_start, out_start = [], [], [] + for sh in shapes: + sil_start.append(len(poly)) + poly += sh["sil"] + for sh in shapes: + out_start.append(len(poly)) + poly += sh["out"] + return poly, sil_start, out_start + + +def records(shapes): + """Per triangle, four float4: (v0, flags), (e1, r), (e2, g), (n, b).""" + rows = [] + for sh in shapes: + for t in sh["tris"]: + a, b, c = t["v"] + e1, e2 = b - a, c - a + r, g, bl = t["col"] + rows += [(*a, t["flags"]), (*e1, r), (*e2, g), (*t["n"], bl)] + return rows + + +def boxes(shapes): + """Per box, two float4: (lo, first triangle), (hi, triangle count); each model's first box.""" + rows, box_start, k = [], [], 0 + for sh in shapes: + box_start.append(len(rows) // 2) + for lo, hi, first, count in sh["leaves"]: + rows += [(*lo, k + first), (*hi, count)] + k += len(sh["tris"]) + return rows, box_start + + +def write_rust(shapes): + tri_start, rows = [], records(shapes) + k = 0 + for sh in shapes: + tri_start.append(k) + k += len(sh["tris"]) + poly, sil_start, out_start = polygons(shapes) + box_rows, box_start = boxes(shapes) + lines = [ + "//! Les deux curseurs de Prism Glow en MAILLAGE : les facettes tracées sur l'art 2D", + "//! (`design/cursors/prism-glow/model`), taillées en cristal. Fichier GÉNÉRÉ par", + "//! `design/cursors/prism-glow/model/export_compositor.py` : ne pas l'éditer à la main.", + "//!", + "//! Repère du modèle du mode 15 : unité = côté du sprite 128 px du thème, origine au hotspot,", + "//! x à droite, y vers le bas, z vers la caméra, dessus du serti en z = 0. Chaque triangle du", + "//! cristal tient en quatre float4 : (v0, drapeaux), (e1, r), (e2, g), (normale, b) ; drapeaux =", + "//! paroi (1) + 2 × arêtes réelles (bit k : l'arête opposée au sommet k). Le fond plat, en", + "//! z = -épaisseur, est laissé au shader. `POLYS` tient les silhouettes (bord extérieur du", + "//! serti) puis les contours du cristal (son bord intérieur). `BOXES` range les triangles", + "//! par voisinage : deux float4 par boîte, (coin bas, premier triangle), (coin haut, nombre).", + "", + "/// Un des deux modèles : ses triangles et sa silhouette dans les tables, sa boîte de dessin.", + "pub struct PrismModel {", + " pub tri_start: usize,", + " pub tri_count: usize,", + " pub sil_start: usize,", + " pub sil_count: usize,", + " pub out_start: usize,", + " pub out_count: usize,", + " pub box_start: usize,", + " pub box_count: usize,", + " /// Largeur de la boîte, hauteur 1 (le sprite), en unités du modèle.", + " pub size: [f32; 2],", + " /// Hotspot, fraction de la boîte.", + " pub hotspot: [f32; 2],", + " /// Haut de la silhouette, fraction de la hauteur.", + " pub top: f32,", + " /// Sommet du cristal au-dessus de z = 0, et épaisseur sous z = 0.", + " pub height: f32,", + " pub thick: f32,", + "}", + "", + "/// Arrondi des arêtes du serti, en unités du modèle (`PRISM_BEVEL` des shaders).", + f"pub const BEVEL: f32 = {fl(bevel(shapes))};", + "", + "/// Flèche puis main, dans l'ordre des identifiants du shader.", + "pub const MODELS: [PrismModel; 2] = [", + ] + for i, sh in enumerate(shapes): + lines += [ + " PrismModel {", + f" tri_start: {tri_start[i]},", + f" tri_count: {len(sh['tris'])},", + f" sil_start: {sil_start[i]},", + f" sil_count: {len(sh['sil'])},", + f" out_start: {out_start[i]},", + f" out_count: {len(sh['out'])},", + f" box_start: {box_start[i]},", + f" box_count: {len(sh['leaves'])},", + f" size: [{fl(sh['size'][0])}, {fl(sh['size'][1])}],", + f" hotspot: [{fl(sh['hotspot'][0])}, {fl(sh['hotspot'][1])}],", + f" top: {fl(sh['top'])},", + f" height: {fl(sh['height'])},", + f" thick: {fl(sh['thick'])},", + " },", + ] + lines += ["];", "", f"pub const TRIS: [[f32; 4]; {len(rows)}] = ["] + lines += [f" [{', '.join(fl(v) for v in r)}]," for r in rows] + lines += ["];", "", f"pub const POLYS: [[f32; 2]; {len(poly)}] = ["] + lines += [f" [{fl(x)}, {fl(y)}]," for x, y in poly] + lines += ["];", "", f"pub const BOXES: [[f32; 4]; {len(box_rows)}] = ["] + lines += [f" [{', '.join(fl(v) for v in r)}]," for r in box_rows] + lines += ["];", ""] + (SRC / "prism_mesh.rs").write_text("\n".join(lines), encoding="utf-8", newline="\n") + + +def table_lines(shapes, lang): + rows = records(shapes) + poly, sil_start, out_start = polygons(shapes) + box_rows, box_start = boxes(shapes) + bcounts = [len(sh["leaves"]) for sh in shapes] + counts = [len(sh["tris"]) for sh in shapes] + scounts = [len(sh["sil"]) for sh in shapes] + ocounts = [len(sh["out"]) for sh in shapes] + if lang == "hlsl": + v4 = lambda r: f"float4({', '.join(fl(v) for v in r)})" + v2 = lambda p: f"float2({fl(p[0])}, {fl(p[1])})" + head4 = f"static const float4 PRISM_TRIS[{len(rows)}] = {{" + headb = f"static const float4 PRISM_BOXES[{len(box_rows)}] = {{" + head2 = f"static const float2 PRISM_POLY[{len(poly)}] = {{" + tail = "};" + ints = lambda name, vals: f"static const int {name}[2] = {{ {vals[0]}, {vals[1]} }};" + scalar = f"static const float PRISM_BEVEL = {fl(bevel(shapes))};" + elif lang == "metal": + v4 = lambda r: f"float4({', '.join(fl(v) for v in r)})" + v2 = lambda p: f"float2({fl(p[0])}, {fl(p[1])})" + head4 = f"constant float4 PRISM_TRIS[{len(rows)}] = {{" + headb = f"constant float4 PRISM_BOXES[{len(box_rows)}] = {{" + head2 = f"constant float2 PRISM_POLY[{len(poly)}] = {{" + tail = "};" + ints = lambda name, vals: f"constant int {name}[2] = {{ {vals[0]}, {vals[1]} }};" + scalar = f"constant float PRISM_BEVEL = {fl(bevel(shapes))};" + else: + ints = lambda name, vals: f"var {name} = array({vals[0]}, {vals[1]});" + scalar = f"const PRISM_BEVEL: f32 = {fl(bevel(shapes))};" + out = [ints("PRISM_TRI_START", [0, counts[0]]), ints("PRISM_TRI_COUNT", counts), + ints("PRISM_SIL_START", sil_start), ints("PRISM_SIL_COUNT", scounts), + ints("PRISM_OUT_START", out_start), ints("PRISM_OUT_COUNT", ocounts), + ints("PRISM_BOX_START", box_start), ints("PRISM_BOX_COUNT", bcounts)] + if lang == "wgsl": + # The triangles, polygons and boxes themselves go in a uniform buffer that compositor_linux.rs + # fills from prism_mesh.rs (a polygon point per vec4, the stride of a uniform array). As + # a `const` or `var` table, lavapipe copies them into every invocation of every + # layer: a frame without any cursor rendered 3.4 times slower. + return out + ["struct PrismMesh {", f" tris: array, {len(rows)}>,", + f" poly: array, {len(poly)}>,", + f" boxes: array, {len(box_rows)}>,", "}", scalar] + out.append(head4) + out += [f" {v4(r)}," for r in rows] + out[-1] = out[-1].rstrip(",") + out += [tail, headb] + out += [f" {v4(r)}," for r in box_rows] + out[-1] = out[-1].rstrip(",") + out += [tail, head2] + out += [f" {v2(p)}," for p in poly] + out[-1] = out[-1].rstrip(",") + out += [tail, scalar] + return out + + +BEGIN = "prism mesh: generated by design/cursors/prism-glow/model/export_compositor.py" +END = "end of the prism mesh" + + +def write_shaders(shapes): + for name, lang in (("shaders.hlsl", "hlsl"), ("shaders.metal", "metal"), ("vk_shaders/layer.wgsl", "wgsl")): + path = SRC / name + text = path.read_text(encoding="utf-8") + a, b = text.find(BEGIN), text.find(END) + if a < 0 or b < 0: + print(f"{name}: no markers yet, tables not written") + continue + start = text.index("\n", a) + 1 + stop = text.rindex("\n", 0, b) + 1 + text = text[:start] + "\n".join(table_lines(shapes, lang)) + "\n" + text[stop:] + path.write_text(text, encoding="utf-8", newline="\n") + print(f"{name}: tables written") + + +if __name__ == "__main__": + main() diff --git a/design/cursors/prism-glow/model/prism-glow.blend b/design/cursors/prism-glow/model/prism-glow.blend new file mode 100644 index 000000000..8edc4553b Binary files /dev/null and b/design/cursors/prism-glow/model/prism-glow.blend differ diff --git a/design/cursors/prism-glow/model/prism-glow.glb b/design/cursors/prism-glow/model/prism-glow.glb new file mode 100644 index 000000000..2aaa2d145 Binary files /dev/null and b/design/cursors/prism-glow/model/prism-glow.glb differ diff --git a/design/cursors/prism-glow/model/prism.json b/design/cursors/prism-glow/model/prism.json new file mode 100644 index 000000000..5aae7a713 --- /dev/null +++ b/design/cursors/prism-glow/model/prism.json @@ -0,0 +1,985 @@ +{ + "arrow": { + "hotspot": [ + 208, + 42 + ], + "verts": { + "A": [ + 237, + 100, + 0.0 + ], + "B": [ + 443, + 290, + 0.0 + ], + "F": [ + 712, + 549, + 0.0 + ], + "K": [ + 543, + 563, + 0.0 + ], + "J": [ + 612, + 744, + 0.0 + ], + "I": [ + 492, + 807, + 0.0 + ], + "H": [ + 394, + 597, + 0.0 + ], + "G": [ + 241, + 732, + 0.0 + ], + "D": [ + 241, + 491, + 0.0 + ], + "C": [ + 366, + 374, + 108.0 + ], + "E": [ + 458, + 539, + 76.0 + ], + "X": [ + 515, + 449, + 66.0 + ] + }, + "outline": [ + "A", + "B", + "F", + "K", + "J", + "I", + "H", + "G", + "D" + ], + "facets": [ + [ + "A", + "C", + "D" + ], + [ + "A", + "B", + "C" + ], + [ + "B", + "X", + "C" + ], + [ + "B", + "F", + "X" + ], + [ + "C", + "X", + "E" + ], + [ + "X", + "F", + "E" + ], + [ + "E", + "F", + "K" + ], + [ + "C", + "E", + "H" + ], + [ + "C", + "H", + "G" + ], + [ + "C", + "G", + "D" + ], + [ + "E", + "I", + "H" + ], + [ + "E", + "J", + "I" + ], + [ + "E", + "K", + "J" + ] + ], + "colors": [ + "#01f8ee", + "#cadffc", + "#a643fc", + "#c348fc", + "#033df4", + "#002cc0", + "#124efb", + "#00208a", + "#001b80", + "#00b7fb", + "#0022b7", + "#008afc", + "#00fbfb" + ], + "silhouette": [ + [ + 214, + 40 + ], + [ + 207, + 43 + ], + [ + 204, + 49 + ], + [ + 204, + 793 + ], + [ + 207, + 796 + ], + [ + 212, + 796 + ], + [ + 229, + 784 + ], + [ + 381, + 653 + ], + [ + 470, + 846 + ], + [ + 475, + 853 + ], + [ + 481, + 854 + ], + [ + 642, + 770 + ], + [ + 654, + 762 + ], + [ + 654, + 753 + ], + [ + 588, + 594 + ], + [ + 628, + 588 + ], + [ + 768, + 576 + ], + [ + 773, + 572 + ], + [ + 773, + 568 + ], + [ + 768, + 560 + ], + [ + 675, + 464 + ], + [ + 604, + 393 + ], + [ + 478, + 272 + ], + [ + 284, + 96 + ], + [ + 221, + 42 + ] + ] + }, + "hand": { + "hotspot": [ + 1220, + 50 + ], + "verts": { + "I1": [ + 1180, + 122, + 0.0 + ], + "I2": [ + 1221, + 85, + 0.0 + ], + "I3": [ + 1261, + 90, + 0.0 + ], + "I4": [ + 1290, + 132, + 0.0 + ], + "IT1": [ + 1224, + 118, + 46.0 + ], + "IT2": [ + 1274, + 135, + 35.0 + ], + "IT3": [ + 1191, + 262, + 54.0 + ], + "IL": [ + 1179, + 265, + 0.0 + ], + "P1": [ + 1292, + 427, + 0.0 + ], + "W1": [ + 1321, + 456, + 0.0 + ], + "M1": [ + 1323, + 334, + 0.0 + ], + "M2": [ + 1356, + 302, + 0.0 + ], + "M3": [ + 1395, + 309, + 0.0 + ], + "M4": [ + 1415, + 340, + 0.0 + ], + "MT1": [ + 1354, + 334, + 46.0 + ], + "MT2": [ + 1390, + 344, + 35.0 + ], + "P2": [ + 1339, + 479, + 59.0 + ], + "MB1": [ + 1386, + 481, + 54.0 + ], + "MB2": [ + 1415, + 462, + 0.0 + ], + "RL": [ + 1441, + 467, + 0.0 + ], + "R1": [ + 1443, + 368, + 0.0 + ], + "R2": [ + 1476, + 338, + 0.0 + ], + "R3": [ + 1515, + 345, + 0.0 + ], + "R4": [ + 1536, + 377, + 0.0 + ], + "RT1": [ + 1480, + 367, + 43.0 + ], + "RT2": [ + 1513, + 381, + 34.0 + ], + "RB": [ + 1448, + 475, + 57.0 + ], + "RB1": [ + 1476, + 498, + 54.0 + ], + "RB2": [ + 1535, + 489, + 0.0 + ], + "PK": [ + 1567, + 502, + 0.0 + ], + "K1": [ + 1568, + 403, + 0.0 + ], + "K2": [ + 1600, + 375, + 0.0 + ], + "K3": [ + 1637, + 382, + 0.0 + ], + "K4": [ + 1658, + 412, + 0.0 + ], + "KT1": [ + 1601, + 404, + 40.0 + ], + "KT2": [ + 1640, + 420, + 30.0 + ], + "K5": [ + 1658, + 608, + 0.0 + ], + "PR": [ + 1651, + 622, + 0.0 + ], + "BR": [ + 1522, + 818, + 0.0 + ], + "BL": [ + 1208, + 820, + 0.0 + ], + "HC": [ + 1406, + 640, + 76.0 + ], + "Q": [ + 1178, + 572, + 49.0 + ], + "TB": [ + 1128, + 682, + 0.0 + ], + "TT2": [ + 1015, + 490, + 0.0 + ], + "TL": [ + 1013, + 427, + 0.0 + ], + "TT": [ + 1069, + 402, + 0.0 + ], + "TR": [ + 1133, + 449, + 0.0 + ], + "TT1": [ + 1065, + 435, + 43.0 + ], + "TH": [ + 1144, + 541, + 40.0 + ] + }, + "outline": [ + "I1", + "I2", + "I3", + "I4", + "P1", + "W1", + "M1", + "M2", + "M3", + "M4", + "MB2", + "RL", + "R1", + "R2", + "R3", + "R4", + "RB2", + "PK", + "K1", + "K2", + "K3", + "K4", + "K5", + "PR", + "BR", + "BL", + "TB", + "TT2", + "TL", + "TT", + "TR", + "TH", + "Q", + "IL" + ], + "facets": [ + [ + "IT1", + "IT2", + "IT3" + ], + [ + "IT1", + "I2", + "I3", + "IT2" + ], + [ + "I1", + "I2", + "IT1" + ], + [ + "I1", + "IT1", + "IT3", + "IL" + ], + [ + "I3", + "I4", + "IT2" + ], + [ + "IT2", + "I4", + "P1", + "IT3" + ], + [ + "IL", + "IT3", + "P1", + "Q" + ], + [ + "Q", + "P1", + "W1" + ], + [ + "Q", + "W1", + "P2" + ], + [ + "Q", + "P2", + "HC" + ], + [ + "MT1", + "MT2", + "P2" + ], + [ + "MT1", + "M2", + "M3", + "MT2" + ], + [ + "M1", + "M2", + "MT1" + ], + [ + "M1", + "MT1", + "P2", + "W1" + ], + [ + "M3", + "M4", + "MT2" + ], + [ + "MT2", + "M4", + "MB2", + "MB1", + "P2" + ], + [ + "RT1", + "RT2", + "RB" + ], + [ + "RT1", + "R2", + "R3", + "RT2" + ], + [ + "R1", + "R2", + "RT1" + ], + [ + "R1", + "RT1", + "RB", + "RL" + ], + [ + "R3", + "R4", + "RT2" + ], + [ + "RT2", + "R4", + "RB2", + "RB1", + "RB" + ], + [ + "KT1", + "KT2", + "PK" + ], + [ + "KT1", + "K2", + "K3", + "KT2" + ], + [ + "K1", + "K2", + "KT1" + ], + [ + "K1", + "KT1", + "PK" + ], + [ + "K3", + "K4", + "KT2" + ], + [ + "KT2", + "K4", + "K5", + "PR", + "PK" + ], + [ + "P2", + "MB1", + "MB2", + "RL", + "RB", + "RB1", + "RB2", + "PK", + "HC" + ], + [ + "HC", + "PK", + "BR" + ], + [ + "PK", + "PR", + "BR" + ], + [ + "HC", + "BR", + "BL" + ], + [ + "Q", + "HC", + "BL" + ], + [ + "TT1", + "TH", + "TT2" + ], + [ + "TL", + "TT", + "TT1", + "TT2" + ], + [ + "TT", + "TR", + "TH", + "TT1" + ], + [ + "TT2", + "TH", + "TB" + ], + [ + "TH", + "Q", + "TB" + ], + [ + "Q", + "BL", + "TB" + ] + ], + "colors": [ + "#b2fcfc", + "#01fbfc", + "#00faf3", + "#01faf4", + "#00b7f4", + "#00a5eb", + "#bf54fc", + "#662ffa", + "#7237fa", + "#7651fc", + "#88fbfc", + "#00e8ee", + "#00f5f4", + "#76fafb", + "#00beed", + "#00b1f0", + "#8cfcfc", + "#00f3f3", + "#00e1ed", + "#01edf3", + "#00cef6", + "#00b7f7", + "#82fbfc", + "#01f1f5", + "#00e0ee", + "#01f1f3", + "#00c0f2", + "#6f31fb", + "#0129a6", + "#1542fb", + "#0021af", + "#002eba", + "#01f7fc", + "#bffcfc", + "#01fafc", + "#00e3f0", + "#00ddee", + "#00beef", + "#008cf5" + ], + "silhouette": [ + [ + 1209, + 50 + ], + [ + 1169, + 85 + ], + [ + 1145, + 109 + ], + [ + 1143, + 113 + ], + [ + 1142, + 404 + ], + [ + 1137, + 404 + ], + [ + 1073, + 360 + ], + [ + 1066, + 361 + ], + [ + 988, + 401 + ], + [ + 978, + 409 + ], + [ + 976, + 477 + ], + [ + 977, + 500 + ], + [ + 1060, + 640 + ], + [ + 1179, + 846 + ], + [ + 1186, + 852 + ], + [ + 1197, + 856 + ], + [ + 1530, + 856 + ], + [ + 1543, + 852 + ], + [ + 1556, + 839 + ], + [ + 1695, + 623 + ], + [ + 1695, + 401 + ], + [ + 1651, + 348 + ], + [ + 1647, + 346 + ], + [ + 1589, + 338 + ], + [ + 1584, + 340 + ], + [ + 1566, + 357 + ], + [ + 1538, + 317 + ], + [ + 1531, + 311 + ], + [ + 1469, + 301 + ], + [ + 1464, + 303 + ], + [ + 1445, + 320 + ], + [ + 1416, + 276 + ], + [ + 1412, + 273 + ], + [ + 1358, + 264 + ], + [ + 1348, + 264 + ], + [ + 1330, + 280 + ], + [ + 1328, + 256 + ], + [ + 1329, + 200 + ], + [ + 1327, + 121 + ], + [ + 1283, + 61 + ], + [ + 1278, + 57 + ], + [ + 1225, + 50 + ] + ] + } +} diff --git a/design/cursors/prism-glow/model/prism_geo.py b/design/cursors/prism-glow/model/prism_geo.py new file mode 100644 index 000000000..28a115496 --- /dev/null +++ b/design/cursors/prism-glow/model/prism_geo.py @@ -0,0 +1,157 @@ +"""Prism Glow cursor as a faceted mesh: the facets traced from ../source.png, and their heights. + +Coordinates are source-PNG pixels (x right, y down); `z` is the height above the navy rim's top in +the same pixels (unlisted vertices sit on the rim). `outline` is the crystal's edge against the rim, +`facets` are the flat faces of the 2D art, each an ordered polygon. + + python prism_geo.py # writes prism.json: vertices, facets, their colours in the art, silhouettes + +Then build the Blender scene and the glTF from it with build_blend.py. +""" + +import json +from pathlib import Path + +import cv2 +import numpy as np + +HERE = Path(__file__).resolve().parent + +ARROW = { + "hotspot": (208, 42), + "verts": { + "A": (237, 100), "B": (443, 290), "F": (712, 549), "K": (543, 563), "J": (612, 744), + "I": (492, 807), "H": (394, 597), "G": (241, 732), "D": (241, 491), + "C": (366, 374), "E": (458, 539), "X": (515, 449), + }, + "outline": ["A", "B", "F", "K", "J", "I", "H", "G", "D"], + "z": {"C": 108, "E": 76, "X": 66}, + "facets": [ + ["A", "C", "D"], ["A", "B", "C"], ["B", "X", "C"], ["B", "F", "X"], + ["C", "X", "E"], ["X", "F", "E"], ["E", "F", "K"], + # the notch H sits on the E-G line: one concept facet, folded along C-H + ["C", "E", "H"], ["C", "H", "G"], ["C", "G", "D"], + ["E", "I", "H"], ["E", "J", "I"], ["E", "K", "J"], + ], +} + +HAND = { + "hotspot": (1220, 50), + "verts": { + # index finger + "I1": (1180, 122), "I2": (1221, 85), "I3": (1261, 90), "I4": (1290, 132), + "IT1": (1224, 118), "IT2": (1274, 135), "IT3": (1191, 262), "IL": (1179, 265), + "P1": (1292, 427), + # middle finger + "W1": (1321, 456), "M1": (1323, 334), "M2": (1356, 302), "M3": (1395, 309), + "M4": (1415, 340), "MT1": (1354, 334), "MT2": (1390, 344), "P2": (1339, 479), + "MB1": (1386, 481), "MB2": (1415, 462), + # ring finger + "RL": (1441, 467), "R1": (1443, 368), "R2": (1476, 338), "R3": (1515, 345), + "R4": (1536, 377), "RT1": (1480, 367), "RT2": (1513, 381), "RB": (1448, 475), + "RB1": (1476, 498), "RB2": (1535, 489), + # pinky + "PK": (1567, 502), "K1": (1568, 403), "K2": (1600, 375), "K3": (1637, 382), + "K4": (1658, 412), "KT1": (1601, 404), "KT2": (1640, 420), "K5": (1658, 608), + "PR": (1651, 622), + # palm + "BR": (1522, 818), "BL": (1208, 820), "HC": (1406, 640), "Q": (1178, 572), + # thumb + "TB": (1128, 682), "TT2": (1015, 490), "TL": (1013, 427), "TT": (1069, 402), + "TR": (1133, 449), "TT1": (1065, 435), "TH": (1144, 541), + }, + # Each fingertip is cut like a gem: a table (the white facet), crown facets down to the rim. + # The thumb web (TH, Q) rises above the rim so the thumb's lower facets are not flat. + "z": { + "IT1": 46, "IT2": 35, "IT3": 54, + "MT1": 46, "MT2": 35, "P2": 59, "MB1": 54, + "RT1": 43, "RT2": 34, "RB": 57, "RB1": 54, + "KT1": 40, "KT2": 30, + "TT1": 43, "TH": 40, "Q": 49, + "HC": 76, + }, + "outline": [ + "I1", "I2", "I3", "I4", "P1", "W1", "M1", "M2", "M3", "M4", "MB2", "RL", "R1", "R2", + "R3", "R4", "RB2", "PK", "K1", "K2", "K3", "K4", "K5", "PR", "BR", "BL", "TB", "TT2", + "TL", "TT", "TR", "TH", "Q", "IL", + ], + "facets": [ + # index: table, crown, sides + ["IT1", "IT2", "IT3"], ["IT1", "I2", "I3", "IT2"], ["I1", "I2", "IT1"], + ["I1", "IT1", "IT3", "IL"], ["I3", "I4", "IT2"], ["IT2", "I4", "P1", "IT3"], + ["IL", "IT3", "P1", "Q"], + # palm under the index (W1 sits on the P1-P2 line) + ["Q", "P1", "W1"], ["Q", "W1", "P2"], ["Q", "P2", "HC"], + # middle finger + ["MT1", "MT2", "P2"], ["MT1", "M2", "M3", "MT2"], ["M1", "M2", "MT1"], + ["M1", "MT1", "P2", "W1"], ["M3", "M4", "MT2"], ["MT2", "M4", "MB2", "MB1", "P2"], + # ring finger + ["RT1", "RT2", "RB"], ["RT1", "R2", "R3", "RT2"], ["R1", "R2", "RT1"], + ["R1", "RT1", "RB", "RL"], ["R3", "R4", "RT2"], ["RT2", "R4", "RB2", "RB1", "RB"], + # pinky + ["KT1", "KT2", "PK"], ["KT1", "K2", "K3", "KT2"], ["K1", "K2", "KT1"], + ["K1", "KT1", "PK"], ["K3", "K4", "KT2"], ["KT2", "K4", "K5", "PR", "PK"], + # palm + ["P2", "MB1", "MB2", "RL", "RB", "RB1", "RB2", "PK", "HC"], + ["HC", "PK", "BR"], ["PK", "PR", "BR"], ["HC", "BR", "BL"], ["Q", "HC", "BL"], + # thumb + ["TT1", "TH", "TT2"], ["TL", "TT", "TT1", "TT2"], ["TT", "TR", "TH", "TT1"], + ["TT2", "TH", "TB"], ["TH", "Q", "TB"], ["Q", "BL", "TB"], + ], +} + + +def area(poly): + p = np.asarray(poly, float) + return 0.5 * np.sum(p[:, 0] * np.roll(p[:, 1], -1) - np.roll(p[:, 0], -1) * p[:, 1]) + + +def check(shape, name): + """The facets tile the outline exactly: no gap, no overlap, no stray vertex.""" + v = shape["verts"] + outline = abs(area([v[k] for k in shape["outline"]])) + facets = sum(abs(area([v[k] for k in f])) for f in shape["facets"]) + unused = set(v) - {k for f in shape["facets"] for k in f} + assert abs(facets / outline - 1) < 1e-9 and not unused, (name, facets / outline, unused) + + +def facet_colors(shape, img): + """Median colour of each facet in the art, away from its edges.""" + v = shape["verts"] + out = [] + for f in shape["facets"]: + mask = np.zeros(img.shape[:2], np.uint8) + cv2.fillPoly(mask, [np.array([v[k] for k in f], np.int32)], 255) + inner = cv2.erode(mask, np.ones((7, 7), np.uint8)) + px = img[(inner if inner.any() else mask) > 0][:, :3] + b, g, r = np.median(px, axis=0) + out.append("#%02x%02x%02x" % (int(r), int(g), int(b))) + return out + + +def silhouette(img, x0, x1): + """Outer edge of the navy rim, from the art's alpha.""" + a = (img[:, :, 3] > 127).astype(np.uint8) + a[:, :x0] = 0 + a[:, x1:] = 0 + contours, _ = cv2.findContours(a, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) + c = max(contours, key=cv2.contourArea) + return cv2.approxPolyDP(c, 1.2, True)[:, 0, :].tolist() + + +if __name__ == "__main__": + img = cv2.imread(str(HERE.parent / "source.png"), cv2.IMREAD_UNCHANGED) + out = {} + for shape, name, (x0, x1) in ((ARROW, "arrow", (0, 887)), (HAND, "hand", (887, 1774))): + check(shape, name) + z = shape["z"] + out[name] = { + "hotspot": shape["hotspot"], + "verts": {k: [*p, float(z.get(k, 0))] for k, p in shape["verts"].items()}, + "outline": shape["outline"], + "facets": shape["facets"], + "colors": facet_colors(shape, img), + "silhouette": silhouette(img, x0, x1), + } + (HERE / "prism.json").write_text(json.dumps(out, indent=1) + "\n", newline="\n") + print({k: f"{len(v['facets'])} facets" for k, v in out.items()}) diff --git a/design/cursors/requirements.md b/design/cursors/requirements.md index 5495e39c7..1cf03d987 100644 --- a/design/cursors/requirements.md +++ b/design/cursors/requirements.md @@ -21,15 +21,15 @@ their 3D models. - Every replacement theme and supported cursor state must work with the existing 3D cursor option. -- Every replacement arrow and hand except Prism Glow's is a real volume, - sculpted in the compositor's shaders (`crates/compositor/src/sculpt.rs`): - lighting, normals, self shadows and contact shadows respond to cursor tilt - and rotation. A PNG face or a height map is not a model. +- Every replacement arrow and hand is a real volume, modelled in the + compositor's shaders (`crates/compositor/src/sculpt.rs`): lighting, normals, + self shadows and contact shadows respond to cursor tilt and rotation. A PNG + face or a height map is not a model. - A rounded extrusion of the PNG silhouette remains the fallback for cursor - states that do not have a dedicated theme model, and for Prism Glow, whose - faceted drawing is extruded as drawn. The four sculpted packs add + states that do not have a dedicated theme model. Each pack adds style-specific volume: gloves with separate fingers, beveled voxels, flat - cut-paper sheets, and cushions set in the rim of their drawn outline. + cut-paper sheets, cushions set in the rim of their drawn outline, and a + faceted glass crystal that refracts the picture under it. - Model the arrow and hand separately for every sculpted theme. Do not apply one contour, material, or roundness rule to all designs. - Preserve the cursor hotspot through hover, tilt, yaw, click, and size changes. @@ -42,7 +42,7 @@ their 3D models. | Theme | Arrow | Hand | | --- | --- | --- | | Studio Ink | Black rim, raised ivory band, recessed satin black field. | Ivory glove cushion; the black outline stays as a physical rim, with black ridges between the fingers. | -| Prism Glow | The faceted drawing extruded as drawn, beveled edge; no sculpted model. | The same. | +| Prism Glow | A glass crystal cut along the drawing's facets, in a thin navy rim; the picture under it shows through, refracted with a slight dispersion, privacy blurs included; the facets glow faintly in their drawn colours. Modelled from the 2D drawing alone, keeping its polygonal simplicity. | The same, a crystal hand. | | Pop Coral | Cut paper: a coral sheet on a navy sheet, flat tops, matte; the yellow offset becomes a flat sheet behind; the click dashes become flat pieces. | A yellow sheet on a navy sheet, flat tops, matte; the coral offset becomes a flat sheet behind; the coral click dashes become flat pieces. | | Pixel Candy | Simplified pixel art that reads at 20 px as at full size; the 3D model is one cube per pixel of the same grid as the sprite. | A pixel hand from the same kind of grid: index, three knuckles, thumb. | | Star Sprout | Puffy mint cushion; the navy outline stays as a physical rim (a navy tray and a rounded bead) around it, the star and its leaves in front, each in its own navy rim. | Puffy ivory glove in the same navy rim, with navy ridges between the fingers; separate mint cuff framed in navy; the star and leaves in front, as in `star-sprout/3d-reference.png`. | @@ -55,7 +55,8 @@ becomes an inlay, side material, separate piece, or disappears. - `contact-sheet.png`: 2D PNG themes. - `3d-concept.png`: the 3D reference sheet the eight sculpted models follow - (Prism Glow is extruded), not rendered by the app. + (Prism Glow's crystal follows its 2D drawing instead), not rendered by the + app. - `star-sprout/3d-reference.png`: a closer view of the Star Sprout hand; also not rendered by the app. - `3d-direction.md`: per-theme modeling notes. diff --git a/docs/3d-effects-v2.md b/docs/3d-effects-v2.md index 41528c542..0f7faf447 100644 --- a/docs/3d-effects-v2.md +++ b/docs/3d-effects-v2.md @@ -233,9 +233,8 @@ inconnues sont ignorées). ### B.2 Le réglage **Un seul interrupteur**, `cursor.model3d` (« 3D cursor », **éteint par défaut**) : il passe -**chaque état** du curseur en 3D. La flèche et la main de quatre des cinq thèmes d'origine -deviennent leur modèle sculpté (B.3 bis) ; celles de Prism Glow, les autres états, et tous ceux -du thème par défaut (les seize de `DEFAULT_CURSOR_SPRITES` : flèche, I, main, croix, mains +**chaque état** du curseur en 3D. La flèche et la main des cinq thèmes d'origine deviennent +leur modèle (B.3 bis) ; les autres états, et tous ceux du thème par défaut (les seize de `DEFAULT_CURSOR_SPRITES` : flèche, I, main, croix, mains ouverte et fermée, redimensionnements, déplacement, interdit, attente…), leur sprite extrudé. Curseur masqué, l'interrupteur est grisé et son info-bulle dit pourquoi. Éteint, la frame est celle d'avant **à l'octet** (vérifié à plat et incliné contre le commit de base). @@ -292,11 +291,19 @@ distance signée écrites dans les trois shaders (`sculpt_proto`, `sculpt_materi pour Pixel Candy ; pour Studio Ink et Star Sprout des pièces qui gardent le trait de leur dessin : un plateau et un jonc de la couleur du trait, un coussin de couleur dedans (`s_rimmed`) ; pour Pop Coral les mêmes formes en papier découpé, des feuilles au dessus plat -(`s_paper`). Prism Glow n'a pas de modèle : son dessin à facettes est extrudé comme tout sprite -(B.3). Le PNG du thème reste l'art en 2D. En 3D, la scène nomme le modèle ; `sculpt.rs` en tient -la boîte, qui pose le hotspot (pointe de la flèche, bout de l'index), règle la garde au sol et -borne la boîte de dessin. Emplacement du cbuffer : `trail_a` = [modèle, épaisseur sous z = 0, -hauteur au-dessus, 0] (`cursor_model_cb`). +(`s_paper`). Prism Glow est à part : un **cristal en maillage**, ses facettes tracées sur son +seul dessin 2D (`design/cursors/prism-glow/model/`), serti dans sa silhouette extrudée, marine. +Le shader le lance de rayons, triangles rangés par boîtes englobantes (un rayon saute les +boîtes qu'il rate) : réfraction par canal (une légère dispersion), réflexions totales internes, +et sortie par son fond plat sur l'image sous le curseur ; chaque facette luit un peu de sa +couleur du dessin, ses plis d'un liseré clair. Cette image est une copie de la frame composée +prise juste avant le curseur : le métrage, puis ses flous de confidentialité, puis le curseur, +net par-dessus. Seuls des pixels déjà floutés passent donc à travers le verre. +`export_compositor.py` écrit le maillage dans `prism_mesh.rs` et dans les trois shaders. Le PNG +du thème reste l'art en 2D. En 3D, la scène nomme le modèle ; `sculpt.rs` en tient la boîte, qui +pose le hotspot (pointe de la flèche, bout de l'index), règle la garde au sol et borne la boîte +de dessin. Emplacement du cbuffer : `trail_a` = [modèle, épaisseur sous z = 0, hauteur +au-dessus, 0] (`cursor_model_cb`). Pixel Candy est un pixel art dessiné une fois, en grille, dans `scripts/generate-pixel-candy-voxels.mjs` : le script en tire ses PNG 2D et les tables des trois shaders, un cube par pixel plein, tous de même hauteur, colorés comme leur pixel (contour prune, diff --git a/src/lib/cursor/cursorThemes.test.ts b/src/lib/cursor/cursorThemes.test.ts index c6dceb37a..e712e8ad4 100644 --- a/src/lib/cursor/cursorThemes.test.ts +++ b/src/lib/cursor/cursorThemes.test.ts @@ -67,9 +67,7 @@ describe("normalizeCursorThemeId", () => { // The compositor models these two states itself (`crates/compositor/src/sculpt.rs`, which // knows the same names): in 3D the scene names the model, and the sprite stays the flat art. - // Prism Glow is the exception: its drawing is extruded, like the default art. - const sculptedThemes = CURSOR_THEMES.filter((theme) => theme.id !== "prism-glow"); - it.each(sculptedThemes)("names the sculpted 3D arrow and hand of $name", (theme) => { + it.each(CURSOR_THEMES)("names the sculpted 3D arrow and hand of $name", (theme) => { const flat = resolveCursorSprites(theme.id); const sprites = resolveCursorSprites(theme.id, [], true); for (const state of ["arrow", "pointer"] as const) { @@ -80,11 +78,8 @@ describe("normalizeCursorThemeId", () => { expect(sprites.text).toBe(DEFAULT_CURSOR_SPRITES.text); }); - it.each([ - DEFAULT_CURSOR_THEME_ID, - "prism-glow", - ])("leaves %s to the extruded sprite in 3D", (id) => { - for (const sprite of Object.values(resolveCursorSprites(id, [], true))) { + it("leaves the default art to the extruded sprite in 3D", () => { + for (const sprite of Object.values(resolveCursorSprites(DEFAULT_CURSOR_THEME_ID, [], true))) { expect(sprite.sculpt).toBeUndefined(); } }); diff --git a/src/lib/cursor/cursorThemes.ts b/src/lib/cursor/cursorThemes.ts index fb7cf2e11..b3a743bdc 100644 --- a/src/lib/cursor/cursorThemes.ts +++ b/src/lib/cursor/cursorThemes.ts @@ -136,9 +136,9 @@ export function readCursorAsArrow( * * To add one: drop arrow.png/pointer.png into public/cursors// and add an entry here * with hotspots normalized to the 32-logical reference (divide a 128px-pack hotspot by 4). - * Mark an asset `sculpted` when the compositor has a 3D model for it (`sculpt.rs`); other states - * are extruded from their sprite in 3D, and so is all of Prism Glow, whose faceted art reads - * best as it is drawn. An id that leaves this list reads back as the default art + * Mark an asset `sculpted` when the compositor has a 3D model for it (`sculpt.rs`; Prism Glow's + * is a crystal mesh traced on its drawing, `prism_mesh.rs`); other states are extruded from their + * sprite in 3D. An id that leaves this list reads back as the default art * through `normalizeCursorThemeId`, so a project saved with it still opens. */ export const CURSOR_THEMES: readonly CursorTheme[] = [ @@ -174,6 +174,7 @@ export const CURSOR_THEMES: readonly CursorTheme[] = [ height: 32, hotspotX: 6.3456, hotspotY: 2.0672, + sculpted: true, }, pointer: { assetPath: "cursors/prism-glow/pointer.png", @@ -181,6 +182,7 @@ export const CURSOR_THEMES: readonly CursorTheme[] = [ height: 32, hotspotX: 11.968, hotspotY: 2.0352, + sculpted: true, }, }, },