···29293030paper:
3131 just
3232+ # just analyze_times disabled because it needs manual adjustements in the render loop pipeline diagram
3233 ./shapemaker examples dna-analysis-machine --resolution 1920 paper/dna-analysis-machine.png
3334 ./shapemaker examples shapeshed --resolution 1920 paper/shapeshed.svg
3435 ./shapemaker examples colors-shed --resolution 1920 paper/colorshed.svg
···4041 #!/usr/bin/env bash
4142 cd examples/gallery
4243 ./fill.rb
4444+4545+analyze_times:
4646+ just
4747+ rm timings.log
4848+ python script/debug-performance.py
+22
paper/bibliography.yaml
···117117 url:
118118 value: https://www.xnview.com/fr/
119119 date: 2025-03-23
120120+121121+rustcrates:
122122+ title: The Rust Programming Language § 7.1 Packages and Crates
123123+ type: book
124124+ author:
125125+ - Steve Klabnik
126126+ - Carol Nichols
127127+ - Chris Krycho
128128+ url:
129129+ value: https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html
130130+ date: 2025-03-23
131131+132132+rusttraits:
133133+ title: "The Rust Programming Language § 10.2 Traits: Defining Shared Behavior"
134134+ type: book
135135+ author:
136136+ - Steve Klabnik
137137+ - Carol Nichols
138138+ - Chris Krycho
139139+ url:
140140+ value: https://doc.rust-lang.org/book/ch10-02-traits.html
141141+ date: 2025-03-23
paper/main.pdf
This is a binary file and will not be displayed.
+222-56
paper/main.typ
···1313 caption: caption,
1414)
15151616-#let diagram(caption: "", content) = figure(
1616+#let diagram(caption: "", size: 100%, content) = figure(
1717 caption: caption,
1818 kind: image,
1919- content,
1919+ scale(size, content, reflow: true),
2020+)
2121+2222+#let codesnippet(caption: "", content, lang: "rust") = block(
2323+ inset: 2em,
2424+ fill: luma(230),
2525+ radius: 4pt,
2626+ width: 100%,
2727+ breakable: false,
2828+ raw(
2929+ lang: lang,
3030+ content,
3131+ ),
2032)
21332234#show link: underline
···213225214226Bien évidemment, surtout s'il s'agit d'une vidéo synchronisée à sa bande son, il ne suffit pas de générer une frame aléatoire chaque seconde. Il faut pouvoir _réagit à des moments et rythmes clés du morceau_.
215227228228+216229= Une _crate_ Rust avec un API sympathique
217230231231+Pour implémenter cette génération, il faut donner donc un moyen à l'artiste de décrire sa procédure de génération.
232232+233233+Ainsi, Shapemaker est une bibliothèque réutilisable, ou _crate_ dans l'écosystème Rust @rustcrates.
234234+235235+La création d'un procédé de génération est conceptualisée par un canvas, composé de une ou plusieurs couches ou _layers_ d'objets. Ces objets sont _colorés_ (possèdent une information sur la manière dont il faut les remplir: bleu solide, hachures cyan, etc.), et peuvent également subir des filtres et transformations #footnote[Avec un peu de recul, le terme d'objet texturé est plus approprié, mais le code n'a pas encore changé]. Ils sont aussi _placés_ dans l'espace du canvas: le canvas possède une information de _région_, un intervalle 2D de points valables. Les objets se placent dans cette région, en stockant dans leur structure les coordonnées de _points_ marquant leur positionnement dans l'espace (coins pour un #raw(lang: "rust", "Object::Rectangle"))
236236+218237#diagram(
219219- caption: [Pipeline],
220220- scale(80%, reflow: true)[
221221- ```dot
222222- digraph G {
223223- rankdir="LR";
224224- compound=true;
225225- node[shape="record"];
238238+ caption: [Modèle objet du Canvas],
239239+ size: 90%,
240240+ ```dot
241241+ digraph {
242242+ // rankdir="LR";
243243+ node [shape="record"];
226244227227- subgraph cluster_0 {
228228- label = "Render loop"
229229- style = "filled"
230230- color = "#f0f0f0"
245245+ Canvas -> Layer [label="1+"]
246246+ region2 [label="Region"]
247247+ Layer -> region2
248248+ Canvas -> Region [label=".world_region"]
249249+ point2 [label="Point"]
250250+ Region -> point2 [label="RegionIterator"]
251251+ Layer -> ColoredObject [label="0+"]
252252+ Object -> "Object::Rectangle,\nObject::Circle,\n…" -> Point
253253+ ColoredObject -> Object
254254+ ColoredObject -> Fill
255255+ ColoredObject -> Transform
256256+ ColoredObject -> Filter
257257+ Fill -> "Fill::Solid,\nFill::Hatches,\n…" -> Color
258258+ Transform -> "Transform::Rotate,\nTransform::Translate,\n…"
259259+ Filter -> "Filter::Blur,\nFilter::Glow,\n…"
260260+ }
261261+ ```,
262262+)
263263+264264+Ce modèle mental permet de travailler plus efficacement car il est bien plus proche de la manière dont on a tendance à penser l'art visuel: sur Illustrator par exemple, ce sont des objets, organisés en plusieurs couches, qui possèdent des attributs dictant leur remplissage.
265265+266266+Les concepts de transformations et de filtres sont également très proche de ce qu'on peut retrouver dans des logiciels de création d'images raster, comme Photoshop.
267267+268268+269269+270270+#grid(
271271+ columns: (1fr, 1fr),
272272+ gutter: 1em,
273273+ [
274274+ La bibliothèque fournit une grande quantité de fonctions utiles pour redimensionner des régions, en prendre le milieu.
275275+276276+ La partie purement géométrique de la bibliothèque, définissans `Point`, `Region` et leurs opérations utiles associées (itérer les points d'une région, calculer le milieu d'une région, etc.), sont regroupées dans `shapemaker::geometry`.
277277+278278+ Les définitions des objets et de tout leurs aspects visuels (`Fill`, `Transform`, `Filter`, `Color`, `Object`, `ColoredObject`) sont regroupées dans `shapemaker::objects`.
279279+280280+ Il y a également `shapemaker::random` qui regroupe des fonctions de génération aléatoire, permettant d'introduire facilement et de manière plus ou moins granulaire, une part d'aléatoire dans le processus de génération: `Region.random_point()`, `Color::random()`, etc.
281281+282282+ Enfin, `shapemaker::rendering` implémente le rendu d'un canvas et de tout ce qu'il contient en SVG
283283+ ],
284284+ diagram(
285285+ caption: [Dépendances entre les modules de la bibliothèque],
286286+ size: 60%,
287287+ raw(
288288+ lang: "mermaid",
289289+ cut-between(
290290+ it => it == "```mermaid",
291291+ it => it == "```",
292292+ read("../src/README.md"),
293293+ ),
294294+ ),
295295+ ),
296296+)
231297232232-233233- // Create a more circular arrangement using rank constraints
234234- { rank=same; "next frame"; rasterize; }
235235- { rank=same; hooks; "render to SVG"; }
236236- { rank=same; canvas; }
237237-238238- // Set specific weights to encourage circular layout
239239- "next frame" -> hooks [weight=2];
240240- hooks -> canvas [weight=2];
241241- canvas -> "render to SVG" [weight=2];
242242- "render to SVG" -> rasterize [weight=2];
243243- rasterize -> "next frame" [weight=2];
244244-245245- // Add some balancing invisible edges
246246- "next frame" -> canvas [style=invis, weight=0.5];
247247- hooks -> "render to SVG" [style=invis, weight=0.5];
248248- canvas -> rasterize [style=invis, weight=0.5];
249249- "render to SVG" -> "next frame" [style=invis, weight=0.5];
250250- rasterize -> hooks [style=invis, weight=0.5];
251251- }
252298253253- syncdata[label="sync data"];
254299255255- audioin[label="stems .wav + BPM"]
256256- midi[label="MIDI export"]
257257- flp[label=".flp project file"]
300300+= Rendu en images
258301259259- midi -> syncdata
260260- audioin -> syncdata
261261- flp -> syncdata
302302+Maintenant que l'on a cette structure, il est bien évidemment essentiel de pouvoir la rendre en un fichier image exploitable, en PNG par exemple.
262303263263- syncdata -> "next frame"
304304+L'idée est d'exploiter le standard SVG et tout l'écosystème existant autour pour éviter d'avoir à ré-implémenter un moteur de rasterisation à la main: SVG possède déjà énormément de fonctionnalités, et faire ainsi nous permet de fournir un "escape hatch" et de fournir à Shapemaker des fragments de code SVG pour des cas spécifiques que la bibliothèque ne couvrirait pas, à travers `Object::RawSVG`, qui prend en argument un arbre SVG brut.
264305265265- usercode[label="user code"];
266266- usercode -> hooks
306306+Ce processus de rendu est réalisé via l'implémentation d'un trait, une sorte d'équivalent des interfaces dans les langages orientés objet @rusttraits:
267307268268- "rasterize" -> "video encoder"
269269- syncdata -> audio -> "video encoder"
270270- }
271271- ```
272272- ]
308308+#codesnippet(
309309+ lang: "rust",
310310+ cut-around(
311311+ it => it.trim().starts-with("pub trait SVGRenderable"),
312312+ it => it == "}",
313313+ read("../src/rendering/renderable.rs"),
314314+ ),
273315)
274316317317+Ce _trait_ est ensuite implémenté par la plupart des structures de `shapemaker::graphics`:
318318+319319+/ Canvas: rendu de toutes ses `Layer`, en prenant garde à les ordonner correctement pour que les premières couches soit déssinées par dessus les dernières
320320+/ Layer: rendu de l'ensemble des `ColoredObject` qu'elle contient, en les regroupant dans un groupe SVG #raw(lang: "svg", "<g>")
321321+/ ColoredObject: rendu de l'`Object` qu'il contient, en appliquant les transformations et filtres
322322+/ Object: dépend de la variante: `Object::Rectangle` est rendu comme un #raw(lang: "svg", "<rect>"), `Object::Circle` est rendu comme un #raw(lang: "svg", "<circle>"), etc.
323323+/ Fill: dépend de la variante: simple attribut SVG `fill` pour `Fill::Solid`, utilisation de #raw(lang: "svg", "<pattern>") pour `Fill::Hatches`, etc.
324324+/ Transform: attribut SVG `transform`
325325+/ Filter: définition d'un #raw(lang: "svg", "<filter>") avec les attributs correspondants
326326+/ Color: utilise le `ColorMapping` donné pour réaliser sa variante en une valeur de couleur SVG (notation hexadécimale)
327327+275328#diagram(
276276- caption: [Organisation des sous-modules],
277277- raw(
278278- lang: "mermaid",
279279- cut-between(
280280- it => it == "```mermaid",
281281- it => it == "```",
282282- read("../src/README.md"),
283283- ),
329329+ caption: [Objets rendables en SVG],
330330+ size: 60%,
331331+ ```dot
332332+ digraph {
333333+ // rankdir="LR";
334334+ node [shape="record", style="filled", fillcolor="#e0e000"];
335335+336336+ Canvas -> Layer
337337+ region2 [label="Region", style="solid"]
338338+ Layer -> region2
339339+ Canvas -> Region
340340+ point2 [label="Point", style="solid"]
341341+ Region -> point2
342342+ Layer -> ColoredObject
343343+ Point[style="solid"]
344344+ Object -> "Object::Rectangle,\nObject::Circle,\n…" -> Point
345345+ ColoredObject -> Object
346346+ ColoredObject -> Fill
347347+ ColoredObject -> Transform
348348+ ColoredObject -> Filter
349349+ Fill -> "Fill::Solid,\nFill::Hatches,\n…" -> Color
350350+ Transform -> "Transform::Rotate,\nTransform::Translate,\n…"
351351+ Filter -> "Filter::Blur,\nFilter::Glow,\n…"
352352+ }
353353+ ```,
354354+)
355355+Les arguments `cell_size` et `object_sizes` permettent de réaliser en valeur concrètes (pixels) les valeurs de taille abstraites: la distance unitaire entre deux points est définie par `cell_size`, et les tailles des objets, qui, par choix, n'est pas contrôlable finement, sont définies par `object_sizes`.
356356+357357+#codesnippet(
358358+ lang: "rust",
359359+ cut-around(
360360+ it => it.trim().starts-with("pub struct ObjectSizes"),
361361+ it => it == "}",
362362+ read("../src/graphics/objects.rs"),
284363 ),
285364)
286365287366288367= Render loop et hooks
368368+369369+#diagram(
370370+ caption: [Pipeline],
371371+ size: 60%,
372372+ ```dot
373373+ digraph G {
374374+ rankdir="LR";
375375+ compound=true;
376376+ node[shape="record"];
377377+378378+ subgraph cluster_0 {
379379+ label = "Render loop"
380380+ style = "filled"
381381+ color = "#f0f0f0"
382382+383383+ // Set specific weights to encourage circular layout
384384+ "next frame" -> hooks [weight=2, label="Trigger"];
385385+ hooks -> canvas [weight=2, label="Modify"];
386386+ canvas -> frame [weight=2, label="Render"];
387387+ frame -> "next frame" [weight=2];
388388+ }
389389+390390+ syncdata[label="sync data"];
391391+392392+ audioin[label="stems .wav + BPM"]
393393+ midi[label="MIDI export"]
394394+ flp[label=".flp project file"]
395395+396396+ midi -> syncdata
397397+ audioin -> syncdata
398398+ flp -> syncdata
399399+400400+ syncdata -> "next frame"
401401+402402+ usercode[label="user code"];
403403+ usercode -> hooks [label="Specifies"]
404404+405405+ frame -> video
406406+ syncdata -> audio -> video
407407+ }
408408+ ```,
409409+)
410410+411411+289412290413= Sources de synchronisation
291414···450573)
451574452575= Performance
576576+577577+#grid(
578578+ columns: (auto, auto),
579579+ diagram(
580580+ caption: [Détail de la boucle de rendu],
581581+ scale(90%, reflow: true)[
582582+ ```dot
583583+ digraph G {
584584+ compound=true;
585585+ node[shape="record"];
586586+587587+ hooks -> canvas [label="Modify"];
588588+ subgraph cluster_tosvg {
589589+ label = "SVG string rendering [0.2ms]"
590590+ canvas -> render_to_svg [label="0.1ms"]
591591+ "render_to_svg" -> stringify_svg [label="0.1ms"]
592592+ }
593593+ subgraph cluster_rasterize {
594594+ label = "Encode frame [167ms]"
595595+ stringify_svg -> "svg string"
596596+ "svg string" -> "usvg tree" [label="48ms"]
597597+ "usvg tree" -> pixmap [label="11ms"]
598598+ pixmap -> "hwc frame" [label="108ms"]
599599+ }
600600+ }
601601+ ```
602602+ ],
603603+ ),
604604+ [
605605+ // #figure(caption: "Durées d'éxécution par tâche, pour une vidéo de test de 5 secondes", csvtable(read("../results.csv"), columns: (auto, auto, auto), inset: 10pt))
606606+ #figure(
607607+ caption: "Durées d'éxécution par tâche, pour une vidéo de test de 5 secondes",
608608+ table(
609609+ columns: 3,
610610+ inset: 0.75em,
611611+ [*Tâche*], [*Durée [ms]*], [*\#*],
612612+ ..csv("../results.csv").slice(1).flatten()
613613+ ),
614614+ )
615615+616616+ Comme on peut le remarquer, il y a un gain de performance assez conséquent de possible si l'on parvient à utiliser usvg, non seulement pour la rastérisation, mais également pour la construction de l'arbre SVG: sur une boule de rendu de 167 ms, *on passe 29% du temps à parser un arbre SVG sérialisé, alors que l'on vient de construire cette arbre*.
617617+ ],
618618+)
453619454620= Conclusion
455621