Blender to Arnold Export - Part II: Cameras

Jan Walter June 19, 2023 [DCC] #blender #arnold #python

The last part (Blender to Arnold Export - Part I: The Addon) introduced the Blender addon without talking really about the Python code behind it. The Python code already supported exporting polymesh nodes to Arnold (using the Python API of Blender and Arnold). The splash screen scene for Blender 3.5.1 contains 2012 such nodes:

$ grep polymesh ~/Downloads/blender_35_splash_nicole_morena.ass | wc -l
2012

But to be able to render something we need at least a camera node. Arnold knows several of those camera nodes:

$ /usr/local/Arnold/bin/kick -nodes n | grep camera
 camera_projection                shader
 cyl_camera                       camera
 fisheye_camera                   camera
 ortho_camera                     camera
 persp_camera                     camera
 spherical_camera                 camera
 uv_camera                        camera
 vr_camera                        camera

But for now lets start with just a ortho_camera node (needed for the splash screen scene), and a persp_camera node (for most other scenes). Lets first look at the resulting image:

Arnold rendering with a persp_camera

At that point I was still using an Arnold persp_camera node and zoomed in a bit to match the Blender viewport, but I fixed that later by providing the Arnold ortho_camera node:

Arnold rendering without shaders and lights

So the code I added to the already existing Python code can be seen here in the output of git diff:

diff --git a/blender/io_scene_ass/export_ass.py b/blender/io_scene_ass/export_ass.py
index d57f389..ef11aaf 100644
--- a/blender/io_scene_ass/export_ass.py
+++ b/blender/io_scene_ass/export_ass.py
@@ -1,8 +1,11 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 
+# Python modules
+import math
 import os
-import sys
 import platform
+import sys
+# Blender modules
 import bpy
 from mathutils import Matrix #, Vector, Color
 #from bpy_extras import io_utils, node_shader_utils
@@ -61,6 +64,11 @@ class AssExporter:
         arnold.AiBegin()
         # we need an arnold universe below
         universe = arnold.AiUniverse()
+        # options
+        options = arnold.AiUniverseGetOptions(universe)
+        # get camera (used for rendering) from scene
+        scene = bpy.context.scene
+        camera_name = scene.camera.name
         # Blender objects
         EXPORT_GLOBAL_MATRIX = Matrix()
         depsgraph = context.evaluated_depsgraph_get()
@@ -88,7 +96,66 @@ class AssExporter:
                         except RuntimeError:
                             me = None
                         if me is None:
-                            print('WARNING: ignore "%s" ...' % ob.name)
+                            if ob.type == 'CAMERA':
+                                if ob.name == camera_name:
+                                    percent = scene.render.resolution_percentage / 100.0
+                                    xresolution = int(scene.render.resolution_x * percent)
+                                    yresolution = int(scene.render.resolution_y * percent)
+                                    # xres
+                                    arnold.AiNodeSetInt(options, "xres", xresolution)
+                                    # yres
+                                    arnold.AiNodeSetInt(options, "yres", yresolution)
+                                    if ob.data.type == 'ORTHO':
+                                        # ortho_camera
+                                        camera = arnold.AiNode(universe,
+                                                               "ortho_camera",
+                                                               camera_name)
+                                        # screen_window
+                                        ortho_scale = ob.data.ortho_scale / 2.0
+                                        arnold.AiNodeSetVec2(camera,
+                                                             "screen_window_min",
+                                                             -ortho_scale, -ortho_scale)
+                                        arnold.AiNodeSetVec2(camera,
+                                                             "screen_window_max",
+                                                             ortho_scale, ortho_scale)
+                                    else:
+                                        # persp_camera
+                                        camera = arnold.AiNode(universe,
+                                                               "persp_camera",
+                                                               camera_name)
+                                        # fov
+                                        aspect = xresolution / float(yresolution)
+                                        if aspect >= 1.0:
+                                            fov = math.degrees(ob.data.angle)
+                                        else:
+                                            fov = 2 * math.degrees(math.atan((aspect * 16.0) / lens))
+                                        arnold.AiNodeSetFlt(camera, "fov", fov)
+                                    # options needs a link to camera
+                                    arnold.AiNodeSetPtr(options, "camera", camera)
+                                    # matrix
+                                    am = arnold.AtMatrix()
+                                    arnold.AiM4Identity(am)
+                                    am[0][0] = ob_mat[0][0]
+                                    am[0][1] = ob_mat[1][0]
+                                    am[0][2] = ob_mat[2][0]
+                                    am[0][3] = ob_mat[3][0]
+                                    am[1][0] = ob_mat[0][1]
+                                    am[1][1] = ob_mat[1][1]
+                                    am[1][2] = ob_mat[2][1]
+                                    am[1][3] = ob_mat[3][1]
+                                    am[2][0] = ob_mat[0][2]
+                                    am[2][1] = ob_mat[1][2]
+                                    am[2][2] = ob_mat[2][2]
+                                    am[2][3] = ob_mat[3][2]
+                                    am[3][0] = ob_mat[0][3]
+                                    am[3][1] = ob_mat[1][3]
+                                    am[3][2] = ob_mat[2][3]
+                                    am[3][3] = ob_mat[3][3]
+                                    arnold.AiNodeSetMatrix(camera, "matrix", am)
+                                else:
+                                    print('WARNING: ignore "%s" ...' % ob.name)
+                            else:
+                                print('WARNING: ignore "%s" ...' % ob.name)
                             continue
                         # _must_ do this first since it re-allocs arrays
                         mesh_triangulate(me)
@@ -122,6 +189,7 @@ class AssExporter:
                         ##print('DEBUG: obnamestring = %s' % obnamestring)
                         # arnold
                         polymesh = arnold.AiNode(universe, "polymesh", obnamestring)
+                        arnold.AiNodeSetBool(polymesh, "smoothing", False)
                         vlist = arnold.AiArrayAllocate(len(me_verts), 1,
                                                        arnold.AI_TYPE_VECTOR)
                         numVertexIndices = 0

The first couple of lines just give access to the Arnold options node, which later will link back to the camera node we will create (and set the render resolution, which we get from Blender).

$ grep options blender_35_splash_nicole_morena.ass -A 5
options
{
 xres 1327
 yres 1250
 camera "CAM-wide"
}

If we just search for camera we see how that link works, it mentions the name of a node, which in this case has a type of ortho_camera:

$ grep camera blender_35_splash_nicole_morena.ass
 camera "CAM-wide"
ortho_camera

The node itself and it's name can be seen here:

$ grep ortho_camera blender_35_splash_nicole_morena.ass -A 10
ortho_camera
{
 name CAM-wide
 matrix
 0.637181938 0.770702124 -0.00418198621 0
 -0.198978737 0.169744015 0.965191424 0
 0.744584918 -0.614170372 0.261511147 0
 8.4063282 -6.93524885 4.93189812 1
 screen_window_min -1.97714305 -1.97714305
 screen_window_max 1.97714305 1.97714305
}

So we also export the matrix and four values (two VECTOR2 parameters) to define the screen_window settings of the Arnold ortho_camera node:

$ /usr/local/Arnold/bin/kick -info ortho_camera
node:         ortho_camera
type:         camera
output:       (null)
parameters:   20
multioutputs: 0
filename:     <built-in>
version:      7.2.2.0

Type          Name                              Default
------------  --------------------------------  --------------------------------
VECTOR[]      position                          0, 0, 0
VECTOR[]      look_at                           0, 0, -1
VECTOR[]      up                                0, 1, 0
MATRIX[]      matrix                            identity
ENUM          handedness                        right
FLOAT         near_clip                         0.0001
FLOAT         far_clip                          1e+30
VECTOR2[]     screen_window_min                 -1, -1
VECTOR2[]     screen_window_max                 1, 1
FLOAT         shutter_start                     0
FLOAT         shutter_end                       0
ENUM          shutter_type                      box
VECTOR2[]     shutter_curve                     (empty)
ENUM          rolling_shutter                   off
FLOAT         rolling_shutter_duration          0
FLOAT         motion_start                      0
FLOAT         motion_end                        1
FLOAT         exposure                          0
NODE          filtermap                         (null)
STRING        name

For now we do not care about the other parameters. Let's check what else is missing right now instead:

$ /usr/local/blender-3.5.1-linux-x64/blender
Read prefs: /home/jan/.config/blender/3.5/config/userpref.blend
Read blend: /home/jan/Downloads/blender_35_splash_nicole_morena.blend
...
00:00:00  3712MB         | log started Mon Jun 19 12:28:34 2023
00:00:00  3712MB         | Arnold 7.2.2.0 ...
...
WARNING: ignore "CAM-closeup" ...
WARNING: ignore "Mball.001" ...
WARNING: ignore "Mball.002" ...
WARNING: ignore "Point.011" ...
WARNING: ignore "Point.010" ...
WARNING: ignore "Point.008" ...
WARNING: ignore "Light" ...
WARNING: ignore "Area" ...
WARNING: ignore "Area.002" ...
WARNING: ignore "Point.001" ...
WARNING: ignore "Area.003" ...
WARNING: ignore "Area.004" ...
WARNING: ignore "Point.009" ...
WARNING: ignore "Point.007" ...
WARNING: ignore "Area.001" ...
WARNING: ignore "Point" ...
00:00:44  6766MB         | [ass] writing scene to /home/jan/Downloads/blender_35_splash_nicole_morena.ass (mask=0xFFFF) ...
00:00:47  6765MB         | [ass] wrote 156391329 bytes, 2014 nodes in 0:03.26
00:00:48  6765MB         |  
00:00:48  6765MB         | releasing resources
00:00:48  6765MB         |  unloading 3 plugins
00:00:48  6765MB         |   closing usd_proc.so ...
00:00:48  6765MB         |   closing cryptomatte.so ...
00:00:48  6765MB         |   closing alembic_proc.so ...
00:00:48  6764MB         |  unloading plugins done
00:00:48  6764MB         | Arnold shutdown

Most of the ignored Blender nodes are lights, which we would need next, so we can render using shaders.

...
class AssExporter:
    def __init__(self):
        self.filename = None

    def export(self, context, filename):
        self.filename = filename
        # AiBegin
        arnold.AiBegin()
...
        for collection in bpy.data.collections:
            if collection.hide_render or collection.use_fake_user:
                continue
            else:
                objects = collection.objects
                for i, ob_main in enumerate(objects):
                    # ignore dupli children
                    if ob_main.parent and ob_main.parent.instance_type in {'VERTS', 'FACES'}:
                        continue
                    obs = [(ob_main, ob_main.matrix_world)]
                    if ob_main.is_instancer:
                        obs = []
                        obs += [(dup.instance_object.original, dup.matrix_world.copy())
                                for dup in depsgraph.object_instances
                                if dup.parent and dup.parent.original == ob_main]
                    for ob, ob_mat in obs:
                        ob_for_convert = ob.evaluated_get(depsgraph)
                        try:
                            me = ob_for_convert.to_mesh()
                        except RuntimeError:
                            me = None
                        if me is None:
                            if ob.type == 'CAMERA':
                                if ob.name == camera_name:
...
                                else:
                                    print('WARNING: ignore "%s" ...' % ob.name)
                            else:
                                print('WARNING: ignore "%s" ...' % ob.name)
                            continue
...
        # AiSceneWrite
        params = arnold.AiParamValueMap()
        mask = arnold.AI_NODE_ALL
        arnold.AiParamValueMapSetInt(params, "mask", mask)
        arnold.AiParamValueMapSetBool(params, "binary", False)
        arnold.AiSceneWrite(universe, filename, params)
        # AiEnd
        arnold.AiEnd()
...

The Arnold image above was rendered using the -is option:

$ /usr/local/Arnold/bin/kick -h | grep '\-is '
  -is                 Ignore shaders

Like this (omitting other options used to render the .ass file):

$ /usr/local/Arnold/bin/kick -is blender_35_splash_nicole_morena.ass ...

Back to top