Comment faire des shadings à deux dimensions mais aussi efficacement qu'avec CGShading ?

Je travaille sur un color picker et donc je dessine plein de dégradé avec CGGradient ou CGShading.
J'ai reproduit les sliders colorés du color picker de macOS sans trop d'effort avec AppKit (SwiftUI n'est utilisé que comme wrapper pour la preview) :

Niveau technique je ne redessine le fond que quand il y en a besoin et garde une version rasterisée dans une CGImage. Les sliders RGB utilisent un CGGradient et pour HSB j'ai des CGShading customs qui permettent de dessiner des dégradé entre deux couleurs mais en faisait varier les composantes HSB plutôt que RGB. J'en ai fait un package Swift pour ceux qui seraient intéressés.

Mais j'ai aussi prévu d'ajouter ce genre de contrôle:

Ici il est configuré pour faire varier la hue en X et la sat en Y. J'ai tenté de superposer 2 shadings en changeant le blend mode et c'est ce que j'obtiens de plus approchant après quelques (beaucoup) essais / erreurs:

C'est inutilisable et j'ai comme l'impression que le blend mode se base sur les composantes HSL. Ce qui est une piste, mais je peux me tromper. Mais avant de me taper des shading HSL j'aimerai bien certaines certitudes...

Bref j'ai tenté le coup en dessinant directement dans un bitmap en créant un CGContext et en le rasterisant dans une CGImage. Ça marche mais c'est leeeeent. Je m'en doutais, je voulais voir combien ça serait lent. Et c'est inutilisable en l'état.

Alors que faire ? Core Image ? J'en ai pas fait depuis presque 10 ans et je me souviens que le rendu GPU était crado sur Mac et le rendu CPU trop lent. Mais j'imagine que peux lui redonner sa chance sauf si vous me dites que ça n'a pas changé.

Ou une technique de shading 2D que je ne connais pas ? Le faire en Metal me parait un peu exagéré, d'autant que je n'y ai jamais touché.

Réponses

  • klogklog Membre
    avril 2022 modifié #2

    Tu peux créer une petite MTKView, et faire un rendu avec un fragment shader (très simple à écrire)... Avantage, c'est super souple (tu peux tout faire dans le fragment shader) et super rapide. Inconvénient : la mise en place du rendu Metal sur la MTKView est un peu laborieuse la première fois, mais une fois écrite, tu peux te servir de ce mécanisme pour plein d'autres trucs... Les samples d'exemples Apple sont plutôt bien écrits.

    La mise en cache est automatique (dans la texture Metal du buffer de rendu), et tu peux la convertir assez simplement en CGImage.

    Il y a un autre avantage d'utiliser ce type de dispositif quand tu travailles sur la couleur/ luminosité : Metal permet d'englober l'environnement EDR d'Apple (et donc les écrans HDR), ce qui n'est pas le cas sur une NSView standard.

  • Tu me tente bien avec Metal @klog c'est toujours cool de découvrir une nouvelle technologie.

    Par contre l'histoire de la MTKView me refroidi un peu. Ça m'arrange pas d'ajouter une subviews à ma hiérarchie. J'aime les traditions et là c'est du NSControl + NSActionCell comme le faisait nos ancêtres.
    L'idéal serait du rendu offscreen directement stocké dans une CGImage ou un NSBitmapImageRep que j'affiche à chaque passe de draw et que je mets à jour au besoin.

    J'ai un peux regardé et ça m'a l'air assez touffu et je ne sais pas trop par où commencer, tu peux me donner un nom de classe à utiliser ? Après je fouille et je me débrouille. Bien sûr si tu as un guide complet qui explique tout sous la main je prends 😉

    PS: tu peux me donner la définition de l'acronyme EDR au passage ?

  • klogklog Membre
    avril 2022 modifié #4

    @Pyroh a dit :
    Tu me tente bien avec Metal @klog c'est toujours cool de découvrir une nouvelle technologie.

    Par contre l'histoire de la MTKView me refroidi un peu. Ça m'arrange pas d'ajouter une subviews à ma hiérarchie. J'aime les traditions et là c'est du NSControl + NSActionCell comme le faisait nos ancêtres.

    Tu veux dire que tu veux du NSControl + NSActionCell ? Parce que MTKView hérite de NSView... MTKView est simplement là pour te simplifier un peu la vie.

    L'idéal serait du rendu offscreen directement stocké dans une CGImage ou un NSBitmapImageRep que j'affiche à chaque passe de draw et que je mets à jour au besoin.

    "Directement dans un CGImage ou ..." à partir de Metal ça ne sera pas possible. Mais le rendu offscreen est possible, et la construction d'une CGImage à partir de la texture du buffer de rendu de la MTKView est triviale (et très rapide).

    J'ai un peux regardé et ça m'a l'air assez touffu et je ne sais pas trop par où commencer

    Ca je te l'accorde, Metal n'est pas simple d'accès. L'idéal c'est de dépiauter la doc et les samples d'Apple qui sont plutôt bien foutus.

    tu peux me donner un nom de classe à utiliser ?

    Les objets Metal sont essentiellement des protocoles.

    Il y a tous les objets qui permettent de construire le pipeline de rendu (c'est le plus bordélique à comprendre, même si c'est assez bien structuré) :
    MTLDevice
    MTLCommandQueue
    MTLCommandEncoder

    Il y a les classes de ressources :
    MTLBuffer (don't tu n'auras probablement pas besoin)
    MTLTexture (dans lesquels tu feras ton rendu)

    Après il y a un language de shaders qui est du C++ avec des trucs en plus (et en moins) afin de servir l'objectif de rendu (specs : https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf). C'est la dedans que tu pourras faire ce que tu veux à fond la caisse.

    La doc :
    https://developer.apple.com/documentation/metal/

    Point d'entrée pour développeurs :
    https://developer.apple.com/metal/

    Je te conseille de suivre pas à pas le fonctionnement d'un sample de chez Apple (https://developer.apple.com/metal/sample-code/). Un bon point de départ pour la configuration initiale d'un pipeline basique est : https://developer.apple.com/documentation/metal/using_metal_to_draw_a_view_s_contents

    Intéressant aussi comme point de départ : https://developer.apple.com/documentation/metal/using_a_render_pipeline_to_render_primitives

    PS: tu peux me donner la définition de l'acronyme EDR au passage ?

    Extended Dynamic Range.
    En gros la mécanique d'Apple est la suivante : tu fais tes rendus dans des buffers RGBA dont les composantes sont des floats ou half floats, et tout ce qui dépasse 1 n'est pas limité à 1 comme habituellement, mais est directement mappé sur la capacité HDR du moniteur.

    Par exemple, l'écran des derniers MBP permet 1600 nits, avec un point blanc à 500 nits... donc pour un canal dont la valeur est 1, tu auras une intensité de 500 nits, et pour un canal de valeur 1600 / 500 = 3.2, tu auras 1600 nits. Avantage ? Tu as du HDR, un rendu plus fidèle quand les contrastes sont élevés, et des teintes préservés en hautes lumières (tu gardes les teintes avec de hautes intensités : [1 1 2] apparaitra bleu alors qu'en SDR il sera clampé à [1 1 1] et donc blanc).

    Apple fournit les fonctions qui te permettent d'obtenir les valeurs max de l'écran dans laquelle beigne ta vue.

    N'hésite pas à me poser des questions... j'essaierai d'y répondre :wink:

  • PyrohPyroh Membre

    Disclaimer: J'ai mis plus d'une semaine à écrire cette réponse. Au fur et à mesure que j'expliquais mes soucis j'avais des idées de résolution. Bref vous m'avez bien aidé sans le savoir !

    Avant toute chose, merci @klog pour ta réponse. J'ai pas pu répondre avant parce que j'ai eu beaucoup de travail client ces derniers jours (c'est aussi bien de gagner des sous) et je voulais finir avant de faire un retour.

    Concrètement j'ai regardé Metal et ma première réaction a été "Mer il et fou !". Plein de chose à mettre en place, de boilerplates et de trucs que je ne suis pas sur de comprendre. Je me suis demandé si le jeu en valait la chandelle.
    Mais bref je commence à algorithmer avec dans l'idée de faire un MTLBuffer pour calculer rapidement les composantes du buffer d'une CGImage. Comme ça je peux réutiliser mon algo et je ne m'attaque qu'à la mise en place de Metal. Puis je me suis rendu compte que c'était complètement con. Alors je suis allé dans le sens de MTLTexture mais en cherchant comment faire un rendu sur une texture j'ai appris que CoreImage s'était bien amélioré et qu'on me conseillait d'utiliser ce dernier. Parce que maintenant c'est juste des kernels Metal "simplifiés".

    Sans trop rentrer dans les détails ni dans mes errances j'obtiens ça :

    Ça fonctionne bien. J'ai un kernel pour le premier, un pour le deuxième et le dernier c'est plus complexe.
    Pour 1 et 2 je met un aplat de couleur noir ou blanc, c'est selon, et j'affiche l'image générée par le kernel en faisant varier l'alpha. Au final on ne génère l'image que quand il y a redimensionnement ou changement de color space.
    Pour le dernier j'ai un aplat correspondant à la couleur unie, un dégradé blanc vers noir blendé avec screen et un autre blendé avec multiply. Le tout rasterisé dans une image et regénéré quand la valeur hue change, quand il faut redimensionner ou quand le color space change.

    Les performances sont bonnes sauf quand on a vraiment des très grandes vues (>800pt) mais ça c'est pas super grave c'est pas pas fait pour.

    J'aurai aimé ajouter un peu de dithering sur les images générées avec CoreImage mais j'ai pas trouvé de méthode propre pour le faire correctement je vais essayer d'ajouter un filtre noise voir ce que ça donne. Les suggestions sont les bienvenues mais n'oublions pas qu'avec CoreImage sauce Metal il n'y a pas de fonction pour générer des nombres aléatoires. Voire pas du tout même sans Metal.

    Maintenant j'ai un autre soucis, c'est au niveau du blending du knob. J'ai pas fait dans l'originalité et j'ai pompé ce qu'Apple fait pour son propre color picker :

    Sauf que eux ont une composition un peu particulière :

    Si la brightness est > 50% on a un knob sombre. Sinon c'est le knob clair.
    Je n'arrive pas à reproduire cette composition. Non seulement il est visible sur le spectre mais aussi sur le fond de la fenêtre. Là pour le coup j'ai besoin d'aide parce que j'ai encore du mal avec le blending. Je n'arrive pas non plus à mettre la main sur la routine d'affichage dans le code décompilé d'AppKit (qui m'a pourtant bien aidé jusque là, via Hopper pour ceux que ça intéresse).
    J'ai aussi tenté d'ajouter une NSVisualEffectView pour afficher le knob, testé tous les materials mais ça rend pas pareil.

    Sinon @klog, merci pour l'explication sur l'EDR. Mais ça on est bien d'accord que dans mon cas j'ai pas vraiment à m'en soucier vu que c'est le color space qui se débrouille avec, non ? C'est le même principe avec les extended color spaces on jette les valeurs et il fait au mieux avec l'écran. Ou j'ai pas bien compris ?

  • CéroceCéroce Membre, Modérateur

    @Pyroh a dit :
    il n'y a pas de fonction pour générer des nombres aléatoires. Voire pas du tout même sans Metal.

    Les fragment shaders sont des fonctions pures, aussi comme les générateurs de nombres pseudo aléatoires utilisent généralement une seed basée sur l'heure, ce n'est pas possible à moins de passer l'heure au shader.

    Regarde ces exemples en GLSL, tu verras qu'ils utilisent comme astuce des fonctions qui génèrent assez de variations pour sembler visuellement aléatoires:
    https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83

  • klogklog Membre

    @Pyroh a dit :
    Concrètement j'ai regardé Metal et ma première réaction a été "Mer il et fou !". Plein de chose à mettre en place, de boilerplates et de trucs que je ne suis pas sur de comprendre. Je me suis demandé si le jeu en valait la chandelle.

    Oui Metal est une techno un peu abrupte... Même si le jeu en vaut la chandelle...

    Sinon @klog, merci pour l'explication sur l'EDR. Mais ça on est bien d'accord que dans mon cas j'ai pas vraiment à m'en soucier vu que c'est le color space qui se débrouille avec, non ? C'est le même principe avec les extended color spaces on jette les valeurs et il fait au mieux avec l'écran. Ou j'ai pas bien compris ?

    Dans ton cas, effectivement, je ne pense pas que cela ait beaucoup d'intérêt. C'est un truc à prendre en considération quand on veut la garantie d'un environnement HDR respecté (ce qui est le cas si tu gères des photos au format HEIC prises avec un iphone), sachant, par exemple, que NSImageView ne gère pas le HDR (et donc les écrans HDR et donc l'environnement EDR).

  • CéroceCéroce Membre, Modérateur

    @klog a dit :
    NSImageView ne gère pas le HDR (et donc les écrans HDR et donc l'environnement EDR).

    Intéressant.

Connectez-vous ou Inscrivez-vous pour répondre.