2017年8月22日火曜日

グリースペンシルからメッシュ

画像を基準にメッシュを作りたくて作成したアドオンを公開します。

画像のアルファから境界線を判別するものも作成したのですが
今回はグリースペンシルで囲った範囲にメッシュを作るものになります


メッシュ作成部分はCOA Toolsアドオンのコードを参考に細かい三角メッシュで埋めるようになっています

bl_info = {
    "name": "grease_pencil storoke to mesh",
    "description": "UV/画像エディッタの上のグリースペンシルのストロークからメッシュの作成",
    "author": "Yukimi",
    "version": (0,2),
    "blender": (2, 6, 0),
    "location": "Object",
    "warning": "",
    "wiki_url": "",
    "tracker_url": "",
    "category": "Object"}

import bpy
import bmesh
def triangle_fill(bm):
    #三角面作成
    triangle_fill = bmesh.ops.triangle_fill(bm,edges=bm.edges,use_beauty=True)
    if triangle_fill["geom"] == []:
        return False
    else:
        return True

def average_edge_cuts(bm,cuts=1):
    ### collapse short edges
    edges_len_average = 0
    edges_count = 0
    shortest_edge = 10000
    for edge in bm.edges:
        if True:#edge.is_boundary:
            edges_count += 1
            length = edge.calc_length()
            edges_len_average += length
            if length < shortest_edge:
                shortest_edge = length
    edges_len_average = edges_len_average/edges_count

    subdivide_edges = []
    for edge in bm.edges:
        cut_count = int(edge.calc_length()/shortest_edge*0.5)*cuts
        if cut_count < 0:
            cut_count = 0
        if not edge.is_boundary:
            subdivide_edges.append([edge,cut_count])
    for edge in subdivide_edges:
        bmesh.ops.subdivide_edges(bm,edges=[edge[0]],cuts=edge[1])

def smooth_verts(bm):
    ### smooth verts
    smooth_verts = []
    for vert in bm.verts:
        if not vert.is_boundary:
            smooth_verts.append(vert)
    for i in range(50):   
        bmesh.ops.smooth_vert(bm,verts=smooth_verts,factor=1.0,use_axis_x=True,use_axis_y=True,use_axis_z=True)    

def bm_pos_to_uv(bm):
    ##UVの設定
    uv_list = [[vert.co[0], vert.co[1]] for vert in bm.verts]
    uv_layer = bm.loops.layers.uv.verify()
    bm.faces.layers.tex.verify()
    for bm_face in bm.faces:
        for v in bm_face.loops:
            id = v.vert.index
            v[uv_layer].uv = uv_list[id]

def stroke_to_mesh(stroke, img):
    me = bpy.data.meshes.new('gpenMesh') 
    #オブジェクトの作成
    obj = bpy.data.objects.new(img.name, me) 
    #オブジェクトをシーンにリンク
    bpy.context.scene.objects.link(obj)
    bm = bmesh.new() # BMeshから空のメッシュを作成
    verts = []
    #bmesh頂点データの作成
    for p in stroke.points:
        pos = p.co
        v = bm.verts.new( [pos[0], pos[1], 0.0] )
        verts.append(v)
    bm.verts.index_update()
    #エッジデータの作成
    for i in range(len(verts)-1):
        bm.edges.new( (verts[i], verts[i +1]) )
    bm.edges.new((verts[-1], verts[0]))
    #メッシュで埋める
    fill_ok = triangle_fill(bm)#三角面作成
    if fill_ok:
        average_edge_cuts(bm)
        bm.verts.index_update()
        bmesh.ops.triangulate(bm,faces=bm.faces)
        smooth_verts(bm) #頂点のスムーズ
        bm_pos_to_uv(bm) #UVの設定
    bm.to_mesh(me) #メッシュの作成
    # 画像をUVにリンク
    for i in range( len(obj.data.polygons) ):
        obj.data.uv_textures[0].data[i].image = img
    return( obj )
############################################
class GpenToMesh(bpy.types.Operator):
    '''add Roop  '''
    bl_idname = "action.gpen2mesh"
    bl_label = "make mesh from grease_pencil"
    def execute(self, context):
        #UV/Imageエディッタで使用中のGreasePencil
        gpen_strokes = context.active_gpencil_layer.active_frame.strokes
        #UV/Imageエディッタで表示中の画像
        img = context.edit_image
        for stroke in gpen_strokes:
            stroke_to_mesh(stroke,img)
        return {'FINISHED'}
#メニューへの登録
def mesh_menu_func(self, context):
    self.layout.operator("action.gpen2mesh", 
        text="GreasePencilからメッシュ作成" )
        
def register():
    bpy.utils.register_module(__name__)
    bpy.types.IMAGE_MT_image.prepend(mesh_menu_func)
 
def unregister():
    bpy.utils.unregister_module(__name__)
    bpy.types.INFO_MT_file_import.remove(mesh_menu_func)
 
if __name__ == "__main__":
    #unregister()
    register()

############################################

コードを実行するとかアドオンとして登録すると
UV/画像エディッタの画像メニューに「GreasePencilからメッシュ作成」という項目が登録されます

今回のコードでは 上記理由でUV/画像エディッタで囲った範囲を元に
XY平面上の0から1.0の範囲にメッシュを作成するようになっていますが
少しコードを変更すれば 3Dビューの方で描いたグリースペンシルに沿ってメッシュを作成することもできるかと思います
グリースペンシルの頂点のデータをそのまま使うため
ドローモードで描いたストロークでは細かくなりすぎるため多角形モードで描いた方がいいかもしれません

いかがでしょうか

2017年7月1日土曜日

ボーンからメッシュ形状を作成するアドオン

Blenderでボーンのアニメーションを作るにあたって
簡易的な形状でレンダリング等、 ボーンを基準にメッシュが作成できればと思うことが多かったので作成してみました。

Blenderでは簡易形状の作成だけなら
メッシュオブジェクトでボーン状に線ポリゴンを作成して スキン(Skin)モデファイアをつけて
「アーマーチュアを作成」ボタンでアーマーチュアを作成する手順もあります

ただ 自分のやりたかった用途では操作の手順が煩雑な上に
関節部分の形状や ひとかたまりの形状しか作れない等のデメリットがありました

そこでボーン形状からのモデリングです
Blenderではアーマチュアの表示形式が選べて デフォルトは八面体ですが
今回はエンベロープ表示を利用します

Blenderのボーンはヘッドとテイルで構成されていて
画面の表示は球形のヘッドとテイルを表す球体と 間を結ぶボディ形状という形になっています

編集モードでこの球を選択してAlt+Sを押すと径を変更することができます
プロパティシェルフで大きさを数値指定することもできますが、ボーンを移動させた時に数値が元に戻ってしまう等、不具合があるようです

位置や大きさを調整することで普通にモデリングするよりも素早く人体っぽい感じの見た目のものを作れるかと思います
ただし、これはアーマチュアですのでレンダリングしても表示されないですし
モデリングのスナップ対象にすることもできません

そこで作ったのがこのアドオンです
bl_info = {
    "name": "activebone to mesh",
    "description": "形状をカメラ方向から投影",
    "author": "Yukimi",
    "version": (0,2),
    "blender": (2, 6, 0),
    "location": "Object",
    "warning": "",
    "wiki_url": "",
    "tracker_url": "",
    "category": "Object"}
import bpy
import math
from mathutils import Vector, Matrix
class active_bone_to_mesh(bpy.types.Operator):
    bl_idname = "object.active_bone_to_mesh"
    bl_label = "選択ボーンからメッシュ"
    bl_description = "選択したボーンのエンベロープを元にカプセル形状を作成"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        if context.mode == 'EDIT_ARMATURE':
            obj = context.active_object
            #グローバル座標への変換マトリクス
            matrix_world = obj.matrix_world
            #エディットモードの状態で情報を更新
            obj.update_from_editmode()
            bones = obj.data.bones
            for b in bones:
                if b.select:
                    head_radius = b.head_radius
                    tail_radius = b.tail_radius
                    distance = (b.tail_local -b.head_local).length
                    mat = matrix_world*b.matrix_local
                    (vertices,faces) = get_cupsule_base(head_radius, tail_radius, distance)
                    #座標の変換
                    vertices = [mat*Vector(v) for v in vertices]
                    name = b.name
                    #メッシュの作成
                    mesh_obj = add_mesh_from_data(name,vertices,faces)
                    #作成元のボーンのウエイトを付加
                    v_group = mesh_obj.vertex_groups.new(name)
                    v_index = list( range( len(vertices) ) )
                    v_group.add(v_index, 1.0, 'REPLACE')
                    #モデファイアの付加
                    mesh_obj.modifiers.new("Armature", 'ARMATURE')
                    mesh_obj.modifiers["Armature"].object = obj
        return {'RUNNING_MODAL'}
#選択したボーンからカプセル形状を作成
def get_cupsule_base(head_radius, tail_radius, distance, roat_div = 12):
    #平行断面の座標を計算
    r_pos = []
    #両端の球面の分割数
    spire_dev= 10
    delta_rad = math.pi/spire_dev
    #ヘッド側の球面上
    for i in range(spire_dev-1, spire_dev//2 , -1):
        t = delta_rad * float(i)
        pos = [head_radius *math.cos(t), head_radius *math.sin(t)]
        r_pos.append( pos )
    #ヘッドとテールの間を結ぶ線の区間
    #球を結ぶ区間の分割数
    span_div = 4
    P_1 = [0,head_radius]
    P_2 = [distance, tail_radius]
    for i in range(span_div):
        t = float(i)/ span_div
        pos_x = (1-t)*P_1[0] + t*P_2[0]
        pos_y = (1-t)*P_1[1] + t*P_2[1]
        r_pos.append( [pos_x, pos_y] )
    #テール側の球面上
    for i in range((spire_dev//2 -1), 0 , -1):
        t = delta_rad * float(i+1)
        pos = [tail_radius *math.cos(t) +distance, tail_radius *math.sin(t)]
        r_pos.append( pos )
    #回転座標の計算
    pos_list = []
    for r in range(roat_div):
        t = 2* r *math.pi/roat_div
        for p in r_pos:
            pos = [ p[1] *math.cos(t), p[0], p[1] *math.sin(t) ]
            pos_list.append(pos)
    face_list = []
    #面のデータを作成
    #軸方向の頂点数
    n_count = len(r_pos)
    #両端の面データを作成
    f = [i* n_count for i in range(roat_div)]
    face_list.append(f)
    f = [((i+1)* n_count-1) for i in reversed(range(roat_div))]
    face_list.append(f)
    #回転部分の面データの作成
    for r in range(roat_div-1):
        for t in range(n_count-1):
            #面の頂点id
            f = [r*n_count + t, r*n_count + t+1, (r+1)*n_count +t +1 , (r+1)*n_count +t]
            face_list.append(f)
    #回転部分の面を閉じる列の面データ
    for t in range(n_count-1):
        f = [(roat_div-1)*n_count + t, (roat_div-1)*n_count + t+1, t +1, t ]
        face_list.append(f)
    return(pos_list, face_list)
#データからメッシュを作成
def add_mesh_from_data(name,vertices,faces):
    mesh = bpy.data.meshes.new(name)
    obj = bpy.data.objects.new(name, mesh)
    bpy.context.scene.objects.link(obj)
    mesh.from_pydata(vertices, [], faces)
    mesh.update()
    return( obj )

################################################
#メニュー項目の設定
def menu_funk(self, context):
    self.layout.operator("object.active_bone_to_mesh")
 # アドオン有効化時の処理
def register():
    bpy.utils.register_module(__name__)
    bpy.types.INFO_MT_edit_armature_add.append(menu_funk)
# アドオン無効化時の処理
def unregister():
    bpy.utils.unregister_module(__name__)
    bpy.types.INFO_MT_edit_armature_add.remove(menu_funk)
################################################

アーマーチュアの編集モードのメニューの「追加」の項目に「選択ボーンからメッシュ」
という項目が追加されます
ボーン一つ一形状でヘッド テイルの設定に合わせた半球とそれを結ぶ形状を作り
元ボーンに合わせた頂点ウエイトやアーマチュア変形のモデファイアを付加してあります

2017年6月8日木曜日

炎パーティクルメモ



Blenderで思い通りの炎を作りたくてテストしてるうちに
意図とは違うものの面白いものができたのでメモ

炎の発生源をパーティクルで配置したオブジェクトしたものにする方式。

パーティクルのレンダーオブジェクトに指定する形状
適当な形状からパーティクルを発生させて
物理演算の煙を有効に フローのタイプを炎フローソースを設定したパーティクルに
炎以外の周りの部分が白くならないように発生源のマテリアルを設定
「高解像度の煙」は計算量が増えるので品質とのトレードオフ
でも、これがないと個人的に好みの煙等にはほど遠い。
立ち上る炎にはしないのでドメインの温度差は0.01以下

2017年5月10日水曜日

XY座標をUVに

Blenderの作業で3Dビュー上の頂点座標をUVマップの値に変換する必要があって
アドオンを作ってみたのでメモ。

厳密に値を合わせるのでなければ通常の「プロジェクション」展開や
たまに使う程度なら UV投影(UVProject)モデファイアを使えばいいので
これ自体を他の人が使うことはなさそうですが UV設定のスクリプトの参考に残しておきます。

bl_info = {
    "name": "projection UV as XY plane",
    "description": "XY平面の座標をUVに",
    "author": "Yukimi",
    "version": (0,2),
    "blender": (2, 6, 0),
    "location": "Object",
    "warning": "",
    "wiki_url": "",
    "tracker_url": "",
    "category": "Object"}

import bpy
class UV_ptoject_xy(bpy.types.Operator):
    bl_idname = "object.uv_ptoject_xy"
    #bl_label = "Projection UV  XY-plane"
    bl_label = "XY平面の座標をUVに"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        #形状の取得
        obj = context.active_object
        #グローバル座標への変換マトリクス
        matrix_world = obj.matrix_world
        #頂点座標の取得
        location_list = [v.co*matrix_world for v in obj.data.vertices]
        uv_list = [p[:2] for p in location_list]
        #UVレイヤがない場合は新規作成
        if obj.data.uv_layers.active_index == -1:
            obj.data.uv_textures.new()
        mode = context.mode
        if  (mode == 'OBJECT'):
            self.on_objmode(obj, uv_list)
        elif (mode == 'EDIT_MESH'):
            self.on_editmode(obj, uv_list)
        return {'RUNNING_MODAL'}
    def on_editmode(self, obj, uv_list):
        """
        編集モードでのUVの設定
        """
        #状態の更新
        bpy.context.edit_object.update_from_editmode()
        import bmesh
        mesh = obj.data
        bm = bmesh.from_edit_mesh(mesh)   # メッシュからBMeshを作成
        #アクティブなUV_layer
        #(bmeshで使うにはUV LayerもBMeshオブジェクトでないといけない)
        uv_layer = bm.loops.layers.uv.active
        #UVの設定
        for bm_face in bm.faces:
            for v in bm_face.loops:
                id = v.vert.index
                v[uv_layer].uv = uv_list[id]
        bmesh.update_edit_mesh(mesh)
            
                    
    def on_objmode(self, obj, uv_list):
        """
        オブジェクトモードでのUVの設定(アドオンでは未使用)
        """
        #アクティブなUVレイヤを取得
        uv_index =obj.data.uv_layers.active_index
        uv_layer = obj.data.uv_layers[uv_index]
        #UVの設定
        for f in obj.data.polygons:
            for i,loop_id in enumerate(f.loop_indices):
                uv_layer.data[ loop_id ].uv = uv_list[i]

################################################
#メニュー項目の設定
def menu_funk(self, context):
    self.layout.operator("object.uv_ptoject_xy")
 # アドオン有効化時の処理
def register():
    bpy.utils.register_module(__name__)
    bpy.types.VIEW3D_MT_uv_map.append(menu_funk)
 
# アドオン無効化時の処理
def unregister():
    bpy.utils.unregister_module(__name__)
    bpy.types.VIEW3D_MT_uv_map.remove(menu_funk)
################################################    
 
 
# メイン処理
if __name__ == "__main__":
    register()

編集モード時のUVマッピングメニューの末尾に登録しています

非常に単純な機能ですが
UVの取得は少し手順が多いのと
エディットモードでのデータを操作するのにbmeshを使って "bmesh.from_edit_mesh(mesh)" で今ある形状からBmeshオブジェクトを作成し
"bmesh.update_edit_mesh(mesh)" で変更を適応する
といったことは やり方を忘れてしまいがちですね

2017年1月18日水曜日

3DカーソルをZY平面上に移動

ミラーモデファイアを使ってオブジェクトを編集しているときなどに
軸になる位置に3Dカーソルを移動したい場合がありますが
標準の機能にはそういった機能はないようなのでアドオンで作りました


bl_info = {
    "name": "カーソルをオブジェクトのZY平面に",
    "author": "Yukimi",
    "version": (1, 0),
    "blender": (2, 60, 0),
    "location": "View3D > Cursor",
    "description": "3DカーソルをオブジェクトのXY平面上に移動",
    "warning": "",
    "wiki_url": "",
    "tracker_url": "",
    "category": "3D View"
}
import bpy
#カーソルのX値を0に
class set_cursor_xy_plane(bpy.types.Operator):
    bl_idname = "cursor.set_cursor_x"
    bl_label = "カーソルをZY平面に"
    def execute(self, context):
        active_obj = bpy.context.active_object
        space = bpy.context.space_data
        if active_obj:
            space.cursor_location[0] = active_obj.location.x
        else:
            space.cursor_location[0] = 0
        return {'FINISHED'}
################################################
def menu_funk(self, context):
    self.layout.operator("cursor.set_cursor_x")

def register():
    bpy.utils.register_module(__name__)
    bpy.types.VIEW3D_MT_snap.append(menu_funk)

# アドオン無効化時の処理
def unregister():
    bpy.utils.unregister_module(__name__)
    bpy.types.VIEW3D_MT_snap.remove(menu_funk)
################################################    


# メイン処理
if __name__ == "__main__":
    register()
Shift+Sのスナップメニューに追加する形式にしてみました

ミラーで円形状になるものを編集している時に
「カーソル>選択物」 「カーソルをZY平面に」 と順に実行することで
円の中心部分にカーソルを移動できます

オブジェクトが回転している場合には現状対応していませんが
いかがでしょうか

2016年12月17日土曜日

Windowsで.blendファイルのプレビュー

Blenderはレンダリング画像をプレビューとして保存することができて
エクスプローラーでファイルの中身を確認できるのが便利だったのですが
家のマシンをWindeow10にしたところ プレビューが表示できなくなってしまっていて、 

対処法を探しても 検索一発で見つけることができずに多少時間がかかったので メモ


blenderartists.org の
.Blend files not using preview thumbnails in windows explorerに書いてあることそのままで

スタートメニューを右クリックするメニューからコマンドプロンプト(管理者)を選んで実行して

黒画面のコマンドプロンプト上で

chdir c:\program files\blender foundation\blender

と入力してEnter(Blenderの実行ファイルのあるところに移動)

blender -R


と入力してうまくいっていると
エクスプローラー上で.blendファイルを再選択すると プレビューが表示されるようになってる
はず。
単純なことなものの
新マシン導入時にしか使わなくて忘れてしまいそうなのでメモってみましたがどうでしょうか。

2016年12月16日金曜日

オブジェクトを前後左右からレンダリングしたい!

他の人に作っているモデルを見せたい場合に
前後左右のキャプチャ画面を渡しているってことが多いかと思います。

いちいちキャプをとって画像処理ソフトでつなげるのも面倒なので
一気にレンダリングしてしまうスクリプトを書いてみました

3Dビューで選択している形状が収まるようにカメラの設定を調整して
前後左右平行投影でのレンダリングと 現在のカメラ設定でレンダリングした画像を作成し [Combineed_IMG]という名前で横に並べた画像を UV/画像エディッタに作成します

import bpy
import math
import os
import numpy as np
import mathutils
#レンダリングの幅を固定
render_width = 300


#レンダリング画像の保存パス
render_path = bpy.context.scene.render.filepath

def render_func(context):
    #シーンで使用しているカメラ
    camera = context.scene.camera
    if len(context.selected_objects) == 0: return
    ###############################
    ###########設定の退避###########
    ##レンダリングサイズ
    render = context.scene.render
    ref_render_x = render.resolution_x
    ref_render_y = render.resolution_y
    ref_render_percentage =render.resolution_percentage
    ##カメラタイプ(透視投影,平行投影など)
    ref_camera_type = camera.data.type
    ##位置
    ref_location = camera.location.copy()
    ref_rotate = camera.rotation_euler.copy()
    ##################################
    
    ##オブジェクトモードに
    bpy.ops.object.mode_set(mode = 'OBJECT')
    #bpy.ops.object.select_all(action="DESELECT")
    ##################################
    ##カメラ設定
    ###レンダリングサイズ
    (bbox_width, center_pos) = get_bound_data(context)
    set_render_size(context, bbox_width)
    #初期状態でレンダリング
    bpy.ops.render.render()
    #保存
    f_name = "view4.png"
    seve_render(f_name)
    ###平行投影
    camera.data.type = 'ORTHO'
    #平行投影のスケールを設定(幅がBU基準)
    camera.data.ortho_scale = max(bbox_width)*1.2
    #カメラ位置を(0, -10, 0.3) 回転を(90°, 0, 0)に
    camera_pos =get_camera_pos_top(bbox_width, center_pos)
    camera.location = camera_pos
    camera.rotation_euler = (math.radians(90.0), 0, 0)
    #########################################
    pos = mathutils.Vector((camera_pos))
    mat_rot1 = mathutils.Matrix.Rotation(math.radians(90.0), 4, 'Z')
    mat_trs = mathutils.Matrix.Translation(mathutils.Vector(center_pos))
    mat_rot = mat_trs *mat_rot1 *mat_trs.inverted()
    for i in range(4):
        #レンダリング
        bpy.ops.render.render()
        #保存
        f_name = "view%s.png" % i
        seve_render(f_name)
        #90度回転
        pos = mat_rot * pos
        camera.location = pos
        cam_rot = (math.radians(90.0), 0, math.radians(90.0)*(i+1))
        camera.rotation_euler = cam_rot
    ##################################
    ###########設定の書き戻し###########
    ###レンダリングサイズ
    render.resolution_x = ref_render_x
    render.resolution_y = ref_render_y
    render.resolution_percentage = ref_render_percentage
    ##カメラタイプ
    camera.data.type = ref_camera_type
    ##位置
    camera.location = ref_location
    camera.rotation_euler = ref_rotate
    #########################################
    combine_image(render_path)

#選択形状から描画範囲のデータを取得(中心点, 幅)
def get_bound_data(context):
    objects = context.selected_objects
    bb_point_list = []
    #選択形状のbbox値をグローバル座標で取得
    for obj in objects:
        bbox_list = [mathutils.Vector(v[:]) for v in obj.bound_box]
        mat = obj.matrix_world
        bb_point_list += [mat*v for v in bbox_list]
    #範囲を取得
    bbox_width = []
    center_pos = []
    for i in range(3):
        min_i = min(bb_point_list, key = (lambda x: x[i]))[i]
        max_i = max(bb_point_list, key = (lambda x: x[i]))[i]
        bbox_width.append( max_i - min_i )
        center_pos.append( (max_i + min_i)/2 )
    return(bbox_width, center_pos)
#レンダリングサイズの設定(幅を固定)
def set_render_size(context, bbox_width):
    render = context.scene.render
    print(bbox_width)
    #縦の長さを取得
    render_height = int(render_width*(bbox_width[2]/max(bbox_width[:2])))
    render.resolution_x = render_width
    render.resolution_y = render_height
    render.resolution_percentage = 100
#正面画像レンダリング用のカメラ位置を設定
def get_camera_pos_top(bbox_width, center_pos):
    distance = max(bbox_width)
    return(center_pos[0], center_pos[1]-distance,center_pos[2])
#画像の保存
def seve_render(f_name):
    img_path = os.path.join(render_path,f_name)
    bpy.data.images['Render Result'].save_render(filepath=img_path)

#画像の読み込み
def load_tex(f_path):
    img = bpy.data.images.load(f_path)
    return(img)

#rgbaの画像をnparrayに変換
def img_to_nparray(img):
    bit_len = len(img.pixels)
    (width,height) = img.size
    channels = img.channels #色数
    #numpy arrayを作成
    pixlist = np.array(img.pixels)
    pixlist = pixlist.reshape( height, width, 4)
    return( pixlist )
    
#結合処理
def combine_image(render_path):
    width = 0
    height = 0
    image_list = []
    for i in range(5):
        f_name = "view%s.png" % i
        img_path = os.path.join(render_path,f_name)
        img = load_tex(img_path)
        image_list.append(img)
        width += img.size[0]
        height = max(height, img.size[1])
    combine_img_np = np.zeros((height, width, 4))
    offset = 0
    for img in image_list:
        #読み込んだ画像をnumpy配列に
        np_array = img_to_nparray(img)
        (height, width, deps) = np_array.shape
        #結合処理
        combine_img_np[0:height, offset:offset + width] = np_array
        combine_img_np[:,offset] = np.ones(4)
        offset += width
    #numpy配列からBlender画像データオブジェクトを作成
    img_name = 'Combineed_IMG'
    (height, width, deps) = combine_img_np.shape
    image_object = bpy.data.images.new(name=img_name, width=width, height=height)
    image_object.pixels = list(combine_img_np.flatten())
    
render_func(bpy.context)

選択形状を基準にレンダリングするため 形状を選択していない場合は動かなかったりと
書き捨てに近いスクリプトですが いかがでしょうか