Dorian Fevrier's blog - Mot-clé - .assJe m’appelle FEVRIER Dorian, je suis infographiste 3D passionné par mon métier, l’informatique en général, l’internet, la programmation et l’évolution de tout ce petit monde. Vous trouverez sur ce blog des tutoriaux, mes coups de cœurs, avis, etc.2024-01-02T23:48:05+01:00FEVRIER Dorianurn:md5:695d9c73474c33ce3dab043823509c4bDotclearRécupérer les valeurs par défaut de kickurn:md5:061970a8b7419d2bdd37cb7e76e51e582022-12-10T23:47:00+01:002023-01-24T13:03:50+01:00NarannScript et code.assarnoldcommandekickpythonrendu<p><img alt="" class="media" src="https://www.fevrierdorian.com/blog/public/billets/2022_12_10_kick_default/kick_default_tn.png" style="float: left; margin: 0px 1em 1em 0px; width: 128px; height: 128px;" />Aujourd’hui, un petit bout de code très simple pour récupérer les valeurs par défaut que Arnold (ou la ligne de commande <em>kick</em>) donne à ses nœuds quand on ne les lui fournit pas.</p>
<p>Ce type d’information est utile, entre autres, dans un pipeline qui génère ses <code>.ass</code> lui-même. :siffle:</p>
<p>Bonne lecture ! :sauteJoie:</p> <h3>La ligne de commande <em>kick</em></h3>
<p>La première chose à faire avec une ligne de commande, c’est de regarder d’accéder à sa page d’aide :</p>
<pre>
<code class="language-bash">$ kick --help</code></pre>
<p>Dès lors, on identifie rapidement deux arguments intéressants :</p>
<pre>
<code>-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)
</code></pre>
<p>L’argument <code>-nodes</code> liste les nœuds et <code>-info</code> donne des informations sur les paramètres de ces nœuds :</p>
<pre>
<code>$ 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
...</code></pre>
<p>Le nœud <code>options</code> é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 :</p>
<pre>
<code>$ 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 </code></pre>
<p>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:</p>
<h3>Le code</h3>
<p>Comme d’habitude, je vous donne le script en brut et je l’explique après :</p>
<pre>
<code class="language-python">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))</code></pre>
<p>Et voilà, du pâté. :enerve:</p>
<p>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:</p>
<h3>Explication</h3>
<p>Alors c’est parti !</p>
<pre>
<code class="language-python">from __future__ import (absolute_import,
division,
print_function,
unicode_literals)</code></pre>
<p>Houdini étant en pleine transition vers Python 3, ce bloc permet de garder un peu de cohérence entre Python 2 et 3.</p>
<p>Ensuite nous avons deux classes :</p>
<pre>
<code class="language-python">
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)</code></pre>
<p>Ces deux classes sont des <em>dataclass</em> (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 <a>namedtuple</a> (mais vous pouvez utiliser l’un et l’autre si vous êtes pressé).</p>
<p>J’implémente souvent les <code>__repr__</code> sur mes <em>dataclass</em> pour visualiser rapidement les objets dans le debugger ou via des <code>print()</code>.</p>
<ul>
<li><code>ANodeDef</code> stockera les informations (nom et type) liées à chaque nœud.</li>
<li><code>AAttrDef</code> stockera les informations (nom, type et valeur par défaut) liées à chaque paramètre de nœuds.</li>
<li>Les objets <code>AAttrDef</code> seront mis dans la liste <code>attrs</code> de chaque <code>ANodeDef</code>.</li>
</ul>
<p>Ça nous donnera, grosso modo, une hiérarchie sous la forme :</p>
<pre>
<code>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
...
...</code></pre>
<p>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… ».</p>
<p><img alt="Dico ou classes de données, nous sommes différents" class="media" src="https://www.fevrierdorian.com/blog/public/billets/2022_12_10_kick_default/nous_sommes_different.jpg" style="margin: 0px auto; display: table; width: 500px; height: 714px;" /></p>
<p>Ne perdez pas votre temps en débats stériles, vous avez déjà perdu, alors avançons :</p>
<pre>
<code class="language-python"># 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)</code></pre>
<p>Cette partie est la plus surement la plus simple à comprendre : On lance <code>kick --help</code> et on affiche uniquement la première ligne, celle qui affiche la version :</p>
<pre>
<code>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</code></pre>
<p>La suite :</p>
<pre>
<code class="language-python"># 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)</code></pre>
<p>Ça commence à devenir intéressant. On lance (via <code>subprocess.Popen()</code>) la commande <code>kick -nodes</code> 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 <code>ANodeDef</code> qu’on ajoute à la liste globale <code>node_defs</code>.</p>
<p>Chaque ligne ressemblera à :</p>
<pre>
<code> node type information_supplémentaires</code></pre>
<p>Par exemple :</p>
<pre>
<code>...
add shader
alembic shape (procedural)
complex_ior shader [deprecated]
...</code></pre>
<p>L’expression régulière <code>node_line_re</code> s’occupe d’attraper le contenu de chaque ligne :</p>
<pre>
<code> \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</code></pre>
<p>Une fois qu’on a cette liste de nœuds, on va lancer la ligne de commande <code>kick -info</code> pour chaque nœud et attraper le résultat, c’est ce que fait le bloc de code suivant :</p>
<pre>
<code class="language-python"># 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)</code></pre>
<p>Le code est plus verbeux, mais on va expliquer tout ça. La sortie de la commande <code>kick -info</code> ressemble à ça :</p>
<pre>
<code>Type Name Default
------------ -------------------------------- --------------------------------
BYTE visibility 255
BYTE sidedness 255
BOOL receive_shadows true
BOOL self_shadows true</code></pre>
<p>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:</p>
<p>La seule subtilité c’est qu’on attend (via la variable <code>param_start</code>) d’avoir passé la ligne commençant par <code>----</code> avant de commencer à parser chaque paramètre.</p>
<p>Après tous les <code>if</code> passé, on récupère les valeurs de l’expression régulière, on fabrique un objet <code>AAttrDef</code> et on l’ajoute à la liste des attributs du nœud qu’on inspecte. :reflechi:</p>
<p>Et maintenant qu’on a notre petite hiérarchie, on affiche le tout en triant par nom :</p>
<pre>
<code class="language-python"># 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))</code></pre>
<p>Cela nous donne ça :</p>
<pre>
<code>options (options)
AA_adaptive_threshold FLOAT 0.015
AA_sample_clamp FLOAT 1e+30
AA_sample_clamp_affects_aovs BOOL false
etc.</code></pre>
<p>À vous de l’afficher comme vous l’entendez ! :banaeyouhou:</p>
<h3>Conclusion</h3>
<p>Python, c’est bien, il faut faire du Python, faites du Python.</p>
<p style="text-align: center;">:marioCours:</p>