La ligne de commande kick

La première chose à faire avec une ligne de commande, c’est de regarder d’accéder à sa page d’aide :

$ kick --help

Dès lors, on identifie rapidement deux arguments intéressants :

-nodes [n|t]        List all installed nodes, sorted by Name (default) or Type
-info [n|u] %s      Print detailed information for a given node, sorted by Name or Unsorted (default)

L’argument -nodes liste les nœuds et -info donne des informations sur les paramètres de ces nœuds :

$ kick -nodes
built-in nodes sorted by name:
 abs                              shader
 add                              shader
 alembic                          shape (procedural)
 ambient_occlusion                shader
 aov_read_float                   shader
 aov_read_int                     shader
...

Le nœud options étant le nœud de paramètres de rendu d’Arnold, il y a de fortes chances que la ligne de commande qui vous intéresse en venant ici soit :

$ kick -info options
node:         options
type:         options
output:       (null)
parameters:   119
multioutputs: 0
filename:     <built-in>
version:      7.1.4.1

Type          Name                              Default
------------  --------------------------------  --------------------------------
INT           AA_samples                        1
INT           AA_seed                           1
FLOAT         AA_sample_clamp                   1e+30
FLOAT         indirect_sample_clamp             10
BOOL          AA_sample_clamp_affects_aovs      false
INT           AA_samples_max                    20
FLOAT         AA_adaptive_threshold             0.015
INT           threads                           0
ENUM          thread_priority                   low
BOOL          abort_on_error                    true
BOOL          abort_on_license_fail             false
BOOL          skip_license_check                false
RGB           error_color_bad_texture           1, 0, 0
RGB           error_color_bad_pixel             0, 0, 1
RGB           error_color_bad_shader            1, 0, 1
STRING[]      outputs                           (empty)
STRING[]      light_path_expressions            (empty)
NODE[]        aov_shaders                       (empty)
INT           xres                              320
INT           yres                              240
INT           region_min_x                      -2147483648
INT           region_min_y                      -2147483648
INT           region_max_x                      -2147483648
INT           region_max_y                      -2147483648
FLOAT         pixel_aspect_ratio                1
ENUM          fis_filter                        none
FLOAT         fis_filter_width                  3
INT           bucket_size                       64
ENUM          bucket_scanning                   spiral
VECTOR2[]     buckets                           (empty)
BOOL          ignore_textures                   false
BOOL          ignore_shaders                    false
BOOL          ignore_atmosphere                 false
BOOL          ignore_lights                     false
BOOL          ignore_shadows                    false
BOOL          ignore_subdivision                false
BOOL          ignore_displacement               false
BOOL          ignore_bump                       false
BOOL          ignore_motion                     false
BOOL          ignore_motion_blur                false
BOOL          ignore_dof                        false
BOOL          ignore_smoothing                  false
BOOL          ignore_sss                        false
BOOL          ignore_operators                  false
BOOL          ignore_imagers                    false
STRING[]      ignore_list                       (empty)
INT           auto_transparency_depth           10
INT           texture_max_open_files            0
FLOAT         texture_max_memory_MB             4096
BOOL          texture_per_file_stats            false
STRING        texture_searchpath                
BOOL          texture_automip                   true
INT           texture_autotile                  0
BOOL          texture_accept_untiled            true
BOOL          texture_accept_unmipped           true
BOOL          texture_use_existing_tx           true
BOOL          texture_auto_generate_tx          true
STRING        texture_auto_tx_path              
INT           texture_failure_retries           0
BOOL          texture_conservative_lookups      true
FLOAT         texture_max_sharpen               1.5
NODE          camera                            (null)
NODE          subdiv_dicing_camera              (null)
BOOL          subdiv_frustum_culling            false
FLOAT         subdiv_frustum_padding            0
NODE          background                        (null)
BYTE          background_visibility             255
NODE          atmosphere                        (null)
NODE          shader_override                   (null)
NODE          color_manager                     (null)
NODE          operator                          (null)
FLOAT         meters_per_unit                   1
STRING        scene_units_name                  
FLOAT         indirect_specular_blur            1
FLOAT         luminaire_bias                    1e-06
FLOAT         low_light_threshold               0.001
BOOL          skip_background_atmosphere        false
BOOL          sss_use_autobump                  false
BYTE          max_subdivisions                  255
INT           curves_rr_start_depth             0
BOOL          curves_rr_aggressive              true
FLOAT         reference_time                    0
FLOAT         frame                             0
FLOAT         fps                               24
STRING        osl_includepath                   
STRING        procedural_searchpath             
STRING        plugin_searchpath                 
BOOL          procedural_auto_instancing        true
BOOL          enable_procedural_cache           true
BOOL          parallel_node_init                true
BOOL          enable_new_quad_light_sampler     true
BOOL          enable_new_point_light_sampler    true
BOOL          enable_progressive_render         false
BOOL          enable_adaptive_sampling          false
BOOL          enable_dependency_graph           false
BOOL          enable_microfacet_multiscatter    true
BOOL          enable_deprecated_hair_absorp...  false
BOOL          dielectric_priorities             true
BOOL          enable_fast_ipr                   true
BOOL          force_non_progressive_sampling    false
FLOAT         imager_overhead_target_percent    1
INT           GI_diffuse_depth                  0
INT           GI_specular_depth                 0
INT           GI_transmission_depth             2
INT           GI_volume_depth                   0
INT           GI_total_depth                    10
INT           GI_diffuse_samples                2
INT           GI_specular_samples               2
INT           GI_transmission_samples           2
INT           GI_sss_samples                    2
INT           GI_volume_samples                 2
ENUM          render_device                     CPU
ENUM          render_device_fallback            error
STRING        gpu_default_names                 *
INT           gpu_default_min_memory_MB         512
INT           gpu_max_texture_resolution        0
BOOL          gpu_sparse_textures               true
INT           min_optix_denoiser_sample         0
STRING        name                              

Mais on va essayer d’aller plus loin et de récupérer les valeurs par défaut de tous les nœuds d’Arnold. :reflexionIntense:

Le code

Comme d’habitude, je vous donne le script en brut et je l’explique après :

from __future__ import (absolute_import,
                        division,
                        print_function,
                        unicode_literals)

import re
import subprocess


class ANodeDef(object):
    def __init__(self, name, type, rest=None):
        self.name = name
        self.type = type
        self.rest = rest
        self.attrs = []

    def __repr__(self):
        return "{}('{}', '{}')".format(self.__class__.__name__,
                                       self.name,
                                       self.type)


class AAttrDef(object):
    def __init__(self, name, type, default):
        self.name = name
        self.type = type
        self.default = default

    def __repr__(self):
        return "{}('{}', '{}', '{}')".format(self.__class__.__name__,
                                             self.name,
                                             self.type,
                                             self.default)


# Print Arnold version.
process = subprocess.Popen(['kick', '--help'],
                           stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT)
out, err = process.communicate()

first_line = out.split("\n")[0]

print(first_line)

# Get node list.
process = subprocess.Popen(['kick', '-nodes'],
                           stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT)
out, err = process.communicate()
node_line_re = re.compile(r"\s(?P<name>\w+)\s+(?P<type>\w+)(?P<rest>.*)")
node_defs = []

for line in out.split("\n"):

    match_grp = node_line_re.match(line)

    if not match_grp:
        continue

    name = match_grp.group('name')
    type_ = match_grp.group('type')
    rest = match_grp.group('rest')
    node = ANodeDef(name, type_, rest)

    node_defs.append(node)

# For each node, get its default parameters.
attr_line_re = re.compile(r"(?P<type>\w+)\s+(?P<name>\w+)\s+(?P<default>.*)")

for node_def in node_defs:

    process = subprocess.Popen(['kick', '-info', node_def.name],
                               stdout=subprocess.PIPE,
                               stderr=subprocess.STDOUT)
    out, err = process.communicate()

    param_start = False

    for line in out.split("\n"):

        if line.startswith("----"):
            param_start = True
            continue

        if not param_start:
            continue

        match_grp = attr_line_re.match(line)

        if not match_grp:
            continue

        type_ = match_grp.group('type')
        name = match_grp.group('name')
        default = match_grp.group('default')

        attr = AAttrDef(name, type_, default)
        node_def.attrs.append(attr)

# Print final output.
for node_def in sorted(node_defs, key=lambda n: n.name):

    print("{} ({})".format(node_def.name, node_def.type))

    for attr in sorted(node_def.attrs, key=lambda a: a.name):

        print("  {} {} {}".format(attr.name, attr.type, attr.default))

Et voilà, du pâté. :enerve:

Vous pouvez copier-coller ça et voir ce que ça donne. Mais nous savons tous que ce qui anime vos vies, c’est le besoin de comprendre les choses, pas vrai ? :baffed:

Explication

Alors c’est parti !

from __future__ import (absolute_import,
                        division,
                        print_function,
                        unicode_literals)

Houdini étant en pleine transition vers Python 3, ce bloc permet de garder un peu de cohérence entre Python 2 et 3.

Ensuite nous avons deux classes :


class ANodeDef(object):
    def __init__(self, name, type, rest=None):
        self.name = name
        self.type = type
        self.rest = rest
        self.attrs = []

    def __repr__(self):
        return "{}('{}', '{}')".format(self.__class__.__name__,
                                       self.name,
                                       self.type)


class AAttrDef(object):
    def __init__(self, name, type, default):
        self.name = name
        self.type = type
        self.default = default

    def __repr__(self):
        return "{}('{}', '{}', '{}')".format(self.__class__.__name__,
                                             self.name,
                                             self.type,
                                             self.default)

Ces deux classes sont des dataclass (classes de données). L’idée étant de stocker des données dans des objets spécifiques pour éviter d’utiliser des dictionnaires ou des namedtuple (mais vous pouvez utiliser l’un et l’autre si vous êtes pressé).

J’implémente souvent les __repr__ sur mes dataclass pour visualiser rapidement les objets dans le debugger ou via des print().

  • ANodeDef stockera les informations (nom et type) liées à chaque nœud.
  • AAttrDef stockera les informations (nom, type et valeur par défaut) liées à chaque paramètre de nœuds.
  • Les objets AAttrDef seront mis dans la liste attrs de chaque ANodeDef.

Ça nous donnera, grosso modo, une hiérarchie sous la forme :

ANodeDef
  name: alembic
  type: shape
  rest: (procedural)
  attrs: [
    AAttrDef
      name: invert_normals
      type: BOOL
      default: false
    AAttrDef
      name: ray_bias
      type: FLOAT
      default: 1e-06
    ...
...

Beaucoup de prise de tête pour rien, me rétorqueriez-vous, « Utilise des dicos ! Ça va plus vite, le code c’est quand même plus beau quand ça fait un max de choses en une seule ligne, gnagnagna… ».

Dico ou classes de données, nous sommes différents

Ne perdez pas votre temps en débats stériles, vous avez déjà perdu, alors avançons :

# Print Arnold version.
process = subprocess.Popen(['kick', '--help'],
                           stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT)
out, err = process.communicate()

first_line = out.split("\n")[0]

print(first_line)

Cette partie est la plus surement la plus simple à comprendre : On lance kick --help et on affiche uniquement la première ligne, celle qui affiche la version :

Arnold 7.1.4.1 [c989b21f] linux x86_64 clang-10.0.1 oiio-2.4.1 osl-1.12.0 vdb-7.1.1 adlsdk-7.4.2.47 clmhub-3.1.1.43 rlm-14.2.5 optix-6.6.0 2022/11/29 11:24:49

La suite :

# Get node list.
process = subprocess.Popen(['kick', '-nodes'],
                           stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT)
out, err = process.communicate()
node_line_re = re.compile(r"\s(?P<name>\w+)\s+(?P<type>\w+)(?P<rest>.*)")
node_defs = []

for line in out.split("\n"):

    match_grp = node_line_re.match(line)

    if not match_grp:
        continue

    name = match_grp.group('name')
    type_ = match_grp.group('type')
    rest = match_grp.group('rest')
    node = ANodeDef(name, type_, rest)

    node_defs.append(node)

Ça commence à devenir intéressant. On lance (via subprocess.Popen()) la commande kick -nodes qui va nous lister tous les nœuds disponibles, et on interprète chaque ligne pour en extraire les informations qu’on stocke dans un objet ANodeDef qu’on ajoute à la liste globale node_defs.

Chaque ligne ressemblera à :

 node                             type information_supplémentaires

Par exemple :

...
 add                              shader
 alembic                          shape (procedural)
 complex_ior                      shader          [deprecated]
...

L’expression régulière node_line_re s’occupe d’attraper le contenu de chaque ligne :

   \s        (?P<name>\w+)         \s+               (?P<type>\w+)          (?P<rest>.*)
1 espace|un mot appelé name|1 ou plusieurs espaces|un mot appelé type|le reste, optionnel appelé rest

Une fois qu’on a cette liste de nœuds, on va lancer la ligne de commande kick -info pour chaque nœud et attraper le résultat, c’est ce que fait le bloc de code suivant :

# For each node, get its default parameters.
attr_line_re = re.compile(r"(?P<type>\w+)\s+(?P<name>\w+)\s+(?P<default>.*)")

for node_def in node_defs:

    process = subprocess.Popen(['kick', '-info', node_def.name],
                               stdout=subprocess.PIPE,
                               stderr=subprocess.STDOUT)
    out, err = process.communicate()

    param_start = False

    for line in out.split("\n"):

        if line.startswith("----"):
            param_start = True
            continue

        if not param_start:
            continue

        match_grp = attr_line_re.match(line)

        if not match_grp:
            continue

        type_ = match_grp.group('type')
        name = match_grp.group('name')
        default = match_grp.group('default')

        attr = AAttrDef(name, type_, default)
        node_def.attrs.append(attr)

Le code est plus verbeux, mais on va expliquer tout ça. La sortie de la commande kick -info ressemble à ça :

Type          Name                              Default
------------  --------------------------------  --------------------------------
BYTE          visibility                        255
BYTE          sidedness                         255
BOOL          receive_shadows                   true
BOOL          self_shadows                      true

On a donc besoin d’une expression régulière que je ne détaille pas, car elle est assez similaire à la précédente. :redface:

La seule subtilité c’est qu’on attend (via la variable param_start) d’avoir passé la ligne commençant par ---- avant de commencer à parser chaque paramètre.

Après tous les if passé, on récupère les valeurs de l’expression régulière, on fabrique un objet AAttrDef et on l’ajoute à la liste des attributs du nœud qu’on inspecte. :reflechi:

Et maintenant qu’on a notre petite hiérarchie, on affiche le tout en triant par nom :

# Print final output.
for node_def in sorted(node_defs, key=lambda n: n.name):

    print("{} ({})".format(node_def.name, node_def.type))

    for attr in sorted(node_def.attrs, key=lambda a: a.name):

        print("  {} {} {}".format(attr.name, attr.type, attr.default))

Cela nous donne ça :

options (options)
  AA_adaptive_threshold FLOAT 0.015
  AA_sample_clamp FLOAT 1e+30
  AA_sample_clamp_affects_aovs BOOL false
  etc.

À vous de l’afficher comme vous l’entendez ! :banaeyouhou:

Conclusion

Python, c’est bien, il faut faire du Python, faites du Python.

:marioCours: