Skip to content

Commit 9d8d5db

Browse files
committed
feat(cursor): sculpt the original themes' 3D arrow and hand
In 3D the compositor now models the arrow and hand of the five original themes as volumes in its shaders (mode 15), instead of a PNG face over a relief map. A close lamp, self shadows and occlusion light them, and the extruded sprites of the other states. sculpt.rs holds each model's id, box and hotspot; the scene names the model per sprite. The relief maps and their generator are removed.
1 parent 0cdc9bc commit 9d8d5db

50 files changed

Lines changed: 2858 additions & 1038 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎crates/compositor/src/compositor_linux.rs‎

Lines changed: 112 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -341,8 +341,8 @@ pub struct Compositor {
341341
/// touche depuis appartient au jeu actif et ne peut pas etre evince -- voir
342342
/// `cached_image`.
343343
img_frame_start: std::cell::Cell<u64>,
344-
/// Champs et reliefs des sprites de curseur (mode 15), RG16F, par chemin, avec leur forme.
345-
/// Pas d'eviction : l'ensemble des cartes livrees reste inferieur a quelques dizaines de Mo.
344+
/// Champs de distance des sprites de curseur (mode 15), R16F, par chemin, avec leur forme.
345+
/// Pas d'eviction : seuls les sprites du theme en cours y passent (~2,6 Mo pour les seize).
346346
sdf_cache: RefCell<std::collections::HashMap<String, (wgpu::Texture, SpriteShape)>>,
347347

348348
/// Copie mipmappee de la frame composee, lue par les annotations « flou »
@@ -1321,27 +1321,22 @@ impl Compositor {
13211321
Ok((tex, w, h))
13221322
}
13231323

1324-
/// Champ et relief du sprite `path` (binding 2 du mode 15) et sa forme, calcules au
1324+
/// Champ de distance du sprite `path` (binding 2 du mode 15) et sa forme, calcules au
13251325
/// premier appel. Parite `compositor_windows::cursor_sdf`.
1326-
fn cursor_sdf(
1327-
&self,
1328-
path: &str,
1329-
depth_path: Option<&str>,
1330-
) -> Result<(wgpu::Texture, SpriteShape)> {
1331-
let cache_key = format!("{path}\0{}", depth_path.unwrap_or_default());
1332-
if let Some(hit) = self.sdf_cache.borrow().get(&cache_key) {
1326+
fn cursor_sdf(&self, path: &str) -> Result<(wgpu::Texture, SpriteShape)> {
1327+
if let Some(hit) = self.sdf_cache.borrow().get(path) {
13331328
return Ok(hit.clone());
13341329
}
1335-
let sdf = crate::cursor_sdf::CursorSdf::load_with_depth(path, depth_path)?;
1336-
let texels = sdf.rg16_bytes();
1330+
let sdf = crate::cursor_sdf::CursorSdf::load(path)?;
1331+
let texels = sdf.f16_bytes();
13371332
let size = wgpu::Extent3d { width: sdf.width, height: sdf.height, depth_or_array_layers: 1 };
13381333
let tex = self.gpu.device.create_texture(&wgpu::TextureDescriptor {
13391334
label: Some("cursor-sdf"),
13401335
size,
13411336
mip_level_count: 1,
13421337
sample_count: 1,
13431338
dimension: wgpu::TextureDimension::D2,
1344-
format: wgpu::TextureFormat::Rg16Float,
1339+
format: wgpu::TextureFormat::R16Float,
13451340
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
13461341
view_formats: &[],
13471342
});
@@ -1355,13 +1350,13 @@ impl Compositor {
13551350
&texels,
13561351
wgpu::TexelCopyBufferLayout {
13571352
offset: 0,
1358-
bytes_per_row: Some(sdf.width * 4),
1353+
bytes_per_row: Some(sdf.width * 2),
13591354
rows_per_image: Some(sdf.height),
13601355
},
13611356
size,
13621357
);
13631358
let entry = (tex, sdf.shape);
1364-
self.sdf_cache.borrow_mut().insert(cache_key, entry.clone());
1359+
self.sdf_cache.borrow_mut().insert(path.to_string(), entry.clone());
13651360
Ok(entry)
13661361
}
13671362

@@ -2832,9 +2827,9 @@ impl Compositor {
28322827
// Curseur modelise (mode 15) : ce meme sprite extrude, `plan_cursor` en a tire la
28332828
// pose. Sprite au binding 1 (texY), champ au binding 2 (texU). Parite Windows/macOS.
28342829
if let Some(pose) = plan.model {
2835-
match self.cursor_sdf(&sprite.path, sprite.model_depth_path.as_deref()) {
2830+
match self.cursor_sdf(&sprite.path) {
28362831
Ok((sdf, shape)) => {
2837-
let shape = SpriteShape { hotspot, ..shape };
2832+
let shape = crate::frame_geometry::model_shape(sprite, shape);
28382833
let sdf_view = sdf.create_view(&wgpu::TextureViewDescriptor::default());
28392834
for placement in placements {
28402835
let Some(cb) = cursor_model_cb(
@@ -5137,16 +5132,52 @@ mod tests {
51375132
comp.set_cursor(track.clone());
51385133
comp.set_cursor_time(Some(2.0));
51395134
comp.set_timeline_time(Some(2.0));
5135+
let cfg = model_cfg();
5136+
unsafe {
5137+
comp.compose_frame(screen.as_ptr(), screen.as_ptr(), 0.0, &cfg).expect("compose_frame");
5138+
comp.readback_direct().expect("readback_direct").2
5139+
}
5140+
}
5141+
5142+
fn model_cfg() -> crate::config::Cfg {
51405143
let mut cfg = crate::config::Cfg::c8();
51415144
cfg.bg_blur = false;
51425145
cfg.zoom = false;
51435146
cfg.layout_anim = false;
51445147
cfg.cursor = true;
51455148
cfg.mblur_n = 1;
51465149
cfg.shadow = false;
5147-
unsafe {
5148-
comp.compose_frame(screen.as_ptr(), screen.as_ptr(), 0.0, &cfg).expect("compose_frame");
5149-
comp.readback_direct().expect("readback_direct").2
5150+
cfg
5151+
}
5152+
5153+
/// Le pixel du hotspot et l'unité du modèle (px) que `plan_cursor` donne à la scène que
5154+
/// `compose_model` rend.
5155+
fn model_tip(json: &str, track: &crate::cursor::CursorTrack) -> ([f32; 2], f32) {
5156+
let scene = crate::scene::Scene::from_json(json).expect("scene json");
5157+
let (cfg, live, render_px, src) = (model_cfg(), live_params_from_scene(&scene), [1280.0, 720.0], [640.0, 360.0]);
5158+
let g = plan_frame(&FrameGeometryInput {
5159+
render_px,
5160+
screen_tex_px: src,
5161+
screen_visible_px: src,
5162+
webcam_visible_px: src,
5163+
u_max: 1.0,
5164+
v_max: 1.0,
5165+
frame: 0.0,
5166+
cfg: &cfg,
5167+
live,
5168+
scene: Some(&scene),
5169+
cursor: Some(track),
5170+
timeline_t_override: Some(2.0),
5171+
programme_time: None,
5172+
});
5173+
let input = CursorPlanInput { render_px, u_max: 1.0, v_max: 1.0, cfg: &cfg, live, scene: Some(&scene), track, t: 2.0 };
5174+
let plan = plan_cursor(&g, &input).expect("un curseur a dessiner");
5175+
match plan.placement {
5176+
CursorPlacement::Tilted { plane_pt, quad, center_px, .. } => {
5177+
let (x, y) = quad.point_px(plane_pt[0], plane_pt[1]);
5178+
([center_px[0] + x, center_px[1] + y], plan.size_px * quad.scale)
5179+
}
5180+
CursorPlacement::Upright { center } => ([center[0] * render_px[0], center[1] * render_px[1]], plan.size_px),
51505181
}
51515182
}
51525183

@@ -5384,6 +5415,67 @@ mod tests {
53845415
assert!(failures.is_empty(), "{failures:#?}");
53855416
}
53865417

5418+
/// Pendant de `the_sculpted_cursors_stand_at_the_hotspot` (Windows) : la flèche et la main
5419+
/// sculptées des thèmes d'origine (`sculpt.rs`) passent par le WGSL, tiennent au hotspot, en
5420+
/// bas à droite de lui, et portent leur ombre en l'air.
5421+
#[test]
5422+
fn the_sculpted_cursors_stand_at_the_hotspot() {
5423+
let Some(gpu) = gpu() else { return };
5424+
let comp = Compositor::new_sized(&gpu, 1280, 720).expect("Compositor::new_sized");
5425+
let (y, uv) = model_screen_planes(false);
5426+
let blue = FakeFrame::from_planes(&gpu, 640, 360, &y, &uv);
5427+
let (y, uv) = model_screen_planes(true);
5428+
let orange = FakeFrame::from_planes(&gpu, 640, 360, &y, &uv);
5429+
let extruded = model_scene_json("null", Some(true), "default", true, 5.0);
5430+
let hidden = model_scene_json("null", Some(true), "default", false, 5.0);
5431+
let bare = compose_model(&comp, &blue, &hidden, &model_track("arrow", false, 0.5));
5432+
let mut failures = Vec::new();
5433+
for state in ["arrow", "pointer"] {
5434+
let still = model_track(state, false, 0.5);
5435+
let sprite = compose_model(&comp, &blue, &extruded, &still);
5436+
for theme in ["studio-ink", "prism-glow", "pop-coral", "pixel-candy", "star-sprout"] {
5437+
// Le sprite reste celui du theme par defaut : seul le nom pose le modele.
5438+
let json = extruded
5439+
.replace(&format!(r#"/{state}.png","#), &format!(r#"/{state}.png","sculpt":"{theme}/{state}","#));
5440+
let (hover, hover_b) = (compose_model(&comp, &blue, &json, &still), compose_model(&comp, &orange, &json, &still));
5441+
model_save(&format!("sculpt-{theme}-{state}"), &hover);
5442+
if hover == sprite {
5443+
failures.push(format!("{theme}/{state}: le sprite extrude au lieu du modele"));
5444+
}
5445+
let (tip, u) = model_tip(&json, &still);
5446+
let mask = model_opaque(&hover, &hover_b, &bare);
5447+
let (mut body, mut c, mut near) = (0usize, [0.0f32; 2], f32::MAX);
5448+
for (i, _) in mask.iter().enumerate().filter(|(_, m)| **m) {
5449+
let (x, y) = ((i % 1280) as f32, (i / 1280) as f32);
5450+
body += 1;
5451+
c = [c[0] + x, c[1] + y];
5452+
near = near.min((x - tip[0]).hypot(y - tip[1]));
5453+
}
5454+
let c = [c[0] / body.max(1) as f32, c[1] / body.max(1) as f32];
5455+
let shadow = (0..1280 * 720)
5456+
.filter(|&i| {
5457+
let (a, b) = (&hover[i * 4..i * 4 + 3], &bare[i * 4..i * 4 + 3]);
5458+
!mask[i] && a != b && model_luma(a) < model_luma(b) - 6.0
5459+
})
5460+
.count();
5461+
println!("{theme}/{state} : unite {u:.1} px, corps {body} px, a {near:.1} px du hotspot {tip:?}, centroide {c:?}, ombre {shadow} px");
5462+
if (body as f32) < 0.12 * u * u {
5463+
failures.push(format!("{theme}/{state}: {body} px de corps pour {u:.0} px d'unite"));
5464+
}
5465+
if near > 0.08 * u {
5466+
failures.push(format!("{theme}/{state}: le modele est a {near:.1} px du hotspot"));
5467+
}
5468+
if !(c[0] > tip[0] && c[1] > tip[1] + 0.2 * u) {
5469+
failures.push(format!("{theme}/{state}: corps en {c:?}, pas en bas a droite de {tip:?}"));
5470+
}
5471+
if shadow * 10 < body * 3 {
5472+
failures.push(format!("{theme}/{state}: pas d'ombre en l'air ({shadow} px)"));
5473+
}
5474+
}
5475+
}
5476+
assert!(failures.is_empty(), "{failures:#?}");
5477+
}
5478+
53875479
/// Pendant de `tests/cursor_tap_render.rs` (Windows) : sous un lissage qui traine loin
53885480
/// derriere la souris, la pointe du modele se pose sur la pastille rouge du clic, et l'anneau
53895481
/// de l'impact (mode 16) l'entoure, a plat et incline.

‎crates/compositor/src/compositor_macos.rs‎

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,8 @@ pub struct Compositor {
282282
/// Valeur de `img_tick` au début de la frame en cours. Tout ce qui a été touché depuis
283283
/// appartient au jeu actif et ne peut pas être évincé — voir `cached_image`.
284284
img_frame_start: std::cell::Cell<u64>,
285-
/// Champs et reliefs des sprites de curseur (mode 15), RG16F, par chemin, avec leur forme.
286-
/// Pas d'éviction : l'ensemble des cartes livrées reste inférieur à quelques dizaines de Mo.
285+
/// Champs de distance des sprites de curseur (mode 15), R16F, par chemin, avec leur forme.
286+
/// Pas d'éviction : seuls les sprites du thème en cours y passent (~2,6 Mo pour les seize).
287287
sdf_cache: RefCell<
288288
std::collections::HashMap<String, (metal::Texture, crate::frame_geometry::SpriteShape)>,
289289
>,
@@ -908,23 +908,18 @@ impl Compositor {
908908
Ok((tex, w, h))
909909
}
910910

911-
/// Champ et relief du sprite `path` (texture(4) du mode 15) et sa forme, calculés au
912-
/// premier appel. Parité `compositor_windows::cursor_sdf`. RG16Float : filtrable sur tous
911+
/// Champ de distance du sprite `path` (texture(4) du mode 15) et sa forme, calculés au
912+
/// premier appel. Parité `compositor_windows::cursor_sdf`. R16Float : filtrable sur tous
913913
/// les GPU Apple, contrairement au R32Float.
914-
fn cursor_sdf(
915-
&self,
916-
path: &str,
917-
depth_path: Option<&str>,
918-
) -> Result<(metal::Texture, crate::frame_geometry::SpriteShape)> {
919-
let cache_key = format!("{path}\0{}", depth_path.unwrap_or_default());
920-
if let Some(hit) = self.sdf_cache.borrow().get(&cache_key) {
914+
fn cursor_sdf(&self, path: &str) -> Result<(metal::Texture, crate::frame_geometry::SpriteShape)> {
915+
if let Some(hit) = self.sdf_cache.borrow().get(path) {
921916
return Ok(hit.clone());
922917
}
923-
let sdf = crate::cursor_sdf::CursorSdf::load_with_depth(path, depth_path)?;
924-
let texels = sdf.rg16_bytes();
918+
let sdf = crate::cursor_sdf::CursorSdf::load(path)?;
919+
let texels = sdf.f16_bytes();
925920
let tex = make_texture(
926921
&self.gpu.device,
927-
metal::MTLPixelFormat::RG16Float,
922+
metal::MTLPixelFormat::R16Float,
928923
sdf.width,
929924
sdf.height,
930925
metal::MTLStorageMode::Shared,
@@ -937,10 +932,10 @@ impl Compositor {
937932
},
938933
0,
939934
texels.as_ptr() as *const std::ffi::c_void,
940-
(sdf.width * 4) as u64,
935+
(sdf.width * 2) as u64,
941936
);
942937
let entry = (tex, sdf.shape);
943-
self.sdf_cache.borrow_mut().insert(cache_key, entry.clone());
938+
self.sdf_cache.borrow_mut().insert(path.to_string(), entry.clone());
944939
Ok(entry)
945940
}
946941

@@ -1977,12 +1972,9 @@ impl Compositor {
19771972
let (tex, iw, ih) = self.cached_image(sprite.path.as_str())?;
19781973
// Sans champ de distance, repli sur le sprite plat plutôt qu'aucun curseur. Parité Linux.
19791974
if let Some(pose) = model {
1980-
match self.cursor_sdf(sprite.path.as_str(), sprite.model_depth_path.as_deref()) {
1975+
match self.cursor_sdf(sprite.path.as_str()) {
19811976
Ok((sdf, shape)) => {
1982-
let shape = crate::frame_geometry::SpriteShape {
1983-
hotspot: [sprite.hotspot_x, sprite.hotspot_y],
1984-
..shape
1985-
};
1977+
let shape = crate::frame_geometry::model_shape(sprite, shape);
19861978
if let Some(cb) = crate::frame_geometry::cursor_model_cb(
19871979
placement, size_px, pose, shape, a, clip,
19881980
) {

‎crates/compositor/src/compositor_windows.rs‎

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,8 @@ pub struct Compositor {
179179
/// Valeur de `img_tick` au début de la frame en cours. Tout ce qui a été touché depuis
180180
/// appartient au jeu actif et ne peut pas être évincé — voir `cached_image`.
181181
img_frame_start: std::cell::Cell<u64>,
182-
/// Champs et reliefs des sprites de curseur (mode 15), RG16F, par chemin, avec leur forme.
183-
/// Pas d'éviction : l'ensemble des cartes livrées reste inférieur à quelques dizaines de Mo.
182+
/// Champs de distance des sprites de curseur (mode 15), R16F, par chemin, avec leur forme.
183+
/// Pas d'éviction : seuls les sprites du thème en cours y passent (~2,6 Mo pour les seize).
184184
sdf_cache: RefCell<HashMap<String, (ID3D11ShaderResourceView, SpriteShape)>>,
185185
/// Masque de segmentation du sujet webcam, R8 à la résolution du modèle. Écrit par
186186
/// `set_webcam_mask` depuis le thread d'inférence, lu au moment de dessiner la webcam.
@@ -1088,24 +1088,19 @@ impl Compositor {
10881088
Ok((srv.unwrap(), w, h))
10891089
}
10901090

1091-
/// Champ et carte de relief du sprite `path` (t4 du mode 15) et sa forme, calculés au premier appel.
1092-
unsafe fn cursor_sdf(
1093-
&self,
1094-
path: &str,
1095-
depth_path: Option<&str>,
1096-
) -> Result<(ID3D11ShaderResourceView, SpriteShape)> {
1097-
let cache_key = format!("{path}\0{}", depth_path.unwrap_or_default());
1098-
if let Some(hit) = self.sdf_cache.borrow().get(&cache_key) {
1091+
/// Champ de distance du sprite `path` (t4 du mode 15) et sa forme, calculés au premier appel.
1092+
unsafe fn cursor_sdf(&self, path: &str) -> Result<(ID3D11ShaderResourceView, SpriteShape)> {
1093+
if let Some(hit) = self.sdf_cache.borrow().get(path) {
10991094
return Ok(hit.clone());
11001095
}
1101-
let sdf = crate::cursor_sdf::CursorSdf::load_with_depth(path, depth_path)?;
1102-
let texels = sdf.rg16_bytes();
1096+
let sdf = crate::cursor_sdf::CursorSdf::load(path)?;
1097+
let texels = sdf.f16_bytes();
11031098
let td = D3D11_TEXTURE2D_DESC {
11041099
Width: sdf.width,
11051100
Height: sdf.height,
11061101
MipLevels: 1,
11071102
ArraySize: 1,
1108-
Format: DXGI_FORMAT_R16G16_FLOAT,
1103+
Format: DXGI_FORMAT_R16_FLOAT,
11091104
SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
11101105
Usage: D3D11_USAGE_IMMUTABLE,
11111106
BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32,
@@ -1114,15 +1109,15 @@ impl Compositor {
11141109
};
11151110
let init = D3D11_SUBRESOURCE_DATA {
11161111
pSysMem: texels.as_ptr() as *const c_void,
1117-
SysMemPitch: sdf.width * 4,
1112+
SysMemPitch: sdf.width * 2,
11181113
SysMemSlicePitch: 0,
11191114
};
11201115
let mut tex: Option<ID3D11Texture2D> = None;
11211116
self.dev.CreateTexture2D(&td, Some(&init), Some(&mut tex))?;
11221117
let mut srv: Option<ID3D11ShaderResourceView> = None;
11231118
self.dev.CreateShaderResourceView(&tex.unwrap(), None, Some(&mut srv))?;
11241119
let entry = (srv.unwrap(), sdf.shape);
1125-
self.sdf_cache.borrow_mut().insert(cache_key, entry.clone());
1120+
self.sdf_cache.borrow_mut().insert(path.to_string(), entry.clone());
11261121
Ok(entry)
11271122
}
11281123

@@ -1535,10 +1530,9 @@ impl Compositor {
15351530
// Sans champ de distance, repli sur le sprite plat plutôt que sur le curseur math.
15361531
// Parité Linux.
15371532
if let Some(pose) = model {
1538-
match self.cursor_sdf(path, sprite.model_depth_path.as_deref()) {
1533+
match self.cursor_sdf(path) {
15391534
Ok((sdf, shape)) => {
1540-
let shape =
1541-
SpriteShape { hotspot: [sprite.hotspot_x, sprite.hotspot_y], ..shape };
1535+
let shape = crate::frame_geometry::model_shape(sprite, shape);
15421536
if let Some(cb) = crate::frame_geometry::cursor_model_cb(
15431537
placement, size_px, pose, shape, a, clip,
15441538
) {

0 commit comments

Comments
 (0)