Skip to content

load_camera

CameraLoader

Bases: HoudiniLoader

Load camera from an Alembic file

Source code in client/ayon_houdini/plugins/load/load_camera.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
class CameraLoader(plugin.HoudiniLoader):
    """Load camera from an Alembic file"""

    product_types = {"camera"}
    label = "Load Camera (abc)"
    representations = {"abc"}
    order = -10

    icon = "code-fork"
    color = "orange"

    camera_aperture_expression = "default"

    _match_maya_render_mask_expression = """
# Match maya render mask (logic from Houdini's own FBX importer)
node = hou.pwd()
resx = node.evalParm('resx')
resy = node.evalParm('resy')
aspect = node.evalParm('aspect')
aperture *= min(1, (resx / resy * aspect) / 1.5)
return aperture
"""

    @classmethod
    def get_options(cls, contexts):
        return [
            EnumDef(
                "cameraApertureExpression",
                label="Camera Aperture Expression",
                items=[
                    {"label": "Houdini Default", "value": "default"},
                    {"label": "Match Maya render mask", "value": "match_maya"}
                ],
                default=cls.camera_aperture_expression,
                tooltip=(
                    "Set the aperture expression on the camera from the "
                    "Alembic using either:\n"
                    "- Houdini default expression\n"
                    "- Match the Maya render mask (which matches Houdini's "
                    "FBX Import camera expression."
                )
            )
        ]

    def load(self, context, name=None, namespace=None, options=None):
        # Format file name, Houdini only wants forward slashes
        file_path = self.filepath_from_context(context).replace("\\", "/")

        # Get the root node
        obj = hou.node("/obj")

        # Define node name
        namespace = namespace if namespace else context["folder"]["name"]
        node_name = "{}_{}".format(namespace, name) if namespace else name

        # Create a archive node
        node = self.create_and_connect(obj, "alembicarchive", node_name)

        # TODO: add FPS of project / folder
        node.setParms({"fileName": file_path, "channelRef": True})

        # Apply some magic
        node.parm("buildHierarchy").pressButton()
        node.moveToGoodPosition()

        # Create an alembic xform node
        nodes = [node]

        camera = get_camera_from_container(node)

        if options.get("cameraApertureExpression",
                       self.camera_aperture_expression) == "match_maya":
            self._match_maya_render_mask(camera)

        set_camera_resolution(camera, entity=context["folder"])
        self[:] = nodes

        return pipeline.containerise(node_name,
                                     namespace,
                                     nodes,
                                     context,
                                     self.__class__.__name__,
                                     suffix="")

    def update(self, container, context):
        node = container["node"]

        # Update the file path
        file_path = self.filepath_from_context(context)
        file_path = file_path.replace("\\", "/")

        # Update attributes
        node.setParms({"fileName": file_path,
                       "representation": context["representation"]["id"]})

        # Store the cam temporarily next to the Alembic Archive
        # so that we can preserve parm values the user set on it
        # after build hierarchy was triggered.
        old_camera = get_camera_from_container(node)
        temp_camera = old_camera.copyTo(node.parent())

        # Rebuild
        node.parm("buildHierarchy").pressButton()

        # Apply values to the new camera
        new_camera = get_camera_from_container(node)
        transfer_non_default_values(temp_camera,
                                    new_camera,
                                    # The hidden uniform scale attribute
                                    # gets a default connection to
                                    # "icon_scale" just skip that completely
                                    ignore={"scale"})

        # Detect whether the camera was loaded with the "Match Maya render
        # mask" before. If so, we want to maintain that expression on update.
        if (
                self._match_maya_render_mask_expression
                in get_expression(temp_camera.parm("aperture"))
        ):
            self._match_maya_render_mask(new_camera)

        set_camera_resolution(new_camera)

        temp_camera.destroy()

    def switch(self, container, context):
        self.update(container, context)

    def remove(self, container):
        node = container["node"]
        node.destroy()

    def create_and_connect(self, node, node_type, name=None):
        """Create a node within a node which and connect it to the input

        Args:
            node(hou.Node): parent of the new node
            node_type(str) name of the type of node, eg: 'alembic'
            name(str, Optional): name of the node

        Returns:
            hou.Node

        """
        if name:
            new_node = node.createNode(node_type, node_name=name)
        else:
            new_node = node.createNode(node_type)

        new_node.moveToGoodPosition()
        return new_node

    def _match_maya_render_mask(self, camera):
        """Workaround to match Maya render mask in Houdini"""
        parm = camera.parm("aperture")
        print(f"Applying match Maya render mask expression to: {parm.path()}")

        expression = parm.expression()
        expression = expression.replace("return ", "aperture = ")
        expression += self._match_maya_render_mask_expression
        parm.setExpression(expression, language=hou.exprLanguage.Python)

create_and_connect(node, node_type, name=None)

Create a node within a node which and connect it to the input

Parameters:

Name Type Description Default
node(hou.Node)

parent of the new node

required
node_type(str) name of the type of node, eg

'alembic'

required
name(str, Optional

name of the node

required

Returns:

Type Description

hou.Node

Source code in client/ayon_houdini/plugins/load/load_camera.py
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
def create_and_connect(self, node, node_type, name=None):
    """Create a node within a node which and connect it to the input

    Args:
        node(hou.Node): parent of the new node
        node_type(str) name of the type of node, eg: 'alembic'
        name(str, Optional): name of the node

    Returns:
        hou.Node

    """
    if name:
        new_node = node.createNode(node_type, node_name=name)
    else:
        new_node = node.createNode(node_type)

    new_node.moveToGoodPosition()
    return new_node

transfer_non_default_values(src, dest, ignore=None)

Copy parm from src to dest.

Because the Alembic Archive rebuilds the entire node hierarchy on triggering "Build Hierarchy" we want to preserve any local tweaks made by the user on the camera for ease of use. That could be a background image, a resolution change or even Redshift camera parameters.

We try to do so by finding all Parms that exist on both source and destination node, include only those that both are not at their default value, they must be visible, we exclude those that have the special "alembic archive" channel expression and ignore certain Parm types.

Source code in client/ayon_houdini/plugins/load/load_camera.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def transfer_non_default_values(src, dest, ignore=None):
    """Copy parm from src to dest.

    Because the Alembic Archive rebuilds the entire node
    hierarchy on triggering "Build Hierarchy" we want to
    preserve any local tweaks made by the user on the camera
    for ease of use. That could be a background image, a
    resolution change or even Redshift camera parameters.

    We try to do so by finding all Parms that exist on both
    source and destination node, include only those that both
    are not at their default value, they must be visible,
    we exclude those that have the special "alembic archive"
    channel expression and ignore certain Parm types.

    """

    ignore_types = {
        hou.parmTemplateType.Toggle,
        hou.parmTemplateType.Menu,
        hou.parmTemplateType.Button,
        hou.parmTemplateType.FolderSet,
        hou.parmTemplateType.Separator,
        hou.parmTemplateType.Label,
    }

    src.updateParmStates()

    for parm in src.allParms():

        if ignore and parm.name() in ignore:
            continue

        # If destination parm does not exist, ignore..
        dest_parm = dest.parm(parm.name())
        if not dest_parm:
            continue

        # Ignore values that are currently at default
        if parm.isAtDefault() and dest_parm.isAtDefault():
            continue

        if not parm.isVisible():
            # Ignore hidden parameters, assume they
            # are implementation details
            continue

        expression = get_expression(parm)
        if expression is not None and ARCHIVE_EXPRESSION in expression:
            # Assume it's part of the automated connections that the
            # Alembic Archive makes on loading of the camera and thus we do
            # not want to transfer the expression
            continue

        # Ignore folders, separators, etc.
        if parm.parmTemplate().type() in ignore_types:
            continue

        print("Preserving attribute: %s" % parm.name())
        dest_parm.setFromParm(parm)