본문 바로가기
실습 & 활동/Computer vision

[Blender] Camera circle path python script 작성

by sim0609 2024. 3. 12.

아래 코드는 blender에서 camera가 obj를 중심으로 circle path로 회전하며, 3D obj에서 2D 이미지를 rendering하는 코드이다. 이미지를 rendering할 때 필요한 함수를 크게 8개로 구성했다.

bound_box_center(target)

대상 객체의 경계 상자(bounding box) 중심을 계산하고, 이 함수는 객체의 모든 꼭짓점을 월드 좌표계로 변환한 후, 이 꼭짓점들의 평균 위치를 구하여 경계 상자의 중심점을 찾는다.

character_center(target)

대상 객체의 경계 상자 중심을 계산하고, 이 중심의 높이(z 좌표)만 이용하고 나머지 x와 y 좌표는 0으로 설정해 캐릭터의 중심점을 정의한다.

bound_box_render(target)

대상 객체의 경계 상자를 시각적으로 표시하기 위한 메시(mesh) 객체를 생성하고, 경계 상자의 모든 꼭짓점을 사용해 와이어프레임(wireframe) 형태의 bound-box를 만든다.

camera_setting(camera, center)

카메라를 특정 위치(center)를 바라보도록 설정한다. "TRACK_TO" 제약 조건을 사용해 카메라가 지정된 빈 객체(empty object)를 향하도록 하고, empty object는 chracter center 또는 bound box center에 배치할 수 있다.

rendering_img(r, d, interval, empty)

카메라를 이용해 지정된 각도와 간격으로 여러 각도에서 렌더링을 수행하며, 주어진 중심점을 중심으로 카메라를 원형 경로로 이동시키면서 이미지를 렌더링한다.

light_setting(point_light_radius, num_lights, target, point_light_intensity, sun_light_intensity)

Scene에 포인트 라이트(point light)와 선 라이트(sun light)를 추가해 대상 객체를 조명한다. point light의 조명 개수를 지정할 수 있고, point light과 sun light의 강도 또한 설정할 수 있다. 

background_setting(R, G, B, A)

Scene의 배경 색상을 설정하고, world background의 색상과 투명도를 조정할 수 있다.

material_setting(target, specular_rate, roughness_rate)

대상 객체의 재질(material) 설정을 조정하며, 대상 객체의 재질의 광택(specular, 빛에 반사되는 정도)과 거칠기(roughness) 값을 설정해 재질의 외관을 변경한다.

Python Script Code

import bpy
import math
from mathutils import Vector

###################### bound box center ######################

def bound_box_center(target):
    # character's bounding box
    bound_box = target.bound_box

    # bounding box has 8 corners
    # 월드 좌표계에서 경계 상자의 꼭짓점 좌표를 계산
    world_vertices = [target.matrix_world @ Vector(corner) for corner in bound_box]

    # 꼭짓점 좌표 출력(order: vector(x, y, z))
    for idx, vertex in enumerate(world_vertices):
        print(f"Corner {idx}: {vertex}")
    
    # sum whole corner's location and divide to 8
    char_bound_box_center = sum((Vector(v) for v in bound_box), Vector()) / 8

    # bound box center point in world coordinate
    world_center = target.matrix_world @ char_bound_box_center

    print("bound box center:", world_center) 
    
    return world_center

###################### character center ######################

def character_center(target):
    # character's bounding box
    bound_box = target.bound_box

    # bounding box has 8 corners
    # 월드 좌표계에서 경계 상자의 꼭짓점 좌표를 계산
    world_vertices = [target.matrix_world @ Vector(corner) for corner in bound_box]

    # 꼭짓점 좌표 출력(order: vector(x, y, z))
    for idx, vertex in enumerate(world_vertices):
        print(f"Corner {idx}: {vertex}")
    
    # sum whole corner's location and divide to 8
    char_bound_box_center = sum((Vector(v) for v in bound_box), Vector()) / 8

    # bound box center point in world coordinate
    world_center = target.matrix_world @ char_bound_box_center
    
    char_center = (0, 0, world_center[2]+0.2)

    print("character center:", char_center) 
    
    return char_center

###################### render bound box ######################

def bound_box_render(target):
    # define bound box name
    bound_box_name = target.name + "_BoundingBox"
    
    # 기존 BoundingBox가 존재하는지 검사하고 삭제
    if bound_box_name in bpy.data.objects:
        bpy.data.objects.remove(bpy.data.objects[bound_box_name], do_unlink=True)
        
    # 대상 객체의 Bounding Box의 8개 꼭짓점을 월드 좌표계로 변환
    world_vertices = [target.matrix_world @ Vector(corner) for corner in target.bound_box]
    
    # make mesh data - include (vertex, edge, face) 
    mesh_data = bpy.data.meshes.new(bound_box_name)
    # make mesh object - include object's location, rotation, scale, translation
    mesh_obj = bpy.data.objects.new(bound_box_name, mesh_data)
    # 씬에 메시 객체 추가
    bpy.context.collection.objects.link(mesh_obj)

    # 메시 데이터에 꼭짓점(버텍스)와 면(페이스) 추가
    edges = [(0, 1), (1, 2), (2, 3), (3, 0),
         (4, 5), (5, 6), (6, 7), (7, 4),
         (0, 4), (1, 5), (2, 6), (3, 7)]
    faces = []
    mesh_data.from_pydata(world_vertices, edges, faces)

    # 메시 업데이트
    mesh_data.update()

    # 생성된 메시 객체를 선택하고 원본 객체의 위치로 이동
    mesh_obj.select_set(True)
    bpy.context.view_layer.objects.active = mesh_obj

    # 뷰포트에서 경계 상자를 볼 수 있도록 객체의 디스플레이 타입 설정
    mesh_obj.display_type = 'WIRE'

###################### camera setting ######################

def camera_setting(camera, center):
    # 카메라가 Bounding Box or Character의 중심을 바라보게 설정
    # add empty object at center
    empty_obj = bpy.data.objects.get('CenterEmpty')
    
    if empty_obj: 
        bpy.data.objects.remove(empty_obj, do_unlink=True)
        
    bpy.ops.object.empty_add(type='PLAIN_AXES', location=center)
    empty = bpy.context.object
    empty.name = 'CenterEmpty'
    
    # TRACK_TO 제약 조건 추가
    # 기존에 'TRACK_TO' 제약 조건이 있다면 제거
    track_constraints = [c for c in camera.constraints if c.type == 'TRACK_TO']
    for c in track_constraints:
        camera.constraints.remove(c)

    # 새 'TRACK_TO' 제약 조건 추가(track to: 카메라가 특정 대상을 바라보도록 강제합니다)
    track_to = camera.constraints.new(type='TRACK_TO')
    track_to.target = empty
    # 카메라가 대상을 바라보는 조건 정의
    track_to.up_axis = 'UP_Y'
    track_to.track_axis = 'TRACK_NEGATIVE_Z'
    
    return empty

###################### rendering ######################

def rendering_img(r, d, interval, empty):
    # 렌더링 설정
    bpy.context.scene.render.image_settings.file_format = 'PNG'
    render = bpy.context.scene.render
    render.resolution_x = 1024
    render.resolution_y = 1024
    
    if empty == empty1:
        # 원의 중심, 반지름, 각도 단위
        circle_center = empty1.location
        file_name = 'bound_box'
    else:
        circle_center = empty2.location
        file_name = 'character'
        
    radius = r
    degrees = d

    # 각도마다 카메라 위치 계산 및 렌더링
    for angle in range(0, degrees, interval):  # 18도 간격으로 회전
        radians = math.radians(angle)
        # 원의 경로 상에서 카메라의 위치 계산
        x = circle_center[0] + radius * math.cos(radians)
        y = circle_center[1] + radius * math.sin(radians)
        z = circle_center[2]
    
        # 카메라 위치 설정
        camera.location = (x, y, z)
    
        # 렌더링을 위한 파일 경로 설정
        bpy.context.scene.render.filepath = f'C:/Users/user/Desktop/blender_python/circle_path/camera_circle_path/{file_name}_center/image_{angle}.png'
    
        # 렌더링 실행
        bpy.ops.render.render(write_still=True)

###################### light setting ######################

def light_setting(point_light_radius, num_lights, target, point_light_intensity, sun_light_intensity):
    
    # 씬 내의 모든 객체를 순회
    for obj in bpy.context.scene.objects:
        # 객체가 라이트인 경우
        if obj.type == 'LIGHT':
            # 라이트의 유형이 포인트(Point) 또는 선(Sun)인 경우
            if obj.data.type == 'POINT' or obj.data.type == 'SUN':
                # 해당 라이트 객체 삭제
                bpy.data.objects.remove(obj, do_unlink=True)

    # 360도를 라이트의 수로 나누어 각 라이트의 각도를 계산
    angle_step = 2 * math.pi / num_lights

    for i in range(num_lights):
        # 각 라이트의 위치 계산
        angle = angle_step * i
        x = target.location.x + point_light_radius * math.cos(angle)
        y = target.location.y + point_light_radius * math.sin(angle)
        z = target.location.z + 1.0  # 높이 조정
    
        # 포인트 라이트 생성
        bpy.ops.object.light_add(type='POINT', location=(x, y, z))
        point_light = bpy.context.object
        point_light.data.energy = point_light_intensity  # 포인트 라이트의 강도 설정

    # 선(Sun) 라이트 추가
    bpy.ops.object.light_add(type='SUN', location=(target.location.x, target.location.y, target.location.z + 20))
    sun_light = bpy.context.object
    sun_light.data.energy = sun_light_intensity  # 선 라이트의 강도 설정
    sun_light.rotation_euler = (math.radians(45), math.radians(30), math.radians(0))  # 선 라이트의 방향 조정

###################### background setting ######################

def background_setting(R, G, B, A):
    # 월드 설정에 접근
    world = bpy.context.scene.world
    
    if world.use_nodes:
        # 노드 유형이 'BACKGROUND'인 노드를 찾기
        bg_node = next(node for node in world.node_tree.nodes if node.type == 'BACKGROUND')
    
        # 노드의 색상을 흰색으로 설정
        bg_node.inputs['Color'].default_value = (R, G, B, A)  # RGBA, A는 투명도를 나타냄
        # 배경 밝기를 설정 (기본값은 1)
        bg_node.inputs['Strength'].default_value = 1

###################### specular & roughness setting ######################

def material_setting(target, specular_rate, roughness_rate):
    
    # Armature 오브젝트 찾기
    target_obj = bpy.data.objects.get(target.name)
    mat = None

    # Armature 오브젝트가 존재하는지 확인
    if target_obj:
        # Armature 오브젝트의 자식 오브젝트들 순회
        for child_obj in target_obj.children:
            # 자식 오브젝트가 Mesh 타입인 경우
            if child_obj.type == 'MESH':
                print(child_obj.name) # Erin
                
                if not child_obj.data.materials:
                    # 재질이 없는 경우 새 재질 생성 및 할당
                    mat = bpy.data.materials.new(name="New_Material")
                    child_obj.data.materials.append(mat)
                else:
                    # 첫 번째 재질을 가져옴
                    mat = child_obj.data.materials[0]
                    print(mat)
    
        # 재질이 'Principled BSDF' 셰이더 노드를 사용하는지 확인
        if mat and mat.use_nodes and mat.node_tree.nodes:
            nodes = mat.node_tree.nodes
            principled = next((node for node in nodes if node.type == 'BSDF_PRINCIPLED'), None)
            
            # Specular와 Roughness 값 설정
            if principled and 'IOR Level' in principled.inputs:
                principled.inputs['IOR Level'].default_value = specular_rate

            if principled:
                principled.inputs['Roughness'].default_value = roughness_rate
            else:
                print("can't find Principled BSDF shader node")
        else:
            print("material has no node tree")
            

# 카메라 객체
camera = bpy.data.objects['Camera']
# 렌더링할 대상 객체
target = bpy.data.objects['Casual_F_0039']

world_center = bound_box_center(target)
char_center = character_center(target) # bound box center의 height만 가져오기
print(world_center)
print(char_center)

bound_box_render(target)

# 포인트 라이트를 배치할 원의 반지름
point_light_radius = 5
# 원 주위에 배치할 포인트 라이트의 수
num_lights = 5
point_light_intensity = 200
sun_light_intensity = 1
light_setting(point_light_radius, num_lights, target, point_light_intensity, sun_light_intensity)

R, G, B, A = 1, 1, 1, 0
background_setting(R, G, B, A)

specular_rate = 1
roughness_rate = 0.2
material_setting(target, specular_rate, roughness_rate)

r = 4
d = 360
interval = 18
empty1 = camera_setting(camera, world_center)  
print(empty1.location)     
rendering_img(r, d, interval, empty1)

empty2 = camera_setting(camera, char_center)
print(empty2.location)    
rendering_img(r, d, interval, empty2)

 

위 코드는 아래 사진처럼 blender의 python script에 붙여 넣어서 원하는 rendering 결과물을 얻을 수 있다.

(단, 객체는 사용자가 지정해야 함)

Output1

- bound box를 Camera center로 설정할 경우

Output2

- character를 Camera center로 설정할 경우