# Copyright (c) <2021> Side Effects Software Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # 2. The name of Side Effects Software may not be used to endorse or # promote products derived from this software without specific prior # written permission. # # THIS SOFTWARE IS PROVIDED BY SIDE EFFECTS SOFTWARE "AS IS" AND ANY EXPRESS # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN # NO EVENT SHALL SIDE EFFECTS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import unreal class ProcessHDA(object): """ An object that wraps async processing of an HDA (instantiating, cooking/processing/baking an HDA), with functions that are called at the various stages of the process, that can be overridden by subclasses for custom funtionality: - on_failure() - on_complete(): upon successful completion (could be PostInstantiation if auto cook is disabled, PostProcessing if auto bake is disabled, or after PostAutoBake if auto bake is enabled. - on_pre_instantiation(): before the HDA is instantiated, a good place to set parameter values before the first cook. - on_post_instantiation(): after the HDA is instantiated, a good place to set/configure inputs before the first cook. - on_post_auto_cook(): right after a cook - on_pre_process(): after a cook but before output objects have been created/processed - on_post_processing(): after output objects have been created - on_post_auto_bake(): after outputs have been baked Instantiate the processor via the constructor and then call the activate() function to start the asynchronous process. """ def __init__( self, houdini_asset, instantiate_at=unreal.Transform(), parameters=None, node_inputs=None, parameter_inputs=None, world_context_object=None, spawn_in_level_override=None, enable_auto_cook=True, enable_auto_bake=False, bake_directory_path="", bake_method=unreal.HoudiniEngineBakeOption.TO_ACTOR, remove_output_after_bake=False, recenter_baked_actors=False, replace_previous_bake=False, delete_instantiated_asset_on_completion_or_failure=False): """ Instantiates an HDA in the specified world/level. Sets parameters and inputs supplied in InParameters, InNodeInputs and parameter_inputs. If bInEnableAutoCook is true, cooks the HDA. If bInEnableAutoBake is true, bakes the cooked outputs according to the supplied baking parameters. This all happens asynchronously, with the various output pins firing at the various points in the process: - PreInstantiation: before the HDA is instantiated, a good place to set parameter values before the first cook (parameter values from ``parameters`` are automatically applied at this point) - PostInstantiation: after the HDA is instantiated, a good place to set/configure inputs before the first cook (inputs from ``node_inputs`` and ``parameter_inputs`` are automatically applied at this point) - PostAutoCook: right after a cook - PreProcess: after a cook but before output objects have been created/processed - PostProcessing: after output objects have been created - PostAutoBake: after outputs have been baked - Completed: upon successful completion (could be PostInstantiation if auto cook is disabled, PostProcessing if auto bake is disabled, or after PostAutoBake if auto bake is enabled). - Failed: If the process failed at any point. Args: houdini_asset (HoudiniAsset): The HDA to instantiate. instantiate_at (Transform): The Transform to instantiate the HDA with. parameters (Map(Name, HoudiniParameterTuple)): The parameters to set before cooking the instantiated HDA. node_inputs (Map(int32, HoudiniPublicAPIInput)): The node inputs to set before cooking the instantiated HDA. parameter_inputs (Map(Name, HoudiniPublicAPIInput)): The parameter-based inputs to set before cooking the instantiated HDA. world_context_object (Object): A world context object for identifying the world to spawn in, if spawn_in_level_override is null. spawn_in_level_override (Level): If not nullptr, then the HoudiniAssetActor is spawned in that level. If both spawn_in_level_override and world_context_object are null, then the actor is spawned in the current editor context world's current level. enable_auto_cook (bool): If true (the default) the HDA will cook automatically after instantiation and after parameter, transform and input changes. enable_auto_bake (bool): If true, the HDA output is automatically baked after a cook. Defaults to false. bake_directory_path (str): The directory to bake to if the bake path is not set via attributes on the HDA output. bake_method (HoudiniEngineBakeOption): The bake target (to actor vs blueprint). @see HoudiniEngineBakeOption. remove_output_after_bake (bool): If true, HDA temporary outputs are removed after a bake. Defaults to false. recenter_baked_actors (bool): Recenter the baked actors to their bounding box center. Defaults to false. replace_previous_bake (bool): If true, on every bake replace the previous bake's output (assets + actors) with the new bake's output. Defaults to false. delete_instantiated_asset_on_completion_or_failure (bool): If true, deletes the instantiated asset actor on completion or failure. Defaults to false. """ super(ProcessHDA, self).__init__() self._houdini_asset = houdini_asset self._instantiate_at = instantiate_at self._parameters = parameters self._node_inputs = node_inputs self._parameter_inputs = parameter_inputs self._world_context_object = world_context_object self._spawn_in_level_override = spawn_in_level_override self._enable_auto_cook = enable_auto_cook self._enable_auto_bake = enable_auto_bake self._bake_directory_path = bake_directory_path self._bake_method = bake_method self._remove_output_after_bake = remove_output_after_bake self._recenter_baked_actors = recenter_baked_actors self._replace_previous_bake = replace_previous_bake self._delete_instantiated_asset_on_completion_or_failure = delete_instantiated_asset_on_completion_or_failure self._asset_wrapper = None self._cook_success = False self._bake_success = False @property def asset_wrapper(self): """ The asset wrapper for the instantiated HDA processed by this node. """ return self._asset_wrapper @property def cook_success(self): """ True if the last cook was successful. """ return self._cook_success @property def bake_success(self): """ True if the last bake was successful. """ return self._bake_success @property def houdini_asset(self): """ The HDA to instantiate. """ return self._houdini_asset @property def instantiate_at(self): """ The transform the instantiate the asset with. """ return self._instantiate_at @property def parameters(self): """ The parameters to set on on_pre_instantiation """ return self._parameters @property def node_inputs(self): """ The node inputs to set on on_post_instantiation """ return self._node_inputs @property def parameter_inputs(self): """ The object path parameter inputs to set on on_post_instantiation """ return self._parameter_inputs @property def world_context_object(self): """ The world context object: spawn in this world if spawn_in_level_override is not set. """ return self._world_context_object @property def spawn_in_level_override(self): """ The level to spawn in. If both this and world_context_object is not set, spawn in the editor context's level. """ return self._spawn_in_level_override @property def enable_auto_cook(self): """ Whether to set the instantiated asset to auto cook. """ return self._enable_auto_cook @property def enable_auto_bake(self): """ Whether to set the instantiated asset to auto bake after a cook. """ return self._enable_auto_bake @property def bake_directory_path(self): """ Set the fallback bake directory, for if output attributes do not specify it. """ return self._bake_directory_path @property def bake_method(self): """ The bake method/target: for example, to actors vs to blueprints. """ return self._bake_method @property def remove_output_after_bake(self): """ Remove temporary HDA output after a bake. """ return self._remove_output_after_bake @property def recenter_baked_actors(self): """ Recenter the baked actors at their bounding box center. """ return self._recenter_baked_actors @property def replace_previous_bake(self): """ Replace previous bake output on each bake. For the purposes of this node, this would mostly apply to .uassets and not actors. """ return self._replace_previous_bake @property def delete_instantiated_asset_on_completion_or_failure(self): """ Whether or not to delete the instantiated asset after Complete is called. """ return self._delete_instantiated_asset_on_completion_or_failure def activate(self): """ Activate the process. This will: - instantiate houdini_asset and wrap it as asset_wrapper - call on_failure() for any immediate failures - otherwise bind to delegates from asset_wrapper so that the various self.on_*() functions are called as appropriate Returns immediately (does not block until cooking/processing is complete). Returns: (bool): False if activation failed. """ # Get the API instance houdini_api = unreal.HoudiniPublicAPIBlueprintLib.get_api() if not houdini_api: # Handle failures: this will unbind delegates and call on_failure() self._handle_on_failure() return False # Create an empty API asset wrapper self._asset_wrapper = unreal.HoudiniPublicAPIAssetWrapper.create_empty_wrapper(houdini_api) if not self._asset_wrapper: # Handle failures: this will unbind delegates and call on_failure() self._handle_on_failure() return False # Bind to the wrapper's delegates for instantiation, cooking, baking # etc events self._asset_wrapper.on_pre_instantiation_delegate.add_callable( self._handle_on_pre_instantiation) self._asset_wrapper.on_post_instantiation_delegate.add_callable( self._handle_on_post_instantiation) self._asset_wrapper.on_post_cook_delegate.add_callable( self._handle_on_post_auto_cook) self._asset_wrapper.on_pre_process_state_exited_delegate.add_callable( self._handle_on_pre_process) self._asset_wrapper.on_post_processing_delegate.add_callable( self._handle_on_post_processing) self._asset_wrapper.on_post_bake_delegate.add_callable( self._handle_on_post_auto_bake) # Begin the instantiation process of houdini_asset and wrap it with # self.asset_wrapper if not houdini_api.instantiate_asset_with_existing_wrapper( self.asset_wrapper, self.houdini_asset, self.instantiate_at, self.world_context_object, self.spawn_in_level_override, self.enable_auto_cook, self.enable_auto_bake, self.bake_directory_path, self.bake_method, self.remove_output_after_bake, self.recenter_baked_actors, self.replace_previous_bake): # Handle failures: this will unbind delegates and call on_failure() self._handle_on_failure() return False return True def _unbind_delegates(self): """ Unbinds from self.asset_wrapper's delegates (if valid). """ if not self._asset_wrapper: return self._asset_wrapper.on_pre_instantiation_delegate.add_callable( self._handle_on_pre_instantiation) self._asset_wrapper.on_post_instantiation_delegate.add_callable( self._handle_on_post_instantiation) self._asset_wrapper.on_post_cook_delegate.add_callable( self._handle_on_post_auto_cook) self._asset_wrapper.on_pre_process_state_exited_delegate.add_callable( self._handle_on_pre_process) self._asset_wrapper.on_post_processing_delegate.add_callable( self._handle_on_post_processing) self._asset_wrapper.on_post_bake_delegate.add_callable( self._handle_on_post_auto_bake) def _check_wrapper(self, wrapper): """ Checks that wrapper matches self.asset_wrapper. Logs a warning if it does not. Args: wrapper (HoudiniPublicAPIAssetWrapper): the wrapper to check against self.asset_wrapper Returns: (bool): True if the wrappers match. """ if wrapper != self._asset_wrapper: unreal.log_warning( '[UHoudiniPublicAPIProcessHDANode] Received delegate event ' 'from unexpected asset wrapper ({0} vs {1})!'.format( self._asset_wrapper.get_name() if self._asset_wrapper else '', wrapper.get_name() if wrapper else '' ) ) return False return True def _handle_on_failure(self): """ Handle any failures during the lifecycle of the process. Calls self.on_failure() and then unbinds from self.asset_wrapper and optionally deletes the instantiated asset. """ self.on_failure() self._unbind_delegates() if self.delete_instantiated_asset_on_completion_or_failure and self.asset_wrapper: self.asset_wrapper.delete_instantiated_asset() def _handle_on_complete(self): """ Handles completion of the process. This can happen at one of three stages: - After on_post_instantiate(), if enable_auto_cook is False. - After on_post_auto_cook(), if enable_auto_cook is True but enable_auto_bake is False. - After on_post_auto_bake(), if both enable_auto_cook and enable_auto_bake are True. Calls self.on_complete() and then unbinds from self.asset_wrapper's delegates and optionally deletes the instantiated asset. """ self.on_complete() self._unbind_delegates() if self.delete_instantiated_asset_on_completion_or_failure and self.asset_wrapper: self.asset_wrapper.delete_instantiated_asset() def _handle_on_pre_instantiation(self, wrapper): """ Called during pre_instantiation. Sets ``parameters`` on the HDA and calls self.on_pre_instantiation(). """ if not self._check_wrapper(wrapper): return # Set any parameters specified for the HDA if self.asset_wrapper and self.parameters: self.asset_wrapper.set_parameter_tuples(self.parameters) self.on_pre_instantiation() def _handle_on_post_instantiation(self, wrapper): """ Called during post_instantiation. Sets inputs (``node_inputs`` and ``parameter_inputs``) on the HDA and calls self.on_post_instantiation(). Completes execution if enable_auto_cook is False. """ if not self._check_wrapper(wrapper): return # Set any inputs specified when the node was created if self.asset_wrapper: if self.node_inputs: self.asset_wrapper.set_inputs_at_indices(self.node_inputs) if self.parameter_inputs: self.asset_wrapper.set_input_parameters(self.parameter_inputs) self.on_post_instantiation() # If not set to auto cook, complete execution now if not self.enable_auto_cook: self._handle_on_complete() def _handle_on_post_auto_cook(self, wrapper, cook_success): """ Called during post_cook. Sets self.cook_success and calls self.on_post_auto_cook(). Args: cook_success (bool): True if the cook was successful. """ if not self._check_wrapper(wrapper): return self._cook_success = cook_success self.on_post_auto_cook(cook_success) def _handle_on_pre_process(self, wrapper): """ Called during pre_process. Calls self.on_pre_process(). """ if not self._check_wrapper(wrapper): return self.on_pre_process() def _handle_on_post_processing(self, wrapper): """ Called during post_processing. Calls self.on_post_processing(). Completes execution if enable_auto_bake is False. """ if not self._check_wrapper(wrapper): return self.on_post_processing() # If not set to auto bake, complete execution now if not self.enable_auto_bake: self._handle_on_complete() def _handle_on_post_auto_bake(self, wrapper, bake_success): """ Called during post_bake. Sets self.bake_success and calls self.on_post_auto_bake(). Args: bake_success (bool): True if the bake was successful. """ if not self._check_wrapper(wrapper): return self._bake_success = bake_success self.on_post_auto_bake(bake_success) self._handle_on_complete() def on_failure(self): """ Called if the process fails to instantiate or fails to start a cook. Subclasses can override this function implement custom functionality. """ pass def on_complete(self): """ Called if the process completes instantiation, cook and/or baking, depending on enable_auto_cook and enable_auto_bake. Subclasses can override this function implement custom functionality. """ pass def on_pre_instantiation(self): """ Called during pre_instantiation. Subclasses can override this function implement custom functionality. """ pass def on_post_instantiation(self): """ Called during post_instantiation. Subclasses can override this function implement custom functionality. """ pass def on_post_auto_cook(self, cook_success): """ Called during post_cook. Subclasses can override this function implement custom functionality. Args: cook_success (bool): True if the cook was successful. """ pass def on_pre_process(self): """ Called during pre_process. Subclasses can override this function implement custom functionality. """ pass def on_post_processing(self): """ Called during post_processing. Subclasses can override this function implement custom functionality. """ pass def on_post_auto_bake(self, bake_success): """ Called during post_bake. Subclasses can override this function implement custom functionality. Args: bake_success (bool): True if the bake was successful. """ pass