0
$\begingroup$

I'm quite fluent with programming in general, and scripting in 3D (particularly with Houdini, python+vex), but I'm new to scripting in Blender.

What I'd like to do is render each frame of an animation multiple times with different textures (and eventually, more different parameters, but I'll get to that once this is done).

texture_paths = [...] # list of paths to textures
for frame in range(1, num_frames):
   for texture_path in texture_paths:
    set_texture(texture_path)

    texture_name = os.path.splitext(os.path.basename(texture_path))[0]
    output_path = os.path.join(output_root, texture_name, f'{texture_name}.{frame}.exr'
    set_render_path(output_path)
    render_frame(frame)

Looking around, I think set_render_path and render_frame could be defined as follows (pls correct if wrong or if there are gotchas I'm missing)

def set_render_path(output_path):
    bpy.context.scene.render.filepath = output_path

def render_frame(frame):
    bpy.context.scene.frame_set(frame)
    bpy.ops.render.render(animation=False, write_still=True)

but I can't for the life of me figure out how to programmatically change the texture (i.e. image in an image node in a shader graph). When I look at the log when I manually load an image via the GUI, it just says bpy.ops.image.open. So it seems this is dependent on which node and context is active. When I select the node on which I'd like to change the texture, the log says bpy.ops.node.select(deselect_all=True, select_passthrough=True, location=(248, 328))! Which seems like it's selecting based on where my mouse clicked, which seems insane! I.e. if I organize my node graph, will my script stop working? (in Houdini, I would simply access any node - even parameter - directly via a hierarchy of unique IDs. E.g. "/context/parent/child/node/parameter").

How can I programmatically change the image of a specific image node in a shader graph in Blender?

(I did see this post, but they solved it by duplicating objects and materials, and hiding objects before render. This is not feasible for me as I have dozens of textures. Changing texture on the fly would be much more preferable if possible.)

$\endgroup$

2 Answers 2

1
$\begingroup$

Try this setup with a default cube and a default material "Material" then add an Image Texture and then run the script.

enter image description here

import bpy
import os

output_root = 'C:\\output\\'

texture_paths = [
    'C:\\path\\to\\textures\\tex-1.jpg',
    'C:\\path\\to\\textures\\tex-2.jpg',
    'C:\\path\\to\\textures\\tex-3.jpg'
]

def set_render_path(output_path):
    bpy.context.scene.render.filepath = output_path

def render_frame(frame):
    bpy.context.scene.frame_set(frame)
    bpy.ops.render.render(animation=False, write_still=True)

def set_texture(texture_path):
    filename = os.path.basename(texture_path)
    bpy.ops.image.open(filepath=texture_path)
    image = bpy.data.images[filename]
    bpy.data.materials['Material'].node_tree.nodes["Image Texture"].image = image

for frame in range(1, 3):
   for texture_path in texture_paths:
        set_texture(texture_path)
        texture_name = os.path.splitext(os.path.basename(texture_path))[0]
        output_path = os.path.join(output_root, texture_name, f'{texture_name}.{frame}.exr')
        set_render_path(output_path)
        render_frame(frame)
$\endgroup$
2
  • $\begingroup$ ah awesome thanks. I figured out a slightly different solution, directly setting the filepath property of the bpy.data.images already associated with the material in use. It's very helpful to see in your solution, how to directly access nodes and their properties. $\endgroup$
    – memo
    Commented Aug 3, 2022 at 22:26
  • 1
    $\begingroup$ nice! u can use the python console and its autocomplete function to easily test and find props n funcs by pressing tab. like type bpy.<tab> and it will show u a list or autocompletes. $\endgroup$
    – Harry McKenzie
    Commented Aug 3, 2022 at 22:37
1
$\begingroup$

ok I figured it out, it was super simple in the end :) Instead of changing the image on the node, I change the image in the image slot that the node is associated with. (This was a concept that I wasn't familiar with, but turns out to be very simple).

def set_texture(tex_path, image_name):
    bpy.data.images[image_name].filepath = tex_path

For others wanting to do something similar, my full code is below

import bpy
import os

def set_texture(tex_path, image_name='sky'):
    bpy.data.images[image_name].filepath = tex_path

def set_render_path(render_path):
    bpy.context.scene.render.filepath = render_path

def render_frame(frame):
    bpy.context.scene.frame_set(frame)
    bpy.ops.render.render(animation=False, write_still=True)

def render(tex_paths, output_root, overwrite=False, frames=(1, 1000)):
    for frame in range(*frames):
        print('-'*80)
        print('Frame', frame)
        for tex_path in tex_paths:
            tex_filename = os.path.splitext(os.path.basename(tex_path))[0]
            print('Texture', tex_filename, end=' ... ')

            render_name = f'tex_{tex_path}'
            render_path = os.path.join(output_root, render_name, f'{render_name}.{frame:04}.exr')

            do_render = True
            if os.path.exists(render_path):
                print('EXISTS', end=' ... ')
                if overwrite:
                    do_render = True
                    print('OVERWRITING')
                else:
                    do_render = False
                    print('SKIPPING')
            else:
                do_render = True
                print('RENDERING')

            if do_render:
                set_texture(tex_path)
                set_render_path(render_path)
                render_frame(frame)
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .