Hi, I’m trying to make an addon that makes octane scattering a little easier, all it does is create a spawner object as I’ve found having the object being scattered on be the spawner is unreliable, appends a material to it, sets that up as the octane geo-node and so on.
I’m trying to do this with ChatGPT (I know that is probably why I am running into issues but without it I wouldn’t even be able to try this) because I do not know anything about coding, and I’m running into an issue, upon creating a spawner I get a bunch of errors saying:
“C:\Users\andre\OctaneBlender28.14\4.0\scripts\addons\octane_init_.py”, line 228, in view_update
File “C:\Users\andre\OctaneBlender28.14\4.0\scripts\addons\octane_init_.py”, line 40, in get_active_render_engine
RecursionError: maximum recursion depth exceeded while calling a Python object
Location: C:\Users\andre\OctaneBlender28.14\4.0\scripts\addons\octane\nodes\base_node_tree.py:892"
I am not sure why this is happening, and also, when I create a spawner for a scatter it slows blender down a lot, making it so that the asset library reloads thumbnails when I try to move an object, and the movement itself gets very choppy.
The script specifies a path to a blend file containing the scatter materials, I cannot attach the file as I am a new user, but it is a standard setup for an octane scatter, with the exception of naming the node which specifies the surface to scatter onto “ScatterSurface” and the nodes that specify objects to scattered “ScatterObject”
this is the script I am using:
import bpy
import os
# A list to keep track of created spawners
created_spawners = []
# Path to the asset file containing the materials
asset_file_path = "D:/assets/OctaneScatterAddon/ScatterAssets/Scatters.blend"
# Function to load materials from the specified asset file
def load_materials_from_asset_file(filepath):
if not os.path.exists(filepath):
return []
# Open the blend file to list materials
with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to):
materials = data_from.materials
return materials
# List of materials in the asset file
asset_materials = load_materials_from_asset_file(asset_file_path)
def get_or_create_collection(name, parent=None):
if name in bpy.data.collections:
return bpy.data.collections[name]
else:
new_collection = bpy.data.collections.new(name)
if parent:
parent.children.link(new_collection)
else:
bpy.context.scene.collection.children.link(new_collection)
return new_collection
def append_or_link_material(material_name, asset_path):
if material_name in bpy.data.materials:
return bpy.data.materials[material_name]
material_path = os.path.join(asset_path, "Material", material_name)
with bpy.data.libraries.load(asset_path, link=True) as (data_from, data_to):
if material_name in data_from.materials:
data_to.materials = [material_name]
return bpy.data.materials.get(material_name)
class OBJECT_OT_create_scatter_spawner(bpy.types.Operator):
bl_idname = "object.create_scatter_spawner"
bl_label = "Scatter on active"
bl_description = "Creates a new Scatter Spawner cube and assigns it to the active object"
material: bpy.props.EnumProperty(
name="Material",
description="Select a material for the Scatter Spawner",
items=[(mat, mat, "") for mat in asset_materials]
)
def execute(self, context):
global created_spawners
# Ensure an active object is selected
if not context.object:
self.report({'ERROR'}, "No active object selected")
return {'CANCELLED'}
active_object = context.object
active_object_name = active_object.name
spawner_name = f"{active_object_name}_Spawner"
# Check for existing spawners with the same name and add suffix if necessary
spawner_index = 1
while spawner_name in bpy.data.objects:
spawner_name = f"{active_object_name}_Spawner_{spawner_index:03d}"
spawner_index += 1
# Store the current mode and switch to object mode
current_mode = bpy.context.mode
if current_mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
# Create a new cube
bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, -10))
# Get the created cube object
cube = bpy.context.object
cube.name = spawner_name
# Append or link the selected material from the asset file
scatter_material = append_or_link_material(self.material, asset_file_path)
# Make a single-user copy of the material and assign it to the cube
if scatter_material:
single_user_material = scatter_material.copy()
single_user_material.name = f"{active_object_name}_Spawner"
if cube.data.materials:
cube.data.materials[0] = single_user_material
else:
cube.data.materials.append(single_user_material)
# Set the material name to the specified property
cube.data.octane.octane_geo_node_collections.node_graph_tree = single_user_material.name
# Assign the active object as the ScatterSurface
scatter_node = single_user_material.node_tree.nodes.get("ScatterSurface")
if scatter_node:
scatter_node.object_ptr = active_object # Assign active object directly
# Set the specified string to the second property
cube.data.octane.octane_geo_node_collections.osl_geo_node = "Scatter on surface"
# Add the spawner to the list
created_spawners.append(cube.name)
# Organize the spawners into collections
scatter_collection = get_or_create_collection("Scatter")
spawner_collection = get_or_create_collection("ScatterSpawners", scatter_collection)
if cube.name not in spawner_collection.objects:
spawner_collection.objects.link(cube)
# Ensure the object is not linked to any other collection
for collection in bpy.data.collections:
if collection.name not in {"Scatter", "ScatterSpawners"} and cube.name in collection.objects:
collection.objects.unlink(cube)
# Ensure the 3D viewport is in object mode
if bpy.context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
# Set the origin to world origin
bpy.context.view_layer.objects.active = cube
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS')
cube.location = (0, 0, 0)
# Restore the previous mode if it's a valid mode
try:
if current_mode != 'OBJECT':
bpy.ops.object.mode_set(mode=current_mode)
except Exception as e:
self.report({'WARNING'}, f"Failed to switch back to mode {current_mode}: {e}")
self.report({'INFO'}, f"{spawner_name} created")
return {'FINISHED'}
class OBJECT_OT_delete_scatter_spawner(bpy.types.Operator):
bl_idname = "object.delete_scatter_spawner"
bl_label = "Delete Scatter Spawner"
bl_description = "Deletes the specified Scatter Spawner object"
name: bpy.props.StringProperty()
def execute(self, context):
obj = bpy.data.objects.get(self.name)
if obj:
# Unlink from all collections
for collection in obj.users_collection:
collection.objects.unlink(obj)
# Remove the object
bpy.data.objects.remove(obj, do_unlink=True)
if self.name in created_spawners:
created_spawners.remove(self.name)
self.report({'INFO'}, f"{self.name} deleted")
else:
self.report({'ERROR'}, f"{self.name} not found")
return {'FINISHED'}
class OBJECT_PT_scatter_spawner_panel(bpy.types.Panel):
bl_label = "Scatter Spawner Panel"
bl_idname = "OBJECT_PT_scatter_spawner_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Scatter Spawner"
def draw(self, context):
layout = self.layout
# Add button to create a new scatter spawner with material selection
layout.operator_menu_enum("object.create_scatter_spawner", "material", text="Scatter on active")
# Display the list of created spawners
layout.label(text="Created Spawners:")
scatter_spawners_collection = bpy.data.collections.get("ScatterSpawners")
if scatter_spawners_collection:
self.draw_collection(layout, scatter_spawners_collection)
def draw_collection(self, layout, collection):
row = layout.row()
row.label(text=collection.name, icon='OUTLINER_COLLECTION')
for obj in collection.objects:
self.draw_object(layout, obj)
def draw_object(self, layout, obj):
row = layout.row()
row.separator(factor=1.5) # Indentation for hierarchy
sub = row.row(align=True)
sub.scale_x = 0.8 # Scale for a darker background
sub.label(text=obj.name, icon='OBJECT_DATA')
# Create a sub-layout for buttons with some spacing
button_row = sub.row(align=True)
button_row.scale_x = 1.2 # Adjust scale for spacing
button_row.operator("object.select_scatter_spawner", text="", icon='RESTRICT_SELECT_OFF').name = obj.name
button_row.operator("object.delete_scatter_spawner", text="", icon='X').name = obj.name
class OBJECT_OT_select_scatter_spawner(bpy.types.Operator):
bl_idname = "object.select_scatter_spawner"
bl_label = "Select Scatter Spawner"
bl_description = "Selects the specified Scatter Spawner object"
name: bpy.props.StringProperty()
def execute(self, context):
# Deselect all objects first
bpy.ops.object.select_all(action='DESELECT')
obj = bpy.data.objects.get(self.name)
if obj:
bpy.context.view_layer.objects.active = obj
obj.select_set(True)
self.report({'INFO'}, f"{self.name} selected")
else:
self.report({'ERROR'}, f"{self.name} not found")
return {'FINISHED'}
# Register the classes
def register():
bpy.utils.register_class(OBJECT_OT_create_scatter_spawner)
bpy.utils.register_class(OBJECT_PT_scatter_spawner_panel)
bpy.utils.register_class(OBJECT_OT_select_scatter_spawner)
bpy.utils.register_class(OBJECT_OT_delete_scatter_spawner)
# Unregister the classes
def unregister():
bpy.utils.unregister_class(OBJECT_OT_create_scatter_spawner)
bpy.utils.unregister_class(OBJECT_PT_scatter_spawner_panel)
bpy.utils.unregister_class(OBJECT_OT_select_scatter_spawner)
bpy.utils.unregister_class(OBJECT_OT_delete_scatter_spawner)
# Run the register function
if __name__ == "__main__":
register()