Skip to content

Instantly share code, notes, and snippets.

@rotoglup
Last active September 26, 2022 10:19
Show Gist options
  • Save rotoglup/47e399de1a6f220327a0de9ab2d79db0 to your computer and use it in GitHub Desktop.
Save rotoglup/47e399de1a6f220327a0de9ab2d79db0 to your computer and use it in GitHub Desktop.
Some Blender 2.80+ scripting notes

Scene, view, collections, etc.

Create a new collection

collection = bpy.data.collections.get(collectionName)
if collection is None:
    collection = bpy.data.collections.new(collectionName)
    bpy.context.scene.collection.children.link(collection)

# make it the active collection
bpy.context.view_layer.active_layer_collection = bpy.context.view_layer.layer_collection.children[collection.name]

Add object to active collection

bpy.context.view_layer.active_layer_collection.collection.objects.link(obj)

Cycles

I had crashes while scripting the CUDA devices selection (b3d v2.83 + 4x GTX 2080 GPU).

The original code was :

preferences = bpy.context.preferences
cycles_preferences = preferences.addons["cycles"].preferences
cuda_devices, opencl_devices = cycles_preferences.get_devices()
...

After reading a comment in blender source code :

/* Lazy initialize devices. On some platforms OpenCL or CUDA drivers can
   * be broken and cause crashes when only trying to get device info, so
   * we don't want to do any initialization until the user chooses to. */

Changed code to the following, seems to prevent (most?) crashes :

...
devices = cycles_preferences.get_devices_for_type('CUDA')
...

Object linked materials

import bpy

def remove_object_linked_material_slots():
    """
    2020/10/21 - In Blender 2.80-2.90+, Cycles do *not* instance objects sharing the same
    mesh datablock when they have materials linked to 'Object' in their material_slots.
    """

    all_objs = dict()

    for obj in bpy.data.objects:

        if obj.type != 'MESH':
            continue

        mesh = obj.data
        mtls = tuple( [ slot.material for slot in obj.material_slots ] )
        key = ( mesh, mtls )
        
        all_objs.setdefault(key, []).append(obj)

    print("*" * 60)

    for key in all_objs:

        objs = all_objs[key]
        if len(objs) > 1:

            mesh, mtls = key
            
            newmesh = mesh.copy()

            for obj in objs:
                obj.data = newmesh
                for i, mtl in enumerate(mtls):
                    mtl_slot = obj.material_slots[i]
                    mtl_slot.link = 'DATA'
                    mtl_slot.material = mtl

Materials

  • Material slots :
    • bpy.ops.object.material_slot_add({'object':obj})
    • bpy.ops.object.material_slot_remove({'object': obj})
  • Could not a find a way to get material preview images from a script, browsing the source code, they seem to be generated in an async way only in the related UI components

GLTF export

To export ambient occlusion textures, one have to use a glTF Settings node group, which can be created by script :

import bpy

# create a group
gltfSettings_group = bpy.data.node_groups.new('glTF Settings', 'ShaderNodeTree')

# create group inputs
group_inputs = gltfSettings_group.nodes.new('NodeGroupInput')
group_inputs.location = (-100,0)
gltfSettings_group.inputs.new('NodeSocketFloat','Occlusion')

# create group outputs
group_outputs = gltfSettings_group.nodes.new('NodeGroupOutput')
group_outputs.location = (100,0)

RenderEngine

Blender API documentation

class RenderEngine(bpy.types.RenderEngine):
    
    bl_idname = "MY_CUSTOM_RENDER_ENGINE"
    bl_label  = "Custom Render Engine"
    bl_use_preview = False  # doesn't supports being used for rendering previews of materials, lights and worlds
    bl_use_shading_nodes_custom = False  # Use Cycles and Eevee material nodes

    def __init__(self):
        
        print("RenderEngine.__init__")

    def __del__(self):
        """
        If no reference on 'self' is kept :
        * '__del__' is called immediately
        * 'self' is valid and usable

        In some other cases, the call is delayed :
        * 'self' is 'invalid', weakref to 'self' is not None
        * accessing 'self.some_attribute' will raise 'ReferenceError: StructRNA of type RenderEngine has been removed'
        * after that, weakref to 'self' becomes None
        """
        print("RenderEngine.__del__", self)

    def render(self, depsgraph):
        
        return

    def view_update(self, context, depsgraph):

        return

    def view_draw(self, context, depsgraph):

        return
import bpy
def nodeTreeRemoveAllNodes(nodeTree):
"""
Preserve the 'output node'
"""
allNodes = nodeTree.nodes[:]
outputNode = nodeTree.get_output_node('ALL')
for node in allNodes:
if node == outputNode:
continue
nodeTree.nodes.remove(node)
def copyNodeTree(srcNodeTree, dstNodeTree, reuseOutputNode=True):
# TODO(nll) handle 'parent' nodes
# TODO(nll) handle node groups
srcToDst = {}
if reuseOutputNode:
srcOutputNode = srcNodeTree.get_output_node('ALL')
dstOutputNode = dstNodeTree.get_output_node('ALL')
srcToDst[srcOutputNode] = dstOutputNode
# Copy all nodes into destination node_tree
# Including property values (for example, ShaderNodeBsdfPrincipled.distribution)
ATTRS_TO_SKIP = set( [ "__doc__", "__module__", "__slots__", "rna_type", "bl_rna", "dimensions", "id_data", "type", "name", "inputs", "outputs", "internal_links", "parent", "select", "bl_idname", "bl_label", "bl_description", "bl_icon", "bl_static_type", "bl_width_default", "bl_width_min", "bl_width_max", "bl_height_default", "bl_height_min", "bl_height_max" ] )
for node in srcNodeTree.nodes:
if node in srcToDst:
continue
newNode = dstNodeTree.nodes.new(node.bl_idname)
for key in dir(newNode):
if key in ATTRS_TO_SKIP:
continue
attr = getattr(node, key)
if callable(attr):
continue
setattr(newNode, key, attr)
srcToDst[node] = newNode
# Sockets : set default_value and recreate links
for srcNode, dstNode in srcToDst.items():
for socketIndex, srcInputSocket in enumerate(srcNode.inputs):
try:
dstInputSocket = dstNode.inputs[socketIndex]
except IndexError:
# Can happen when relinking output nodes of different types
continue
if hasattr(srcInputSocket, 'default_value'):
dstInputSocket.default_value = srcInputSocket.default_value
for link in srcInputSocket.links:
srcOutputSockIndex = findSocketIndex(link.from_node.outputs, link.from_socket)
dstOutputNode = srcToDst[link.from_node]
dstOutputSocket = dstOutputNode.outputs[srcOutputSockIndex]
dstNodeTree.links.new(dstInputSocket, dstOutputSocket)
def findSocketIndex(sockets, socket):
for index, s in enumerate(sockets):
if s == socket:
return index
raise ValueError()
# Example use
srcMtl = bpy.data.materials['Material']
dstMtl = bpy.data.materials['Material.001']
srcNodeTree = srcMtl.node_tree
dstNodeTree = dstMtl.node_tree
srcMtl = bpy.data.materials['Material.001']
srcNodeTree = srcMtl.node_tree
dstNodeTree = bpy.data.lights['Light'].node_tree
print("*" * 80)
nodeTreeRemoveAllNodes(dstNodeTree)
copyNodeTree(srcNodeTree, dstNodeTree)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment