위치 기반 동역학 (position-based dynamics) 코스 노트

Shape matching으로 대표되는 소프트 바디 시뮬레이션 방법을 여러 가지 용도로 잘 구현해서 써먹고 있었던 저는, 언제나 position-based dynamics도 관심이 있었습니다. 특히 unified paticle physics 라는 이름의 논문이 SIGGRAPH에서 선보여진 이후로는 언제나 그랬습니다. 고체, 액체, 기체 등등을 파티클 기반의 시뮬레이션으로 실시간에 그럴싸하게 만들 수 있다니, 그 자체로 매력적이죠 ㅎㅎ

언젠가 시간나면 좀 공부해봐야지 했었던 나날이 거의 10년이 된 시점인데, 시간이 남으면 보려고 했던 PBD의 코스 노트를 슬쩍슬쩍 번역해서 보려고 합니다.


위치 기반 동역학 코스 노트

원문: A Survey on Position Based Dynamics, 2017

저자
  • 얀 벤더 (Jan Bender, RWTH Aachen University)
  • 마티아스 뮐러 (Matthias Müller, NVIDIA PhysX Research)
  • 마일스 맥클린 (Miles Macklin, NVIDIA PhysX Research)

그림 1 그림 1: 위치 기반 동역학 프레임워크 내에서 시뮬레이션된 선택된 장면들

요약

기계적인 효과에 대한 물리 기반 시뮬레이션은 20여년 이상 컴퓨터 그래픽스의 중요한 연구 주제였습니다. 이 분야의 고전적인 방법은 뉴턴의 제 2법칙을 이산화하고 다양한 힘을 결정하여 변형 가능한 물체의 늘어남, 전단 (shearing), 굽힘, 혹은 유체의 압력 및 점도와 같은 다양한 효과를 시뮬레이션하는 것입니다. 이러한 힘이 주어졌을 때 속도, 그리고 최종적으로 위치는 가속도 결과의 수치 적분에 의해 결정됩니다.

지난 몇 년 동안 위치 기반 (position-based) 시뮬레이션 방법이 그래픽 커뮤니티에서 인기를 끌었습니다. 고전적인 시뮬레이션 접근 방식과 달리 이 방법은 준-정적 (quasi-static) 문제의 해를 기반으로 각 시뮬레이션 단계의 위치 변화를 직접 계산합니다. 따라서 위치 기반 접근 방식은 빠르고, 안정적이고, 제어가 가능하여 인터랙티브 환경에서 사용하기에 적합합니다. 이 방법은 일반적으로 힘 기반 (force-based) 방법만큼 정확하지는 않지만, 시각적으로 타당한 정도를 제공합니다. 따라서 위치 기반 시뮬레이션의 주요 응용 분야는 가상 현실, 컴퓨터 게임, 영화 및 광고의 특수 효과입니다.

이 튜토리얼에서는 먼저 위치 기반 동역학의 기본 개념을 소개합니다. 그런 다음 다양한 솔버를 제시하고 이를 준수 제약 (compliant constraint) 과 관련하여 암묵적 (implicit) 오일러 방법의 변형 공식과 비교합니다. 이러한 솔버의 수렴을 개선하기 위한 접근법을 논의합니다. 또한 탄성 막대, 천, 체적 변형체, 강체 시스템 및 유체를 시뮬레이션하기 위해 위치 기반 방법을 적용하는 방법을 보여줍니다. 또한 이방성 (anisotropy) 이나 가소성 (plasticity) 과 같은 복잡한 효과를 시뮬레이션하는 방법을 시연하고 성능을 개선하기 위한 접근 방식을 소개합니다. 마지막으로 전망을 제시하고 미해결 문제를 논의합니다.

  • 키워드: physically-based animation, position-based dynamics, deformable solids, rigid bodies, fluids
  • 카테고리 및 주제 설명자 (ACM CCS에 따름): Computer Graphics [I.3.7]: Three-Dimensional Graphics and Realism—Animation

튜토리얼 상세

발표자

얀 벤더 (Jan Vender) 얀 벤더는 2007년 초에 칼스루에 대학교에서 박사 학위를 받았습니다. 그의 논문 주제는 멀티바디 시스템의 인터랙티브 동역학 시뮬레이션이었습니다. 이후 칼스루에 공과대학에서 박사 후 연구원 (2007~2010년) 으로, 다름슈타트 공과대학교 계산공학 대학원 조교수 (2010~2016년) 로 물리 기반 시뮬레이션 분야에서 연구를 계속했습니다. 2016년부터는 아헨공과대학교 비주얼 컴퓨팅 연구소의 교수로 재직하며 컴퓨터 애니메이션 그룹을 이끌고 있습니다. 현재 연구 분야는 강체 동역학, 탈형성 (deformable) 고체, 유체, 위치 기반 방법, 충돌 감지 및 해결, 절단, 프랙처링, 실시간 시각화 등 다양합니다. 주요 그래픽스 컨퍼런스의 프로그램 위원회에서 활동했으며 IEEE 컴퓨터 그래픽스 및 응용 분야의 부편집장을 맡고 있습니다.

마티아스 뮐러 (Matthias Müller) 마티아스 뮐러는 1999년 취리히 연방 공과대학교에서 고밀도 고분자 시스템의 원자론적 시뮬레이션으로 박사 학위를 받았습니다. MIT 컴퓨터 그래픽스 그룹에서 박사후 과정(1999~2001년)을 거치면서 거시적 물리 기반 시뮬레이션으로 분야를 변경했습니다. 입자 기반 물 시뮬레이션 및 시각화, 유한 요소 기반 연성체, 천 시뮬레이션, 골절 시뮬레이션에 관한 논문을 발표했습니다. 그의 연구의 주요 초점은 컴퓨터 게임에서 사용할 수 있는 무조건 안정적이고 빠르며 제어 가능한 시뮬레이션 기법입니다. 이 튜토리얼과 가장 관련이 깊은 그는 위치 기반 시뮬레이션 기법 분야의 창시자 중 한 명입니다. 2002년에는 게임 미들웨어 회사인 노보덱스(2004년 AGEIA에 인수)를 공동 설립하여 연구 책임자로서 물리 시뮬레이션 라이브러리 PhysX의 혁신적인 새 기능 확장을 담당했습니다. 2008년 초에 AGEIA Technologies, Inc.를 인수한 이후 NVIDIA의 PhysX 연구팀장으로 재직하고 있습니다.

마일스 맥클린 (Miles Macklin) 마일스 맥클린은 NVIDIA의 PhysX 연구팀의 연구원입니다. 2013년부터 위치 기반 역학을 사용하여 구축된 NVIDIA Flex라는 통합 물리 라이브러리를 개발하고 있습니다. 플렉스의 기술은 SIGGRAPH에서 두 편의 논문을 발표했으며, 현재 여러 게임 및 시각 효과 스튜디오에 배포되었습니다. NVIDIA에 입사하기 전에는 소니 컴퓨터 엔터테인먼트에서 초기 플레이스테이션3 개발, 런던의 락스테디 스튜디오에서 배트맨 아캄 시리즈, 샌프란시스코의 루카스아트에서 스타워즈 프랜차이즈의 시각 효과 엔지니어로 게임 업계에서 일했습니다. 현재 그는 GPU를 이용한 실시간 시뮬레이션 및 렌더링 방법에 대해 연구하고 있습니다.

필요한 배경

이 튜토리얼에서는 물리 기반 애니메이션의 기초에 대해 간략한 소개를 하지만, 이 분야에 대한 일반적인 지식이 있으면 좋습니다.

잠재적 대상 고객

이 튜토리얼은 인터랙티브 물리 기반 시뮬레이션 방법에 관심이 있는 컴퓨터 애니메이션 분야의 연구자 및 개발자를 대상으로 합니다. 이 튜토리얼은 중급 수준의 튜토리얼입니다.

서론

강체, 연체 또는 천과 같은 솔리드 오브젝트의 시뮬레이션은 30년 이상 컴퓨터 그래픽스 분야에서 중요하고 활발한 연구 주제였습니다. 이 분야는 80년대 후반 Terzopoulos와 그의 동료들에 의해 그래픽스 분야에 소개되었습니다 [TPBF87a]. 그 이후로 많은 연구가 발표되었고 그 수는 빠르게 증가하고 있습니다. 이러한 발전을 문서화한 다양한 서베이 논문 [GM97, MTV05, NMK* 06, MSJT08, BET14] 이 존재합니다.

이 튜토리얼에서는 특별한 종류의 시뮬레이션 기법, 즉 위치 기반 접근법 [BMO*14] 에 초점을 맞춥니다. 이러한 방법은 원래 고체 시뮬레이션을 위해 개발되었습니다. 그러나 최근의 일부 연구에서는 위치 기반의 컨셉이 유체 및 관절 강체를 시뮬레이션하는 데에도 사용될 수 있음을 보여주었습니다. 고전적인 동역학 시뮬레이션 방법은 시스템의 운동량 변화를 적용된 힘에 대한 함수로 공식화하고 가속도 및 위치의 수치 적분을 통해 위치를 계산해 나갑니다. 반면 위치 기반 접근 방식은 준-정적 (quasi-static) 문제에 대한 해를 기반으로 위치를 직접 계산합니다.

물리 시뮬레이션은 계산 과학 (computational science) 분야에서 잘 연구된 문제여서, 유한 요소법 (Finite Element Method, FEM) [OH99], 유한 차분법 (Finite Differences Method) [TPBF87b], 유한 체적법 (Finite Volume Method) [TBHF03], 경계 요소법 (Boundary Element Method) [JP99], 혹은 입자 기반 (particle-based) 접근법 [DSB99, THMG04] 과 같이 잘 정립된 많은 방법이 그래픽스 분야에 채택되었습니다. 계산 물리학 및 화학에서 컴퓨터 시뮬레이션의 주요 목표는 실제 실험을 대체하여 가능한 한 정확도를 높이는 것입니다. 반면, 컴퓨터 그래픽스에서 물리 기반 시뮬레이션 방법의 주요 응용 분야는 영화와 광고의 특수 효과, 그리고 최근에는 컴퓨터 게임과 기타 인터랙티브 시스템입니다. 이 분야에서는 속도와 제어 가능성이 가장 중요한 요소이며 정확성 측면에서 요구되는 것은 시각적 개연성뿐입니다. 이는 실시간 애플리케이션에서 특히 그렇습니다.

위치 기반 방식은 특히 인터랙티브 환경에서 사용하기에 적합합니다. 높은 수준의 제어 기능을 제공하며 간단하고 빠른 명시적 시간 통합 방식을 사용하는 경우에도 안정적입니다. 이러한 접근 방식은 단순성, 견고성, 속도 덕분에 최근 컴퓨터 그래픽과 게임 업계에서 큰 인기를 끌고 있습니다.

충돌 감지는 모든 시뮬레이션 시스템에서 중요한 부분입니다. 그러나 이 주제에 대한 적절한 논의는 이 튜토리얼의 범위를 벗어납니다. 따라서 린과 고츠초크의 연구 [LG98] 와 테쉬너 외의 연구 [TKH*05] 를 참고하시기 바랍니다.

배경

컴퓨터 그래픽스에서 동역학 시스템을 시뮬레이션하는 데 가장 많이 사용되는 접근 방식은 힘 기반입니다. 내부 및 외부의 힘을 축적하여 뉴턴의 운동 제 2 법칙에 따라 가속도를 계산합니다. 그런 다음 시간에 대한 적분 방법을 사용하여 속도와 최종적으로 물체의 위치를 업데이트합니다. 몇몇 시뮬레이션 방법 (대부분 강체 시뮬레이터지만) 은 충격량 (impulse) 기반 동역학을 사용하고 속도를 직접 수정합니다 [BFS05, Ben07]. 이와 대조적으로, 기하 기반 (geometry based) 방법은 속도 레이어도 생략하고 즉시 위치값을 수정합니다. 위치 기반 접근법의 가장 큰 장점은 제어 가능성입니다. 힘 기반 시스템에서 명시적 적분 기법 (explicit integration scheme) 의 오버슈팅 문제를 피할 수 있습니다. 또한 충돌 제약 (collision constraint) 을 쉽게 처리할 수 있고 포인트를 유효한 위치에 프로젝션 시킴으로써 관통 (penetration) 문제를 완전히 해결할 수 있습니다.

힘 기반 접근 방식 중 가장 간단한 방법 중 하나는 질량-스프링 (mass-spring) 네트워크로 고체를 표현하고 시뮬레이션하는 것입니다. 질량-스프링 시스템은 스프링으로 연결된 점 질량 집합으로 구성됩니다. 이러한 시스템의 물리학은 간단하며 시뮬레이터도 쉽게 구현할 수 있습니다. 그러나 이 간단한 방법에는 몇 가지 중요한 단점이 있습니다.

  • 물체의 동작은 스프링 네트워크가 설정된 방식에 따라 달라집니다.
  • 원하는 동작을 얻기 위해 스프링 상수를 조정하는 것이 어려울 수 있습니다.
  • 질량 스프링 네트워크는 체적 보존 (volume preservation) 이나 체적 반전 (volume inversion) 방지와 같은 체적 관련 현상을 직접적으로 포착할 수 없습니다.

유한 요소법은 고체를 유한한 수의 점 질량으로 대체하는 대신 전체 부피를 고려하기 때문에 위의 모든 문제를 해결합니다. 여기서 오브젝트는 볼륨을 유한한 크기의 여러 요소로 분할하여 이산화됩니다. 이러한 이산화는 질량-스프링 접근법에서와 같이 정점이 질점의 역할을 하고 요소 (보통 사면체) 가 여러 질점에 동시에 작용하는 일반화된 스프링으로 생각할 수 있는 메시를 생성합니다. 두 경우 모두 질점 또는 메쉬 정점에 적용되는 힘은 정점의 속도와 메쉬의 실제 변형으로 인해 계산됩니다.

이 튜토리얼에서는 속도 및 가속도 레이어를 생략하고 위치를 직접 수정하는 위치 기반 시뮬레이션 방법에 중점을 둡니다. 여기서는 먼저 물리 기반 시뮬레이션의 기본 사항을 소개한 후 다음 섹션에서 위치 기반 개념을 설명합니다.

운동 방정식

각 입자 $i$는 질량 $m_i$, 위치 $\mathbf{x}_i$, 그리고 속도 $\mathbf{v}_i$ 이렇게 세 개의 속성을 가지고 있습니다. 입자의 운동 방정식은 뉴턴의 제 2 법칙으로부터 유도됩니다:

%%% \begin{equation} \dot{\mathbf{v}}_i = \frac{1}{m_i} \mathbf{f}_i \end{equation} %%%

이때 $\mathbf{f}_i$는 입자 $i$에 적용되는 모든 힘의 합입니다. $\dot{\mathbf{x}}$와 $\mathbf{v}$의 관계는 속도 운동학 관계로 설명할 수 있습니다:

%%% \begin{equation} \dot{\mathbf{x}}_i = \mathbf{v} \end{equation} %%%

입자가 오직 세 개의 평행 이동 자유도만 가지고 있는 반면, 강체는 세 개의 회전 자유도 또한 가지고 있습니다. 따라서, 강체는 관성 텐서 $\mathbf{I}_i \in \mathbb{R}^{3 \times 3}$, 일반적으로 단위 쿼터니온 $\mathbf{q}_i \in \mathbb{H}$로 나타내는 회전 방향, 그리고 각속도 $\mathbf{\omega}_i \in \mathbb{R}^3$ 같은 추가적인 속성이 필요합니다. 강체의 경우 일반적으로 원점이 질량 중심에 있고 관성 텐서가 지역 좌표계에서 대각 행렬이 되게끔 축의 방향을 회전시킨 지역 좌표계가 선택됩니다.

뉴턴의 제2법칙은 실제로 입자에만 적용됩니다. 오일러는 강체를 무한한 수의 입자의 집합으로 간주하여 이 법칙을 강체의 경우로 확장했습니다. 따라서 강체에 대한 운동 방정식은 뉴턴-오일러 방정식이라고도 합니다. 강체의 회전 부분에 대한 운동 방정식은 다음과 같습니다:

%%% \begin{equation} \dot{\mathbf{\omega}}_i = \mathbf{I}_i^{-1} \left( \mathbf{\tau}_i - \left( \mathbf{\omega}_i \times \left( \mathbf{I}_i \mathbf{\omega}_i \right) \right) \right) \end{equation} %%%

이때 $\mathbf{\tau}_i$는 모든 모멘트의 합입니다. 모멘트는 순수 모멘트일수도 있고, 혹은 힘 $\mathbf{f}$가 점 $\mathbf{p}$에 작용하고 $\mathbf{x}$가 질량 중심일 때 힘 $\mathbf{t}=\left(\mathbf{p} - \mathbf{x}\right) \times \mathbf{f}$의 부산물일수도 있습니다. 회전부의 속도 운동학 관계는 다음과 같이 정의됩니다:

%%% \begin{equation} \dot{\mathbf{\omega}}_i = \frac{1}{2} \tilde{\mathbf{\omega}}_i \mathbf{q}_i \end{equation} %%%

이때 $\tilde{\mathbf{\omega}}_i$는 쿼터니온 $\left[ 0, \omega^x_i, \omega^y_i, \omega^z_i \right]$ 입니다.

시간 적분

제약이 없는 입자 또는 강체에 대한 시뮬레이션 단계는 각각 방정식 (1)-(2) 또는 방정식 (1)-(4)의 수치 적분에 의해 수행됩니다. 위치 기반 동역학 분야에서 가장 널리 사용되는 적분법은 다음에서 소개하는 심플렉틱 오일러 (symplectic Euler) 방법입니다.

잘 알려진 명시적 오일러 (explicit Euler) 방법와 달리 심플렉틱 오일러는 위치 벡터를 적분할 때 시간 $t_0$ 대신 시간 $t_0 + \Delta t$에서의 속도를 사용합니다. 그러면 입자에 대한 시간 적분은 다음 방정식에 의해 수행됩니다:

%%% \mathbf{v}_i \left( t_0 + \Delta t \right) = \mathbf{v}_i \left( t_0 \right) + \Delta t \frac{1}{m_i} \mathbf{f}_i\left( t_0 \right) %%%

%%% \mathbf{x}_i \left( t_0 + \Delta t \right) = \mathbf{x}_i \left( t_0 \right) + \Delta t \mathbf{v}_i \left( t_0 + \Delta t \right) %%%

강체의 경우 방정식 (3)과 (4) 또한 적분되어야 합니다. 심플렉틱 오일러 방법을 사용하면 다음과 같은 결과를 얻을 수 있습니다:

%%% \mathbf{\omega}_i \left( t_0 + \Delta t \right) = \mathbf{\omega}_i \left( t_0 \right) + \Delta t \mathbf{I}^{-1}_i \left( t_0 \right) \cdot \left( \mathbf{\tau}_i\left( t_0 \right) - \left( \mathbf{\omega}_i \left( t_0 \right) \times \left( \mathbf{I}_i \left( t_0 \right) \mathbf{\omega}_i \left( t_0 \right) \right) \right) \right) %%%

%%% \mathbf{q}_i \left( t_0 + \Delta t \right) = \mathbf{q}_i \left( t_0 \right) + \Delta t \frac{1}{2} \tilde{\mathbf{\omega}}_i \left( t_0 + \Delta t \right) \mathbf{q}_i \left( t_0 \right) %%%

수치 오류 때문에 쿼터니온이 회전을 나타내기 위해 만족해야하는 조건 $\left| \mathbf{q} \right| = 1$이 적분 이후 위반될 수 있음을 주의하세요. 따라서 쿼터니온 값은 매 시간 적분 단계 후에 정규화 되어야 합니다.

심플렉틱 오일러는 1차 적분기 (first-order integrator) 이며 알고리즘의 예측 단계에만 사용됩니다. 위치 기반 동역학 (Position Based Dynamics, PBD) 에서는 뒤의 “암묵적 방법과의 연결”절에 설명할 것처럼 구속력 (constraint force) 이 암묵적으로 적분됩니다.

제약 (constraint)

제약 조건은 물체의 상대적인 움직임을 제한하는 등식 및 부등식 형태의 운동학적 제한입니다. 등식 제약과 부등식 제약은 각각 양방향 (bilateral) 제약과 일방향 (unilateral) 제약이라고 합니다. 일반적으로 제약은 위치 및 회전 방향 변수, 선형 및 각속도, 그리고 모든 차수의 도함수에 대한 함수입니다. 그러나 위치 기반 시뮬레이션 방법은 위치에 의존적인 제약, 강체의 경우에는 여기에 추가로 회전 방향에 의존적인 제약까지만 고려합니다. 따라서 양방향 제약은 다음과 같은 함수로 정의됩니다:

%%% C \left( \mathbf{x}_{i_1}, \mathbf{q}_{i_1}, \ldots, \mathbf{x}_{i_{n_j}}, \mathbf{q}_{i_{n_j}}
\right) = 0 %%%

그리고 일방향 제약은 다음과 같습니다:

%%% C \left( \mathbf{x}_{i_1}, \mathbf{q}_{i_1}, \ldots, \mathbf{x}_{i_{n_j}}, \mathbf{q}_{i_{n_j}}
\right) \ge 0 %%%

여기서 $\left\{ i_1, \ldots, i_{n_j} \right\}$는 물체 인덱스의 집합이고 $n_j$는 제약의 카디널리티입니다. 일반적으로 PBD에서 사용되는 제약은 위치와 시간에만 의존적이고 속도에는 의존적이지 않습니다. 이러한 제약을 홀로노믹 (holonomic) 이라고 합니다.

제약은 운동학적 제한이므로 동역학에도 영향을 미칩니다. 고전적인 방법은 제약이 있는 동역학 시스템을 시뮬레이션하기 위해서 힘을 결정합니다. 예를 들어, 위치 에너지 $E = \frac{k}{2} C^2$를 정의하고 힘을 $f = - \nabla E$로 유도하거나 (soft constraint), 제약 동역학으로부터 유도된 라그랑주 승수를 통해 (hard constraint) 이를 수행합니다 [Wit97]. 이와 달리 위치 기반 접근법은 모든 제약들을 충족하기 위해 물체의 위치와 회전 방향을 직접 수정합니다.

위치 기반 동역학의 핵심

이번 절에서는 속도 및 가속도 레이어를 생략하고 위치에서 바로 작동하는 접근 방식인 위치 기반 동역학 (PBD) 를 소개합니다 [MHHR07]. 알고리즘을 설명하겠습니다. 그런 다음 시뮬레이션할 물체를 설명하는 제약 시스템을 푸는 방법에 대해 구체적으로 설명하겠습니다.

다음에서는 입자 시스템에 대한 위치 기반 접근 방식을 먼저 소개합니다. 강체를 처리하기 위한 확장은 “강체 동역학”절에서 설명합니다.

알고리즘

시뮬레이션할 오브젝트는 N개의 입자 집합과 M개의 제약 집합으로 표현됩니다. 각 제약에 대해 0에서 1 사이의 범위에서 제약의 강도를 정의하는 강성 (stiffness) 매개변수 k를 도입합니다. 이를 통해 사용자는 물체의 탄성을 더 세밀하게 제어할 수 있습니다.

시간 적분

이 데이터와 타임 스텝 $\Delta t$가 주어지면 알고리즘 1에 설명된 대로 시뮬레이션이 진행됩니다. 이 알고리즘은 시간에 대한 2차 시스템을 시뮬레이션하기 때문에, 시뮬레이션 루프가 시작되기 전에 (1)-(3)에서 입자의 위치와 속도를 모두 지정해야 합니다. 라인 (5)-(6)은 속도와 위치에 대해 간단한 심플렉틱 오일러 적분 단계를 수행합니다. 새로운 위치값 $\mathbf{p}_i$는 원래 위치 변수에 직접 할당되지 않고 예측에만 사용됩니다. 충돌 제약과 같은 비영구적인 외부 제약 조건은 각 타임 스텝이 시작될 때마다 식 (7)에서 처음부터 생성됩니다. 여기서 원본 위치와 예측 위치는 연속 충돌 감지를 수행하기 위해 사용됩니다. 그런 다음 솔버 (8)-(10)가 예측된 위치가 $M_{Coll}$ 외부 제약과 $M$ 내부 제약 조건을 만족하도록 반복적으로 보정합니다. 마지막으로 보정된 위치 $\mathbf{p}_i$를 사용하여 위치 속도를 업데이트합니다. 여기서는 위치와 함께 속도를 업데이트하는 것이 필수적입니다. 이 작업을 수행하지 않으면 시뮬레이션에서 2차 시스템의 올바른 동작이 생성되지 않습니다. 보시다시피, 여기서 사용된 적분 방식은 Verlet 방식과 매우 유사합니다. 또한 시뮬레이션할 객체를 설명하기 위해 일련의 제약 조건을 사용하는 Jos Stam의 Nucleus 솔버 [Sta09]와도 밀접한 관련이 있습니다. 가장 큰 차이점은 Nucleus는 위치가 아닌 속도에 대한 제약 조건을 푼다는 점입니다.

 1: for all vertices $i$ do
 2:  initialize $x_i = x_i^0$, $v_i = v_i^0$, $w_i = 1 / m_i$
 3: end for
 4: loop
 5: for all vertices $i$ do $v_i <- v_i + \Delta t w_i f_{ext} (x_i)$
 6: for all vertices $i$ do $p_i <- x_i + \Delta t v_i$
 7: for all vertices $i$ do getCollConstraints($x_i -> p_i$)
 8: loop $solverIteration$ times
 9:   projectConstraints(C_1, ..., C_{M+M_{Coll}}, p_1, ..., p_N)
10: end loop
11: for all vertices $i$ do
12:   $v_i <- (p_i - x_i) / \Delta t$
13:   $x_i <- p_i$
14: end for
15: velocityUpdate(v_1, ..., v_N)
16: end loop

알고리즘 1: 위치 기반 동역학

감쇠 (damping)

일반적으로 동역학 시뮬레이션의 품질은 적절한 감쇠 체계와의 통합으로 개선할 수 있습니다. 긍정적인 효과로 감쇠은 물체의 점 위치의 시간적 진동을 줄여 안정성을 향상시킬 수 있습니다. 이를 통해 더 큰 타임 스텝을 사용할 수 있으므로 동역학 시뮬레이션의 성능이 향상됩니다. 반면에 감쇠은 시뮬레이션된 물체의 동적인 움직임을 변경합니다. 그 결과 원하는 효과(예: 변형 가능한 고체의 진동 감소)를 얻거나 물체 전체의 선운동량 또는 각운동량 변화와 같은 교란 효과를 얻을 수 있습니다.

일반적으로 감쇠 항 $\mathbf{C}\dot{\mathbf{X}}$는 물체의 운동 방정식에 통합될 수 있는데, 여기서 $\dot{\mathbf{X}}$는 위치의 모든 1차 도함수의 벡터를 나타냅니다. 사용자 정의 행렬 C가 대각 행렬인 경우 점의 절대 속도가 감쇠되며, 이를 점 감쇠 (point damping) 라고도 합니다. 이러한 점 감쇠력을 적절히 계산하면 점의 가속도가 감소하여 수치 안정성이 향상됩니다. 이러한 특성은 마찰과 같은 일부 설정에서는 바람직합니다. 그러나 일반적인 경우 점 감쇠력으로 인한 물체의 전반적인 감속은 바람직하지 않습니다. 점 감쇠력은 예를 들어 [TF88] 또는 [PB88]에서 점 대 못과 같은 기하적 제약이 있는 동역학 시뮬레이션에 점 감쇠력을 사용하는 데 사용됩니다.

변형 가능한 물체의 선운동량 및 각운동량을 보존하기 위해서는 일반적으로 스프링 감쇠력이라고 하는 대칭 감쇠력을 사용할 수 있습니다. 이러한 힘은 행렬 $\mathbf{C}$에서 대각선 부분이 아닌 항목으로 나타낼 수 있습니다. 감쇠력은 예를 들어 Baraff와 Witkin [BW98] 또는 Nealen 외 [NMK*06]에 의해 설명됩니다. 이러한 힘은 위치 기반 방법에도 적용될 수 있습니다. 하지만 Baraff와 Witkin, Nealen 등의 접근 방식은 물체의 지오메트리의 토폴로지 정보에 의존하기 때문에 쉐입 매칭과 같은 메시리스 (meshless) 기법에는 적용할 수 없습니다.

점 및 스프링 감쇠을 사용하면 현재 속도 또는 상대 속도를 줄일 수 있습니다. 그러나 일반적으로 다음 타임 스텝에서의 예측 속도 또는 상대 속도를 고려하는 것이 더 적절합니다.

감쇠에서의 흥미로운 대안이 [SGT09]에서 제시되었습니다. 여기서는 대칭적이고 운동량을 보존하는 힘의 개념을 메시리스 표현으로 확장했습니다. 전역적인 대칭 감쇠력이 물체의 질량 중심을 기준으로 계산됩니다. 이러한 힘은 선운동량을 보존하는 반면, 각운동량의 보존은 상대 위치에 힘을 프로젝션하거나 선형 프로그래밍을 사용하여 토크를 제거함으로써 보장됩니다. [SGT09]에 제시된 접근 방식은 감쇠력을 반복적으로 계산합니다. 그러나 이 논문은 반복 프로세스의 수렴과 반복을 수행하지 않고도 솔루션을 직접 계산할 수 있는 방법도 보여줍니다. 따라서 이 접근법은 연결 정보 유무에 관계없이 임의의 위치 기반 변형 모델에 대한 감쇠력을 계산하는 효율적인 대안이 될 수 있습니다. 이 접근 방식은 사용자 정의 클러스터의 진동을 전역적으로 또는 지역적으로 감쇠하는 데 사용할 수 있습니다.

솔버

풀어야 하는 시스템

알고리즘 1에서 솔버 단계 (8)-(10)의 목표는 입자의 예측 위치가 모든 제약을 만족하도록 수정하는 것입니다. 이후 이 문서에서는 알고리즘 1과는 달리 솔버가 작동하는 입자의 위치에 대하여 위치를 나타내는 일반적인 기호인 $\mathbf{x}$를 사용할 것입니다. 알고리즘 1에는 더 큰 내용이 있으며 예측된 위치를 이전 타임 스텝에서의 위치와 구분하기 위해 기호 $\mathbf{p}$를 사용했습니다.

풀어야 하는 문제는 $3N$개의 모르는 위치값 성분에 대한 $M$개의 방정식 집합으로 구성되며, 여기서 이제 $M$은 제약의 총 개수입니다. 이 시스템은 대칭일 필요는 없습니다. $M > 3N (M < 3N)$ 이면 시스템이 과도 (과소) 결정된 것입니다. 비대칭성에 더하여 이 방정식은 일반적으로 비선형입니다. 간단한 거리 제약인 $C \left(\mathbf{x}_1, \mathbf{x}_2\right) = \left| \mathbf{x}_1 - \mathbf{x}_2 \right|^2 - d^2$ 함수는 비선형 방정식을 만들어 냅니다. 상황을 더욱 복잡하게 만드는 것은, 충돌이 등식이 아니라 부등식을 만들어 낸다는 사실입니다. 등식과 부등식이 있는 비대칭 비선형 시스템을 푸는 것은 어려운 문제입니다.

$\mathbf{x}$가 $\left[ \mathbf{x}_1^T, \ldots, \mathbf{x}_N^T \right]^T$와 같이 위치 벡터들을 합친 벡터이고 제약 함수 $C_j$가 합쳐진 벡터 $\mathbf{x}$를 입력받아 정의된 위치 좌표값 중 일부만 사용한다고 합시다. 그러면 풀어야 할 시스템은 다음과 같이 쓸 수 있습니다:

%%% C_1 \left( \mathbf{x} \right) \succ 0 %%%

%%% \ldots %%%

%%% C_M \left( \mathbf{x} \right) \succ 0 %%%

여기서 $\succ$은 $=$ 혹은 $\ge$를 뜻합니다. 뉴턴-랩슨 반복법은 등식만 있는 비선형 대칭 시스템만 푸는 방법입니다. 그 과정은 솔루션의 첫번째 추측값으로 시작합니다. 그다음 각 제약 함수는 다음 식을 이용하여 현재 솔루션에 대한 이웃 형태로 선형화됩니다:

%%% C \left( \mathbf{x} + \Delta \mathbf{x} \right) = C \left( \mathbf{x} \right) + \nabla C \left( \mathbf{x} \right) \cdot \Delta \mathbf{x} + O \left( \left| \Delta \mathbf{x} \right|^2 \right) = 0 %%%

이 식은 전역적인 수정 벡터 $\Delta \mathbf{x}$에 대한 선형 방정식을 만들어 냅니다:

%%% \nabla C_1 \left( \mathbf{x} \right) \cdot \Delta \mathbf{x} = -C_1 \left( \mathbf{x} \right) %%%

%%% \ldots %%%

%%% \nabla C_M \left( \mathbf{x} \right) \cdot \Delta \mathbf{x} = -C_M \left( \mathbf{x} \right) %%%

여기서 $\nabla C_j \left( \mathbf{x} \right)$는 제약 함수 $C_j$를 자신의 모든 매개변수에 대해 미분한 항을 담고 있는 $1 \times N$ 차원의 벡터이고, 여기서 $N$은 제약 함수와 관련된 $\mathbf{x}$ 안의 요소 개수입니다. 이는 또한 선형 시스템의 $j$번째 행이기도 합니다. 행렬의 행 $\nabla C_j \left( \mathbf{x} \right)$와 우변의 스칼라값 $-C_j \left( \mathbf{x} \right)$ 둘다 상수인데, 이는 이 시스템을 풀기 전에 위치 $\mathbf{x}$에서 평가되었기 때문입니다. $M = 3N$ 이고 등식만 주어지면, 이 시스템은 예를 들어 preconditioned conjugate gradient method 같은 아무 선형 솔버나 써도 풀 수 있습니다. $\Delta \mathbf{x}$에 대해 풀리면 현재 솔루션은 $\mathbf{x} \leftarrow \mathbf{x} + \Delta \mathbf{x}$로 업데이트 됩니다. 이 과정을 반복하면서 이후 새로운 위치에서의 $\nabla C_j \left( \mathbf{x} \right)$와 $-C_j \left( \mathbf{x} \right)$를 평가하여 새로운 선형 시스템을 생성합니다.

만약 $M \ne 3N$이라면 만들어진 선형 시스템 행렬은 비대칭이고 역행렬을 구할 수 없습니다. Goldenthal 등 [GHF*07]은 최소 자승의 개념에서 최상의 솔루션을 만들어 내는 시스템 행렬에 대한 의사 역행렬을 사용하여 이 문제를 해결합니다. 여전히 부등식 제약은 직접적으로 다루는 것이 불가능합니다.

비선형 가우스-자이델 (Gauss-Seidel) 솔버

PBD 접근법에서는 비선형 가우스-자이델 방법이 사용됩니다. 이 방법은 각 제약 방정식을 따로 풀어냅니다. 각 제약은 연관된 모든 입자 위치에 대한 하나의 스칼라 방정식 $C \left( \mathbf{x} \right) \succ 0$을 만들어 냅니다. 따라서 이 서브시스템은 매우 과소 결정되어 있습니다. PBD는 이를 다음과 같이 풀어냅니다. 다시 한번, $\mathbf{x}$가 주어졌을 때 $C \left( \mathbf{x} + \Delta \mathbf{x} \right) \succ 0$를 만족하는 보정 벡터 $\Delta \mathbf{x}$를 찾고자 합니다. PBD 또한 제약 함수를 선형화하지만 각 제약을 개별적으로 합니다. 제약 방정식은 다음과 같이 근사화됩니다:

%%% \begin{equation} C \left( \mathbf{x} + \Delta \mathbf{x} \right) = C \left( \mathbf{x} \right) + \nabla C \left( \mathbf{x} \right) \cdot \Delta \mathbf{x} \succ 0 \end{equation} %%%

시스템이 과소 결정되는 문제는 $\Delta \mathbf{x}$를 $\nabla C \left( \mathbf{x} \right)$의 방향에 있도록 제한함으로써 해결되는데, 이는 선운동량 및 각운동량을 보존하는 데에도 필요합니다. 이는 다음과 같은 보정 벡터로 위 방정식을 푸는데 단 하나의 스칼라 라그랑지안 승수 $\lambda$를 찾아야 한다는 의미입니다:

%%% \begin{equation} \Delta \mathbf{x} = \lambda \mathbf{X}^{-1} \nabla C \left( \mathbf{x} \right)^T \end{equation} %%%

여기서 $\mathbf{M} = diag \left( m_1, m_2, \ldots, m_N \right)$입니다. 이는 단일 입자 $i$의 보정 벡터에 대하여 다음과 같은 공식을 만들어 냅니다:

%%% \begin{equation} \Delta \mathbf{x}_i = - \lambda w_i \nabla_{\mathbf{x}_i} C \left( \mathbf{x} \right)^T \end{equation} %%%

여기서 $\lambda$는 다음과 같으며:

%%% \begin{equation} \lambda = \frac{C \left( \mathbf{x} \right)} {\sum_j w_j \left| \nabla_{\mathbf{x}_j} C \left( \mathbf{x} \right) \right|^2} \end{equation} %%%

$w_i = \frac{1}{m_i}$입니다. 모든 위치를 붙인 벡터 $\mathbf{x}$에 대해 수식화하면 다음 식을 얻습니다:

%%% \begin{equation} \lambda = \frac{C \left( \mathbf{x} \right)} {\nabla C \left( \mathbf{x} \right) \mathbf{M}^{-1} \nabla C \left( \mathbf{x} \right)^T} \end{equation} %%%

앞서 설명한 바와 같이 이 솔버는 제약 함수들을 선형화합니다. 하지만 뉴턴-랩슨 방법과는 달리 선형화가 각 제약마다 개별적으로 일어납니다. 선형화가 각 거리 제약의 프로젝션에 영향을 주지 않는다는 점은 중요한 사항입니다. 전체적으로는 비선형일지라도 하나의 거리 제약은 탐색 방향 위에서 일어나는 제약 그라디언트 상에서 선형이기 때문입니다. 이는 ‘사면체 메쉬’ 절에서 살펴보게 될 사면체 부피 제약 같은 다른 제약에 대해서도 마찬가지로 참입니다. 이러한 종류의 제약은 한번의 스텝으로 해결될 수 있습니다. 제약이 처리된 후 위치가 즉각적으로 업데이트 되기 때문에, 이러한 업데이트는 다음 제약의 선형화에도 영향을 마치게 되는데 이는 선형화가 실제 위치에 의존적이기 때문입니다. 각 제약은 미지의 라그랑주 승수 $\lambda$에 대해 하나의 스칼라 방정식을 생성하기 때문에 비대칭은 문제가 되지 않습니다. 부등식도 $C \left( \mathbf{x} \right) \ge 0$ 인지 먼저 체크해봄으로써 자명하게 처리가 됩니다. 부등식을 만족한다면 해당 제약은 그냥 넘어가면 됩니다.

각 제약이 프로젝션 전에 개별적으로 선형화된다는 점은 뉴턴 반복의 전체 전역적 솔루션에 대해 선형화를 고정한 채로 유지시키는 전역적 접근법에 비해 솔버를 더 안정적으로 만들어 줍니다.

아직까지는 강성 매개변수 $k$에 대해 고려하지 않았습니다. 이를 도입하는 몇 가지 방법이 있습니다. 가장 간단한 방법은 보정 벡터 $\Delta \mathbf{x}$에 $k \in \left[ 0, \ldots, 1 \right]$을 곱하는 것입니다. 그러나 솔버가 여러 번 반복함에 따라서 $k$의 영향이 비선형적이 됩니다. $n_s$번의 솔버 반복 이후 단일 거리 제약에 남는 에러값은 $\Delta \mathbf{x} \left( 1 - k \right)^{n_s}$가 됩니다. 선형적인 관계를 만들기 위해서는 보정 벡터에 $k$를 직접 곱하지 않고 $k’ = 1 - \left(1 - k \right)^{n_s}$를 곱합니다. 이 변환을 통해서 에러는 $\Delta \mathbf{x} \left( 1 - k’ \right)^{n_s} = \Delta \mathbf{x} \left(1 - k \right)$가 되고, 따라서 목표로 하는 $n_s$에 상관없이 $k$와 선형적인 의존 관계가 됩니다. 하지만 결과적으로 만들어진 재질에 대한 강성도는 여전히 시뮬레이션 타임 스텝에 대해 의존적입니다. 실시간 환경에서는 통상적으로 고정된 타임 스텝을 사용하므로 이러한 의존성은 큰 문제가 되지 않습니다.

계층적 (hierarchical) 솔버

가우스-자이델 방법은 안정적으로 구현하기 쉽지만 통상적으로 전역 솔버에 비해 심각하게 느리게 수렴합니다. 주된 원인은 에러 보정이 한 제약에서 다른 제약으로 지역적으로 전파되기 때문입니다. 따라서 가우스-자이델 방법은 고주파 에러를 저주파 에러보다 훨씬 더 빠르게 안정시키기 때문에 스무서라고 부릅니다.

가우스-사이델 방법의 수렴 속도를 높이기 위해 널리 사용되는 방법은 거친 (coarse) 메쉬가 도메인 전체에 에러 수정을 빠르게 전파하도록 하는 메쉬 계층 구조를 만드는 것입니다. 스무더는 계층 구조의 모든 메시에서 하나씩 작동하며, 에러 수정은 일반적으로 세밀한 수준에서 거친 수준으로 그리고 다시 여러 주기로 서로 다른 해상도의 메시로 이월됩니다. 이 기법을 다중 그리드 (multi-grid) 방식[GW06]이라고 합니다. 거친 메쉬에서 세밀한 메쉬로, 세밀한 메쉬에서 거친 메쉬로 보정값을 전송하는 것을 각각 연장 (prolongation) 및 제한 (restriction) 이라고 합니다. 다중 그리드 방법은 계층 구조가 생성되는 방식, 제한 및 연장 연산자의 세분화 방법, 메쉬가 처리되는 순서에서 차이가 있습니다.

[Mül08]에서 뮐러 등은 이 기법을 사용하여 계층적 위치 기반 동역학(HPBD)을 소개했습니다. 이들은 최초 시뮬레이션 메시를 계층 구조의 가장 미세한 메시로 정의하고 이전 메시의 입자들의 하위 집합만 유지하는 방식으로 더 거친 메시를 생성합니다. 계층 구조는 가장 거친 수준에서 가장 미세한 수준으로 한 번만 순회합니다. 따라서 연장 연산자만 정의하면 됩니다. 특정 단계의 각 입자가 이전의 더 거친 단계의 입자 두 개 이상에 연결되도록 함으로써 (그림 2 참조), 연장은 더 거친 단계의 인접 입자로부터 정보를 보간하는 것과 같습니다. 또한 이들은 원래 메시의 제약을 기반으로 거친 메시에서 거리 제약을 생성하는 방법도 제안하였습니다. 이러한 거친 제약은 일방 (unilateral) 이어야 하며, 즉 현재 거리가 휴식 상태 거리보다 클 경우에만 작용해야 하며 그렇지 않으면 구부러짐이나 접힘을 방지할 수 있다는 점에 유의해야 합니다.

그림 2 그림 2: 메시 계층구조의 구성: 세밀한 레벨 $l$은 표시된 모든 입자와 점선으로 표시된 제약으로 구성됩니다. 그 다음 거친 레벨 $l + 1$에는 검은색 입자의 적절한 하위 집합과 실선 제약으로 구성되어 있습니다. 각 세밀한 흰색 입자는 화살표로 표시된 부모인 최소 $k ( = 2 )$ 개의 검은색 입자에 연결되어야 합니다.

암묵적 방법과의 연결

Liu 등[LBOK13]이 지적한 바와 같이, PBD는 암묵적 역방향 오일러 적분 방식과 밀접한 관련이 있습니다. 이는 역방향 오일러를 위치에 대한 제약이 있는 최소화라고 생각해 보면 알 수 있습니다. 운동 방정식의 전통적인 암묵적 오일러 시간 이산화부터 시작해 봅시다:

%%% \begin{equation} \mathbf{x}^{n+1} = \mathbf{x}^n + \Delta t \mathbf{v}^{n+1} \end{equation} %%%

%%% \begin{equation} \mathbf{v}^{n+1} = \mathbf{v}^n + \Delta t \mathbf{M}^{-1} \left( \mathbf{F}_{ext} + k \nabla \mathbf{C}^{n+1} \right) \end{equation} %%%

이때 $\mathbf{C}$는 제약 전위 벡터를, $k$는 강성 파라미터를 나타냅니다. 위 식에서 속도를 제거하면 다음을 얻습니다:

%%% \begin{equation} \mathbf{M} \left( \mathbf{x}^{n+1} - 2 \mathbf{x}^n + \mathbf{x}^{n-1} - \Delta t^2 \mathbf{M}^{-1} \mathbf{F}_{ext} \right) = \Delta t^2 k \nabla \mathbf{C}^{n+1} \end{equation} %%%

식 (12)는 다음 최적화 식의 1차 최적 조건으로 볼 수 있습니다:

%%% \begin{equation} \min_{\mathbf{x}} \frac{1}{2} \left( \mathbf{x}^{n+1} - \tilde{\mathbf{x}} \right)^T \mathbf{M} \left( \mathbf{x}^{n+1} - \tilde{\mathbf{x}} \right) - \Delta t^2 k \nabla \mathbf{C}^{n+1} \end{equation} %%%

여기서 $\tilde{\mathbf{x}}$는 예측된 위치값이며, 다음과 같이 주어집니다:

%%% \begin{equation} \tilde{\mathbf{x}} = 2 \mathbf{x}^n - \mathbf{x}^{n-1} + \Delta t^2 \mathbf{M}^{-1} \mathbf{F}_{ext} \end{equation} %%%

%%% \begin{equation} = \mathbf{x}^n + \Delta t \mathbf{v}^n + \Delta t^2 \mathbf{M}^{-1} \mathbf{F}_{ext} \end{equation} %%%

$k \rightarrow \infty$로 가는 극한을 취하면 다음과 같은 제약이 있는 최적화식을 얻을 수 있습니다:

%%% \begin{equation} \min_{\mathbf{x}} \frac{1}{2} \left( \mathbf{x}^{n+1} - \tilde{\mathbf{x}} \right)^T \mathbf{M} \left( \mathbf{x}^{n+1} - \tilde{\mathbf{x}} \right) \end{equation} %%%

%%% \textit{s.t.} \ C_i \left( \mathbf{x}^{n+1} \right) = 0, i = 1, \ldots, n. %%%

이 최소화 문제는 제약 다양체 상에서 예측된 위치 (질량 가중치 측정) 에 가장 가까운 점을 찾는 것으로 해석할 수 있습니다. PBD는 골드엔탈 외[GHF07]의 빠른 프로젝션 알고리즘의 변형을 사용해 이 최소화를 근사 해결하는데, 이 알고리즘은 먼저 예측 단계를 거친 다음 파티클을 제약 다양체에 반복적으로 프로젝션합니다. PBD는 이 접근법을 따르지만 프로젝션 단계를 푸는 데 사용되는 방법이 다릅니다. [GHF07]과 달리 PBD는 각 뉴턴 단계에서 시스템 전체를 선형화하여 풀지 않습니다. 대신 “풀어야 할 시스템”절에서 설명한 바와 같이 가우스-자이델 방식과 같이 한 번에 하나의 제약을 선형화합니다. 이는 큰 비선형성 제약이 있을 때 PBD를 견고하게 만드는 데 도움이 됩니다.

투영 역학 (projective dynamics) [BML*14]은 제약의 무한 강성에 의존하지 않는 암묵적 방식으로 제약을 처리할 수 있는 PBD의 수정 버전을 제시합니다. 이는 솔루션을 예측된 (관성) 위치로 끌어당기는 역할을 하는 제약을 추가함으로써 달성됩니다.

이차 방법 (second-order methods)

역방향 오일러 방법과의 연결이 설정되었으니 더 높은 차수의 적분 방식을 PBD에 적용할 수 있습니다. [EB08]의 도출에 따라서 2차 정확도의 다단계 방법인 BDF2를 차용하겠습니다. 먼저 2차 정확도의 BDF2 업데이트 방정식을 다음과 같이 작성합니다:

%%% \begin{equation} \mathbf{x}^{n+1} = \frac{4}{3} \mathbf{x}^{n} - \frac{1}{3} \mathbf{x}^{n-1} + \frac{2}{3} \Delta t \mathbf{v}^{n+1} \end{equation} %%%

%%% \begin{equation} \mathbf{v}^{n+1} = \frac{4}{3} \mathbf{v}^{n} - \frac{1}{3} \mathbf{v}^{n-1} + \frac{2}{3} \Delta t \mathbf{M}^{-1} \left( \mathbf{F}_{ext} + k \nabla \mathbf{C}^{n+1} \right) \end{equation} %%%

속도를 제거하고 식을 정리하면 아래와 같이 됩니다:

%%% \begin{equation} \mathbf{M} \left( \mathbf{x}^{n+1} - \tilde{\mathbf{x}} \right) = \frac{4}{9} \Delta t^2 k \nabla \mathbf{C}^{n+1} \end{equation} %%%

여기서 관성 위치 $\tilde{\mathbf{x}}$는 다음과 같이 주어집니다:

%%% \begin{equation} \tilde{\mathbf{x}} = \frac{4}{3} \mathbf{x}^{n} - \frac{1}{3} \mathbf{x}^{n-1} + \frac{8}{9} \Delta t \mathbf{v}^{n} - \frac{2}{9} \Delta t \mathbf{v}^{n-1} + \frac{4}{9} \Delta t^2 \mathbf{M}^{-1} \mathbf{F}_{ext} \end{equation} %%%

식 (19)는 (16)과 같은 형태의 최소화에 대한 최적 조건으로 생각할 수 있습니다. 제약들이 풀리고나면, 업데이트 되는 속도는 식 (17)을 통해 얻을 수 있습니다:

%%% \begin{equation} \mathbf{v}^{n+1} = \frac{1}{\Delta t} \left[ \frac{2}{3} \mathbf{x}^{n+1} - 2 \mathbf{x}^{n} + \frac{1}{2} \mathbf{x}^{n-1} \right] \end{equation} %%%

이쪽 방식이 보다 정확한지 평가하기 위해서는 이전의 위치와 속도만 추가적으로 저장하고 예측 및 속도 업데이트 단계에서 몇 가지 기본 산술을 추가로 수행하면 되며, 나머지 PBD 알고리즘은 변경되지 않습니다. 이 간단한 수정의 이점은 수치적 댐핑이 훨씬 줄어들고 제약 프로젝션에 대한 수렴이 빨라진다는 점입니다. 이는 알고리즘이 이전 타임 스텝 정보를 사용하여 제약 다양체에 더 가까운 예측 위치를 생성하여 프로젝션을 더 빠르게 만든다고 생각하면 이해할 수 있습니다.

XPBD

지금까지 설명한 PBD의 한계 중 하나는 제약 강성이 제약 솔버에서 사용하는 타임 스텝 크기와 반복 횟수에 따라 달라진다는 것입니다. 무한한 제약 반복의 한계에서는 제약이 무한히 강해집니다. 놀랍게도 이러한 무한 강성 솔루션으로의 수렴은 제약 강성 계수를 어떻게 설정하든 상관없이 발생합니다.

PBD의 타임 스텝 및 반복 횟수에 강성이 종속되는 문제는 XPBD[MMC16]라는 확장으로 해결되었습니다. XPBD는 각 제약에 역강성 (inverse stiffness), 혹은 적합성 (compliance) $\alpha = \frac{1}{k}$를 연관시키는 적합성 제약 공식[SLM06]으로부터 파생됩니다.

XPBD를 도출하면 PBD의 반복 중에 각 제약에 대하여 계산된 $\lambda$를 총 승수의 증분 변화로 생각할 수 있음을 알 수 있습니다. 이는 일반적인 PBD의 방정식 (9)를 다음과 같이 수정합니다.

%%% \begin{equation} \Delta \lambda = \frac{ -C \left( \mathbf{x} \right) - \tilde{\alpha} \lambda }{ \nabla C \left( \mathbf{x} \right) \mathbf{M}^{-1} \nabla C \left( \mathbf{x} \right)^T + \tilde{\alpha} } \end{equation} %%%

여기서 $\tilde{\alpha} = \frac{\alpha}{\Delta t^2}$은 타임 스텝으로 스케일된 적합성 파라미터입니다.

이제 각 반복 이후 다음과 같이 시스템 위치값 뿐만 아니라 각 제약의 전체 라그랑지안 승수값도 업데이트 합니다:

%%% \begin{equation} \lambda = \lambda + \Delta \lambda \end{equation} %%%

%%% \begin{equation} \mathbf{x} = \mathbf{x} + \Delta \mathbf{x} \end{equation} %%%

(22)의 분모에 있는 추가항은 어떤 제약이 적용할 수 있는 힘의 양을 제한하는 형태로 작용하는데, 특히 $\lambda$가 커질수록 증가하는 제약의 변화는 작아지게 됩니다. 제로 적합성 ($\alpha = 0$) 의 경우에는 무한 강성의 제약에 따르는 일반적인 PBD와 정확히 일치하는 식을 얻게 됩니다.

전체 라그랑지안 승수 $\lambda$에는 유용한 해석이 있습니다. 이것은 제약이 입자들에 적용하는 힘의 총량을 잰 것으로, 이는 햅틱 장비나 힘에 의존적인 효과를 드라이빙하는데 사용될 수 있는 물리적인 양입니다.

XPBD가 PBD를 더 빠르게 수렴시키지는 않고 강성 솔루션에 도달하기 위해 동일한 반복 횟수가 필요할 것임을 주의하세요.

XPBD를 사용한다고 PBD가 더 빨리 수렴하지는 않으며, 강성 솔루션에 도달하기 위해서는 동일한 수의 반복이 필요합니다. 하지만 XPBD는 잘 정의된 에너지 포텐셜과 상관 관계가 있는 일관된 솔루션을 반환합니다. PBD와 마찬가지로 수렴 전에 솔버가 종료되면 제약 조건을 인위적으로 준수하는 것으로 나타납니다.

특정 제약들

다음으로는 PBD로 관절이 있는 강체 (articulated rigid body), 연체 (soft body), 천 (cloth) 또는 유체 (fluid) 와 같은 다양한 물질을 시뮬레이션하는 데 사용할 수 있는 다양한 제약 조건을 소개합니다. 가독성을 높이기 위해 $\mathbf{x}_{i,j} = \mathbf{x}_{i} - \mathbf{x}_{j}$로 정의하겠습니다.

늘어남 (stretching)

예를 들어 거리 제약 함수 $C \left( \mathbf{x}_1, \mathbf{x}_2 \right) = \left| \mathbf{x}_{1,2} \right| - d$를 고려해 봅시다. 각 점에 대해 미분하면 $\mathbf{n} = \frac{\mathbf{x}_{1,2}}{\left| \mathbf{x}_{1,2} \right|}$일때 각각 $\nabla_{\mathbf{x}_1} C \left( \mathbf{x}_1, \mathbf{x}_2 \right) = \mathbf{n}$, $\nabla_{\mathbf{x}_2} C \left( \mathbf{x}_1, \mathbf{x}_2 \right) = - \mathbf{n}$입니다. 따라서 스케일링 팩터 $\lambda = \frac{\left| \mathbf{x}_{1,2} \right| - d}{1 + 1}$이고 최종 보정은 다음과 같습니다:

%%% \Delta \mathbf{x}_1 = - \frac{w_1}{w_1 + w_2} \left( \left| \mathbf{x}_{1,2} \right| - d \right) \mathbf{n} %%%

%%% \Delta \mathbf{x}_2 = + \frac{w_2}{w_1 + w_2} \left( \left| \mathbf{x}_{1,2} \right| - d \right) \mathbf{n} %%%

이는 [Jak01]에서 거리 제약의 투영에 대한 공식입니다 (그림 3을 참고하세요). 이것들은 일반적인 제약 프로젝션 방법의 특수한 경우로 파생될 수 있습니다.

그림 3 그림 3: 제약 $C \left( \mathbf{x}_1, \mathbf{x}_2 \right) = \left| \mathbf{x}_{1,2} \right| - d$의 프로젝션. 보정 $\Delta \mathbf{x}_i$는 질량의 역수 $w_i = \frac{1}{m_i}$에 따라 가중치를 받습니다.

구부러짐 (bending)

천 시뮬레이션에서는 늘어남 저항 (resistance) 과 더불어서 구부러짐을 시뮬레이션하는 것이 중요합니다. 이를 위해 인접한 삼각형 $\left( \mathbf{x}_1, \mathbf{x}_3, \mathbf{x}_2 \right)$와 $\left( \mathbf{x}_1, \mathbf{x}_2, \mathbf{x}_4 \right)$ 한 쌍에 대하여 다음과 같은 제약 함수와 강성 $k_{bend}$를 사용하여 양방향 구부러짐 제약이 추가됩니다:

%%% C_{bend} \left( \mathbf{x}_1, \mathbf{x}_2, \mathbf{x}_3, \mathbf{x}_4 \right) = \textrm{acos} \left( \frac{\mathbf{x}_{2,1} \times \mathbf{x}_{3,1}}{ \left| \mathbf{x}_{2,1} \times \mathbf{x}_{3,1} \right|} \cdot \frac{\mathbf{x}_{2,1} \times \mathbf{x}_{4,1}}{ \left| \mathbf{x}_{2,1} \times \mathbf{x}_{4,1} \right|} \right) - \phi_0 %%%

스칼라 $\phi_0$는 초기 두 삼각형의 끼인각이고 $k_{bend}$는 천의 구부러짐 강도를 정의하는 전역 파라미터입니다 (그림 4를 참고하세요). 점 $\mathbf{x}_3$과 $\mathbf{x}_4$ 사이의 거리 제약에 더하여 이 구부러짐 항을 이용하는 것의 장점은 이 항이 늘어남과는 독립적이라는 것입니다. 이는 이 항이 에지의 길이와 독립적이기 때문입니다. 그림 9에서는 구부러짐과 늘어남을 독립적으로 튜닝하는 방식을 보여줍니다.

그림 4 그림 4: 구부러짐 저항에 대해서는 제약 함수 $C \left( \mathbf{x}_1, \mathbf{x}_2, \mathbf{x}_3, \mathbf{x}_4 \right) = \textrm{arccos} \left( \mathbf{n}_1 \cdot \mathbf{n}_2 \right) - \phi_0$가 사용됩니다. 현재의 끼인각 $\phi$은 두 삼각형의 법선 사이의 각으로 측정됩니다.

등척성 구부러짐 (isometric bending)

늘어나지 않는 면에 대한 구부러짐 제약은 [BKCW14]에서 소개되었습니다. 이 제약의 정의는 베르구 등[BWH∗06]의 이산화된 등척성 구부러짐 모델을 기반으로 하며, 표면이 등척적으로 변형되는 경우, 즉 에지의 길이가 변하지 않는 경우에 적용할 수 있습니다. 많은 직물들이 심하게 늘어날 수 없기 때문에, 이 방법은 의류 시뮬레이션에 적합한 선택입니다.

각 내부 에지 $e_i$에 대하여 스텐실 $s$는 $e_i$에 이웃하는 두 삼각형으로 구성된 것으로 정의됩니다. 벡터 $\mathbf{x}_s = \left( \mathbf{x}_0, \mathbf{x}_1, \mathbf{x}_2, \mathbf{x}_3 \right)^T$는 스텐실의 네 점을 담고 있고 벡터 $\mathbf{e}_s = \left[ \mathbf{x}_0\mathbf{x}_1, \mathbf{x}_1\mathbf{x}_2, \mathbf{x}_2\mathbf{x}_0, \mathbf{x}_0\mathbf{x}_3, \mathbf{x}_3\mathbf{x}_1 \right]$은 공유 에지로 시작하여 스텐실의 다섯 에지를 담고 있습니다 (그림 5를 참고하세요).

그림 5 그림 5: 등척성 구부러짐 제약은 내부 에지 $e_0$의 스텐실을 사용하여 정의됩니다.

등척성 구부러짐 모델을 사용하여 스텐실의 지역적 헤시안 구부러짐 에너지는 다음과 같이 결정됩니다:

%%% \mathbf{Q} = \frac{3}{A_0 + A_1} \mathbf{K}^T \mathbf{K} %%%

여기서 $A_0$와 $A_1$은 이웃하는 삼각형의 면적이고 $\mathbf{K}$는 다음과 같은 벡터입니다:

%%% \mathbf{K} = \left( c_{01} +c_{04}, c_{02} +c_{03}, −c_{01} −c_{02}, −c_{03} −c_{04} \right) %%%

여기서 $c_{jk} = \textrm{cot} \angle e_j, e_k$입니다. 행렬 $\mathbf{Q} \in \mathbb{R}^{4 \times 4}$는 상수이며 스텐실의 초기 구성으로부터 미리 계산할 수 있습니다. 지역적 헤시안 구부러짐 에너지는 구부러짐 제약을 정의하는데 다음과 같이 사용될 수 있습니다:

%%% C_{bend} \left( \mathbf{x}_s \right) = \frac{1}{2} \sum_{i,j} \mathbf{Q}_{i,j} \mathbf{x}_i^T\mathbf{x}_j %%%

헤시안 구부러짐 에너지가 상수이므로, 그라디언트는 다음과 같이 결정됩니다: %%% \frac{\partial C_{bend}}{\partial \mathbf{x}_i} = \sum_{j} \mathbf{Q}_{i,j} \mathbf{x}_j %%%

그림 6은 소개된 구부러짐 제약을 사용하여 시뮬레이션된 천을 보여줍니다.

그림 6 그림 6: 무거운 구체가 네 동상 위로 던져진 천 조각을 내리누르고 있습니다. 등척성 구부러짐 제약 때문에 사실적인 주름이 만들어집니다.

충돌 (collision)

삼각형 충돌

천 내부의 자체 충돌 (self collision) 은 추가적인 일방향 (unilateral) 제약에 의해 처리할 수 있습니다. 삼각형 $\mathbf{x}_1, \mathbf{x}_2, \mathbf{x}_3$을 통과하여 움직이려는 점 $\mathbf{q}$에 대한 제약 함수는 다음과 같습니다:

%%% C \left( \mathbf{q}, \mathbf{x}_1, \mathbf{x}_2, \mathbf{x}_3 \right) = \left( \mathbf{q} - \mathbf{x}_1 \right) \cdot \frac{\mathbf{x}_{2,1} \times \mathbf{x}_{3,1}}{\left| \mathbf{x}_{2,1} \times \mathbf{x}_{3,1} \right|} - h %%%

여기서 $h$는 천의 두께입니다 (그림 7을 참고하세요). 만약 점이 삼각형 법선을 기준으로 아래에서부터 들어오는 경우 제약 함수는 다음과 같아야 합니다:

%%% C \left( \mathbf{q}, \mathbf{x}_1, \mathbf{x}_2, \mathbf{x}_3 \right) = \left( \mathbf{q} - \mathbf{x}_1 \right) \cdot \frac{\mathbf{x}_{3,1} \times \mathbf{x}_{2,1}}{\left| \mathbf{x}_{3,1} \times \mathbf{x}_{2,1} \right|} - h %%%

그림 7 그림 7: 제약 함수 $C \left( \mathbf{q}, \mathbf{x}_1, \mathbf{x}_2, \mathbf{x}_3 \right) = \left( \mathbf{q} - \mathbf{x}_1 \right) \cdot \mathbf{n} - h$는 점 $\mathbf{q}$가 천 두께 $h$만큼 삼각형 $\mathbf{x}_1, \mathbf{x}_2, \mathbf{x}_3$ 위에 있도록 합니다.

환경 충돌 (environment collision)

예를 들어 삼각형 또는 볼록 메시 (convex mesh) 로 표현되는 운동학적 모양과 입자 사이의 충돌은 먼저 각 입자에 대한 접촉면 후보의 집합을 찾아낸 다음, 각 접촉면 노멀 $\mathbf{n}$에 대해 비관통 제약을 다음과 같은 형태로 시스템에 도입하여 처리할 수 있습니다:

%%% C \left( \mathbf{x} \right) = \mathbf{n}^T \mathbf{x} - d_{rest} = 0 %%%

여기서 $d_{rest}$는 입자가 기본적으로 지오메트리로부터 유지되어야 하는 거리입니다.

입자간 충돌 (particle collsion)

입자간 충돌은 접촉면을 선형화하고 도입하는 것으로 환경과 유사하게 처리할 수 있지만, 다음과 같은 형태로 제약의 비선형적 특성을 유지하는 것이 더 강건한 경우가 많습니다:

%%% C \left( \mathbf{x}_i, \mathbf{x}_j \right) = \left| \mathbf{x}_{i,j} \right| - \left( r_i + r_j \right) \ge 0 %%%

여기서 $r_i$와 $r_j$는 두 입자의 반지름입니다. 이 제약은 [MMCK14]와 같이 알갱이로 구성된 재질을 모델링하는 데 사용할 수 있습니다.

마찰 (friction)

뮐러 등[MHHR07]은 제약 해결 후에 감쇠력을 도입하여 마찰을 처리했습니다. 이 접근법은 약한 마찰 효과에는 적합하지만 위치 제약이 마찰력을 자유롭게 위반할 수 있기 때문에 정적 마찰을 모델링할 수 없습니다. 제약에 비해 마찰이 강한 상황을 모델링하기 위해 (그림 8 참조), 맥클린 등[MMCK14]은 마찰 효과를 위치 레벨 제약 조건 해결의 일부로 포함시킵니다.

그림 8 그림 8: 붕괴 전 모래성 (왼쪽). 300 프레임 후 위치 기반 마찰 모델은 가파른 더미를 유지하고 있는 반면 (가운데), 속도 레벨 마찰 모델은 거의 완전히 무너진 상태입니다 (오른쪽).

입자 간의 상호 침투가 해결되면, 이 타임 스텝 동안 입자의 상대적 접선 변위를 기반으로 마찰 위치 증분이 계산됩니다. 상대 변위는 다음과 같이 계산됩니다:

%%% \begin{equation} \Delta \mathbf{x}_\perp = \left[ \left( \mathbf{x}_i^* - \mathbf{x}_i \right) - \left( \mathbf{x}_j^* - \mathbf{x}_j \right) \right] \perp \mathbf{n} \end{equation} %%%

여기서 $\mathbf{x}_i^\ast$와 $\mathbf{x}_j^\ast$는 충돌하는 입자들의 이전에 적용된 제약 증분을 포함한 후보 위치를, $\mathbf{x}_i$와 $\mathbf{x}_j$는 타임 스텝의 시작점에서의 입자의 위치를, 그리고 $\mathbf{n}=\frac{\mathbf{x}_{ij}^\ast}{\left|\mathbf{x}_{ij}^\ast\right|}$는 접촉 법선 벡터입니다. 입자 $i$의 마찰 위치 증분은 다음과 같이 계산됩니다:

%%% \begin{equation} \Delta \mathbf{x}_i = \frac{w_i}{w_i + w_j} \left\{ \Delta \mathbf{x}_\perp, \left| \Delta \mathbf{x}_\perp \right| < \mu_s d, \atop \Delta \mathbf{x}_\perp \cdot \textrm{min}\left( \frac{\mu_k d}{\left| \Delta \mathbf{x}_\perp \right|}, 1 \right), \mbox{otherwise} \right. \end{equation} %%%

여기서 $d$는 침투 깊이, $\mu_k$와 $\mu_s$는 각각 동적 및 정적 마찰계수를 뜻합니다. 식 (28)의 첫번째 경우는 입자의 상대 속도가 정적 마찰력의 임계점 아래일 때 모든 접선 방향의 움직임을 제거하는 식으로 정적 마찰을 모델링합니다. 두번째 경우는 입자의 침투 깊이에 기반하여 마찰 위치 증분을 제한하는 식으로 동적 쿨롱 마찰을 모델링합니다. 입자 $j$의 위치 변화는 아래와 같이 주어집니다:

%%% \begin{equation} \Delta \mathbf{x}_j = - \frac{w_j}{w_i + w_j} \Delta \mathbf{x}_i \end{equation} %%%

운동학적 모양과의 마찰은 동일한 방법으로 처리되며, 해당 모양은 무한 질량을 가진 것으로 처리되고 접촉면은 도형의 기하적 형태에 의해 정의됩니다.

부피 보존

… 계속 …

참고문헌

TODO

Comment count

Vulkan 오브젝트 이해하기

취미로 프로그래밍을 할 수 있을 정도로 회복된 요즘, 다시 Vulkan 튜토리얼을 보면서 복습 중입니다. 예제를 돌려보면서 어떤 식으로 내가 원하는 형태로 추상화를 할 수 있을까 고민하다가, Vulkan 내 오브젝트 간의 의존성이 어떻게 되는지를 정리를 해보는데, ‘그런 일을 누군가는 이미 하지 않았을까’ 싶어서 검색을 좀 했더니 좋은 글이 나오더군요. 공부하는 차원에서 오랜만에 번역을 해봤습니다.

원문 보기: https://gpuopen.com/learn/understanding-vulkan-objects

Vulkan 오브젝트 이해하기

Vulkan API를 배우는 중요한 부분은 (다른 API와 마찬가지로) 어떤 종류의 오브젝트가 정의되어 있는지, 이들이 무엇을 나타내고 서로 어떻게 연관되어 있는지 이해하는 것입니다. 이를 돕기 위해서 우리는 모든 Vulkan 오브젝트들과 이들의 관계성, 특히 어떤 오브젝트로부터 다른 오브젝트를 만드는지에 대한 순서를 보여주는 다이어그램을 만들었습니다.

각각의 Vulkan 오브젝트는 Vk라는 앞글자가 붙은 특정 타입의 값입니다. 이 앞글자는 다이어그램의 간소화를 위해 생략하였으며 이는 함수 이름 앞에 붙은 vk도 마찬가지입니다. 예를 들면 다이어그램의 Sampler는 VkSampler라는 Vulkan 오브젝트 타입이 있다는 의미입니다. 이러한 타입들은 포인터나 보통의 숫자처럼 취급되어서는 안됩니다. 이 값들을 어떤 방식으로든 해석하려고 하면 안됩니다. 그저 이들을 명확한 핸들로서 취급하고, 함수의 파라미터로 넘기고, 당연하겠지만 필요 없어질 경우 제거하는 것만 잊지 않으면 됩니다. 녹색 바탕의 오브젝트들은 자체 타입이 없고, 대신 부모 오브젝트 내의 uint32_t 타입 정수형 인덱스 값으로 표현됩니다. QueryPool의 Query 같은 것들이 그렇습니다.

화살표가 붙은 실선은 생성 순서를 나타냅니다. 예를 들어 DescriptorSet을 만들기 위해서는 이미 만들어진 DescriptorPool을 지정해야 합니다. 다이아몬드가 붙은 실선은 구성(composition)을 나타내는데, 이는 그 오브젝트가 부모 오브젝트에 이미 존재하므로 별도로 만들 필요가 없이 그냥 가져올 수 있다는 뜻입니다. 예를 들면 Instance 오브젝트로부터 PhysicalDevice 오브젝트들을 열거할 수 있습니다. 점선은 그 밖의 관계를 의미합니다. CommandBuffer에 대한 다양한 명령들을 제출하는 것이 그렇습니다.

이 다이어그램은 크게 세 부분으로 나뉘어져 있습니다. 각 부분은 빨간색으로 표시된 메인 오브젝트를 가지고 있습니다. 다른 모든 오브젝트들은 이 메인 오브젝트로부터 직접적으로 혹은 간접적으로 생성됩니다. 예를 들면 Sampler를 생성하는 vkCreateSampler 함수는 VkDevice를 첫 번째 파라미터로 받습니다. 간소화를 위해서 메인 오브젝트와의 관계성은 다이어그램에 그리지 않았습니다.

vulkan_diagram

모든 오브젝트들에 대하여 간략한 설명을 해보겠습니다:

  • Instance는 첫번째로 만들게 되는 오브젝트입니다. 여러분의 어플리케이션과 vulkan 런타임 간의 연결을 나타내므로 어플리케이션 당 오직 하나만 있어야 합니다. 또한 Instance는 vulkan 사용에 필요한 어플리케이션용 상태값을 모두 저장하므로, Instance를 생성할 때는 여러분에게 필요한 모든 layer와 extension을 지정해야 합니다.
  • PhysicalDevice는 그래픽 카드 같이 vulkan을 사용할 수 있는 특정 장치를 나타냅니다. Instance로부터 이들을 열거한 뒤에 venderID, deviceID, 그리고 지원하는 기능이나 속성, 한계값 등을 질의할 수 있습니다.
    • PhysicalDevice는 사용가능한 QueueFamily들을 열거할 수 있습니다. Graphics queue를 주로 사용하겠지만, Compute나 Transfer 용도만 제공하는 추가 큐를 사용할 수도 있습니다
    • PhysicalDevice는 또한 장치 내 Memory HeapMemory Type을 열거할 수 있습니다. Memory Heap은 RAM의 특정한 풀(pool)을 나타냅니다. 이는 여러분의 메인보드 상에 있는 시스템 RAM이나, 특정 그래픽 카드 상에 있는 비디오 RAM의 특정 메모리 공간, 혹은 구현체가 노출하는 그밖의 호스트/디바이스에 특화된 메모리를 추상화합니다. 메모리를 할당할 때는 Memory Type을 지정해야 합니다. 호스트에서 접근 되어야 한다던지, CPU/GPU 상의 값이 일치해야 한다던지, 혹은 캐시가 되어야 한다던지 등 할당할 메모리에 대한 요구사항을 지정합니다. 디바이스 드라이버에 따라서 이 타입들 간의 임의 조합이 있을 수 있습니다.
  • Device는 논리적 장치 혹은 열린(opened) 장치로 생각할 수 있습니다. 다른 모든 오브젝트들을 만들 준비가 되어 있는, 초기화된 vulkan 장치를 나타내는 메인 오브젝트입니다. DirectX의 Device 오브젝트와 비슷한 개념입니다. Device를 만드는 동안에는 활성화하고 싶은 특성을 지정할 필요가 있는데, 이중에는 anisotropic texture filtering 같은 기본적인 것도 있습니다. 또한 사용하고자 하는 모든 큐의 개수와 각 큐의 QueueFamily들을 기술해줘야 합니다.
  • Queue는 장치에서 실행될 명령들의 큐를 나타내는 오브젝트입니다. GPU에서 수행되는 모든 실질적인 작업들은 CommandBuffer를 채우고 vkQueueSubmit 함수를 이용하여 Queue에 제출하는 방식으로 요청됩니다. Graphics queue와 Compute queue 등 여러개의 큐가 있다면, 서로 다른 CommandBuffer를 각각에게 제출할 수 있습니다. 이러한 방식을 올바르게 사용한다면 상당한 성능 향상을 끌어낼 수 있는 비동기 컴퓨팅이 가능해집니다.
  • CommandPool은 CommandBuffer를 할당하기 위해 사용되는 단순한 오브젝트입니다. 특정한 QueueFamily에 연결되어 있습니다.
  • CommandBuffer는 특정한 CommandPool로부터 할당됩니다. 이것은 논리적 장치에서 수행될 다양한 명령들을 담을 버퍼를 나타냅니다. CommandBuffer에 대한 다양한 함수들을 호출할 수 있는데, 이 함수들은 모두 vkCmd로 시작합니다. 이 명령들은 CommandBuffer가 Queue에 제출되고 Device에 의해 처리될 때 수행되어야 할 작업에 대한 순서, 타입, 파라미터를 지정하는데 쓰입니다.
  • Sampler는 특정한 이미지에 바인딩되지 않고, 필터링 모드 (nearest / linear) 혹은 어드레싱 모드 (repeat / cleamp-to-edge) 같은 단순한 상태값 묶음으로 보면 됩니다.
  • 장치 메모리를 점유하는 리소스는 Buffer와 Image 두 가지 종류가 있습니다. Buffer는 비교적 단순한 편으로, 바이트 단위로 표현된 길이값을 갖는 바이너리 데이터를 위한 컨테이너입니다. 한편 Image는 픽셀 묶음을 표현하는 것으로 다른 그래픽스 API에서는 텍스처라고 알려져 있습니다. 이미지를 생성하는데는 더 많은 파라미터가 필요합니다. 1D, 2D 혹은 3D가 될 수 있고, (R8G8B8A8_UNORM이나 R32_SFLOAT 같이) 다양한 픽셀 포맷을 가질 수도 있고, 여러 개의 배열 레이어나 MIP 레벨 혹은 둘 다 갖기 위해서 여러 장의 이미지로 구성될 수도 있습니다. 이미지는 직접 접근할 수 있는 픽셀들의 단순한 선형 구조로만 구성되지는 않기 때문에, 별도의 오브젝트로 만들어집니다. 이미지는 그래픽 드라이버에 의해 관리되는 구현 별로 특화된 내부 포맷값 (tiling과 layout 등)을 가질 수 있습니다.
  • 특정 길이의 버퍼 혹은 특정 차원의 이미지를 생성하는 것이 자동으로 메모리 할당을 해주지는 않습니다. 여러분에 의해 수동으로 수행되어야 하는 3단계 프로세스가 있습니다. VulkanMemoryAllocator 같은 라이브러리를 사용할 수도 있습니다.
    • DeviceMemory를 할당한다
    • Buffer 혹은 Image를 생성한다
    • vkBindBufferMemoryvkBindImageMemory 함수를 사용하여 둘을 바인딩한다
  • DeviceMemory를 생성해야하는 이유가 바로 이 때문입니다. DeviceMemory는 (PhysicalDevice에서 지원하는) 특정한 메모리 타입으로부터 특정 바이트 길이로 할당된 메모리 블럭을 나타냅니다. Buffer와 Image 별로 각각 DeviceMemory를 할당하면 안됩니다. 대신 큰 메모리 덩어리를 할당하여 그 일부를 Buffer와 Image에 대입시켜야 합니다. 할당은 비용이 큰 연산이고, PhysicalDevice로부터 질의해서 알아낼 수 있는 정보 중에는 최대 할당 개수 한계값도 있습니다.
  • 모든 이미지에 대해 DeviceMemory를 할당하고 바인딩할 의무가 있는 중에서 하나의 예외가 있다면 그것은 Swapchain의 생성 부분입니다. 이것은 여러분의 운영체제에서 그리고 있는 화면 혹은 윈도우 내에 최종 이미지를 표시하는데 사용되는 개념입니다. Swapchain을 만드는 방법은 플랫폼에 따라 다릅니다. 시스템 API를 사용하여 초기화된 윈도우를 이미 가지고 있다면 먼저 SurfaceKHR 오브젝트를 생성해야 합니다. 이는 Instance 오브젝트 뿐만 아니라 몇가지 시스템 종속적인 파라미터를 필요로 합니다. 예를 들면 Windows에서는 HINSTANCEHWND 같은 것들이지요. SurfaceKHR 오브젝트는 윈도우의 Vulkan식 표현이라고 생각할 수 있습니다.
  • 이로부터 SwapchainKHR를 생성할 수 있습니다. 이 오브젝트는 Device가 필요합니다. 이것은 Surface에 나타나게 될 여러 장의 이미지 (더블 혹은 트리플 버퍼링을 사용하는) 를 나타냅니다. Swapchain이 가지고 있는 Image를 쿼리할 수 있습니다. 이 이미지들은 시스템에 의해 할당된 메모리를 이미 가지고 있습니다.
  • Buffer와 Image은 언제나 직접적으로 렌더링에 사용되지는 않습니다. 그 위에 view라고 불리는 또다른 레이어가 있습니다. 데이터베이스의 view와 같은 것으로 생각하면 됩니다. 내부 데이터 셋을 원하는 방식으로 보는데 사용될 수 있는 파라미터 셋인 셈이죠. BufferView는 특정 버퍼를 기반으로 만들어지는 오브젝트입니다. 버퍼 데이터의 일부분만 보도록 제한하기 위해서 오프셋과 범위를 생성 중에 지정할 수 있습니다. 마찬가지로 ImageView도 특정 이미지를 참조하는데 쓰는 파라미터 셋입니다. 여기서는 별도의 (호환되는) 픽셀 포맷, 스위즐링 (swizzling), MIP 레벨이나 배열 레이어의 특정 범위 제한을 통하여 해석된 픽셀값에 접근하도록 할 수 있습니다.
  • 쉐이더가 이러한 리소스들 (Buffer, Image 및 Sampler)에 접근하는 방법은 Descriptor를 통하는 것입니다. Descriptor는 단일 개체로 존재하지 않고 언제나 DescriptorSet의 형태로 그룹을 이룹니다. 그러나 DescriptorSet을 생성하기 전에 DescriptorSetLayout을 생성하여 레이아웃을 지정해야 하는데, 이는 DescriptorSet의 탬플릿과 같은 것입니다. 예를 들어 3D 지오메트리를 그리기 위해 렌더링 패스에 사용되는 쉐이더가 다음과 같은 것들을 요구할 수도 있습니다:
Binding slot Resource
0 vertex shader stage에서 활용할 유니폼 버퍼 (DirectX의 constant buffer)
1 fragment shader stage에서 사용할 유니폼 버퍼
2 샘플링된 이미지
3 fragment shader stage에서 사용할 샘플러
  • DescriptorPool도 생성할 필요가 있습니다. 이것은 DescriptorSet을 할당하기 위한 단순한 오브젝트입니다. DescriptorPool을 생성할 때는 여러분들이 할당할 Descriptor의 종류와 최대 갯수를 지정해야 합니다.
  • 마지막으로 DescriptorSet을 할당합니다. 이를 할당하려면 DescriptorPool과 DescriptorSetLayout 둘 다 필요합니다. DescriptorSet은 실제 Descriptor가 가지고 있는 메모리를 나타내고, Descriptor가 특정한 Buffer, BufferView, Image, 혹은 Sampler를 가리키도록 설정할 수 있습니다. vkUpdateDescriptorSet 함수를 이용하면 이런 설정을 할 수 있습니다.
  • 몇몇 DescriptorSet들은 렌더링 명령에 사용되기 위해 CommandBuffer에 능동적인 셋으로 바인딩 됩니다. 이를 위해는 vkCmdBindDescriptorSets 함수를 사용합니다. 이 함수는 또다른 오브젝트인 PipelineLayout을 필요로 하는데, 그 이유는 여러 개의 DescriptorSet이 바인딩될 수도 있고 Vulkan이 예상해야 하는 DescriptorSet이 몇 개고 어떤 타입인지 미리 알아야 하기 때문입니다. PipelineLayout은 어떤 타입의 DescriptorSet이 CommandBuffer에 바인딩 될 것인지에 대한 측면에서 렌더링 파이프라인의 설정을 나타냅니다. 이로부터 DescriptorSetLayout을 생성합니다.
  • 다른 그래픽스 API에서는 immediate mode 접근법을 취하여 다음에 뭐가 나오면 되는지 그냥 그리면 됩니다. Vulkan에서는 그게 불가능합니다. 대신 매 프레임의 렌더링을 사전에 계획하고 Pass와 Subpass들로 조직화해야 할 필요가 있습니다. Subpass는 별도의 오브젝트는 아니므로 여기서 다루지는 않겠지만, Vulkan의 렌더링 시스템에서 중요한 부분입니다. 다행히도 여러분의 작업을 준비할 때 모든 세부사항을 다 알 필요는 없습니다. 예를 들자면 Queue에 제출할 때 렌더링할 삼각형의 개수를 지정할 수 있는데, Vulkan의 RenderPass를 정의할 때 제일 중요한 부분은 Pass에서 사용하게 될 Attachment의 픽셀 포맷과 갯수입니다.
  • Attachment란 여러분들이 렌더 타겟 (렌더링으로부터 출력된 결과로 사용될 이미지) 이라고 알고 있을지도 모르는 것의 Vulkan식 이름입니다. 여기서는 특정한 이미지를 지정할 필요는 없고, 픽셀 포맷만 기술해주면 됩니다. 예를 들면 단순한 렌더링 패스의 경우 R8G8B8A8_UNORM 포맷의 컬러 Attachment와 D16_UNORM 포맷의 깊이/스텐실 Attachment를 사용할 수도 있습니다. Pass가 시작될 때 이 Attachment들의 내용물을 보존해야 하는지, 버릴지, 아니면 클리어할 지도 여기서 지정합니다.
  • Framebuffer (SwapchainKHR과 혼동하지 마세요) 는 Attachment (렌더 타겟) 으로 사용될 수 있는 실질적인 이미지에 대한 링크를 나타냅니다. Framebuffer 오브젝트는 RenderPass와 ImageView의 묶음을 지정하여 만들게 됩니다. 물론 Framebuffer내 이미지 갯수와 픽셀 포맷은 RenderPass에 지정된 것과 일치해야 합니다. Framebuffer는 Image 위에 있는 또다른 레이어고 기본적으로 특정한 RenderPass의 렌더링 동안 여러 ImageView들이 Attachment로 바인딩 되도록 그룹화합니다. 어떤 RenderPass의 렌더링을 시작할 때마다 vkCmdBeginRenderPass 함수를 호출하고 여기에 Framebuffer를 넘기게 됩니다.
  • Pipeline은 위에서 나열할 대부분의 오브젝트들을 묶어놓은 큰 것입니다. 이것은 전체 파이프라인의 설정을 나타내고 매우 많은 파라미터를 가지고 있습니다. 이 파라미터 중 하나가 PipelineLayout입니다 - Descriptor와 Push constant들의 구성을 정의하죠. ComputePipeline과 GraphicsPipeline 두 가지 종류가 있습니다. ComputePipeline이 좀 더 단순한 편인데, 이쪽이 지원하는 것은 모두 컴퓨팅만 하는 프로그램 (종종 Compute 쉐이더 라고 불리는 것) 이기 때문입니다. GraphicsPipeline이 훨씬 복잡한데, 이쪽은 vertex, fragment, geometry, compute, 그리고 만약 가능하다면 tessellation, 거기에 더해 버텍스 속성, 기본 도형의 토폴로지 종류, 후면 제거, 블랜딩 모드와 같은 모든 파라미터를 감싸는 것이기 때문에 그렇습니다. 이 모든 파라미터들은 예전의 오래된 그래픽스 API (DirectX 9, OpenGL)에서는 개별적인 설정 값으로 존재했었고, 그후에 API가 발전하면서 (DirectX 10, 11) 좀 더 작은 개수의 상태 오브젝트들로 그룹화 되었는데, 지금 Vulkan 같은 현대적인 API에서는 하나의 거대하고 불변하는 오브젝트로 구워져야 하게 되었습니다. 렌더링 하는 동안 필요로하는 각각의 다른 파라미터 셋마다 새로운 Pipeline을 생성해야합니다. 그런 뒤에 vkCmdBindPipeline 함수를 호출하여 CommandBuffer내에 지금 사용하려는 Pipeline을 지정할 수 있습니다.
  • Vulkan에서 쉐이더 컴파일은 여러 단계로 진행됩니다. Vulkan은 GLSL이나 HLSL같은 고수준 쉐이딩 언어를 지원하지 않습니다. 대신 Vulkan은 SPIR-V라 불리는 중간 형태의 포맷을 받아들이는데, 이 포맷은 어떤 고수준 언어에서든 생성할 수 있습니다. SPIR-V로 되어있는 데이터로 채워진 버퍼가 ShaderModule를 생성하는데 사용됩니다. 이 오브젝트는 쉐이더 코드 조각을 나타내는 것으로, 일부가 컴파일된 형태일 가능성이 있지만, 아직 GPU가 실행할 수 있는 형태의 것은 아닙니다. Pipeline을 만들때 여러분들이 사용할 각 쉐이더 스테이지 (vertex, tessellation control, tessellation evaluation, geometry, fragment, 혹은 compute) 에 대하여 ShaderModule과 함께 엔트리 포인트 함수의 이름 (“main” 같은) 을 지정해야 합니다.
  • PipelineCache라고 불리는 헬퍼 오브젝트도 있는데, 파이프라인 생성 속도를 높이는 데 사용될 수 있습니다. Pipeline 생성시에 추가적으로 넘길 수 있는 단순한 오브젝트이지만, 메모리 사용량 감소와 파이프라인 컴파일 시간을 통한 성능 향상에 실제로 도움을 줍니다. 드라이버가 몇가지 중간 데이터를 저장하는데 이것을 내부적으로 사용할 수 있으므로, 비슷한 파이프라인들을 생성하는 것이 더 빨라질 가능성이 생깁니다. 또한 PipelineCache 오브젝트의 상태값을 바이너리 버퍼에 저장하거나 혹은 불러올 수 있고, 디스크에 저장했다가 다음번 어플리케이션 실행 때 이를 사용할 수도 있습니다. 이러한 사용법을 추천드립니다!
  • Query는 Vulkan의 또다른 종류의 오브젝트입니다. GPU에 의해 쓰여진 특정한 숫자값을 읽어오는데 사용됩니다. Occlusion 쿼리 (여러분에게 어떤 픽셀이 렌더링 되었는지 아닌지 알려주는 것, 즉 쉐이딩 전 후 테스트를 모두 통과하여 프레임에 쓰여졌는지에 대한 것) 혹은 Timestamp 쿼리 (GPU 하드웨어 카운터로부터 얻은 타임스탬프 값) 등 서로 다른 종류의 쿼리가 있습니다. Query는 자체 타입이 없는데, 이는 QueryPool 내부에 상주하면서 uint32_t 인덱스값으로만 표현되기 때문입니다. QueryPool 타입과 저장할 쿼리의 개수를 지정함으로써 생성할 수 있습니다. 그러고 나면 vkCmdBeginQuery, vkCmdEndQuery 혹은 vkCmdWriteTimestamp 같은 함수를 사용하여 CommandBuffer에 명령을 제기하는 것으로 사용 가능합니다.
  • 마지막으로 동기화를 위해 사용되는 Fence, Semaphore, Event 같은 오브젝트들이 있습니다. Fence는 어떤 작업이 끝났음을 호스트에게 알려줍니다. 기다리거나 폴링하거나 혹은 호스트에 의해 수동으로 시그널 초기화가 될 수 있습니다. 자체 명령 함수는 가지고 있지 않고 vkQueueSubmit 함수를 통해 넘겨집니다. 제출된 큐가 해당 팬스를 끝내면 시그널 처리가 됩니다.
  • Semaphore는 설정 파라미터 없이 만들어집니다. 이것은 여러 큐들 사이에서의 리소스 접근을 제어하는데 사용됩니다. CommandBuffer 제출의 일부분으로서 시그날 혹은 웨이트될 수 있고 vkQueueSubmit 호출을 통해서도 할 수 있고, 한쪽 큐 (예를 들면 compute) 에서 시그널되고 다른 쪽 큐 (예를 들면 graphics) 에서 이를 기다리게 할 수도 있습니다.
  • Event 또한 설정 파라미터 없이 만들어집니다. vkCmdSetEvent, vkResetEvent, 그리고 vkCmdWaitEvents 함수를 이용하여 CommandBuffer에 제출된 개별 명령에 대해 GPU 상에서 시그널 혹은 웨이트를 걸 수 있습니다. 또한 하나 혹은 여러 개의 CPU 스레드로부터 vkGetEventStatus 함수 호출을 폴링하는 것으로 셋, 리셋, 웨이트할 수도 있습니다. GPU 의 특정 지점에서 동기화가 필요하거나 렌더 패스 내에서 Subpass 의존성이 사용될 경우 vkCmdPipelineBarrier 함수를 이용할 수도 있습니다.

이제 여러분은 GPU가 모든 프레임에서 실질적인 작업을 수행하도록 하기 위해서 (가능한 병렬적으로!) Vulkan 함수를 어떻게 호출해야 하는지 배울 필요가 있겠습니다. 그러면 현대적인 GPU가 제공하는 훌륭하고 유연한 연산 성능을 모두 활용할 여러분만의 방식을 갖게 될 것입니다.

Comment count

올해에는 무슨 일이 있었나 정리해보기

많은 일이 있었던 한 해를 마지막날 정리해봅니다…

1월~2월

  • 그간 꾸려왔던 스타트업 사정이 최악의 상황이 되면서, 어떻게든 다른 먹고 살 길을 만들어야 했었습니다…
  • 배운게 도둑질이라고, 코딩 과외를 서너개 하기 시작함니다.
  • 그 와중에 선배의 부탁으로 대학교의 OpenGL 강의를 맡게됨니다…
  • 대학교가 매우 먼거리에 있었으므로 처음 연락받았을 때는 당연히 거절을 하려 했으나, 코시국의 도움으로(?) 영상 강의가 되어 강의를 할 수 있게 되었습니다
  • 강의 준비를 틈틈히 하면서 회사 일들 정리를 하기 시작함니다

3월~6월

  • 학기가 시작되고 처음으로 해본 영상 강의는, “내가 얼마나 말을 잘 못하는가”를 알게 해준 시간이었다 생각함니다
  • 3시간 분량의 영상을 찍고, 편집하는데 높은 퀄리티를 보장하려면 정말 어마어마한 시간이 걸리겠구나… 싶었습니다
    • 스크립트를 자동으로 뽑아주는 소프트웨어가 있어서 그걸 써보려고 했지만, 제 발음이 안좋아서 + 전공용어는 AI가 알아듣기 힘들어서 써먹을 수가 없었습니다 ㅜㅜ
  • 하다보니 편집을 할 타이밍에 저도 모르게 “짝” 하고 박수 소리를 넣어 표시하는 저를 발견합니다
    • 그래서 방송하시는 분들이 영상 찍기 전에 박수 한번 쳐달라고 하는구나… 하고 자연스레 알게됨니다
  • 당연히 4학년 과목인줄 알고 준비했던 컴퓨터 그래픽스 수업은… 2학년 과목이었습니다
    • 매우 욕심넘치는 커리큘럼덕에 수업을 들었던 2학년 학생들은 정말 죽을 고생을 했을 것입니다…
    • 얼마나 욕심이 넘쳤나면, 마지막에는 저도 잘 모르는 PBR을 다루었습니다…
  • 회사에서는 웹 + webrtc + three.js 기반의 프로젝트를 진행하고 있었습니다
  • 홀로 front/backend를 모두 만드는 풀 스택 개발자분들 존경함니다… 그 정신없는 걸 어찌한담

7월~8월

  • 회사에는 우선 2개월 동안 논다고 얘기하고 무기한 무급 휴직에 돌입했습니다
  • 그게 가능했던 것이, 3월부터 6월까지한 강의에 대한 월급이 3~8월까지 6개월 분으로 쪼개져서 들어왔기 때문에, 7~8월에 약간의 급여를 받을 수 있었거든요 :D
  • 2013년 초였던가, 통으로 1달 놀았던 그 시절을 떠올리며 즐거이 놀려고 하다가…
  • 평소에 테니스를 치는 저를 카메라로 찍어보고 “세상에 이렇게 몸을 못움직일수가” 라고 회상했던 3월쯔음의 기억을 떠올려봅니다
  • 이 타이밍 아니면 기회가 없을거 같아서 난생 처음 PT를 받아보게 됨니다…
  • 그래도 테니스를 쳐오던게 쓸모가 있었는지, PT 선생님이 시키는게 그리 무리같지는 않았습니다
  • 그리고 백수처럼 지낼 수 있게 되다보니, 하루 종일 요리하고 - 먹고 - 운동하고를 반복할 수 있었습니다
  • 그 결과 2개월만에 대략 7키로 정도를 감량하였습니다 (!)

9월~12월

  • 9월이 되기 전, 슬슬 백수 하면서 새로운 직장을 알아봐야겠구나… 라고 생각한 즈음, 오퍼가 들어와서 새 직장에 무사히 이직했습니다
  • 멋대로 살아온터라 어느 회사의 직원이 된 적이 없었으므로, 자연스레 사회 초년생의 마인드가 되어 회사에 과연 잘 적응할 수 있을까 염려하면서 출퇴근을 함니다
  • 그러나 사람이 고민하는 일은 결국 다 비슷하다는 만고의 진리를 깨달으면서, 이전 회사에서 고민해왔던 일들과 비슷한 일들이 제 앞에 떨어지는 상황이 되어, 빠르게 적응하게 되었슴니다…
  • 다시 월급이 들어오기 시작하면서, 집에 있던 구질구질한 옷이며 가구를 싹 내다버렸습니다
    • 뭔가를 확 내다버리는 것은 뭔가를 확 지르는 것과 비슷한 수준의 쾌락을 주는 것 같습니다 (?)
  • 회사를 다니기 시작하니 운동할 시간이 점점 줄어들까 겁이 나서, 유래없이 비싼 아령을 한 쌍 구입해 매일 조금씩 운동을 함니다
  • 12월에는 거의 8년만에 건강검진을 받았는데, 매우 막 산거 치고는 나름 좋게좋게 넘어간 결과지를 받았습니다
    • 특히 체지방률이 정상인 수준으로 돌아왔다는 것이 매우 뿌듯함니다

한 해를 보내면서

  • 직업이 바뀌고, 몸이 바뀌고, 집의 환경이 바뀌고 하면서 다양한 전환점이 된 해라는 생각이 듭니다
  • 이제는 진짜 건강을 생각하지 않으면 안되는 나이가 되었음을 느낍니다
    • 특히 요즘 별 이유없이 발가락이 아파서 더 그렇습니다…
  • 아, 스스로 많이 회복되었다고 느낀 것중 하나는, 2019년 이후로 일 외에는 코드에 전혀 손을 대고 있지 않았었는데, 요즘에는 다시 슬슬 이것저것 시도를 해보게 되었다는 점?
  • 내년에는 더 기운내서 회사 일도 재밌게 하고 취미 프로젝트도 재밌게 하고 싶습니다
Comment count

나의 프로그래밍 로그

  • 2015년 6월 12일 첫번째 정리
  • 2019년 2월 24일 업데이트
  • 2019년 8월 5일 업데이트
  • 2021년 12월 31일 업데이트

잠 안오고 심심해서 써보는 나의 프로그래밍 로그

  • 1990년, 처음으로 MS-Dos와 GW-Basic를 접해봄.
  • 1992년, 마이컴이라는 잡지에서 Turbo-C가 짱 좋다는 이야기를 접함.
  • 1993년, 마이컴이라는 잡지에 실린 ‘나의 자랑 나의 프로그램’이라는 코너에 실린 C 프로그램을 Turbo-C에서 하나하나 타이핑 해봄. 그러나 C 문법에 대해 1도 모르던 꼬꼬마 린델은 or 연산자 |와 표 그릴때나 쓰던 확장 아스키 코드를 헷갈려 하며 무수한 에러가 나는 장면만 봄. 당연히 영어도 할 줄 몰랐으니 에러를 고치는 방법 따윈 몰라서 포기.
  • 1996년인가 쯤, PC통신을 하면서 Game13H라는 라이브러리를 알게되어 화면에 점 하나 찍는걸 해보고 즐거워함. (아마도 내 기억에 13H라는건 320x240x256Color 모드를 의미했던걸로 기억)
  • 1997년 겨울, 걸출한 프로그래머 친구를 만나 같이 게임을 만들려고 하면서 프로그래밍에 손놓고 그림만 그리기 시작.
  • 2000년, 대학교 입학 후 Visual C 6.0을 접함. void main()으로 시작하여 printf()가 난무하는 콘솔 프로그램이나 숙제로 낼 수 있을 정도로만 겨우겨우 만들수 있게됨.
  • 2001년, C의 포인터를 이해하는데 머리를 쥐어짬.
  • 2002년, malloc()이라는 함수를 처음 써봄. 그리고 처음으로 ‘잘못된 연산에 의해 프로그램을 종료합니다…‘를 남발하는 숙제용 프로그램 완성.
  • 2003년 봄, 파일 구조라는 수업을 들으면서 처음으로 파일 입출력이라는걸 억지로 해봄.
  • 역시 2003년 봄, 처음으로 Visual C에 Breakpoint라는 기능이 있다는 걸 알게 됨. 또한 포인터로 선언된 값에 0xCCCCCCCC가 들어있으면 그건 잘못된 거라는 사실도 처음 알게됨 (!).
  • 2003년 가을, DB 수업을 들으면서 PHP를 처음 접해봄. 무리하게 프로젝트를 해보았으나 결과는 GG…
  • 2004년 봄, 컴퓨터 그래픽스 수업을 들으며 OpenGL을 처음 접해봄. 처음으로 콘솔창이 아닌 그림 그리는 화면을 윈도우에 띄워봄…
  • 2004년 1학기 끝나갈 무렵, 학기말 프로젝트에 처음으로 C++ 클래스를 도입해 봄. 그러나 왜 C++을 써야하는지 그 필요성은 전혀 못느끼고 말았음…
  • 2004년 1학기 끝나갈 무렵, 학기말 프로젝트 프로그램의 간지와 인터페이스를 위해 Win32 프로그래밍이라는걸 해봄.
  • 2005년 석사 입학한 뒤, 다관절 강체 물리 엔진을 C 구조체와 함수로만 만들려고 시도. 그래서 모든 함수의 첫 파라미터가 C 구조체인 상황이 시작되고… 처음으로 C++ 클래스를 써볼껄 하면서 후회함. 아무튼 이걸 만들면서 왜 헤더 파일에는 ifndef 매크로를 붙이는지 이해하기 시작.
  • 2005년, OpenGL 삼각형으로 구, 원통 같은걸 직접 그려보기 시작.
  • 2005년, 모션 캡처 데이터 BVH 파일을 읽어들여 구와 원통 같은걸로 모션 캡처 애니메이션을 그려보기 시작.
  • 2005년, Matlab을 처음 접해보고 행렬 연산의 편의성면에서 C와는 비교할 수 없는 편리함을 느끼고 핥기 시작.
  • 2006년, 대학원 수업에서 스케치 기반 인터페이스 수업을 들으면서 나만의 선형대수 라이브러리가 있어야겠단 생각이 들어 모두 inline으로 점철된 선형대수 라이브러리를 만들게 됨. 그 뒤로 대충 7년간 잘 씀…
  • 2006년, OpenCV라는걸 처음 접해보고, 이걸로 사진을 읽어서 이것저것 할 수 있다는 것을 알게됨.
  • 2007년, Matlab 핥기의 정점에 도달… 모션 캡처 로더와 모션 뷰어, 물리 엔진을 모두 Matlab으로만 구현해보게됨…
  • 2006년인가 2007년인가 쯔음… 컴파일러를 만드는 컴파일러가 있단 사실을 구체적으로 알게됨. JavaCC라는 녀석을 프로젝트 때문에 사용해보게 됨. AST를 만들고 이걸 방문하는 Visitor 인터페이스에 대한 개념을 배우게 되면서 자연스럽게 추상 클래스와 구체 클래스간의 차이 및 쓸모를 알게 됨.
  • 2006년인가 2007년에 컴파일러 수업을 들을 때, 처음으로 스택 메모리와 힙 메모리의 차이를 알게됨.
  • 2006년인가 2007년에 컴파일러 수업을 들을 때, 자바를 줄기차게 쓰면서 “자바의 클래스 변수는 선언한다고 초기화 되지 않는다”는 사실을 처음 알게됨.
  • 2008년, 모션 시그널 필터를 구현하면서 처음으로 클래스 상속이란걸 제대로 써보게 됨.
  • 2009년 언젠가, 데이터 형이 다르지만 같은 짓을 하는 코드를 대충 6번정도 작성하다가 템플릿이란 거에 손을 대보게 됨. 컴파일하면서 썡고생 경험.
  • 2009년인가 2010년인가, OpenGL로 직접 UI 버튼이니 슬라이더니 하는 것들을 구현해 봄.
  • 2009년인가 2010년인가, Qt를 처음 접하고 기존에 MFC가 정말 프로그래밍하기 개떡같았다는 걸 뼈저리게 느낌.
  • 2010년인가 2011년, 처음으로 svn이니 git이니 하는 형상관리툴의 존재를 알게됨. 그러나 익숙해지는데는 어마어마한 시간이 지난 후…
  • 2011년인가 2012년인가 python을 처음으로 접해봄. 쩌는 텍스트 처리 능력을 보고 앞으로 C/C++ 가지로 파서 만들지 말아야겠다고 다짐… (했으나 걸핏하면 어딨는지 못찾겠어서 obj 로더같은거는 그냥 C로 파서 만들기를 수차례 반복)
  • 2013년, inline c 함수 기반의 선형대수 라이브러리를 몽땅 클래스 + 연산자 오버로딩으로 개조함. 이때 연산자 오버로딩을 처음 제대로 써봄. 그리고 변수에 붙은 const, 함수에 붙은 const가 어떤 의미인지 제대로 이해하기 시작. 처음으로 ‘설계때부터 프로그래머의 사용방식을 적절히 제한하는 것이 왜 유용한지’ 이해하게 됨.
  • 2013년, kinect SDK를 써보면서 다시는 쓰고 싶지 않았던 win32 프로그래밍에 또 손을 댐. 그렇지만 이번에는 맥에서도 돌려보고 싶은 욕구를 위해 모든 c파일에 include 를 떡칠하는 우를 범하지 않게 됨.
  • 2013년, 안드로이드 프로그래밍을 처음 접함.
  • 2013년, 싱글톤이라는 패턴 클래스를 처음 만들어보고, 왜 이렇게 만드는게 편리한지 뼈저리게 깨달음.
  • 2014년, 예전에 공부한 클래스 상속 / 템플릿을 이용해 모션 데이터를 처리하는 프로그래밍을 하기 시작.
  • 2014년, RFL이라는 혼자 쓸 목적의 라이브러리 프로젝트를 만들면서 문서화를 깔짝대봄
  • 2014년, 전신 모션 트래킹을 위한 패턴 매칭 기반의 트래커 문제를 풀어봄. 먼저 CPU로 풀면 얼마나 느린지 확인하고 (초당 3프레임?), 이걸 OpenCL을 이용하여 가속화 하는 작업을 2015년에 진행함
  • 2015년, OpenCL 코드를 잘못 놀리면 어떻게 되는지 뼈아프게 느낌. macos에서 OpenCL 코드를 잘못 놀리면 커널 패닉이 일어나는 것을 보고 신기해함 (…)
  • 2015년, OpenCL로 kinect fusion 비스무리한 것을 구현하기 위해서 3D TSDF (Truncated Signed Distance Field)를 만들어내는 알고리즘 구현을 해봄. 이와 동시에 rigid ICP를 구현.
  • 2015년, XML 기반 serialization을 하는 낡은 코드를 드러내고 JSONCpp를 붙여봄
  • 2015년, Freetype을 이용하여 직접 vector font를 읽어 OpenGL로 렌더링하는 짓을 해봄. 처음에는 Freetype에서 제공하는 rasterizer를 이용해서 texture를 굽고 OpenGL로 이 텍스처를 뿌리다가, 확대하면 생기는 깍두기를 보면서 회의를 느낌…
  • 2015년, 그러다가 존경해 마지않는 Blinn 선생이 quadratic Bezier curve의 경우 convex 혹은 concave 영역을 삼각형으로 던지면 채워넣어주는 쉐이더를 만들수 있음을 알려주는 논문을 읽고 이걸로 폰트 렌더러를 직접 구현해봄. 회사에서 이걸 보고 예능 자막 스타일 짤방을 만드는 앱으로 발전시키기 시작, Boodl이라는 앱을 만들게 됨…
  • 2015년, 크로스 플랫폼이란걸 해보고 싶어서 같은 C++ 소스 코드를 유지하는 프로젝트를 visual studio용, xcode용 등등으로 따로 만들어대다가 너무 불편해서 cmake를 건드려보기 시작함
  • 2016년, C++ 프로젝트를 js로 컴파일하는 방법이 있다하여 emscripten이라는 녀석을 이용해보기 시작함. 다양한 삽질 끝에 브라우저에서 직접 만든 C++ 렌더러 돌리기 성공.
  • 2017년 6월경, 러스트 기본 문서가 2판으로 업데이트 된 것을 보고 읽기 시작하다가 내용이 친절하고 재밌어서 원문 저장소를 포크하고 원저자에게 한국어로 번역하겠다고 이슈를 만들어 알리고 작업 시작. 2018년 10월 즈음에 appendix를 제외한 대부분의 문서 번역이 완료됨.
  • 2017년 8월경, 백엔드 및 인프라를 담당하던 친구들이 개인 사정으로 인해 회사를 그만두면서, 급하게 백엔드와 인프라를 땜빵하기 위한 공부를 시작. 급하게 공부한 백엔드 환경은 ruby-on-rails였고, 인프라는 aws였음…
  • 2017년 12월경, 프론트엔드를 담당하던 친구가 역시 개인 사정으로 회사를 옮기면서, react-redux-saga 기반의 싱글 페이지 프론트엔드 만드는 법에 대해서도 공부를 하게 됨. 애초에 javascript를 딥하게 써본적도 없는데, 온갖 모던한 문법으로 작성된 redux-saga 코드는 머릿속을 엉키게 만들기에 딱 좋았음
  • 2018년, aws의 기본적인 사용 방법을 알게 되면서 node 기반 백엔드를 약간 핥기 시작함.
  • 2018년 12월, 회사의 모든 iOS 인력이 사라지면서 (…) swift를 꾸역꾸역 공부하기 시작함. 덕분에 RxSwift를 실컷 만져보게 됨.
  • 2019년 초부터, es6 등등의 스펙에 맛들려서 거의 모든 js project 세팅을 babel로 돌리고, async/await를 일반 콘솔용 프로그램에도 적극적으로 활용하기 시작함.
  • 2019년 7월, 개인적으로 사용하려고 만든 binary-to-c-header 툴을 public으로 올렸더니 어떤 외국인 친구가 빌드 방법을 알려달라는 issue를 달아둠… 오픈 소스란 이런 것이구나?
  • 2019년 8월, 난생 처음으로 오픈 소스 컨트리뷰팅을 해봄. bgfx라는 renderer에 vulkan backend를 기본 구현하여 pull request. 대략 4박 5일간의 리뷰 핑퐁 후에 머지됨. 나름 뿌듯한 경험.
  • 2020년 3월, 영상 처리를 위해 gstreamer를 파보기 시작했으나… g-object라는 녀석이 함수 시그니처만 가지고서는 도저히 lifecycle을 알 수 없으며, 수많은 플러그인 중 상당수를 나몰라라 한다는 사실에 좌절
  • 2020년, 결국 ffmpeg으로 갈아타는 대신, 구닥다리 패킷 처리를 어떻게 할 방법이 없을까 하다가 rxcpp를 사용해보기 시작
Comment count

근황 토크

근 1년 넘는 세월동안 업데이트가 없었던 이 블로그. 어떤 일들을 일어났었는지 정리해보자.

러스트 문서 번역

  • 저장소 링크는 여기
  • 빌드된 html 버전은 여기
  • 2017년 6월경, 러스트 기본 문서가 2판으로 업데이트 된 것을 보고 읽기 시작
  • Carol과 Steve 두 사람이 쓴 2판 문서는 좀 장황하긴 하지만 너무나 친절하게 쓰여져 있어서 문득 아무 프로그래밍 언어라도 좀 다뤄본 사람이면 이 정도 설명은 충분히 이해할 수 있겠디고 생각함
  • 2017년 7월 9일, 원문 저장소를 포크하고 원저자에게 한국어로 번역하겠다고 이슈를 만들어 알리고 작업 시작
  • 그 뒤에 페북 러스트 커뮤니티에 도와줄 사람을 모집하여, 대략 5명 정도의 개발자와 함께 틈틈이 번역 시작
  • 그리고 지금 2018년 10월초, 드디어 부록을 제외한 거의 모든 챕터의 번역이 끝남…
  • 논문이나 좀 읽어댔지, 프로그래밍 기술 서적을 번역해 보는 것은 처음이어서 초반에는 고민하는 시간이 타이핑하는 시간보다 훨씬 길었던거 같음
  • 다 번역되고 나면 아마도 새로 나온 2018년 에디션으로 업데이트를 해야 할 거 같음;;;
  • 아무튼 러스트는 역시 참 재밌는 언어라고 생각. 러스트로 뭔가 본격 프로젝트 좀 해보고 싶다;

프론트엔드 / 백엔드 겉핥기

  • 2017년 8월경, 백엔드 및 인프라를 담당하던 친구들이 개인 사정으로 인해 회사를 그만두면서, 급하게 백엔드와 인프라를 땜빵하기 위한 공부를 시작
  • 당시 회사는 ruby-on-rails 기반의 백엔드, 영상 스트림 서버와 이를 보조해주는 go lang 기반의 에이전트 코드를 유지하고 있었음
  • 급하게 ruby와 rails를 공부해서 어떻게든 땜빵 시작
  • ruby는 보면 볼수록 신기한 언어라고 생각함. 문법의 자유로움 (나쁘게 말하면 근본없음) 의 매력이 해커를 부르는 이유가 아닐까 생각
  • 자동 빌드 도구인 fastlane이나, mac 개발자들의 친구 brew가 ruby를 쓰는 건, ruby의 자유로운 (혹은 근본없는) 문법 덕분에 DSL 만들기가 수월해서가 아닐까라고 생각함
  • 여담으로 로드 테스터로 쓰는 jmeter의 시나리오 xml 편집을 힘들어하다가, 이걸 좀 더 편하게 쓸 방법이 없을까 하며 검색하다보니 ruby 코드로 jmeter 시나리오를 작성할 수 있는 ruby-jmeter란 놈도 있단걸 알게 됨… 역시 ruby를 자기네 언어인 마냥 DSL 처럼 쓰고 있는 예;
  • 하지만 이걸 검색하다 같이 알게된 python 기반의 로드 테스트 프레임워크 locust를 보고, 이걸 써봤는데 jmeter보다 훨씬 사람같은 행동을 하는 테스트를 할 수 있었음. locust 강추함니다
  • 한편 go lang은 접할 기회가 별로 없었고 그 당시까지만 해도 ‘뭐, 고랭은 가비지 콜렉터를 쓴다고? 그러면 JVM 기반인가?’ 라고 오해하고 있다가, 어쨌든 코드를 보면서 나름 재밌는 언어구나 라고 생각함. 하지만 코드 관리가 뭔가 아이튠즈 같다는 느낌을 지울수 없음
  • 아무튼 허구헌날 싱글 스레드에 한 프레임당 30ms 이내에 실행이 끝나는 코드나 짜고있던 나한테 시동을 막 걸고 있는 대형 프로젝트의 백엔드 공부는 신기한 경험이었음
  • 그러다가 2017년말, 프론트엔드를 담당하던 친구까지 회사를 옮기게 되면서 급하게 react-redux-saga 기반의 싱글 페이지 프론트엔드도 공부를 하게 됨
  • 애초에 javascript를 딥하게 써본적도 없는데, 온갖 모던한 문법으로 작성된 redux-saga 코드는 머릿속을 엉키게 만들기에 딱 좋았음

소감

  • 2018년 현재, 결국 나는 ruby-rails과 node-express 기반의 백엔드 코드도 볼 수 있게 됐고, 여전히 db와 query에 대해서는 잘 모르지만 postgres / mysql 같은 db에 연결하고 뭔가 얻어오는 것도 할 줄 알게 됐고, react-redux-saga로 된 싱글 페이지 코드도 볼줄 알게 되었고, 간편한 콘솔 툴을 작성할려고 babel 기반으로 모던한 javascript를 쓸 수 있게 되었고, aws에서 ec2를 띄우고 ami를 굽고 elb를 설정하고 s3에 뭔가 올리고 cloudfront에 연결하여 내려받고 등등 기본적인 aws 사용법도 알게 됨
  • react-redux-saga의 경험은 mvc도 별로 제대로 경험하지 못한 나한테는 신선한 경험이었음. 처음에는 ui 하나를 집어넣을때도 여러 코드를 건드려야 하는 귀찮음과 쉽지 않은 제어 흐름 떄문에 골치가 아팠지만, 점점 코드를 늘려나가다 보니 로직과 뷰와 컨테이너가 확실히 분리되는 모양새를 보고 왜 사람들이 리덕리덕거리나 알게 되었음
  • ruby-on-rails와 node-express를 차례로 접하고나서 풀 스택 웹앱 프레임워크와 가벼운 웹앱 프레임워크를 비교할수도 있게 되었음… rails는 ‘rails 안에 다 있음!’이 기조인건지 뭐든 찾으면 나와서 뭐든 빨리 만드는 일에는 적합하다는 생각이 들지만, 뭔짓을 하든 간에 가볍고 스무스하게 동작한다는 느낌은 영 들지 않고, 가벼운 프레임워크를 쓰면 ‘나머지는 알아서’의 느낌이 강하지만, 일단 할일은 충분히 잘 된다는 느낌이 듬
  • 뭔가 많이 알게 되서 좋긴 하지만, 매우 바빠지고 피곤한건 어쩔수가 없구나;
Comment count