Journal Mathsworld: the S-expressed shader language

Posté par  (site web personnel) . Licence CC By‑SA.
15
17
mar.
2023

TLDR: Mathsworld, un outil web pour faire du raytracing avec des scènes décrite en lisp.

Salut 'nal,

J'ai encore commis un code
improbable. En gros, c'est un truc qui prend en entrée une scène écrite sous forme de S-Expression et qui génère un shader WebGL raytraçant la scène.

Pourquoi ? Parce que ça m'amusait. Et que je voulais apprendre des trucs.

Tout à commencé avec mon envie de comprendre comment on générait des images avec des maths (et un ordinateur). J'ai d'abord fait un raycaster façon Wolfenstein 3D. Ça m'a amené à découvrir et jouer avec les SDF. C'était cool, mais je voulais aller plus loin et passer à la 3D (et oui, bien que le rendu donne une impression de 3D, le moteur est purement 2D). J'ai acheté le livre Computer Graphics From Scratch (contenu disponible gratuitement sur le site de l'auteur ici) de Gabriel Gambetta et commencé mon raytracer en rust. Grace au livre, j'ai pu rapidement et facilement faire mes premiers rendus. J'étais content. Mais c'était lent (rendu CPU), et je n'étais pas satisfait du format d'entrée des scènes (YAML).

C'est vers cette période là que Joalland< a posté son lien Painting a Landscape with Maths. Et ça m'a frustré. Parce que les concepts mathématiques sous-jacents sont étonnament simples (accessibles à un niveau Bac, peut-être moins), mais personne n'a envie de d'apprendre à faire de fat shaders GLSL pour pouvoir jouer avec des maths et faire de jolis paysages. Je le sais, parce que moi même j'avais envie de faire de jolis paysages avec des maths sans apprendre à faire de fat shaders GLSL. (Joalland<, tu te demandais comment il avait fait. C'est un fat shader. Ces autres créations sont visibles . Ce sont à chaque fois de fat shaders).

Ça m'a aidé à redéfinir le scope du projet :
- faire un truc genre shadertoy pour simplifier la distribution afin de le rendre plus accessible qu'un utilitaire CLI en rust
- avoir un format de scène facile à utiliser pour les humains ET l'ordinateur, possiblité de scripting, voir de génération procédurale de la scène.
- utiliser le GPU pour avoir des performances décentes.
- posséder un fort syndrome de NIH

Comme format de scène, j'ai choisi les s-expr. C'est plutôt simple à utiliser par un humain, c'est plutot simple à manipuler par l'ordinateur, et ça ouvre des possibilités de scripting for intéressantes pour méta-générer les scènes.

Notre scène de référence:

(scene 
    (camera 
        (vector3 0 0 0) 
        (vector3 1 1 1)) 
    (union (list 
        (sphere 
            (vector3 0 -5001 0)
            5000
            (material 
                (color 1 1 0)
                1000)) 
        (sphere 
            (vector3 0 -1 3)
            1
            (material 
                (color 1 0 0)
                500)) 
        (sphere 
            (vector3 2 0 4)
            1
            (material 
                (color 0 0 1)
                500))
        (sphere 
            (vector3 -2 0 4)
            1
            (material 
                (color 0 1 0)
                10)))) 
    (list 
        (ambiant_light
            0.2) 
        (omni_directional_light
            0.6
            (vector3 2 1 0)) 
        (directional_light
            0.2
            (vector3 1 4 4))))

Un petit truc simple où on déclare 4 sphères colorés et quelques lumières.

À terme, je compte utiliser parenscript qui m'a été recommandé suite à mon dernier journal pour profiter de toute la puissance de Common Lisp et pouvoir décrire des scènes de guedin facilement. Mais pour l'instant, afin de "gagner" du temps, j'ai écrit mon propre parseur. J'avoue, j'avais vraiment envie de découvrir les parseur LL. C'est magique ces trucs, ils méritent leurs propre journal.

Une fois que j'ai chargé la scène en mémoire, je la transforme en shader GLSL que je dessine dans un context WebGL du navigateur. Là, j'ai fait sale. Je suis allé sur Using shaders to apply color in WebGL, copié collé le code comme un sagouin et supprimé comme un sauvage les trucs qui semblaient superflus. Vint la parti rigolote où je réimplémenta mon raytracer rust sous forme de fat shader GLSL capable de faire un rendu de la scène donnée plus haut hardcodé dans le shader. Une fois en possession de ce shader de référence, je m'en suis servi pour faire un template dans lequel j'insère la scène voulu par l'utilisateur, je file ça au GPU, et ka-boom, on a des pixels colorés :

3 sphères colorés vert, rouge et bleu sur un sol jaune

Tu peux jouer avec .

Comme tu peux le constater, l'UI et l'UX sont inexistantes. Tu as une textarea dans lequel tu peux éditer la scène, le canvas dans lequel le rendu est fait, et une textarea qui contient le shader généré et que tu peux ignorer. Le shader est regénéré à la volé lors de l'édition, les erreurs sont remontés dans la console.

Pour l'instant, la syntaxe supporté est la suivante : une expression parenthésée composée d'un identifier et suivi de 0 ou plusieurs arguments pouvant être un nombre ou une expression parenthésée.

S ::= SEXPR $
SEXPR ::= ( identifier ARGS )
ARGS ::= ARG ARGS
ARGS ::= ε
ARG ::= number
ARG ::= SEXPR

Il n'y a aucun retour en cas d'erreur, hormis un message cryptique dans la console du navigateur.

Le format de description de scène est le suivant:

scene: Une scène avec une caméra, des lumières et des objets. Doit être la racine de la S-Expr
 - camera: La caméra
 - root: Le nœud principal. Peut-être de type `union` ou `sphere`
 - lights: Une liste de lumières. Peuvent-être de type `ambiant_light`,  `omni_directional_light` ou `directional_light`


camera:
 - position: (x, y, z) La position de la caméra dans la scène
 - view_port: (hauteur, largeur, distance) Les réglages du view port (en trèèèèèès gros, les réglages de zoom)


union: Une union d'objets
 - nodes: Une liste de nœuds à unir. Peuvent être de type `union` ou `sphere`


sphere: Une sphere
 - position: (x, y, z) La position de la sphère dans la scène
 - radius: Le rayon de la sphère
 - material: Le matériau de la sphère


material:
 - color: (rouge, vert, bleu) La couleur RGB du matériau. Chaque composante est est défini dans l'intervalle [0, 1]
 - specular: l'intensité de l'effet de brillance


ambiant_light: une lumière ambiante
 - intensity: intensité de la lumière


omni_directional_light: une lumière omnidirectionnelle (une amoule)
 - intensity: intensité de la lumière
 - position: (x, y, z


directional_light: une lumière directionnelle (le soleil) 
 - intensity: intensité de la lumière
 - direction: (x, y, z) La direction normalisée de la lumière

À terme, je compte ajouter :

  • plus de formes
  • possibilité de définir ses propres formes
  • matériaux procéduraux
  • le plein support de common lisp pour écrire la scène
  • réécriture du backend du générateur de shader GLSL parce que là c'est vraiment caca :D
  • possibilité de faire des animations
  • une interface plus dans le style de shader toys, avec possibilité d'enregistrer et partager ses créations
  • # Wolfenstein 3D

    Posté par  (site web personnel) . Évalué à 5.

    C'est plus du ray casting que tracing non ?

  • # détail synthaxique

    Posté par  . Évalué à 2. Dernière modification le 17 mars 2023 à 22:53.

    Je ne suis pas très calé en dialectes lisp mais à la lecture du source (qui, si on omet les parenthèses est plutôt claire, parce qu'indenté :troll:), pourquoi a-t'on :

    (union (list (items...))) et pas (union (items...)) ? Autrement dit pourquoi une "list" d'une liste et pas simplement une liste.

    • [^] # Re: détail synthaxique

      Posté par  (site web personnel) . Évalué à 2.

      C'est comme ça qu'ils font en lisp, je n'ai pas cherché ầ étre innovant sur ce point là.

    • [^] # Re: détail synthaxique

      Posté par  (site web personnel, Mastodon) . Évalué à 0.

      Grosso-modo, c'est (commande arguments) et ici, la commande union attend une liste, pas des éléments de listes. Ça tombe bien, la commande list transforme ses arguments en une seule liste (et si l'argument était déjà une liste tant mieux rien ne change).

      Je pense que ton incompréhension vient du fait que tu penses que les parenthèses sont des délimiteurs de listes. Si tu transposes dans d'autres langages, (union ma-liste) correspond à l'appel de fonction union(ma-liste) où ta variable ma-liste va contenir une liste mais ce n'est aucunement un appel à une fonction multi-valeurs union(élément-1, élément-2, …, élément-N) Par contre, list est une telle fonction.

      “It is seldom that liberty of any kind is lost all at once.” ― David Hume

      • [^] # Re: détail synthaxique

        Posté par  . Évalué à 4.

        Si c'est bien mon point : pourquoi union n'a pas le comportement de faire l'union de tous ses arguments au lieu d'attendre que list fasse le travail de rassembler tous ses arguments sous forme de liste et de lui passer une liste. Si list sait travailler sur un nombre non fixe d'arguments, j'imagine que c'est donc possible en LISP. Pourquoi ne pas le faire pour union ? Cela éviterai une imbrication supplémentaire et rendrait le code plus clair.

        • [^] # Re: détail synthaxique

          Posté par  (site web personnel, Mastodon) . Évalué à 0.

          j'imagine que c'est donc possible en LISP.

          Ce comportement est justement possible dans tous les langages, avec un certain coût : c'est le principe des fonctions variadiques

          et rendrait le code plus clair.

          C'est justement plus clair ainsi (en tout cas pour moi) : la commande accepte "un argument" qui représente une liste.
          Ta demande est d'accepter plusieurs arguments, sachant que le langage a choisi d'être flexible… et sans nécessiter de contorsions ni avoir d'effets de bords… Je te laisse transposer dans le langage de ton choix (TypeScript, Java, Eiffel, Pascal, Rust, whatever) pour voir (pourquoi aucun langage dérivé n'a changé cet aspect.)

          Au pire, il faut voir list() comme indiquant/forçant le type/typage, tout comme tu aurais double foo en C.

          “It is seldom that liberty of any kind is lost all at once.” ― David Hume

          • [^] # Re: détail synthaxique

            Posté par  . Évalué à 4.

            C'est pas super clair comme explication.

            sachant que le langage a choisi d'être flexible… et sans nécessiter de contorsions ni avoir d'effets de bords

            ça vient d'où ça ?

            Je te laisse transposer dans le langage de ton choix

            Mais puisque Lisp le permet et l'utilise dans list.

            C'est supporté par la plupart des langages.

            pourquoi aucun langage dérivé n'a changé cet aspect.

            Dérivé de quoi ? de Lisp ? Mais "TypeScript, Java, Eiffel, Pascal, Rust, whatever" ne sont pas dérivés de Lisp. Et Lisp le permet et l'utilise dans list.

            C'est justement plus clair ainsi (en tout cas pour moi)

            En effet tous les goûts sont dans la nature.

            Un exemple en openscad qui sert à faire des modèles 3D :

            union() {
              cylinder(h = 4, r=1);
              rotate([90,0,0]) cylinder(h = 4, r=1);
              rotate([180,0,0]) cylinder(h = 4, r=1);
              rotate([0,90,0]) cylinder(h = 4, r=1);
              rotate([-90,0,0]) cylinder(h = 4, r=1);
              rotate([0,-90,0]) cylinder(h = 4, r=1);
            }

            C'est lisible IMHO. Ils n'ont pas fait union de list de trucs ils ont fait union de trucs.

            Au pire, il faut voir […]

            Au final, je préfère l'explication de l'auteur : "[…], je n'ai pas cherché ầ étre innovant sur ce point là".

            • [^] # Re: détail synthaxique

              Posté par  . Évalué à 1.

              On va prendre l'exemple de CL: union fait l'union de 2 listes, et en retourne 1:

              (union (list 1 2 3) (list 3 4 5))
              ;; => (1 2 3 4 5)

              On manipule des listes d'objets.

              Dans notre exemple on a des objets sphères au lieu des entiers, et surtout on a 1 seule liste donnée en argument et pas 2, ce qui nous fout le doute. Je suppose qu'elle est retournée telle quelle.

              • [^] # Re: détail synthaxique

                Posté par  (site web personnel, Mastodon) . Évalué à -1.

                Oui, ça se comprend qu'une/un seule/seul liste/ensemble donnée en argument foute le doute quand on fait une réunion ensembliste. Ici le mot utilisé dans un sens un peu différent.

                Mais steph1978< semble bloquer sur le mot list et les parenthèses ; d'où ma remarque

                Je pense que ton incompréhension vient du fait que tu penses que les parenthèses sont des délimiteurs de listes

                “It is seldom that liberty of any kind is lost all at once.” ― David Hume

                • [^] # Re: détail synthaxique

                  Posté par  (site web personnel) . Évalué à 2.

                  Oui, ça se comprend qu'une/un seule/seul liste/ensemble donnée en argument foute le doute quand on fait une réunion ensembliste. Ici le mot utilisé dans un sens un peu différent.

                  Tutafé. Ici, c’est une opération ensembliste sur les volumes passés en paramètre. À terme, il y aura aussi l’intersection et la différence. Avant, j’utilisai des opérateurs binaires. L’intention était plus claire mais la scène plus chiante à écrire :

                  (union
                    (union
                      (sphere …)
                      (sphere …))
                    (union
                      (sphere …)
                      (sphere …)))
                  
            • [^] # Re: détail synthaxique

              Posté par  (site web personnel, Mastodon) . Évalué à -1.

              Je te laisse transposer dans le langage de ton choix (TypeScript, Java, Eiffel, Pascal, Rust, whatever) pour voir (pourquoi aucun langage dérivé n'a changé cet aspect.)

              Dérivé de quoi ? de Lisp ? Mais "TypeScript, Java, Eiffel, Pascal, Rust, whatever" ne sont pas dérivés de Lisp.

              Une invitation à transposer dans un autre ne signifie pas que ce langage est dérivé de Lisp --"
              Oui successeurs/descendants de Lisp n'ont pas changé cet aspect. Et l'auteur n'a pas jugé utile non plus d'innover sur cet aspect.

              Ils n'ont pas fait union de list de trucs ils ont fait union de trucs.

              union de trucs = (union trucs) ici, et trucs = (list …) ici.

              Grosso-modo, c'est (commande arguments)

              “It is seldom that liberty of any kind is lost all at once.” ― David Hume

  • # CADQuery

    Posté par  (site web personnel) . Évalué à 2.

    Très intéressant comme approche, il y a quelques temps j'avais essayé de faire un CAD via des DSLs (Langage Spécifique), avec une pré-visualisation dans Intellij (pour l'auto-completion du code).

    C'était tiptop comme projet, mais vu la gueule des designers lors de la présentation, je suis passé à autre chose …

    Tu connais CADQuery ?

    ça devrait t'intéressé …

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.