본문 바로가기

Function

레이 마칭(Ray-Marching) 방식의 인구막대 시각화

레이 마칭(Ray Marching)이란 3차원 물체를 렌더링하는 방식 중 하나다. 물체가 모두 그려진 2차원 이미지 한 장이 있다고 가정하고, 픽셀 하나하나의 색을 어떻게 칠할지를 결정하는 방식으로 접근한다. 

아래 그림처럼, 3차원 실제 공간이 2차원의 투영막에 투영된다고 하자. 그 투영된 이미지가 화면에 렌더링 될 이미지가 된다. 이때 아래 그림에서 카메라의 시점이 곧 관찰자의 시점이 된다.  시선과 물체의 한 점을 잇는 선분을 만들어볼 수 있고, 이 투영막의 어떤 픽셀 하나가 있다고 가정할 때, 그 선분과 투영막이 교차하는 지점이 바로 그 픽셀이 된다. 

출처 : 원본이 어디인지 도저히 찾을 수 없음

여기까지의 서술이 잘 이해가지 않고 생소하다면, 잠시 이 글을 덮어두고 3차원 렌더링 방식의 기본 지식들을 공부할 것을 권한다. 트라이앵글 기반의 3차원 렌더링을 이해하고 난 뒤 레이 마칭을 배운다면, "와, 이렇게도 해 볼 수 있구나!" 라는 감탄을 하게 되는데, 아무런 배경 지식 없이 레이마칭을 배우면 왜 이렇게 해야만 하는지 혼란스러울 수도 있기 때문이다.

 

다시 원 글로 돌아와보자.

레이 마칭 렌더링 방식은 픽셀에 찍힐 점의 색상이 물체의 색일지 그림자가 입혀진 색일지 등등을 결정하기 위해, 시점으로부터 가상의 광선(Ray)을 물체에 닿을때까지 시선과 픽셀을 연결하는 벡터 방향으로 조금씩 진행시킨다.(Marching) 이 때 물체와 닿았는지 아닌지를 판별하기 위해서는 SDF(Signed Distance Function) 방식으로 물체를 정의한 오브젝트들이 필수적이다.

가장 쉬운 예로, 원을 정의하는 아래 공식을 생각해보자.

https://namu.wiki/w/%EC%9B%90(%EB%8F%84%ED%98%95)/%EB%B0%A9%EC%A0%95%EC%8B%9D?from=%EC%9B%90%EC%9D%98%20%EB%B0%A9%EC%A0%95%EC%8B%9D

 

어떤 좌표 x와 y를 이 공식에 넣었을 때 r제곱보다 작은 값이 나오면 그 점은 원 안에 있을테고, 큰 값이 나오면 원 밖에 있을 것이며, r제곱과 일치하는 값이 나오면 원의 둘레상에 있다고 할 수 있다. 이렇게 어떤 좌표들을 넣어서 양수인지 음수인지 0인지 판별할 수 있는 함수들을 SDF라고 부른다.

레이 마칭 렌더링 방식은 이러한 SDF 들과 결합됨으로써 완성된다. 이 방식으로 물체의 표면을 판단하고 나면, 일단 그 픽셀에는 물체의 색을 칠할 수 있다. 그 다음에 다시 그 표면의 점에서 광원의 위치를 향해 '레이를 마칭시키고' 그러다가 광원에 닿기 전에 다른 물체에 부딪친다면 아까 그리려던 물체의 표면은 그림자에 가려진 셈이 된다. 보통 그렇게 두 단계, 즉 물체의 표면이 있는지, 그 물체의 표면은 그림자에 가려져 있는지 계산해서 최종적으로 픽셀의 색을 결정하게 된다.

 

 

레이 마칭(Ray Marching) 방식

 

 

레이 마칭의 개념은 아래 글에서 그림과 함께 조금 더 설명되어 있다.

 

 

레이 마칭(Ray Marching)

레이 마칭이란?

rito15.github.io

 

아래 글에서는 레이트레이싱 방식과 간단히 비교하고, 레이마칭이 어떻게 쓰이는지도 조금 더 설명되어 있다.

 

김성완 교수,

[인벤게임컨퍼런스(IGC) 발표자 소개] 前 미리내소프트웨어 개발이사부터 부산게임아카데미 교수까지 많은 경험을 가진 대한민국 게임사의 산 증인. 지금은 후학을 양성하며 인디게임 커뮤니티

www.inven.co.kr

 

"SDF 정도로 뭘 그릴수 있겠어?"라고 생각하는 사람이 있다면 아래의 영상들을 한번 빠르게 드래그해보길 권한다. Inigo Quilez는 이 분야에서 앞서나가는 천재같은 사람인데, 수학 공식에서 나오는 결과물들을 보고 있자면 정말 놀랍다.

 

 

 

 

레이 마칭 방식의 렌더링 결과물들은 ShaderToy에서 구경하거나 직접 작성해볼 수 있다.

아래 영상은 기본적인 개념을 설명하면서 따라해볼 수 있는 튜토리얼을 소개하고 있다.

아래 링크는 위의 영상 내용의 결과물이다.  Shadertoy와 Ray Marching 을 동시에 이해할 수 있는 기초적인 샘플이다.

 

Shadertoy

 

www.shadertoy.com

위 알고리즘에서의 핵심은 한번에 ray를 얼마만큼 진행시킬지 결정하는 RayMarch 함수다. 조금씩 광선을 진행시킬때마다 화면 안에 존재하는 모든 물체들을 훑으면서 그 물체들까지의 거리를 판별하고, 가장 가까운 물체까지의 거리 만큼 다음 스텝에서 광선을 진행시킨다. 설명에서 원들이 버블처럼 직선을 따라 겹쳐져 있는 그림이 바로 레이마칭 개념의 핵심이다. 위의 예시에서는 물체가 구와 평면 두 개 밖에 없지만, 물체가 많을 경우에도 모든 물체들을 훑어야한다.

따라서 물체의 종류가 많을수록 시간이 오래 걸릴 수 밖에 없다. 그런데 화면에 물체가 아주 많이 보일지라도 그 물체들이 동일 유형이라면 하나의 함수에서 결정된다. 아니, 결정된다기보다, 그렇게 결정되도록 코드를 짠다. 혹은 그렇게 함수 하나로 결정할 수 있도록 화면의 물체들을 선정/설계한다. Shadertoy에서 이런저런 작품들을 보고 있자면 참 대단한 사람들이 많다는 생각을 하게 된다.

 

위 튜토리얼의 함수를 하나하나 보자.

이 함수가 main 함수다. 화면의 픽셀을 하나씩 끌고 와서 어떤 색으로 그릴지 결정한다.

RayMarch 함수를 거치면서, 시점과 그리고자 하는 2차원 평면의 픽셀을 잇는 광선을 진행시켜서 가장 가까운 물체까지의 거리를 얻는다. 그 다음에 GetLight 함수를 통해 빛과 물체 표면의 관계를 계산하여 그 픽셀의 색을 결정한다.

ro는 Ray Origin, rd는 Ray Direction 을 의미한다.

 

RayMarch 함수는 위와 같다. 한 step 씩 광선을 진행시키면서 3차원 상의 진행 지점 각각에서 GetDist 함수를 통해 모든 물체들을 훑으면서 가장 가까운 물체표면까지의 거리를 받아온다.

 

GetDist 함수는 위와 같다. 여기서는 물체가 두 개 뿐이라 함수가 간단한데, 보통 GetDist 혹은 map 이라고 이름 붙은 함수 안에서 화면 안에 존재하는 모든 물체들의 SDF가 구현되어 있다. 이 함수는 그림자 판별 등 많은 함수들에서 2차적으로 자주 호출된다. 물체들을 어떻게 SDF함수로 설계하느냐에 따라 렌더링 퍼포먼스가 크게 달라진다.

 

main 함수에서 특정 픽셀부터 물체까지의 거리를 구했다면, 이제 그 표면의 색상을 결정해야 한다. 여기서는 GetLight라는 함수 하나에서 여러 기능이 구현되어 있는데, 다른 코드의 경우 castShadow 함수와 나머지로 분리해주기도 한다. 물체 표면의 색상을 결정하려면 시점과 물체 표면, 그리고 물체 표면의 법선(normal) 방향과 빛의 위치들을 모두 함께 고려해서 계산해줘야 한다.

이 때 특정 물체 표면의 normal 방향도 결정해야 하는데, 그 부분을 구현한게 바로 아래의 GetNormal 함수다.

미분개념을 이용해서 normal을 구하는 것 같은데, 역시 GetDist 함수를 여러번 호출하고 있다.

 

 

 

 

SDF(Signed Distance Function)

 

위의 함수들에서 GetDist 함수를 제외한 나머지를 일종의 '렌더링 엔진'이라고 부를 수 있다. 그리고 GetDist 함수 내부가 화면에 물체를 그리는 과정과 같다. 다시 말해, 위의 코드에서 GetDist 함수만 다르게 바꿔주면 화면에 다른 물체들이 나타나게 된다.

 

SDF 함수들은 위에서 소개한 Inigo Quilez 홈페이지에 상세히 나열되어 있다.

아래 각각의 링크에서 2차원, 3차원 SDF 함수들의 구현을 찾아볼 수 있다.

 

 

 

Inigo Quilez

Articles on computer graphics, math and art

iquilezles.org

 

Inigo Quilez

Articles on computer graphics, math and art

iquilezles.org

아래 링크를 들어가면 SDF 함수들을 조합해서 달팽이를 그리는 놀라운 GIF를 볼 수 있다. 스크롤을 내려보면 연도별 대표작들도 소개되어 있다.

 

 

Inigo Quilez

Articles on computer graphics, math and art

iquilezles.org

 

사실, 그의 코드와 결과물을 보다 보면 SDF 함수의 조합 이외에도 빛에 대한 놀라운 해석들에 감탄하게 된다. 

 

 

 

 

 

빛과 색

 

 

Shadertoy

 

www.shadertoy.com

Happy Jumping 이라고 이름 붙은 Inigo Quilez의 작품에서 부드럽고 온화한 빛을 볼 수 있는데, 한 줄 한 줄 써내려간 아래의 코드가 바로 그 빛과 색감을 결정해주는 부분들이다.

한줄한줄 설명할 능력은 안되지만, 변수 이름들과 수식을 보면서 한 픽셀의 색을 어떻게 결정하는지 대략적으로 따라가볼 수 있다.

 

 

 

 

인구 막대를 레이 마칭 방식으로 그려보자

 

벌써 1년이 좀 더 넘은 일이긴 한데, 당시에 토지주택연구원에서 의뢰받은 일을 하다가 트라이앵글 기반의 SSAO(Screen Space Ambient Occlusion)방식으로 렌더링하던 결과물을 레이 마칭 방식으로 대체해보고 싶어서 이것저것 공부하면서 시도해봤었다.

위의 이미지가 SSAO 방식. 이것도 그리 나쁘진 않지만, 레이 마칭 방식을 적용한 아래 방식과는 아주 큰 차이가 있다.

 

 

아래가 그 때 사용했던 코드다.

 

rayMarching renderer population

rayMarching renderer population. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

기본 베이스는 위의 Happy Jumping 에서 가져왔다.

 

레이마칭 자체도 당시에 처음 시도해보는 새로운 개념이어서 함수 하나하나의 의미를 이해하는데 좀 시간이 많이 걸렸었다. shadertoy 기반의 코드를 OpenGL로 가져오면서 몇몇 좌표 변환을 해주어야 했는데, OpenGL 셰이더 각 단계에서 정의된 공간의 좌표체계끼리의 행렬 변환은 늘 할때마다 헷갈려서 또 고생을 좀 했다.

 

가장 고생했던 부분은 인구 막대를 그리는 부분이었다.

 

전술했듯, SDF 함수 기반으로 같은 유형의 물체 다수를 그릴 때는 함수 하나를 통과시켜 그리는 것이 불문율이다. 그렇지 않으면 계산 시간이 물체 유형만큼 늘어나기 때문이다. 그런데 인구 막대의 경우 간단한 함수 하나에서 해결할 수 없었다.  모두 같은 유형임에도 불구하고 막대 하나하나의 높이는 데이터에서 가져와야했기 때문이다.

다른 모든 함수를 유지하면서 해볼 수 있는 가장 brute force 한 방식은 모든 marching unit 지점마다 200m 격자로 잘게 쪼개진 인구들을 읽어오는 것이었다. 그런데 하나의 픽셀을 그릴 때 이 map / getDist 함수를 아주아주아주 많이 반복한다. 그 때마다 버퍼에서 모든 데이터를 읽는 방식은 상상도 할 수 없었다. 차라리 같은 개수의 계산을 하는게 훨씬 빠르다. 버퍼에서 데이터를 읽는 속도는 계산 속도에 비해 무진장 느리기 때문이다.

 

그래서 시도해본 방식은 일반적인 marching unit 보다 1/10 혹은 1/100 만큼 촘촘하게 광선을 진행시키면서 데이터를 읽어오는 방식이었다. 하나의 픽셀을 계산할 때 그 픽셀이 어떤 막대들의 영향을 받는지 특정할 수 있다면 데이터를 읽는 회수를 줄일 수 있는데, 그렇게 하기 힘드니 촘촘히 스캔하는 수 밖에 없었다. 

 

혹은 그렇게 촘촘히 스캔하는 정도와 섞어서, 한 픽셀의 주변 데이터들을 읽도록 해봤다. 두 방식을 조합하니 한 프레임에 길게는 10초까지 걸렸지만, 다행히 영상 제작은 아니고 1000장 정도의 스틸 이미지만 만들어내면 되었으므로, 그래도 시작한 일을 끝맺을 정도는 되었다.

 

아래 코드가 바로 인구 막대를 읽어와서 거리를 결정하는 부분이다. 

 

for (int i=0 ; i<1 ; i++) 이라고 된 부분에서 1을 9로 고쳐보기도 했다. 

 

 

CastRay 함수는 이리저리 고치다가 약간 누더기가 되었는데, 여튼 진행을 촘촘하게 시켜서 데이터를 훑도록 했다.

 

 

실패 사례를 보자.

진행을 덜 촘촘하게 시키면 위의 그림처럼 z축으로 높이 올라온 막대의 중간중간이 깨지게 된다.

 

같은 코드라도 시점을 낮춰서 막대의 z=0 베이스와 가장 높은 곳의 화면상 좌표가 많이 차이나게 되면 역시 픽셀이 깨진다.

 

 

아까 위에서 loop 를 9번 돌려서 상하좌우대각선 데이터들을 더 읽으면 깨지는 부분이 사라진다. 대신 데이터를 9배 읽어야 하므로 속도는 그만큼 느려진다.

 

혹은 위의 코드에서 marchingUnit 기본값 0.1 을 0.01 로 낮춰도 똑같이 정상적인 이미지를 얻을 수 있다. 다만 soft shadow 같은 경우는 차이가 난다. soft shadow 알고리즘은 좀 더 넓은 지점에서 물체를 탐색함으로써 그림자 여부를 계산하는데, 그 이유때문에 주변 데이터를 많이 읽는 방식이 더 유리할 수 밖에 없다.

 

물론 동서남북대각선 데이터를 8개 더 읽더라도 그림자가 늘어져서 물체로부터 먼 거리의 픽셀 색상을 계산할 때는 역시 다시 그림자가 거칠어질 수 밖에 없다. 한 점을 중심으로 더 넓은 데이터를 읽어야 그림자가 부드럽게 나올 수 있다.(시도해보지는 않았다)

 

 

일단 그렇게 막대 높이와 데이터 문제를 일단락하고 난 뒤에는 색상이나 여러 빛 조합을 바꿔보기도 했다. 그래봤자 Inigo Quilez가 셋팅해놓은 값을 조금 조정해보거나, shader toy 다른 작품에서 안개 효과를 가져와서 써 보는 정도였지만, 신나게 새로운 렌더링 방식을 적용해봤던 기억이다. 그 이후로 더 진행시켜보지는 못했지만, 1년이 넘어가는 시점에서 기억이 더 사라지기 전에 기록으로 남겨본다.

 

메탈 느낌 적용

 

 

대표
SDF 캡슐 방식 테스트. 거대한 캡슐과 지면이 만나는 부분을 자세히 보면 occlusion 도 적용되어 있다.

 

 

 

색감 테스트

 

 

 

 

아침 안개 적용

 

 

 

지표면 근처에만 좀 더 짙은 안개 적용