텐서플로우는 범용적인 기술로써 딥러닝 뿐 만 아니라 다양한 곳에 사용 될 수 있습니다. 즉 머신러닝 라이브러리를 사용하지 않고 밑바닥에서 무엇인가 만들수 있는 여지가 많다는 뜻이겠지요. 예전에 CUDA 로 Ray-Tracing을 통해 볼륨랜더링을 만들어 본 경험이 있어, 혹시 TensorFlow 로도 할 수 있을 듯 하여 찾아 봤는데 관련 블로그가 있어서 번역 해 보았습니다. (오역이 있을 수 있으니, 진지하게 접근하는 분이라면 원문을 참고) 

[번역] https://medium.com/@awildtaber/building-a-rendering-engine-in-tensorflow-262438b2e062


텐서플로우로 랜더링 엔진 만들기 


이 포스트에서는 텐서플로우를 이용해 만든 랜더링 엔진에 대해  파이썬 코드 샘플 과 함께 알아 볼 것 이다. 아이디어와 코드의 많은 부분은 Íñigo Quílez, Matt Keeter, and the HyperFunproject 에게서 영감을 얻었다. 탱큐~


지오메트릭 표현 : 암시적 surface

(역주: 보통 surface 는 외부면만 있는것, Solid 는 내부가 채워져 있는 것을 지칭하는데 이 문서에선 그냥 합쳐서 말하는 듯) 
 


암시적 surface 는입력점이 물체(geometry)의 내부 또는 외부에 있는지 여부를 나타내는 부호를 반환하는 3차원 공간에서 함수 (f라고 함)를 사용하여 기하학(geometry)을 표현한 것이다. 우리는 음수 기호가 "외부"를 의미하고 양수 기호가 "내부"를 의미하는 규칙을 사용 할 것이며, f (x, y, z) = 0 인 각 입력점 (x, y, z)이 표면에 정확히 놓여 있고, 그 결과 표면은 f 의 솔루션 셋으로 암시적으로 정의되므로 그것들은 "암시적" surface 이라고 불리운다.

일반적으로 f에 대한 다른 가정은 없다. 즉, 반지름이 1 인 원에 대해서는 절차적으로 (프랙탈과 같이) , 대수적으로 (f = sqrt (x ^ 2 + y ^ 2 + z ^ 2) -1) ), 또는 심지어 확률론적으로 정의 될 수 있다.대수 표현으로 정의된 암시적 surface 을 생각해보면, 기본 도형은 구체, 토리(tori), 평면, 타원체 및 원통형이 있을 것이며, 이것들은 모두 간단한 대수 표현식을 가지고 있는데. 예를 들어, plane (x, y, z) = x * a + y * b + z * c + m이 될 것이다. 이것을 파이썬에서 구현하기 위해 closure를 사용하여 값 a, b, c, m을 설정해보자.

def plane(a, b, c, m):
  def expression(x, y, z):
    return x*a + y*b + z*c + m
  return expression

부울 연산과 좌표 변환을 통해 더 복잡한 지오메트리를 작성 해 보자. 부울 연산을 정의하는 방법은 다음과 같다.

# 합쳐서 확장하기 (합집합) 

def union(f, g):
  def unioned(x, y, z):
    return max(f(x, y, z), g(x, y, z))
  return unioned

# 두 평면의 겹치는 부분만 (교집합)

def intersection(f, g):
  def intersected(x, y, z):
    return min(f(x, y, z), g(x, y, z))
  return intersected 

def negation(f):
  def negated(x, y, z):
    return -f(x, y, z)
  return negated

좌표 변환을 이해하기 위해 머리를 좀 굴려야 할 필요가 있긴 하지만, 일반적으로 함수에 대한 입력 좌표를 왜곡(distort) 한다. 예를 들어, 가장 간단한 변환은 다음과 같다.

# 이동 

def translate(a,b,c):
  def translated(x,y,z):
    return (x-a, y-b, z-c)
  return translated

만약 우리가 함수를 이동(translate) 하기를 원한다면 변환(transformation) 함수 f 를 구성하여 f(*translate(a,b,c)(x, y, z))를 작성하고 나면, 이 새로운 이동 함수의 솔루션 셋(solution set)은 (a,b,c) 에 의해 이동(translated) 된다. 예를들어 f (0, 0, 0) = 0이면, (0,0,0)은 f의 표면에 있으므로,  f(*translate(1,0,0)(1,0,0)) => f(1-1, 0-0, 0-0) => f(0,0,0) = 0 처럼 된다면 결과 솔루션 셋은 (1,0,0) 만큼 높아진다.  

이 개념은 원뿔 좌표 변환 및 톱니파 사용을 통한 도메인 반복의 영역으로 상당히 멀리 떨어져있을 수 있다 (암시적 surface 로 그릴 수있는 crazy 한 것들에 대한 Johann Korndörfer’s presentation 참조). 여기 암시적 표면의 매력에 대한 첫 번째 증거를 볼 수 있는데 간단한 대수 방정식으로 엄청나게 복잡한 지오메트리를 간결하게 나타낼 수 있다. 다른 패치가 어디에서 만나고 금지되어 있는지 추적해야 하므로 미세 구조를 갖는 기하학의 경우 이러한 상황에서 경계 표현 ( CAD의 산업 표준 표현) 이 서로 다르기 때문에 복잡한 패치 토폴로지를 사용하여 모델링 형상을 모델링 할 때 특히 유용하다. 

나는 암시적인 surface 를 공간 복잡성에 대한 거래 시간 복잡성으로 생각한다. 메모리에서 모든 토폴로지 스티칭(stitching) 정보를 유지할 필요는 없지만 비용을 지불하기 위해 (잠재적으로) 거대한 대수 방정식을 풀어야 하며 프리미티브를 사용하여 흥미로운 것을 정의하기 위해 큰 숫자의 연산 트리가 필요하다. 결국 스크린에 무엇인가를 렌더링하려면 수십만 포인트의 함수 f를 샘플링 해야 한다.

이 지점에서 아키텍처 과제가 있는데, 우리는 대수 표현을 가능한 한 많이 재사용하고 대량의 입력 데이터 배열에 대해 효율적인 수치 구현을 계산하고 결국 계산을 병렬화하는 방식으로 대수 표현을 작성 해야 하는 운명에 쳐해있다.

이제 우리를 도와 줄 해결사가 등장 할 시간이다.  "텐서플로우" 


텐서플로우: 선언적 그래프 계산 


Tensorflow는 파이썬에서 계산 그래프를 정의하고, C ++ 또는 CUDA에서 그것을 실행하기 위한 프레임 워크이다. 주요 적용 지점은 신경망을 정의하는 것이지만 훨씬 더 일반적인 사용이 가능하게 설계되었습니다. (역주: CUDA, OpenCL , AMP, 텐서폴로우 모두 각각의 장,단이 있지만 내 경우는 요즘 데이터 분석에 촛점이 맞춰져 있기 때문에  많은 정보가 존재하는 텐서플로우에 집중하는게 나을 거 같다. C++을 사랑 하므로, AMP 도 놓치긴 싫지만...) 

                   

       (숫자가 높을 수록 좋음( FPS)  AMP 가 좀 떨어지긴 하지만 이런 벤치마크는 상황에 따라 천차만별임)  



우리가 찾아야 할 일반적인 기능에는 일반적인 하위 표현식 제거, 자동 차별화 및 CUDA 컴파일과 같은 매우 귀중한 기능이 있다. Tensorflow의 주요 데이터 구조는 Tensor 이며, 계산 결과를 나타냅니다. 여러면에서 numpy.ndarray와 유사하도록 설계되었으므로, ndarrays 을 해보셨다면 많은 부분 직감적으로 적용 할 수 있을 것이다. 우리가 두 개의 텐서 a와 b를 가지고 있다면, 산술 연산이나 복잡한 함수를 적용하여 더 많은 계산을 생성 할 수 있게 된다. 예를 들어 a + b는 두 개의 텐서를 더한 결과를 나타내는 새로운 Tensor를 반환 한다.


                                                           제공:  An Introduction to TensorFlow




여기에 계획이 하나 있다. 우리는 텐서 (Tensors)를 입력으로 사용 한 후 함수를 평가하여 계산 그래프를 생성 할 것이다.그런 다음 Tensorflow의 일반적인 하위 표현식 제거를 통해 가능한 한 많은 계산을 자동으로 재사용하고 자동 차별화를 통해 함수의 정확한 derivatives 을 가져올 수 있게 할 것이다 (이는 렌더링에  꽤나 유용 할 것이다). 암시적 surface 를 렌더링하는 두 가지 일반적인 방법으로는 polygonization과 ray-tracing이 있다. 


두 가지 중 더 단순한 방법인 polygonization 부터 시작해 본다.


폴리곤화 (Polygonization)


일을 간단하게하기 위해 우리는 고전적인 Marching Cubes algorithm을 사용하여 주어진 함수 값의 체적 그리드가 주어진 삼각형을 생성하고. 체적 그리드를 생성하기 위해 서는 x, y, z라고하는 세 개의 다른 텐서를 정의 할 것이다. 이 텐서는 함수에 공급할 수있는 좌표를 나타내며, 이 텐서들을 결합하여 하나의 텐서를 나타 낼 것이다. 우리는 Tensorflow의 Variable 클래스를 사용하여 좌표 텐서를 초기화 할 것이지만 데이터를 입력하기 위해 런타임 feed_dict 옵션을 사용할 수도 있다.


min_bounds = [-1,-1,-1] # the geometric coordinate bounds
max_bounds = [1,1,1] 
output_shape = [200,200,200]

resolutions = list(map(lambda x: x*1j, output_shape))

space_grid = np.mgrid[min_bounds[0]:max_bounds[0]:resolutions[0],min_bounds[1]:max_bounds[1]:resolutions[1],min_bounds[2]:max_bounds[2]:resolutions[2]]
space_grid = space_grid.astype(np.float32)

x = tf.Variable(space_grid[0,:,:,:], trainable=False, name="X-Coordinates")
y = tf.Variable(space_grid[1,:,:,:], trainable=False, name="Y-Coordinates")
z = tf.Variable(space_grid[2,:,:,:], trainable=False, name="Z-Coordinates")

draw_op = function(x,y,z)

session = tf.Session()
session.run(tf.initialize_all_variables())
volumetric_grid = tf.session.run(draw_op)

marching_cubes(volumetric_grid) # => list of faces and vertices for rendering


이 렌더링 방법은 디버깅하기 좋으며 빠르고 간단하지만 지오메트리에 거친(Coarse) 간격의 격자로 캡처 할 수 없는 feature를 가진 경우의 이슈는 있다. Raytracing은 일반적으로 폴리곤화 보다는 느리지만 근본적으로 무한한 해상도를 가진  진정 아름다운 장면을 렌더링 할 수 있다.



광선 추적법 (Raytracing) 


raytracing을 사용하려면 크기가 입력 점에서 가장 가까운 점까지의 유클리드 거리보다 항상 작아야 한다는 함수 f 에 대한 추가 가정이 필요하다. 즉 우리는 Signed Distance Functions 로 한정 할 것이다.

근본적으로 raytracing은 수식 f (x, y, z)를 하나의 변수 t 인 광선 길이로 표현하는 것으로 구성되는데 우리는 t에 대해 f (* (ray * t)) = 0을 반복적으로 계산하고 그로부터 우리는 표면의 어디에 있는지를 알게 된다. 화면의 각 픽셀에 대해 이 작업을 수행하여 이미지를 출력하게 되며 각 반복마다 t를 얼마나 많이 증가 시킬지를 적용하는데 있어서 signed 거리 속성이 필요하다. 광선 추적 방정식을 풀기 위해 bisection, 뉴턴 방정식 또는 심지어 Tensorflow의 빌트인 최적화 방안을 사용할 수도 있지만, 어느 것도 정확하고 좋은 성능을 보여주지는 못했다.


def normalize_vector(vector):
  return vector / tf.sqrt(tf.reduce_sum(tf.square(vector), reduction_indices=0))

def vector_fill(shape, vector):
  return tf.pack([
    tf.fill(shape, vector[0]),
    tf.fill(shape, vector[1]),
    tf.fill(shape, vector[2]),
  ])

resolution = (1920, 1080)
aspect_ratio = resolution[0]/resolution[1]
min_bounds, max_bounds = (-aspect_ratio, -1), (aspect_ratio, 1)
resolutions = list(map(lambda x: x*1j, resolution))
image_plane_coords = np.mgrid[min_bounds[0]:max_bounds[0]:resolutions[0],min_bounds[1]:max_bounds[1]:resolutions[1]]

# Find the center of the image plane

camera_position = tf.constant([-2, 0, 0])
lookAt = (0, 0, 0)
camera = camera_position - np.array(lookAt)
camera_direction = normalize_vector(camera)
focal_length = 1
eye = camera + focal_length * camera_direction

# Coerce into correct shape

image_plane_center = vector_fill(resolution, camera_position)

# Convert u,v parameters to x,y,z coordinates for the image plane

v_unit = [0, 0, -1]
u_unit = tf.cross(camera_direction, v_unit)
image_plane = image_plane_center + image_plane_coords[0] * vector_fill(resolution, u_unit) + image_plane_coords[1] * vector_fill(resolution, v_unit)

# Populate the image plane with initial unit ray vectors

initial_vectors = image_plane - vector_fill(resolution, eye)
ray_vectors = normalize_vector(initial_vectors)

t = tf.Variable(tf.zeros_initializer(resolution, dtype=tf.float32), name="ScalingFactor")
space = (ray_vectors * t) + image_plane

# Name TF ops for better graph visualization

x = tf.squeeze(tf.slice(space, [0,0,0], [1,-1,-1]), squeeze_dims=[0], name="X-Coordinates")
y = tf.squeeze(tf.slice(space, [1,0,0], [1,-1,-1]), squeeze_dims=[0], name="Y-Coordinates")
z = tf.squeeze(tf.slice(space, [2,0,0], [1,-1,-1]), squeeze_dims=[0], name="Z-Coordinates")

evaluated_function = function(x,y,z)

# Iteration operation

epsilon = 0.0001
distance = tf.abs(evaluated_function)
distance_step = t - (tf.sign(evaluated_function) * tf.maximum(distance, epsilon))
ray_step = t.assign(distance_step)


각 픽셀의 광선 길이를 얻은 후에는 Tensorflow의 자동 차등화( automatic differentiation) 기능을 사용하여 함수 f의 정확한 수치 미분을 계산하여 surface 를 가져올 수 있습니다. 표면으로 이동함에 따라 f에 의해 생성되는 스칼라 필드가 항상 증가하므로 df가 항상 지오메트리의 내부를 가리키게 되고 표면의 법선이 된다.


light = {"position": np.array([0, 1, 1]), "color": np.array([255, 255, 255])}
gradient = tf.pack(tf.gradients(evaluated_functional, [x,y,z]))
normal_vector = normalize_vector(gradient)
incidence = normal_vector - vector_fill(resolution, light["position"])
normalized_incidence = normalize_vector(incidence)
incidence_angle = tf.reduce_sum(normalized_incidence * normal_vector, reduction_indices=0)

# Split the color into three channels

light_intensity = vector_fill(resolution, light['color']) * incidence_angle

# Add ambient light

ambient_color = [119, 139, 165]
with_ambient = light_intensity * 0.5 + vector_fill(resolution, ambient_color) * 0.5
lighted = with_ambient

# Mask out pixels not on the surface

epsilon = 0.0001
bitmask = tf.less_equal(distance, epsilon)
masked = lighted * tf.to_float(bitmask)
sky_color = [70, 130, 180]
background = vector_fill(resolution, sky_color) * tf.to_float(tf.logical_not(bitmask))
image_data = tf.cast(masked + background, tf.uint8)

image = tf.transpose(image_data)
render = tf.image.encode_jpeg(image)

마지막으로, 렌더링하기에 충분할 정도로 반복 단계를 실행한다!  모든 픽셀이 이미 수렴되었거나 배경의 일부인 경우 루프를 종료하는 수렴 테스트를 추가 할 수 있다.


session = tf.Session()
session.run(tf.initialize_all_variables())
step = 0
while step < 50:
  session.run(ray_step)
  step += 1

session.run(render) # <= returns jpeg data you can write to disk

다양한 프레임을 통해 애니메이트된 최종 제품 :


                                    


아마존 GPU 인스턴스에서 실행할 때 1080p의 프레임을 렌더링하는 데 약 1분이 걸렸다. GPU 셰이더를 통한 레이트레이싱과 비교 하긴 어렵지만 오류역전파를 하려는 경우 Tensorflow에 이미지 텐서를 가질 수 있게 되었다.





여기까지 마치구요. 원문에는 텐서플로우 디버깅 및 심볼릭 계산 트리가 후속 글로 있음을 알려드립니다.


참고:  CUDA Ray -Tracing 



저작자 표시 비영리 동일 조건 변경 허락
신고
Posted by [前草] 이승현 (wowlsh93@gmail.com)