(이하는 A trip through the Graphics Pipeline 2011, part 1 를 날림번역한 것임다.)

Part 1 - 소프트웨어 계층


응용 프로그램

니가 짠 코드ㅇㅇ. 니가 싼 버그(...)도 포함. (주:D3D나 OpenGL API 를 호출하는 코드가 포함된 프로그램을 얘기한다고 보면 될듯)


API 런타임

API 런타임은 다음과 같은 요청들을 니가 짠 코드로부터 받게 된다.
  • 리소스 생성
  • 스테이트 설정
  • 드로우콜 등
API 런타임은 넘겨받은 요청에 다음과 같은 처리를 한 뒤 작업들을 묶어서(batch) 유저 모드 드라이버(UMD)에게 넘겨준다. UMD 에 대한 설명은 바로 다음 섹션임.
  • 설정된 스테이트를 유지, 관리
  • 파라미터 유효성이나 그 외 에러나 일관성 검사 수행
  • 유저가 접근 가능한 리소스 관리
  • 셰이더 유효성 검사나 링키지(linkage)처리(D3D 는 이 단계에서 처리하지만, OpenGL 은 드라이버단에서 처리함)
  • 그 외 몇가지 작업들


유저 모드 그래픽 드라이버(user mode graphics driver, UMD)

CPU단의 마법들이 일어나는 곳. 니가 짠 코드가 호출한 API 에서 크래시가 발생할 때 운영체제가 "여기서 디졌어염 뿌우"하고 일러바치는 곳. 엔비디아는 nvd3dum.dll, AMD는 atiumd*.dll 뭐 그런 거.

유저모드라는 이름에서 알 수 있듯 응용 프로그램 코드와 같은 컨텍스트, 주소공간에서 돌아가고 권한 상승도 없다.

D3D가 호출하는 저수준 API(DDI)를 구현한 곳. DDI는 D3D API 와 유사한 형태인데, 메모리 관리와 같은 부분이 좀 더 명시적으로 처리되는 등의 차이가 있음.

(주 : 이 부분 부터는 순서도 좀 다르게 편집했고 의역도 엄청 들어감미다..)

셰이더 컴파일 과정의 일부가 처리되는 곳. 물론 셰이더는 HLSL컴파일러나 D3D API 런타임에서 처리되는 부분도 있기는 한데, 이러한 과정을 거친 것은 shader token stream 이며 UMD는 이를 받아 하드웨어와 밀접한 저수준의 컴파일단계를 처리한다.

좀 더 상세히 설명하자면, UMD 이전 단계에서의 셰이더 코드는 다음과 같은 검사를 거치게 된다.
  • 문법적으로 올바른지
  • D3D 제한사항들을 올바르게 지키고 있는지. 제한사항은 다음과 같다.
    • 올바른 타입을 사용하였는가
    • 텍스처/샘플 갯수 제한을 넘지 않았는가
    • 상수 버퍼 갯수 제한을 넘지 않았는가 등.
또한 여러 고수준 최적화가 적용된다. 이는 다음과 같다.
  • 다양한 루프 최적화
  • 사용하지 않는 코드 제거
  • constant propagation (주 : 어떤 수식이 컴파일타임에 상수로 계산될 수 있으면 그 식을 상수로 치환하는 것)
  • 분기 예측 등
UMD는 이러한 비싼 최적화들이 적용된 결과물을 넘겨받기 때문에 그만큼 부담을 덜게 된다.

UMD에서 수행되는 여러가지 저수준 최적화도 있는데, 이는 다음과 같다.
  • 레지스터 할당
  • loop unrolling (주 : 루프를 반복된 코드로 확장하는 것) 등
간단히 얘기하자면, 중간코드(Intermediate Representation, IR) 로 변환된 것을 뭔가 조금 더 컴파일해주는 정도라고 할 수 있는데, 이는 (저수준)컴파일러가 뭔가 좋은 결과를 내기 위해 엄청난 일을 할 필요가 없을만큼 D3D 바이트코드가 셰이더 하드웨어에 충분히 가깝기 때문이다. 하지만 그럼에도 불구하고 D3D가 알 필요도 없고 신경쓰지도 않는 하드웨어와 관련된 세부사항들이 있기에, 이 단계는 결코 사소한 단계라고 볼 수 없다. 이러한 세부사항들은 다음과 같다.
  • 하드웨어 자원 제한
  • 스케줄링 제약

그리고 유명한 게임들에 대해서는 엔비댜와 암드의 프로그래머들이 그 셰이더를 디벼보고 직접 새로 최적화해 작성한 셰이더가 대신 돌아가게 하는 경우가 있는데, 그러한 해당 게임의 검출과 셰이더 교체 또한 이 UMD 단에서 이뤄진다.

그리고 또한 몇몇 API state 들은 최종적으로 셰이더에 컴파일되어 적용된다. 예를 들어 드물게 사용되는 기능인 텍스처 경계 상태(texture border state)는 샘플러 코드에 구현되는 것이 아니라 셰이더에 추가 코드로서 에뮬레이션된다.(혹은 쌩까고 아예 동작하지 않도록 무시되기도 함). 이 말인 즉슨 한 셰이더가 대해 각각의 state 조합들이 적용된 여러 버전의 셰이더들이 존재할 수 있다는 얘기임.

덧붙여 말하자면 이게 바로 어떠한 셰이더나 리소스를 새로이 사용할 때에 지연이 발생하는 이유임ㅇㅇ. 왜냐면 수많은 생성/컴파일 작업이 드라이버에 의해 가능한한 늦춰지거든. 언제까지? 걔가 필요할 때 까지. 그래픽스 프로그래머들은 이 이야기를 다른 방향으로 접해서 알고 있는데, 그건 바로 뭔가가 그저 메모리만 공간만 확보받은 채로 있는 게 아니라 확실하게 생성되었음을 보장받으려면 그 뭔가를 사용하는 더미 드로우콜을 날려주는 것으로 "워밍업"을 하라는 것이다. 지저분하고 번거롭지만 이건 글쓴이가 1999년에 3D 하드웨어를 처음 시작했을 때 부터 주욱 그래왔으니 어쩌겠음? 그냥 익숙해지셈.

UMD는 또한 D3D9 "legacy" 셰이더들과 fixed pipeline 들도 처리한다. 물론 그들은 새로운 버전의 셰이더로 변환되어 사용된다. (그리고 그렇게 처리한지도 좀 되었음)

텍스처 생성 명령 처리 같은 메모리 관리도 UMD 에서 처리된다. 실질적으로는 KMD(kernel mode driver) 로 부터 얻은 커다란 메모리 영역을 다시 작은 영역으로 나눠 할당하는 것이다. 비디오 메모리의 어느 영역을 UMD가 볼 수 있는지, 그리고 시스템 메모리의 어느 영역에 GPU 가 접근할 수 있는지 매핑하는 것은 KMD의 권한이므로 UMD가 처리할 수 없다.

UMD는 텍스처의 swizzling 처리(주:원문 글쓴이의 다른 글로 링크함 shader 에서 쓰는 swizzling 과는 다른 개념이며 텍스처 처리시 캐히 히트율을 높이기 위해 연속된 공간에 배치되는 픽셀의 순서를 바꾸어 저장한 텍스처 데이터를 어드레싱하기 위해 주소(오프셋)값의 비트 순서를 바꾸는 것을 말함..인데 언젠가는 원문도 각잡고 번역하든 요약정리를 하든 할 것임. 일단 그 전에는 원문 링크로...아. 이런 건 각주 시스템이 있으면 각주로 다는 게 좋을 거 같네ㅋ)를 하고 시스템 메모리와 (매핑된) 비디오 메모리간의 전송을 스케줄링하는 것과 같은 일을 처리하기도 한다. 물론 swizzling 의 경우 GPU 가 3D 파이프라인이 아닌 2D blitting 유닛을 통해 처리할 수도 있는데, 이렇게 할 수 없는 경우 UMD 에 의해서 CPU단에서 처리되는 것이지만. 가장 중요한 사실은 UMD 가 커맨드 버퍼(혹은 DMA 버퍼. 원문 글쓴이께서 커맨드버퍼와 DMA버퍼라는 단어를 섞어서 쓰시겠답니다ㅋ)의 내용을 작성한다는 것인데, 이 커맨드 버퍼는 KMD가 할당한 것이다. API런타임을 통해 요청한 상태 전환, 그리기 작업 등은 UMD에 의해 하드웨어가 알아먹는 커맨드로 변환된다. 텍스처나 셰이더를 비디오 메모리로 업로드하는 작업 등과 같은 많은 작업들이 이러한 변환에 의해 알아서 호출되게 된다.

일반적으로 드라이버는 UMD로 최대한 많은 처리 요청을 밀어넣으려 할 것인데, 왜냐면 UMD는 유저모드 코드이기 때문이다. 그래서 UMD에 의해 처리되는 작업들은 비싼 커널모드 전환이 필요치 않고, 자유롭게 메모리를 할당할 수 있으며, 작업들을 멀티스레드로 돌릴 수 있고, 등등등.. 왜냐면 UMD는 보통의 DLL로 만들어져 있으니까. (물론 애플리케이션에 의해 바로 로딩되는 것은 아니고 API에 의해 로딩되는 것이긴 하지만.) 이는 드라이버 개발도중 만약 UMD가 크래시하면 이를 사용하는 앱 역시 크래시하지만 시스템은 크래시하지 않고, 시스템이 돌아가는 동안 간단히 교체할 수 있으며(왜냐면 그냥 DLL이니까!), 일반적인 디버거로 디버깅할 수 있는 등의 장점이 있다. 편리하고 효율적이져.


"유저 모드 드라이버" 가 아니라 "유저모드 드라이버들"

UMD는 이미 말했던바와 같이 단지 DLL이다. D3D의 가호를 받고 KMD에 곧바로 연결되어있기는 해도, 여전히 보통의 DLL이다. 그래서 호출한 프로세스와 같은 주소공간에서 돌아간다.

하지만 요즘의 OS들은 멀티태스킹이다. 그리고 사실 그런지 이미 좀 되었지.

계속해서 얘기하고 있는 이 "GPU"라는 물건은 공유 자원이다. 메인 디스플레이를 구동하는 GPU는 단 하나만 존재한다(SLI 나 크로스파이어 구성을 했더라도 말이지). (주 : 이러한 하나 뿐인 GPU를 멀티태스킹 OS에서 공유하여 쓸 수 있게 만드는 것은) 자동으로 되지 않는다. 옛날에는 3D를 사용하는 애플리케이션 하나가 활성화되면 다른 모든 애플리케이션들은 접근을 하지 못하게 하는 식으로 처리했다. 하지만 윈도우 시스템을 렌더링하는 데 GPU를 쓴다면 그렇게 할 수는 없다. 이것이 바로 GPU로의 접근을 중재하여 시분할로 처리해주는 같은 작업들을 행하는 구성요소가 필요한 이유이다.


스케줄러

스케줄러는 시스템 구성요소이다. 하지만 여기서 얘기하는 것은 CPU스케줄러나 IO스케줄러에 대해서가 아니라 그래픽스 스케줄러임을 명심하자. 스케줄러가 행하는 것은 니가 생각하는 바로 그것이다. 3D 파이프라인을 사용하려는 여러 앱들의 접근을 시분할로 중재해주는 것이져. 컨텍스트 스위칭이 발생하면, 최소한 GPU 의 상태 변환은 일어나고(이를 처리하기 위해 커맨드 버퍼에 추가 커맨드가 들어감), 비디오 메모리로 리소스가 올라가거나 내려오는 스와핑이 일어날 수도 있다. 그리고 당연히 어떤 한 시점에서 3D 파이프라인에 커맨드를 주는 것은 단 하나의 프로세스이다.

콘솔 프로그래머들은 PC 3D API 의 상당히 고수준이며 직접 제어할 수 없고 알아서 처리되는(hand-off) 특성에 의해 야기되는 성능 비용에 대해 종종 불만을 표시한다. 하지만 PC 의 3D API 나 드라이버가  해결해야 하는 문제들은 콘솔 게임에 비해 더 복잡하다. 예를 들자면 PC 에서는 모든 스테이트의 변화를 관리하여야 하는데, 이는 다른 앱이 언제 상태를 바꿔버릴지 알 수 없기 때문이다. 또한 PC 에서는 망가진 애플리케이션을 우회하고 이로 인해 발생하는 보이지 않는 성능 문제를 해결해야 한다. 이는 드라이버 개발자 자신을 포함해서 누구에게도 즐겁지 않은 상당히 성가신 관행이지만, 현실 세계에서는 비즈니스 관점이라는 게 있으니까여. 사람들은 잘 돌아가고 있는 것은 계속해서 (아주 부드럽게) 잘 돌아가기를 바라져 넵. 애플리케이션에다 대고 "그건 틀렸어 임마!"라고 외치고는 부루퉁한 표정으로 엄청 느린 길을 택해서 간다면 친구를 아무도 사귈 수 없을겁니다.


커널 모드 드라이버(kernel-mode drver, KMD)

KMD 는 하드웨어를 다루는 부분이다. UMD는 동시에 여러 인스턴스가 실행중일 수 있지만, KMD는 항상 오직 하나만 실행될 수 있다. 그리고 만약 그게 크래쉬된다면 잦되는거져ㅋ. 예전에는 블루스크린이었지만, 요즘 윈도는 크래쉬된 드라이버를 죽였다 다시 살리는 게 가능하다. 물론 이건 최소한 커널 메모리 손상이 아닌 단순한 크래시일때만 가능하다. 그렇지 않다면 끝장인거임ㅋ

커널 모드 드라이버는 모든 '단 하나 뿐인 것들'을 다룬다. 여러 앱이 GPU 메모리를 두고 다투지만, 실제 GPU 메모리는 하나밖에 없다. 누군가는 명령을 내리고 실제로 메모리를 할당하며 이를 매핑해야한다. 이와 유사하게 누군가는 시스템이 시작될 때 GPU를 초기화하거나, 디스플레이 모드를 설정하거나 디스플레이로부터 모드 정보를 얻어온다던가 하드웨어 마우스 커서를 관리하는 일, 하드웨어 타이머를 설정하여 GPU가 일정 시간 응답이 없으면 리셋을 하는 일, 인터럽트에 응답하는 일 등을 해야 한다. 이것이 바로 KMD가 하는 일이다.

또한 컨텐츠 보호 / DRM과 관련하여 비디오 플레이어와 GPU간에 보호/DRM 된 패스를 설정하여, 지저분한 유저 모드 코드들 통해 디코딩된 소중한 픽셀들이 드러나는 일이 없도록 해서, 이를 디스크에 덤프한다던가 하는 등의 끔찍한 일이 일어나지 않도록 해야 한다. KMD 는 이에 일부 관여한다.

가장 중요한 사실은 KMD가 관리하는 커맨드 버퍼가 하드웨어가 실제로 처리하는 바로 그 커맨드 버퍼라는 것이다. UMD 가 생성하는 커맨드 버퍼는 실제 커맨드 버퍼가 아니다. GPU 가 접근 가능한 랜덤한 메모리 조각에 불과하다. 실제로는 다음과 같은 일이 일어난다. UMD 가 커맨드 버퍼 작업을 마치면 이를 스케줄러에 접수시킨다. 스케줄러는 프로세스가 깨어날 때 까지 기다렸다가 UMD 커맨드버퍼를 KMD 커맨드버퍼에 넘겨준다. 그러면 KMD는 (UMD) 커맨드 버퍼를 호출하는 명령을 주 커맨드 버퍼에 써넣는데, 만약 GPU 가 메인 메모리를 읽을 수 없을 경우 (UMD) 커맨드 버퍼의 내용을 비디오 메모리로 먼저 DMA 한 다음에 이 작업을 수행한다. 메인 커맨드 버퍼는 보통 (아주 작은) 링 버퍼로, 거기에 실제 쓰여지는 것은 시스템/초기화 명령과 실제 3D 커맨드 버퍼를 호출하는 명령밖에 없다.

어쨌든 이 커맨드 버퍼는 메모리상에 존재하는 버퍼일 뿐이다. 그래픽 카드에게 이 버퍼의 위치는 보통 읽기 포인터와 쓰기 포인터를 통해 알려진다. 읽기 포인터는 주 커맨드 버퍼의 어디를 GPU 처리하고 있는지를 표시하며, 쓰기 포인터는 KMD 가 명령을 써 놓은 위치가 어디까지인지를(좀 더 정확히 얘기하자면 KMD 가 GPU 에게 어디까지 명령을 작성했는지 알려줬는지를) 나타낸다. 이들은 하드웨어 레지스터로, 메모리에 매핑되어 있으며 KMD 가 이를 주기적으로 업데이트한다(보통 KMD 가 새로운 작업 덩어리를 접수시킬 때 업데이트한다).


버스(bus)

KMD 가 이렇게 써 놨다고 해서 이게 바로 그래픽 카드로 가는 것은 아니다(그래픽 카드가 CPU 다이에 통합되어 있지 않은 다음에야). 먼저 버스를 통과해야 한다. 요즈음은 PCI 익스프레이스이다. DMA 전송 등도 같은 경로를 거친다. 이는 오래 걸리는 것은 아니지만, 어쨌든 이 여행의 또다른 단계이다. 그리고 마침내...


커맨드 프로세서(command processor)!

커맨드 프로세서는 GPU 의 프론트엔드이다. KMD 가 작성한 커맨드들을 실제로 읽어들이는 것이 이곳이다. 이미 글이 충분히 길어졌기에, 나머지는 다음 포스팅에서 계속.


OpenGL 의 몇가지 차이점들

OpenGL 은 이 글에서 설명한 내용과 매우 비슷하다. 하지만 API 와 UMD 가 딱 잘라 나뉘어지지 않는다. 그리고 D3D 와는 달리, (GLSL)셰이더 컴파일은 API가 아니라 드라이버단에서 전부 처리된다. 따라서 3D 하드웨어 제조사들마다 하나씩 GLSL 프론트엔드를 구현하게 되는데, 이는 기본적으로는 같은 스펙을 구현한 것이지만 각자의 버그나 특이성을 갖게 되는 부작용이 발생하게 된다. 노잼이져. 그리고 이는 드라이버가 셰이더를 만날 때 마다 최적화 - 그 '비싼' 최적화들을 포함해서 - 를 처리해 한다는 것을 의미한다. D3D 의 바이트코드는 이런 문제에 대한 매우 깔끔한 해결책이다. 컴파일러가 하나밖에 없으니까. (따라서 제조사간에 미묘하게 달라서 호환되지 않는 방언들이 존재하는 문제도 발생하지 않게 된다.) 그리고 이는 니가 보통 수행하는 비싼 데이터 흐름 분석을 가능하게 한다. <- 원문은 'and it allows for some costlier data-flow analysis than you would normally do' 인데 costlier data-flow analysis 가 뭘 의미하는지 감이 안 잡히네? 뭔가 특정한 걸 의미하는 거 같은데...


생략되고 간소화된 부분들

이 글은 개요이기에 엄청나게 많은 미묘한 사항들을 적당히 얼버무리고 넘어갔다. 예를 들자면 스케줄러의 경우 딱 하나만 있는 것이 아니고 여러 가지 구현이 존재할 수 있다(드라이버가 이를 선택할 수 있음). CPU 와 GPU 간의 동기화에 대해서도 아직까지 전혀 설명하지 않았으며, 기타 등등 여러가지가 있다. 그리고 내가 무언가 중요한 것을 완전히 까먹고 있을 수도 있는데, 이러한 것은 고칠 수 있도록 알려 달라. 하지만 지금은 안녕. 담에보아~
2016/09/13 00:18 2016/09/13 00:18
2013년 즈음이었는지 그보다 더 전이었는지 정확히 기억이 나지 않는데(원래 써놨는데 망할 텍큐 버그 때문에 한 문단을 날리면서...) 어쨌든 A trip through the Graphics Pipeline 2011 를 발견하고 읽으려고 시도했으나 몇 번 시도 끝에 엄청 더디게 진도는 나갔으나 곧 포기.. 또 읽다가 포기. 그리고 몇 년을 묵혀놨었는데...

마침 DirectX9 이후로 팽개쳐둔 그래픽스 API들에 대한 공부도 좀 해둬야겠단 생각이 든 차에 이 글이 다시 생각나서, 이번엔 좀 더 active 하게 읽기 위해 요약을 하면서 기록으로 남기고자 이 글을 시작.

...했으나 쓰다보니 요약이 아니라 전문번역에 가까워졌네-_-

어쨌거나 일단 이 글은 index 파트에 대한 요약임.

(이하는 A trip through the Graphics Pipeline 2011: Index 를 요약한 내용임다)

GPU에 실제로 구현된 D3D/OpenGL 그래픽스 파이프라인에 관한 글임.

GPU에 관해 넓은 범위에서의 개요를 다루거나 각 컴포넌트를 자세히 다루는 논문은 많지만 그 중간을 다루는 것이 없는 게 계속 신경쓰였다. (주:그래서 필자가 저 블로그 포스팅 시리즈들을 썼겠져.)

적어도 D3D9 이상, OpenGL 2.0 이상의 3D API 에 대한 지식을 가지고 있는 프로그래머를 대상으로 함.

초보용 아님.

레지스터라던가, FIFO 라던가, 캐시, 파이프라인이 뭐고 어떻게 동작하지와 같은 하드웨어에 대한 최소한의 지식도 필요함ㅇㅇ.

병렬 프로그래밍 메커니즘에 대한 최소한의 지식. 왜냐면 GPU 자체가 엄청 병렬 처리를 하는 컴퓨터니까.

(원래 여기에 한 문단이 더 있는데, 처음엔 요약으로 시작해서 빼버려도 될 거 같아서 생략했다가 Part 1을 진행하면서 번역 비슷하게 되어버려 좀 애매해졌는데.. 추후에 봐서 번역해 추가하던지 하겠슴ㅋ)

Part 2 - GPU memory architecture and the Command Processor.
Part 3 - 3D pipeline overview, vertex processing.
Part 4 - Texture samplers.
Part 5 - Primitive Assembly, Clip/Cull, Projection, and Viewport transform.
Part 6 - (Triangle) rasterization and setup.
Part 7 - Z/Stencil processing, 3 different ways.
Part 8 - Pixel processing – “fork phase”.
Part 9 - Pixel processing – “join phase”.
Part 10 - Geometry Shaders.
Part 11 - Stream-Out.
Part 12 - Tessellation.
Part 13 - Compute Shaders
2016/08/25 16:09 2016/08/25 16:09
작년 이맘때쯤엔 한창 신규 프로젝트의 세일즈 빌드 작업을 하고 있었다. 작업을 하다 보니 셰이더 코드를 좀 건드릴 일이 있었는데 그때 눈에 밟히는 부분이 있긴 했지만 딱히 파고 들 시간이 없어서 냅두다가 최근에서야 약간 여유가 생겨 다시 파보게 됨.

사실 딱히 복잡하다거나 뭔가 엄청난 신기술이거나 그런 거 아니고, 어찌보면 기본의 기본에 해당하는 부분인데 매번 '일단 돌아가니까 그걸로 ㅇㅋ'로 대충 뭉개고 넘어가던 부분인지라.. 이왕 삽질하면서 알게 된 거 정리라도 해 둬야 나중에 까먹고 또 삽질할때 시간이라도 좀 아끼지 하는 마음으로 써봄.

어쨌거나 문제의 발단이 된 코드는 이건데,

http://wiki.unity3d.com/index.php/MatCap

얼핏 봤을 땐 시선 방향을 범프를 적용해 어찌저찌 틀어서 이걸 이용해 matcap 텍스처의 픽셀을 가져다 쓰는..뭐 그런 코드인 거는 내가 잘 알겠다.

근데 구체적으로 뜯어보려니까 음? 어라라?? 스러운 부분이 한두군데가 아닌 것이었다. 정확히 얘기하자면 한두군데가 이상한 정도가 아니라 아니라 전체를 대충은 알아도 각 코드 한 줄 한 줄이 구체적으로 어떤 의미인지 정확히는 모르고 있었던 것이지라.

여튼 코드에서 뜯어봐야 할 핵심인 부분을 보자면,

vert 함수(버텍스 셰이더 코드)의
TANGENT_SPACE_ROTATION;
o.TtoV0 = mul(rotation, UNITY_MATRIX_IT_MV[0].xyz);
o.TtoV1 = mul(rotation, UNITY_MATRIX_IT_MV[1].xyz);


그리고 frag 함수(픽셀 셰이더 코드)의
vn.x = dot(i.TtoV0, normal);
vn.y = dot(i.TtoV1, normal);

float4 matcapLookup = tex2D(_MatCap, vn*0.5 + 0.5);


각각 세 줄 씩.

일단 버텍스 셰이더 코드를 보자니.. 첨부터 못 보던 매크로 같은 물건이 보인다. TANGENT_SPACE_ROTATION

이건 뭔가 유니티 Shaderlab 에서 제공하는 변수 같은건가 하고 찾아보니까.. 딱히 매뉴얼에 정의가 된 건 안보임(내가 못찾았을수도),

그래서 혹시나 싶어 빌트인 셰이더 소스코드의 UnityCG.cginc 를 까보니 맞네... 거기 정의되어있는 매크로였음.

// Declares 3x3 matrix 'rotation', filled with tangent space basis
#define TANGENT_SPACE_ROTATION \
  float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \
  float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )

보니까 버텍스 셰이더 입력으로 노멀과 탄젠트가 들어오는데, 이걸로 바이노멀을 계산해서 탄젠트 스페이스상의 값으로 회전시켜주는(..것으로 보이는) rotation 이라는 변수명의 3x3 행렬을 만들어주는 매크로.

그리고 바로 다음줄과 그 다음줄에 등장하는 UNITY_MATRIX_IT_MV 라는 역시나 매크로스러워 보이는 이름의 무언가가 있는데.. 처음엔 이름으로 대충 때려잡기로 IT 는 inverse transform 일테고, MV 는 model 이랑 view 행렬을 얘기하는 거겠지..라고 생각.
근데 아니었음. 제대로 찾아보면 나오는데.. "Inverse transpose of model * view matrix" 임ㅋ. 그리고 매크로가 아니라 저거 자체가 변수이기도 하고.

어쨌든 여기까지 해서 모르던 변수나 매크로는 대충 확인이 되었으니.. 이걸 가지고 전체적인 흐름을 다시 한 번 보자면 버텍스 셰이더에서 탄젠트 스페이스로의 변환 행렬과 모델뷰 행렬을 곱해서 픽셀 셰이더로 넘기고, 이걸 픽셀 셰이더에서 노멀맵에서 언팩한 노멀 벡터랑 쿵짝해서 만든 실수값 두 개를 uv좌표로 써서 matcap 텍스처의 픽셀을 가져다 씀..인 것. 뭐 얼핏 봤을때의 그것이랑 같은 얘기. 근데 당장 제일 처음에 버텍스 셰이더에서 탄젠트 스페이스로 보내주는 벡터부터 뭐인지부터 모르겠다는거.

그래서 우선, 어떤 걸 모르고/헷갈리고 있었는지부터 적어보고자 함.

제일 처음이 바로 위에서 얘기한 탄젠트 스페이스로 보내지는..벡터가 뭔지 모르겠는데.. 이건 사실 코드상에서 뭘 곱해주는지가 명시적으로 드러나지 않는 상황. 그래서 나머지 코드들을 해석하고 나서 역으로 끼워맞춰야 하더라. 다 풀고 보니 결과적으로 이렇게 되긴 했는데, 모를 땐 처음부터 막혀서 막 헤매게 된 원인이기도 함.

그 다음이 TANGENT_SPACE_ROTATION 인데..
얘는 오브젝트의 로컬 스페이스에서 한 점의 탄젠트, 바이노멀, 노멀 벡터가 어느 방향인지를 나타내는 값을 가져다 이를 차례대로 행렬의 1, 2, 3행으로 쌓아서 행렬을 만든것.

근데 일단 이게 내가 생각한 것(탄젠트 스페이스로의 변환)이 맞는지 확인해보려고 하니 행렬과 벡터를 곱하는 순서가 어떻게 되는지부터 알아야겠단 생각이 들었음.

지금 회사에서 유니티 쓰기 전까지 일하면서는 주욱 D3D로만 작업을 하다보니 알고있고 익숙한 게 대부분 D3D기준인데, 예를 들자면 D3D 는 행렬에 벡터를 곱할 때 벡터가 먼저 오고 행렬이 뒤에 오는 형태라는 거. 이건 4x4 행렬로 트랜스폼을 한 방에 처리할 때 translation 에 해당하는 성분이 4행의 1, 2, 3열에 오게 된다.

근데 유니티의 셰이더코드는 Cg 기반이고, Cg는 예전에 얼핏 듣기로 행렬과 벡터의 곱셈이 행렬이 먼저 오고 벡터가 나중에 오는 형태라고 알고 있었는데.. 찾아보니 맞게 기억하고 있는거였음. 확인은 위키책의 해당 항목(이쪽이 좀 더 자세한데..위키책 사이트가 종종 터지는듯?) 이나 좀 덜 자세하지만 Cg 의 mul 함수 설명에서.

따라서 유니티 셰이더 코드의 4x4 행렬에서는 translation 에 해당하는 성분은 4열의 1, 2, 3행에 위치함. 사실 이 글에서 translation 위치 자체가 중요한 건 아닌데.. 내가 4x4 트랜스폼 행렬이 어느 방향을 향하는지 기억하는 기준이 translation 행 혹은 열의 위치인지라.

어쨌거나 다시 TANGENT_SPACE_ROTATION 로 돌아가서 보면, 매크로 코드에 의해 이 3x3 행렬의 1행은 탄젠트 벡터, 2행은 바이노멀 벡터, 3행이 노멀 벡터인데,

회전변환행렬을 구성할 때 회전된 결과인 세 축을 알면 그 축을 각각 회전행렬의 1, 2, 3행으로 쓸 수도 있다.. 정도의 아련한(...)기억이 있는지라, 일단 이게 맞는지 확인부터 해야겠다는 생각이 들었음. 근데 검색을 해도 찾기가 어려워서.. 그럼 이 행렬에다 x, y, z축의 방향벡터를 의미하는 (1, 0, 0), (0, 1, 0), (0, 0, 1) 을 곱해준 결과가 뭔지를 확인해보자로 넘어가게 됨.

즉, x축의 방향벡터인 (1, 0, 0)을 TANGENT_SPACE_ROTATION 의 결과물인 rotation 변수에 곱해주면 탄젠트 방향벡터가 나오겠지... 했는데 아니네? 탄젠트 벡터는 rotation 행렬의 1행인데 rotation 에 (1, 0, 0)을 곱해주면 1열 성분의 벡터가 나오자네. 즉 rotation 행렬을 transpose 한 것에 (1, 0, 0)을 곱해줘야 탄젠트벡터가 나오게 되니, 여기서 나는 rotation 변수를 계산할 때 단순히 1, 2, 3행에 탄젠트, 바이노멀, 노멀을 넣고 땡이 아니라 이걸 한 번 transpose 해 줘야 하는 코드에선 이걸 빼먹고 잘못 쓰고 있는 게 아닌가.. 라고 엉뚱한 생각을 했다-_-. 혹은 코드가 잘못되지 않았다면rotation 변수의 의미가 탄젠트 스페이스로의 회전이 아니라 탄젠트 스페이스에서 오브젝트의 로컬 스페이스로의 회전이 아닌가..라고 의심을 하게 됨(역시나 틀린 생각임). 이렇게 첫 부분도 그 다음도 계속 헷갈리고 있으니 나머지 부분도 엄청 삽질하게 될 것은 불을 보듯 뻔하고..

일단 그렇게 아리송다리송@_@한 상태에서.. 나머지 부분은 어케 문맥이랑 끼워맞춰보면 될라나 하는 심정으로 그 다음줄로 넘어가니...
어?
행렬이랑 행렬을 곱하는 게 나올 줄 알았는데 그게 아니라 행렬이랑 행렬의 1, 2 행 성분을 각각 곱해서 그걸 픽셀 셰이더로 넘겨주네?

이걸 보고서 노멀 언패킹때처럼 서로 직교하는 세 축의 경우 두 축만 값을 남기고 나머지 하나는 계산으로 만들어 쓰는건가? 아니면 행렬끼리 계산한 결과와 마지막에 곱해주는 벡터에 x, y 성분만 사용하는건가 하는 등의 생각이 들었는데.. 일단은 여기도 어떤 것인지 확실하게 정하지 못한 상태로 또 넘어감(...제대로 하는 게 뭐냐ㅋ)


뭔지는 몰라도 어쨌든 행렬과 행렬을 곱하는거 같긴 한데.. 그렇다면 앞의 행렬(rotation 변수)과 뒤의 행렬(UNITY_MATRIX_IT_MV 변수) 의 을 곱해줘야 할텐데.. 왜 곱해주는 게 열이 아니라 행(UNITY_MATRIX_IT_MV[0] 과 UNITY_MATRIX_IT_MV[1])일까-_-. 여기서 혹시나, 정말 혹시나 행렬에 대해 [] 연산자를 쓸 경우 행이 아니라 열에 접근하는건가 하고 다시 아까 Cg 레퍼런스를 확인해봤는데 아님. [] 연산자로 접근했을 때 나오는 벡터는 행 하나가 맞음ㅋ.

그렇다면 저 식의 의미는 UNITY_MATRIX_IT_MV 가 아니라 UNITY_MATRIX_IT_MV 의 transpose 행렬을 곱해주는 것임. 그런데 여기서 UNITY_MATRIX_IT_MV를 직교행렬이라고 착각하고, 이것의 transpose 이니 역행렬이구나..라고 또 착각. 저 식이 UNITY_MATRIX_IT_MV 의 역행렬을 곱해주는 게 되려면 UNITY_MATRIX_IT_MV 가 직교행렬이어야 하는데, 정답편에서 얘기하지만 얘는 직교행렬이 아니라 역행렬이 아니라 그냥 transpose 인거. 왜 이런 식을 쓰는지 정확한 의미는 요 아래에서. 어쨌든 식에서 저 부분을 view space 서 model space 로 변환시켜주는 행렬, 다른 말로는 카메라 공간에서 모델의 로컬 공간으로 변환시켜주는 행렬이 된다고 생각했는데... 이 부분은 대략적으로는 맞긴 하지만 역시나 100% 정확한 표현은 아니고. 뭐 어쨌든 이런 혼돈의 카오스 상태에서 픽셀 셰이더 코드로 넘어갔음.

픽셀 셰이더 코드 조각에서는 첫 번째 줄과 두 번째 줄에서 내적을 해 주는 것을.. 버텍스 셰이더에서 픽셀 셰이더까지 이어지는 변환의 일부(마지막) 즉, 버텍스 셰이더에서 넘겨받은 행렬에 해당하는 것에 픽셀 셰이더가 노멀맵에서 뽑아낸 normal 벡터를 곱해서 변환을 하는 것으로 다시 한 번 거하게 착각을 해 버림. 근데 그거 아니구여... 어쨌거나 착각을 하고 있으니 결과가 어떤 의미인지도 감이 안 잡히고 어디서부터 손을 대야할지도 모르겠다 싶은 상황인거. 일단 이렇게 한 이틀 헤매다(...) 3일째에 갑자기 실마리가 떠올랐다. 이제 정답편으로 넘어가보겠심다.

정답편

일단 첫빠따는 TANGENT_SPACE_ROTATION.
얘의 진정한 의미는 모델의 로컬 좌표상의 벡터값을 곱하면 탄젠트 스페이스상의 값이 튀어나오는 행렬, 즉, 모델 공간에서 탄젠트 공간으로 변환시켜주는 행렬임. 변수 이름과 설명에 rotation 이라고 되어 있어서 '회전을 시킨다' 라고 생각을 하게 되어 내 경우엔 더 헷갈렸는데, 사실 이건 방향을 나타내는 벡터가 있는데 이것의 값을 구하기 위해 사용하는 기준 좌표축을 바꿔줌에 따라 값이 변하는 것을 계산하기 위한 행렬임. 자세한 내용은 선형대수에서 기저변환(change of basis) 부분을 참조..해야 할 거임. 근데 지금 참고하는 책이랑 자료에서 학술적으루다가 정확하고 이쁘게 설명해 놓은 게 없음. 그래서 그냥 무식하게 확인한 방법을 설명해보겠심다.

TANGENT_SPACE_ROTATION 의 결과로 나온 rotation 변수에 x축의 방향벡터 (1, 0, 0)을 곱해주는 상황을 다시 한 번 생각해보면, 그 결과 벡터는... 음. 당장은 뭔지 모르겠네-_-? 탄젠트, 바이노멀, 노멀(TBN) 벡터의 각 x 성분으로 만든 벡터인데.. 이것만 봐선 뭔 의민지...

뭔가 알 수 있는게 나오려면 결과가 TBN 벡터 셋 중에 하나여야 하는데, 그러려면 rotation 이 아니라 rotation 의 transpose 에다가 (1, 0, 0) 을 곱해야겠져? 그래야 rotation 의 1행 성분인 탄젠트 벡터가 나오니께.

근데 이 탄젠트 벡터는 어느 공간상의 값이다? 넹. 모델 공간상의 값임다. 왜냐면 이걸 계산하는 데 쓰인 버텍스의 값이 모델 공간상의 값이니까여. 물론 이건 버텍스 셰이더에 넘어오기 이전에 이미 모델 데이터 레벨에서 정해지는 값이라.. 그러니까 애초에 모델링 툴에서 익스포트하거나 익스포트한 걸 엔진으로 임포트 할 때 정해지는(계산되는) 값이긴 한데.. 음.. 더 자세히 설명하면 여기서 너무 늘어질 수 있으니 일단 여기서는 그냥 모델공간상의 값입니다.. 정도로 넘어가는 걸로ㅋ

어쨌거나 x, y, z좌표축에 해당하는 (1, 0, 0), (0, 1, 0), (0, 0, 1)을 rotation 의 transpose 에다 각각 곱해주면 그 결과값은 모델공간에서의 TBN 벡터에 해당하는 방향값을 뱉아냄미당.

그런데 rotation 은 직교행렬(=행렬을 구성하는 서로 다른 행벡터끼리 내적하면 0. 왜냐면 탄젠트, 바이노멀, 노멀은 서로가 서로에게 수직인 벡터)이니까, rotation 의 transpose 는 rotation 의 역행렬이져.

바로 위 두 문장을 조합하면, rotation 의 역행렬은 좌표축 벡터를 모델공간상의 TBN(탄젠트, 바이노멀, 노멀) 벡터로 회전을 시켜주는 행렬이네여. 그럼 rotaion 은 뭐다? 모델공간상의 TBN 벡터를 좌표축 벡터가 되도록 회전시켜주겠져. 즉, 모델 공간에서 TBN벡터에 해당하는 벡터들을 rotation 행렬에 의해 변환시키면 결과값은 좌표축 벡터가 된다는 말임다. 탄젠트 바이노멀 노멀이 좌표축이 되는 공간이 뭐다? 뭐긴뭐야 탄젠트 공간이지.

즉, rotation 을 모델공간상의 값에다 곱해주면 결과값은 탄젠트 공간상의 값이 된다는 거. (워낙 말장난같긴 한데.. 좌표축 변환이라는 게 기준이 되는 축을 바꾸고 그 결과에 의해 값의 의미가 달라지는거라.. 아.. 나도 설명 좀 더 잘하고 싶다....)

한참을 장황하게 설명했는데, 어쨌든 결론은 TANGENT_SPACE_ROTATION 은 모델 공간의 값을 탄젠트 공간의 값으로 변화시켜주는 행렬을 계산해 이걸 rotation 이라는 이름의 변수에 넣어주는 매크로 라는 게 확실해졌슴다. 그리고 아직 끝나지 않았져. 결론은 결론인데 코드 첫 줄에 대한 결론이잖아. 나머지도 해석해야지ㅋ.

그 다음으로 UNITY_MATRIX_IT_MV 의 진정한 의미를 확인해보도록 하겠슴다.
유니티 문서에 따르면 UNITY_MATRIX_MV 는 Inverse transpose of model * view matrix 임다(아까도 한 얘기지만). 근데 말로 써놔서 좀 헷갈리긴 하는데, 저 말의 정확한 의미는 model * view matrix 를 inverse 한 것을 다시 transpose 한 거란 얘기져. transpose 한 것을 inverse 한 게 아님. (중요)

아우.. 뭘 inverse 랑 transpose 랑 구분을 하고 앉았냐. 둘이 같은거니 어느 게 먼저 와도 상관없이 그냥 상쇄되는거 아녀? 라고 생각하신다면.. 아녀. 그거 틀렸슴미다. model * view matrix 가 일단 직교행렬이 아닐 수 있으니까요. 언제 직교행렬이 아니냐? 며는.. 일단 translation 성분 값이 (0, 0, 0) 이 아닐 때도 그렇고, 스케일이 non-uniform 해도 그렇고.

근데 어차피 이 코드에선 4x4 행렬 중에 좌상단 3x3 값만 써서 노멀 회전시키는거니까, 그렇게되면 translation 성분은 사라지니 노멀이 좀 틀어져도 트랜스폼된 결과값만 노멀라이즈 해서 적당히 쓰면 상관없지 않음? 이라는 시도를 해 볼 수도 있겠지만.. 뭐 다른 수가 없다면 그렇게라도 할텐데 수학적으로 정확한 결과를 낼 수 있는 방법이 엄연히 있져. 바로 그게 inverse transpose of MV 라는 해괴한(...) 물건임. 구체적인 사항은 이 게시물에서 언급하는 이 게시물에서, 혹은 Eric Lengyel 저 3D 게임 프로그래밍&컴퓨터 그래픽을 위한 수학3.5장 법선벡터의 변환에서 확인 가능함다. (2판 한글판 기준으로 119쪽 부근). 그냥 간단히는 non-uniform scale 이 적용된 트랜스폼일때에도 노멀을 제대로 변환하는 용도로 사용..정도로만 정리해둡시다. 어쨌든 UNITY_MATRIX_IT_MV 에 의한 변환은 모델 공간의 노멀을 뷰 공간(카메라 공간)의 노멀로 바꾸는 용도임.

그럼 UNITY_MATRIX_IT_MV 의 1열과 2열이 아니라 1행과 2행을 가져다 rotation 에 곱하는 의미는 무엇인가... 우선 버텍스 셰이더 코드 두 번째 줄 우변의 코드를 식으로 옮겨 써 보겠슴다.

rotation x UNITY_MATRIX_IT_MV[0]

그리고 UNITY_MATRIX_IT_MV 를 네 문단 앞에서 얘기한대로  transpose(inverse(model x view)) 로 풀어서 치환해보져.

  rotation x UNITY_MATRIX_IT_MV[0]

= rotation x (transpose(inverse(model x view)))[0]

연산자를 보통은 이렇게 쓰지 않지만, 편의상 행렬의 열벡터를 가져오는 연산을 {} 이라 정의하면, 3x3행렬 M 에 대해서 다음과 같이 쓸 수 있겠져.

M[0] = (transpose(M)){0}

(transpose(M))[0] = M{0}

마지막 하늘색 박스에서 transpose(inverse(model x view)) 부분의 계산 결과를 하나의 행렬로 보고 바로 위 변환(두번째 줄)을 적용하면 다음과 같아집니다.

  rotation x (transpose(inverse(model x view)))[0]

= rotation x (inverse(model x view)){0}

그런데 행렬의 첫번째 열을 구하는 연산 M{0} 은 행렬 M 에 벡터 (1, 0, 0) 을 곱해주는 것과 같습니다. 따라서 다음과 같이 쓸 수 있게 되져.

  rotation x (inverse(model x view)){0}

= rotation x inverse(model x view)) x (1, 0, 0)

즉, 버텍스 셰이더 두 번째 줄 코드의 내용은 rotation x inverse(model x view) x (1, 0, 0) 이라는 식과 같다고 할 수 있겠네여. 이 식을 풀어 써 보면 벡터 (1, 0, 0) 를 model x view 행렬의 역행렬에 곱하고, 이걸 다시 rotation 에 곱해주는 식 되겠슴다.

model x view 행렬은 모델 로컬 공간에서 뷰(카메라) 공간으로 변환시켜주는 행렬인데.. 이것의 역행렬이니까 카메라 공간에서 모델 공간으로 변환시켜주는 행렬이져? 따라서 여기에 곱해주는 벡터 (1, 0, 0) 은 카메라 공간상의 벡터로 봐야 함다. 카메라 공간상에서 벡터 (1, 0, 0) 는? 화면상의 가로축이네여. 즉 식의 뒷부분은 화면상의 가로축을 모델 공간의 값으로 변환합니다.

rotation 은 저으기 위에서 얘기한대로 모델 공간에서 탄젠트 공간으로 변환시켜주는 행렬입니다. 바로 위에서 화면상의 가로축을 모델 공간상의 값으로 변환했는데, 여기에 rotation 을 곱해주면 다시 이 모델 공간상의 값이 탄젠트 공간의 값으로 바뀌는거죠. 결과적으로 이 식은 화면상의 가로축을 탄젠트 공간의 값으로 변환하게 됩니다.

같은 방식으로, 버텍스 셰이더의 세 번째 줄은 화면상의 세로축을 탄젠트 공간의 값으로 변환하게 되죠. 이렇게 구한 두 벡터는 버텍스 셰이더 출력값에 저장되어 픽셀 셰이더로 넘겨지게 됩니다.

픽셀 셰이더 코드의 해석은.. 일단 픽셀 셰이더가 넘겨받은 두 값 i.TtoV0 와 i.TtoV1 부터 봅시다. 얘네들은 바로 위 버텍스 셰이더에서 넘겨준 카메라공간의 가로와 세로축 방향을 나타내는 벡터입니다. 카메라 공간의 가로 세로 이러니 장황한데 좀 더 간단히 얘기하자면 화면의 가로와 세로 방향을 나타내는 벡터라는거져. 근데 이 값은 어느 공간상의 값이다? 넵. 탄젠트 공간.

이 탄젠트 공간상의 값과 내적을 해 주는 normal 벡터는 어디서 온 값이냐.. 이건 가져온 코드에서는 안 보이지만 노멀맵에서 언팩한 값이져(frag 함수 몸체 첫 줄의 UnpackNormal 호출 결과로 받는 값). 얘는 애초에 탄젠트 공간의 값임다. 왜 탄젠트 공간이냐면 탄젠트 공간이니까 탄젠트 공간이라 한 것인데 왜냐고 물으시면.. 이 아니라 이 부분에 대해서 풀어 쓰려니 분량이 너무 늘어날 거 같아 일단 UnpackNormal 의 결과값은 탄젠트 공간이다..라고만 해 두고 넘어가져. 탄젠트 공간상의 무슨 값? 노멀맵에서 가져온거니 노멀값이겠져? 정확히는 픽셀 셰이더에 의해서 그려지고 있는 해당 점의 노멀 방향값이져.

결국 이건 화면의 가로와 세로 방향 벡터랑 해당 점의 노멀 방향 벡터를 내적하는 게 되는데, 둘 다 탄젠트 공간의 값입니다. 따라서 애초에 착각했던것처럼 뭔가 변환은 아니게 되는거져. (굳이 결과에 끼워 맞추자면 텍스처의 uv 공간으로 변환하는거라고 할 수 있을지도 모르겠지만 이러면 더 헷갈리니까 이건 제쳐두고.)

두 벡터의 노멀을 어떤 의미로 봐야하는지를 알기 위해 두 벡터의 기하학적 정의를 한 번 살펴보도록 하져.


'두 벡터의 내적'의 기하학적 정의는 두 벡터의 각각의 길이 값을 곱한 것에 두 벡터 사이각의 cosine 값을 곱한 값..입니다. 이 정의에 따라 픽셀 셰이더의 식을 해석해 보면, 버텍스 셰이더 코드의 첫째 줄은 화면의 가로방향 벡터와 노멀맵에서 가져온 노멀 벡터를 내적한 값을 계산하는 거져.

그런데 화면의 가로 방향 벡터는 식 사이에 숨어 있는 값 (1, 0, 0) 로 등장한 이후 픽셀셰이더로 넘어오기 전까지 계속해서 회전 변환만 적용된 상태입니다. 따라서 최초에 1이었던 길이는 픽셀 셰이더에서 내적 할 때 까지도 여전히 유지되고 있져. 방향 벡터는 보통 길이 1을 유지하도록 하는 경우가 많은데, 특히나 내적을 계산할 때 두 벡터 중 하나가 가 길이 1인 방향 벡터일 경우, 위에서 언급한 벡터 내적의 기하학적 정의에 대입해보면 나머지 한 벡터의 길이값에 두 벡터 사이각의 cosine 값을 곱한 것과 같은 값이 됩니다. 길이가 1인 벡터를 어떤 기준이 되는 벡터라고 보면, 이 기준 벡터와 다른 벡터를 내적한 값은 기준 벡터를 축으로 두었을 때 다른 벡터의 해당 축에 대한 좌표 성분값을 얻는 연산으로 볼 수 있는 것이져. 이런 관점에서 보면, 픽셀 셰이더의 첫째 줄과 둘째 줄 코드는 노멀 벡터를 화면상의 가로축과 세로축을 기준 축으로 하는 좌표계의 좌표값으로 변환하는 코드라 할 수 있슴다. 같은 말이긴 한데 좀 현실세계스러운 비유를 들어 설명하자면, 화면의 가로축과 세로축을 자(ruler)라고 생각하고, 노멀 벡터의 끝이 가르키는 점의 위치가 가로축과 세로축 방향으로 얼마나 되는지를 재어서 그 값을 vn.x 와 vn.y 에 저장하는 것이 픽셀셰이더 첫번째와 두번째 줄이 하는 일인 것이져.

그런데 노멀 벡터 역시 방향벡터로, 길이는 1을 유지하고 있습니다. 혹은 연산 도중 1이 아닌 값이 되게 되더라도 마지막이나 사용할 때에 1이 되도록 조정해주져. 방향 벡터이니 시작점을 원점에 두고, 길이가 1인 모든 벡터의 끝점에 점을 찍어보면... 넵. 아시다시피 이 점들의 집합은 원점을 중심으로 하는 반지름 1인 구면을 이루게 되져. 노멀 벡터들은 이 구면상의 값이라면 어떤 값이든 될 수 있는데, 픽셀 셰이더의 첫째 줄과 둘째 줄에 의한 연산을 하고 나온 결과값 (vn.x, vn.y)은 구면상의 각 점의 가로 세로 좌표값이니 이 값들을 2D 평면에 찍어보면 원점을 중심으로 반지름 1인 원을 가득 채우게 되져.

픽셀셰이더 두번째 줄 까지 계산한 결과를 좌표로 왜 2D평면상에 다시 점을 찍어보느냐는.. 이런 가정을 해 보져. 점을 찍을 평면에 그림이 그려져있다고 말이져. 그리고 해당 좌표에 점을 찍는 대신 그 점을 찍을 위치의 색깔을 가져다 다른 데 쓴다면? 넵. 그게 바로 픽셀셰이더 세번째 줄에서 tex2D 함수가 하는 일임돠.

그런데 tex2D 함수를 자세히 보면 파라미터로 넘겨주는 vx 를 바로 사용하지 않고 가공을 해 주고 있져? 이건 vx 의 x 와 y 값이 -1 에서 1 사이의 값이기 때문이져. 원점을 중심으로 반지름 1 짜리 원 내부의 점들이니까여. -1 에서 1 사이의 값에 0.5 를 곱해주면 -0.5 에서 0.5 사이의 값이 되고, 여기에 다시 0.5 를 더해주면 0 에서 1 사이의 값이 되져. 이렇게 하는 이유는 바로 텍스처 좌표가 0 에서 1 사이의 값을 가져야 하기 때문인 것이라는 거.

이렇게 구한 값을 픽셀셰이더의 출력값으로 사용하게 됩니다. 픽셀 셰이더라는 게 한 점을 그릴 때 그 점의 색깔을 무슨 값으로 하느냐를 계산하는 물건이져. 이 MatCap 셰이더의 경우는 해당 점의 노멀 방향벡터를 화면의 가로세로축이 만드는 평면상으로 옮겨서, 그 평면상에 붙어(...)있는 _MatCap 텍스처의 그 위치의 색깔로 화면에 그려주는 셰이더가 되는 겁니다. 그림을 그리면 좀 이해가 편할텐데 귀찮아서(...) 말로 설명을 하자면 노멀이 화면의 가로세로축과 완전히 수직한 경우, 즉 화면을 뚫고 나오거나 뚫고 들어가거나 하는 경우는 화면의 가로세로축에 의해 만들어지는 평면의 중점, 다시 말해 _MatCap 텍스처의 한 가운데 점의 색깔로 그려집니다. 그걸 기준으로 좌우로 기울어지든 상하로 기울어지든, 중점으로부터 좌/우측 혹은 아래/위쪽 위치에서 픽셀을 얻어 그 색깔로 그려지게 되는 것이져. 그리고 이쯤 되면 눈치 챌 수 있겠지만, _MatCap 텍스처에서 사용되지 않는 부분이 있는데 그건 바로 텍스처의 사각형 영역에서 이 영영을 꽉 채우는 원의 바깥부분, 즉 네 귀퉁이 인근 부분의 픽셀들이져. 얘네들을 가리키려면 해당 노멀의 길이가 1이 넘어야 하는데, 정상적인 노멀은 항상 길이가 1이므로 애는 절대 참조될 수 없기 때문에 영향을 미치지 않는 텍셀들이 되는거져.

어째 막판으로 갈수록 설명은 상세해지면서 내용은 더 날림이 된 거 같긴 한데, 일단 한 번 끊어야겠기에 여기까지 쓰고 일단락해야겠슴다. 나중에 좀 더 알아먹기 쉽게 고칠 수 있는 방법이 생각나면 그때 고치져 뭐. 끗.
2016/07/31 23:59 2016/07/31 23:59
페북에 썼던 글인데.. 찾아가기도 번거롭고 어쨌든 진행을 해야 할 거 같아서 진행상황 확인 및 기록용으로 블로그 포스팅으로 옮김.

일단 입식 환경에서 스탠딩데스크면의 높이, 그러니까 서 있는 바닥면부터 실제 키보드랑 모니터가 올라가는 테이블면까지의 높이차는 1100mm (110cm)

현재 사용하고 있는 좌탁의 높이가 395mm 이므로, 1100mm 에서 이걸 빼면 추가로 구입해야 하는 좌탁의 높이는 705mm 인 것이 된다.

좌탁 구매는 지금 회사에서 쓰고 있는 스탠딩데스크용도 좌탁 주문한 이곳에서. 원하는 높이를 지정해서 주문할 수 있음. 상판도 사무용가구급까지는 아니더라도(..가격 때문에 재질에 한계가 있으니) 사무실서 1년 써 봤는데 무난하게 만족스러웠음.

현재 쓰는 좌탁 가로세로 크기가 1500mm x 600mm 짜리인데, 좌탁 왼쪽편에 있는 저장용 본체...를 오른쪽으로 꺼내서 배치할까 함. 저장용이니까 이건 한 번 세팅해두면 하드디스크 끼고빼고 할 일은 거의 없고, 그나마 저장용으로 쓰는 2테라 하드 8개 중에 여섯개는 전면에서 착탈이 가능하도록 구성을 했지만.. 쓰다보니 결국 하드 뗏다 붙였다 할 일이 몇달에 한 번은 발생하더라고. 그래서 조금이라도 뚜껑 따기 쉽도록 오른쪽 공간으로 이동.

문제는 이렇게 해서 좌탁을 왼쪽으로 완전히 밀어 벽에 붙인 다음 위에다 높이 705mm 짜리 좌탁을 올리면.. 왼쪽 벽면의 베이스랑 기타 걸어둔거랑 충돌이 있다. 그래서 705mm 좌탁은 아래 깐 거랑 같은 가로 길이 1500mm 짜리는 안되겠고, 300mm 줄여서 1200mm x 600mm 짜리 상판으로 주문해야할듯.

이렇게 되면 705mm 짜리 상단 좌탁 아래쪽에 현재 쓰는 39인치 모니터가 들어가야 되는데... 아슬아슬하겠지만 일단 놓을수는 있을듯. 대신 지금 모니터 상단에 올려둔 센터스피커랑 키넥트 카메라는 벽쪽 선반에 올리던가 해야할듯. 그리고 이런 세팅이라 좌탁 주문은 5발다리가 아닌 4발다리로 해야겠는데.. 23인치 모니터 세 대 정도 올리는 정도는 버텨주겠지.

사실 스탠딩 환경 구성 진행이 갑작스럽게 본격화 된 건 중고 23인치 모니터를 오늘 싸게 구하면서 촉발된 감이 없지 않은데.. 뭐 어쨌든 모니터를 구해야겠다고 생각은 했고 21인치 네 개냐 40인치급 UHD 하나냐를 고민했었는데 지금 쓰는 39인치 크기를 생각하면 확실히 이건 데스크탑용으론 좀 불편한 감이 있어서 40인치 UHD는 드랍. 그리고 21인치 신품을 싼 걸로 맞추면 40인치급 UHD모니터 하나랑 비슷해지는데.. 굳이 그럴 필요가 있나 싶기도 하고 그렇게 하면 스탠드도 따로 맞추고 이래저래 귀찮음. 왜 4개였냐면.. 이건 기원이 좀 오래되었는데 작업할 땐 일반적인 멀티모니터로 쓰다가 Eyefinity 로 유사 UHD 환경을 세팅해 써볼까 하던 때가 있었던 적에 나온 생각이라.. 근데 이미 UHD 모니터가 일반화된 마당에 굳이 Eyefinity 같은 걸 쓰는 건 삽질이고...그리고 일해라AMD 드라이버 부서. 니네 Eyefinity 세팅 여전히 까다롭고 버그 많다. 여튼 그래서 쿼드러플모니터 세팅도 드랍.

유니티든 언리얼이든 요즘 엔진 에디터 돌릴 때 싱글모니터는 확실히 불편하고, 듀얼은 되야 쓸만하다 싶음. 트리플은 두 개에다 에디터 띄우고 나머지 하나는 도큐먼트나 웹페이지 띄워서 보려고. 그래서 트리플 모니터. 트리플만 되어도 별도의 브래킷 스탠드를 구해서 써야하지 않을까 하는 생각을 좀 해봤는데... 가격이 셉니다. UHD모니터 한 대 가격에서 조금 빠지는 정도. 그래서 드랍.

어차피 완전히 자유롭게 움직이며 쓸 건 아니고.. 옆으로 밀면서 서로 간격 조정 정도만 하면 되니까 알루미늄 프로파일 같은 걸로 슬라이딩 가능한 세팅으로 직접 구성해보면 어떨까 하는 생각도 들었음. 근데 이건 지금 구성하려는 좌탁 상판이 필요한 강도가 나올지, 그리고 내가 직접 설계조립해서 과연 실용성있는 결과물이 나올지도 의심스러운지라.. 걍 수동으로 밀어가면서 쓸라고. 아직 로또 안됐음.

현재 좌탁 위에 올라가있는 1) 39인치 모니터 2)23인치 구형 LCD모니터 3)24인치 UHD모니터 4)파일서버/이런저런스위치등등 중에서 아쉬운대로 데스크탑 멀티모니터로 쓰던 23인치 구형 LCD는 위에 올릴 좌탁 다리 공간 문제 등 때문에라도 빼버려야 할 거 같은데... 이게 컴포넌트랑 RGB 입력이 있어서 PS2 랑 구엑박을 물려놓는 용도로 쓴단 말이지. 이걸 책상서 빼버리는 건 거의 확정인데.. 이렇게 되면 PS2랑 구엑박도 같이 빠져버려야 되는 상황이라. 어디 PS2랑 구엑박 따로 놓을 공간이 있음 모르겠는데 지금은 자는 영역도 침범받을 정도로 공간이 부족하니-_- 얘네를 어떻게 할 지가 좀 고민이긴 함. 당장 켤 일은 별로 없는데 아주 가끔 땡길때 켜곤 해서...

스탠딩 환경 구성과는 별개이긴 하지만.. 데스크탑들 저장장치 정리 작업도 같은 시기에 진행해야하는지라 메인데탑용 SATA3 확장 카드, 5.25인치 베이에 장착하는 2.5인치 멀티랙도 주문해야함. 메인데탑 PCIE 슬롯 확인하고 내부 베이 및 SATA포트 사용 상황 확인해서 정리해야된다. 옛날엔 이런 거 신나서 했는데 이젠 귀찮다. 아아...
2016/05/10 03:05 2016/05/10 03:05
잘 돌아가고 있는 gitlab 을 뭐하러 업데이트하냐(...) 는 게으름으로 쓰고 있던 8.0.3 이 8.7.0 이 될 때 까지 업데이트를 안 하고 있었는데, 간만에 생각난 김에 업데이트나 하자 싶어 Software Update 를 켜 보니 이런저런 시스템 업데이트등등과 함께 gitlab 업데이트가 보이길래 일단 gitlab 을 제외한 나머지것들만 업데이트.

그리고 다 설치가 된 다음 gitlab 만 업데이트를 하는데, 아니나다를까 하다말고 중간에 뭔가 에러가 뜨면서 제대로 설치가 안된다. 정확히 어떤 메시지가 뜨는 지는 못 적어놨는데.. 대충 '뭔가의 이유로 실패했는데 뭔 이윤지는 안알랴줌' 느낌의 에러 메시지.

근데 이게 몇 번을 설치해도 뭐 때문이다 보여지는 게 없고.. 로그가 어디 남나 하고 뒤져보니 처음 검색해서 나온 파일 위치에 파일은 있는데 사이즈가 0 바이트. 이 쯤 되니 더 찾기도 귀찮고 '어쩌라고? 운영체제부터 밀고 새로 설치하리?' 싶은 생각이 들기 시작.

그러다가 시간이 늦어 일단 자고, 다음날인 오늘 다시 몇 번 다시 업데이트를 시도하다보니.. 업데이트 윈도 아래쪽에 detail 이라는 클릭 가능한 글씨가 보임. 눌러보니까 설치시 콘솔 출력을 주욱 보여주는데.. 뭐가 직직 찍히다가 희떡 닫히길래 이걸 또 두어번. 결국 화면캡쳐의 도움을 받아 /var/opt/gitlab/backups 디렉토리를 만들다 실패하는 상황이라는 걸 확인함.

단순한 윤모씨는 어..저 경로가 없는거니까 그냥 만들어주면 되지 않을까? 해서 짧은 유닉스 커맨드 지식으로 해당 위치에 backups 디렉토리를 만들었으나.. 여전히 비슷한 메시지가 나오는 상황. 이번엔 backups 아래에 db 라는 디렉토리가 없대네? 다시 만들어줌. 그리고 다시 업데이트.. 그랬더니 이번엔 디렉토리가 있어서 문제라고 나옴-_- 아 이건 아닌가부다.. 그러고선 검색하면 나오겠지 하고 찾아봄.

역시나 같은 건으로 질문한 게 있고, 그 질문에 '나도 그런데염' 하고 질문했던 사람이 스스로 해결방법을 찾아 올려놓은 걸 발견했다.

https://gitlab.com/gitlab-org/omnibus-gitlab/issues/644

해답은 아까 내가 시도했던 거랑 비슷한데.. 다른 건 permission 이랑 소유권  설정을 해 줘야 하는거였음.

mkdir /var/opt/gitlab/backups
chown git /var/opt/gitlab/backups
chmod 700 /var/opt/gitlab/backups

누구한테? git 이라는 계정한테...

어쨌든 저렇게 하니까 잘 됨.

이렇게 굳이 글로 남긴 이유는.. 블로그가 방치 상태가 아니라는 의미로다가(...)

그리고 키핑용 글은 페북에다가 남겨놓을라니까 나중에 검색이 힘들어서, 귀찮아도 따로 글로 옮겨둬야겠단 생각이 들었고,

무엇보다 이걸 나름 키핑해야되는 내용이라고 생각한 건 몇 달이나 몇 년(...) 후에 이런 일을 또 겪으면 분명히 또 삽질을 하고 있을 거 같아서.

나는 리눅스를 높은 빈도로 다루는 환경에 있지도 않고, 예전에 빡시게 유닉스계열 운영체제를 써 본 적도 없다. 필요할때마다 잠깐 쓰고 내던져뒀다가 또 쓸 일 있으면 잠깐 만지고.. 근데 이 텀이 길어서 한 번 만진 후 다음 번에 만질 때 쯤이면 그 전에 하던 거 다 까먹은 상태거든. 그래서 부지런히 적어두기라도 해야 나중에 까먹은 거 다시 떠올리기라도 쉬울 거 같다는 생각이 들었다는 거.

(한 줄 요약 - 늙었음)
2016/04/28 01:13 2016/04/28 01:13

블로그 또(...) 재개

분류없음 2013/12/22 05:14 posted by 윤뿌쮸
스팸봇들때문에 텍큐는 안되겠다 싶어서 내렸다가, 워드프레스로 옮겨가려고 보니 마땅찮은 점이 몇가지..해서 블로그 닫은채로 처박아둔게 한 반 년 된 거 같은데,

애플타르트타탕 레시피 계량 계산 다시 해 놓은거 꺼내 보려고

..만은 아니고 마침 텍큐 업데이트도 되고 있는 거 같고 해서 당분간 다시 써보자 싶기도 하고 레시피도 봐야겠고(...) 해서 다시 열었슴다.


근데 이거 뭐 인코딩 설정이나 이런저런 설정을 좀 만져줘야하는듯. 카테고리 클릭해서 들어가는거 안되고(이건 전엔 되었던거 같은데-_-), 검색은 예전부터 안됐는데 여전히 안되고.

게다가 예전에 쓰던 스킨이 뭐였는지 이름도 기억이 안난다.


..였다가 방금 어찌저찌 해서 스킨은 찾아서 적용.

아 오늘은 어제보다 더 늦게 자게 생겼네. 머리아파서 일찍 자려고 했더니만.. 이래갖고 내일 타르트 재료 사러 나갔다 올 수 있으려나.
2013/12/22 05:14 2013/12/22 05:14

오늘의 죽을 뻔 한 이야기

분류없음 2013/03/23 20:52 posted by 윤뿌쮸
(이제는 연간도 아니고.. 격년간 업데이트 블로그가 되어버린 이 곳을 위해 잠시 묵념(...))


진공청소기가 얼마 전 부터 전원을 넣어도 켜졌다말았다 하길래, 전원스위치가 왠지 헐거운 느낌이 들어 접점부분이 눌리던가 해서 접촉불량 나는게 아닌가 하는 생각이 들었다. 접점부분을 지지하는 탄력이 있는 부분이 부러지던가 해서 제대로 접촉이 안 되는게 아닌가 하는거지. 실제로 옆으로 돌려서 들면 되는데 바로 세우면 안되고 하는 증상은 이러한 추측을 더욱 확고히 하게 하였으며, 기왕 이렇게 된 거 청기와로 돌진한다!..가 아니라 까서 확인을 해 보는 것이 엔지니어로서 훌륭한 자세가 아니겠는가 라고 스스로를 대견해 하며 분해하려고 보니,

본체 나사 다섯 개 중에 두 개의 머리가.. 십자도 일자도 아닌 - - 자 형(1자 홈 가운데 부분이 메꿔진 형태)인 것이라.


일자 드라이버 가운데를 갈아내어 써야 하나 어쩌나 고민만 하며 귀차니즘과 게으르니즘의 합공에 말려 몇 주(몇 달 인거 같기도 하다..) 미루다가, 마침 용산에 들른 김에 공구점으로 뛰어들어 "일자드라이버 비슷한데 가운데 홈 파진 드라이버 없나여?" / "ㅇㅇ 이거임 크기 고르셈." / "오오!" 그래서 제일 큰 사이즈는 혹시나 너무 클까 싶어서 한 사이즈 아래 걸 사 갖고 왔는데.. 집에 와서 청소기 나사홈에 넣고 돌리자마자 뚝-_-하고 팁이 부러지더라. 이게 한 삼 주 전? 이야기.

그리고 그 이후로 다시 이런저런 사정(...)에 밀려 용산행을 미루다가.. 시간이 흘러흘러 계절이 바뀌며 날씨가 풀리고 꽃이 피고 나비가 날아다니는.. 건 내 알 바 아닌데 나방파리도 돌아다니기 시작하네?!?

이 집의 유일한..아니 싸니까 여러가지 단점이 있는데 그래도 이 보증금에 방 두개짜릴 어디가서 구하겠냐며..그런 관점에서는 장점이 여러 단점을 상쇄하고도 남지만, 그래도 용서할 수 없는 단점은 바로 겨울이 아닌 계절에는 나방파리가 시도 때도 없이 등장한다는 거.

나방파리를 사전에 박멸할 길을 몇 가지 시도해 보았으나 대부분 방법이 보이는 수를 상당히 줄일 수 있을 뿐 완전히 없앨 수는 없더라. 나는 돈이 없으니 현실과 빠른 타협을 진행하였고, 그 결과 보이는 나방파리를 보이는 족족 진공청소기로 빨아들여 잡는 방법을 선택하게 됨.

사정이 그러한데 겨울을 벗어나는 이 마당에 진공청소기가 이 모냥이니.. 귀차니즘은 용서해도 나방파리 돌아다니는 꼴은 용서할 수 없다며 갑자기 빡이 돌아 용산으로 튀어간다. '저녁시간이긴 하지만 어째저째 아직 문 열었을지도 몰라..' 하지만 도착해보니 문 닫음. 그것도 저번에 그 드라이버 산 데만 닫고 옆에 다른 공구점 두어군데는 아직 안 닫았던데, 다른 곳에는 한 군데도 그 드라이버가 없더라. 울면서(그날 바람이 심해 맞바람 맞으며 걸으니까 눈물 좀 났음) 500m 정도를 걸어 언젠가 가 보려던 용문시장 동네 빵집엘 들러 빵을 사고, 오는 길에 을밀대에 들러 냉면으로 저녁을 해결. 이게 지난 주 얘기.

오늘에 이르러서야 드디어 용산의 그 공구점에서 가장 큰 사이즈의 - - 자 형 드라이버를 사서는(혹시 안 맞거나 지난 번 처럼 또 부러지면 일자드라이버 가운데 갈아내서 쓰려고 만능톱도 샀다), 집에 와 청소기를 분해해본다.

먼지보소.

분해하면서 구석구석에 보이는 먼지들 좀 떨어내 주고,

대망의 전원스위치 부분. 근데 접점부분이 바로 노출된 형태가 아니라 박스형 케이싱에 닫혀 있는 구조네.

하지만 다행스럽게도 납땜한걸 녹이지 않고 위쪽 케이싱 걸쇠만 잘 벗기면 열 수 있는지라 조심스레 열어보니, 이놈도 안쪽에도 먼지가 가득. '하하. 요놈의 먼지들 덕분에 접촉불량이 일어났나보군?'

먼지도 떨고, 접점 부품도 빼내서 빡빡 닦아주고(먼지랑 윤활제같은거 짬뽕인지 하여튼 시커먼게 막 닦여나옴. 이 때만 해도 이거다 싶었다), 혹시나 해서 접접부분들 접촉잘되게 밀어주는 스프링도 꺼내서 살짝 늘려서 탄성 좀 높여주고, 그러다가 그 쪼그만 스프링을 세 번인가 놓쳐서 찾는데 안 보여 쌍욕도 하고. 여튼 이래저래 열심히 떨어내고 닦고 한 다음 재조립. 전원을 넣어보니 잘 된다 싶..다가 다시 금방 같은 증상 발생. 이 산이 아닌가베?

아 뭐지.. 다시 분해해봐야하나.. 걍 싸구려 청소기 수명 다 된거라 치고 다이슨(...)청소기나 알아봐야되나.. 돈 없는데 추석상여금 나오면 그걸로나 어떻게 해 볼수 있을까 그 전엔 무리.. 등등등 오만 생각을 다 하다가 증상은 접촉불량인거 같은데 스위치가 아닌가보다! 좀 더 본격적으로 분해해보자! 는 생각에 이른다. 좀 전에는 완전히 다 열어본 건 아니고 전원스위치부분만 꺼내서 까 본 거였거든.

자 2차 분해 들어갑니다. 다시 전원 플러그 뽑고(이게 중요함.. 오늘 죽을 뻔한게 이거때문이거든) 나사 풀고 본체 커버도 완전히 들어내고, 모터뭉치부분도 들어내서 다시 분해하고, 모터부분도 분해..하다보니 아까는 못 열어본 부분 구석구석에 먼지가 천지삐까리네. 게다가 배기통로부분에 먼지필터같은 게 있는데, 여기에도 먼지가.. 그 뭐냐, 먼지가 너무 꽉차서 고밀도로 압축되어 마치 부직포같은 질감이 되어버린-_- 그런 부분이 등장. '얘가 바람 나가는 걸 막아서 접촉불량이 났나? 그럴 리가..' 라면서도 어쨌든 연 김에 다 청소하자 싶어서 탈탈탈 털고 다 털고 완전히 털고, 전임 GK 못 털어서 아쉬웠던 부분을 여기서 다 해소하려는 듯 털어버린 다음, 다시 조립을 하는데..

하는데, 어라?

전원스위치쪽으로 이어진 선 중에 하나가 심하게 눌려 있는 걸 발견함. 잡았다 요놈! 네놈이 원흉이구나. 선이 심하게 눌려 끊어질락말락하면 충분히 접촉불량을 일으킬 수 있으니까 증상이랑 일치도 하고. 그래서 이땐 얜줄 알았어요.(아니었음)

어쨌거나 발견한 놈은 잡아야지. 처치에 들어간다. 인두랑 납을 꺼내고 작업용 접이상도 꺼내서 세팅하고 인두를 전원에 물린다. 전선 눌린 부분을 절단하고 피복을 양쪽 5mm정도 길이만큼 벗겨낸 다음 납땜 들어가기 전에 잊지 말고 수축튜브를 잘라 전선 한 쪽에 끼워 저으기 안쪽으로 밀어넣어둔다. 이제 본격 납질! 납을 조금 녹인 다음 이걸 전선 끝에 발라주고, 반대쪽 전선 끝에도 납을 발라주고. 작업은 혼자 하고 나는 손이 세 개가 아니니까 전선 한 쪽은 작업대 바닥에 테이프로 붙여 준 다음 나머지 전선 한 쪽과 인두를 각각의 손에 할당하고, 전선의 납이 묻은 부분끼리 맞댄 다음 인두로 마무리. 음 좋다. 납땜질은(전문가 수준엔 한참 못미치지만) 적어도 이제 써먹을만큼은 늘었구나 싶어 혼자서 잠시 만족 5초 정도. 한 쪽으로 밀어 둔 수축튜브를 납땜한 부분을 덮도록 옮긴 다음 라이터..를 켜니까 아놔 이놈 가스 다 나갔네. 어떻게 해야되나 고민하다가 얼마전에 찾은 케익 초 불 붙일때 쓰는 대형 성냥 모양 라이터를 구석에서 찾아 낸 걸 기억하고 갖고와보니.. 이놈도 가스가 다 됐는지 불이 비실비실..켜서 잠깐 지지고 꺼지면 다시 켜고 반복을 수십차례 해서 어찌어찌 수축튜브질도 마무리. 아 힘들었다! 하지만 재조립이 남았지..

어쨌거나 이번엔 성공적일거야! 를 되뇌며 조립완료! 전원인가! 위이잉 소리가 훨씬 커진 느낌(막힌 먼지 다 떨어냈으니까) 근데 또!!! 증상이 다시 발생!

다이슨을 한 5분 정도 그리워하는 시간을 다시 가진 다음,

불현듯 떠오르는 생각인 즉슨,

청소기를 들어올리다가 전원코드를 밟아먹거나 하면서 '아 이거 전원선 끊어지면 어떡하지?' 라고 몇 번이나 걱정했었다..라는 거-_-

떨리는 마음으로 청소기를 다시 들고, 전원선이 본체랑 만나는 부분 근처를 붙잡은 다음 전원을 넣고, 전원선을 움직여보니 전원이 들어왔다가, 다시 전원선을 반대방향으로 움직이니 꺼졌다가, 또다시 아까 방향으으로 움지이니 켜졌다가 아 ㅅㅂ 유레카!!!

흥분된 마음으로 다시 세 번째 본체 분해를 시작합니다. 본체 열고 모터뭉치 덜어내고, 전원기판쪽을 만지작거리며 '이거 단선된 부분은 전원선이 본체로 들어오는 부분 근처인거 같은데, 어느부분을 얼만큼 잘라야 잘 잘랐다고 소문이날까' 쯤을 생각하는 찰나,

찌리릿~

우허허허헉?!??! 인지 뭔지 하여튼 괴성을 내뱉으며 손에 들고 있던 걸 던져버리고 보니,

청소기가 순간적으로 돌아가다가 멈추고 있고,

전원플러그는 전원콘센트에 연결되어 있고


전원플러그는 전원콘센트에 연결되어 있고


전원플러그는 전원콘센트에 연결되어 있고

ㅇㅇ 맞음. 이번엔 정말 확실하다 싶어서 흥분한 나머지 분해하기 전에 전원플러그 뽑는 걸 깜빡했네요??

스스로에게 쌍욕을 하며 전원플러그부터 냅다 뽑고,

작업재개-_-

아까와 같은 요령으로 이번엔 전원선 두 가닥을 전부 자른 다음 납땜하고 수축튜브질. 납땜질은 반복학습주기에 딱 들어맞을 때에 다시 실습-_-에 들어가서 그런지 좀 전 보다 훨씬 이쁘게 완성됨. 여기서 다시 뿌듯뿌듯 해 하면서, 얼마전에 냉장고 도어 경첩부분 깨진거 케이블타이로 수리해낸걸 떠올리며 주말에 재활용센터 수리 알바나 뛸까 이런 뻘생각도 잠시 하고(...). 근데 수축튜브 지지려고 보니 라이터 가스는 다 떨어지고, 밖에 나가기는 귀찮고..아 어째야되나 하다가 결국 제과점 케익칼이랑 같이 처박아둔 생일초 불 붙이는 길쭉한 성냥(아까 얘기한 성냥형 라이터 말고 진짜 성냥)이 생각나서 찾아보니 있다. 이거 써야지. 잘 되네-_- 근데 두 번 하고싶지는 않다. 라이터 하나 사 두고.. 성냥형 라이터 저거는 가스 채워야 할텐데 가스 채우려면 어떻게 해야하지? 이런 생각을 하면서..

세 번째 조립에 들어감. 이쯤되니 조립도 무슨 게임 스피드런 뛰는 거 같더라-_- 금새 조립해낸 다음 전원 연결 먼지통부착 등등등 전원인가! 잘 된다! 요리조리 돌려봐도 꺼지지 않고 잘 된다 끼얏호!

.
.
.
참 기쁘기도 하겠다 드라이버며 사느라 들인 돈에 용산 몇 번 왔다갔다하느라 들어간 시간에 한 번 죽을뻔도 했는데.

근데 다 해놓고 뿌듯해 하는 걸 보면 이런 짓 하는 게 천성이다 싶기는 함ㅋ
2013/03/23 20:52 2013/03/23 20:52

(지인간의 레시피 공유를 목적으로 작성 된 글이며, 저작권에 문제가 될 경우 저작권자께서 요청 주시면 삭제하도록 하겠습니다.)

파비앙 베르또 레시피. (월간 파티시에 2011년 11월호)

(먼저 잡지에 나온 원래 레시피를 그대로 옮기고.. 아래에 제가 만들 때 바꿔서 쓴 부분들을 따로 기재하겠음)

A 캐러멜라이즈드 애플

설탕 1000g
물 200g
버터 250g
사과 4개

1. 냄비에 설탕, 물을 넣고 골든 브라운 색이 날 때 까지 끓인다.
2. 1 에 버터를 넣고 섞은 다음 지름 17cm 의 (원형) 타르트 틀에 붓는다.
3. 껍질을 벗긴 사과를 4조각으로 자른 다음 2에 넣는다.
4. 130도 오븐에 넣고 1시간30분간 익힌다.


B 스위트 반죽

버터 425g
슈거파우더 235g
아몬드파우더 75g
박력분 615g
달걀 3개
소금 2g

1. 차가운 상태의 버터에 함께 체 친 슈거파우더, 아몬드파우더, 박력분을 넣고 섞는다.
2. 1 에 풀어놓은 달걀과 소금을 넣고 반죽한다.
3. 냉장고에서 2시간 동안 휴지한다.
4. 3 을 두께 0.2cm 로 밀어 편다.


마무리

생크림 적당량
식용 금박 적당량

1. 오븐에서 꺼낸 A(캐러멜라이즈드 애플)에 B(스위트 반죽)를 씌운다.
2. 170도 오븐에서 B(스위트 반죽)가 익을 때까지 굽는다.
3. 구워져 나온 2 를 뒤집은 다음 휘핑한 생크림과 식용 금박으로 장식한다.

그럼 이런 모냥새.

사용자 삽입 이미지




여기서부터는 지난번에 제가 만들 때 바꾼 부분들.

원래 레시피의 17cm 원형틀 대신 24cm x 9cm 의 사각틀로 변경.

캐러멜라이즈드 애플은 절반 분량(사과 2개분)으로 만들었는데, 두 번 실패하고 보니 캐러멜 양이 너무 많은 거 같아서 캐러멜은 반으로 줄인데서 다시 25% 더 줄였음. 그래도 좀 많다 싶던데, 이건 너무 줄이게 되면 캐러멜에 사과가 푹 잠기지 않아 제대로 익지가 않을 거 같아서 너무 줄일수는 없더라능. 이건 한번 더 만들어보고 다시 레시피 정리하려다가 일단 올려놓고 나중에 다시 만들게 되면 수정하든지 할게요.

캐러멜도 먼저 부은 다음 사과를 올리는 게 아니라 사과를 틀에 넣고 위에다 부어서 익혔구요.
 
사과를 4 조각으로 자르라는 부분도 16조각으로 변경(세로로 8쪽을 낸 다음 그걸 반으로 잘라 씀)

스위트 반죽은 원래 레시피 분량의 1/3로 반죽한걸 절반으로 나눠 쓰니까 사각틀에 딱 맞았어요. 애초에 1/6 로 반죽하지 않은 건 달걀 갯수 맞추느라. 근데 지금 생각해보니 반죽 두께가 0.2cm 보다는 확실히 더 두꺼웠던듯함.

제일 심각한 레시피 변경은 이 부분인데... 레시피대로 다 만들고 나서 꺼낸 다음 식혀서 뒤집으면... 캐러멜+사과즙이 줄줄 흘러내려요.ㅠㅠ 원래 분량대로 만들어야만 사과에 캐러멜이 이쁘게 코팅되는 모양으로 완성되는건지는 그대로 안 만들어봐서 모르겠는데, 여튼 그렇게 캐러멜을 다 흘려버리고 나면 모양새가 너무 안 좋거든요. 그래서 저는 뒤집을 때 망 아래다 그릇을 받쳐서 캐러멜을 받은 담에.. 이걸 절반정도 분량으로 졸여서(안 졸이고 그냥은 점도가 맞질 않음) 붓으로 위에다 칠해줬음. 귀찮긴 하지만 이래야 모양이 났어요.

그럼 여기서 수정한 레시피로 다시 한 번 정리.


A 캐러멜라이즈드 애플

설탕 375g
물 75g
버터 94g
사과 2개

1. 껍질을 벗긴 사과를 16조각으로 자른 다음 24 x 9 크기 사각 틀에 넣는다.
2. 냄비에 설탕, 물을 넣고 골든 브라운 색이 날 때 까지 끓인다.
3. 2 에 버터를 넣고 섞은 다음 1에 붓는다.
4. 130도 오븐에 넣고 1시간30분간 익힌다.


B 스위트 반죽

버터 142g
슈거파우더 77g
아몬드파우더 25g
박력분 205g
달걀 1개
소금 0.7g

1. 차가운 상태의 버터에 함께 체 친 슈가파우더, 아몬드파우더, 박력분을 넣고 섞는다.
2. 1 에 풀어놓은 달걀과 소금을 넣고 반죽한다.
3. 냉장고에서 2시간 동안 휴지한다.
4. 3의 절반을 0.2cm ..인지는 모르겠고;; 틀에 맞는 크기가 되게 밀어 편다.


마무리

1. 오븐에서 꺼낸 A(캐러멜라이즈드 애플)에 B(스위트 반죽)를 씌운다.
2. 170도 오븐에서 B(스위트 반죽)가 익을 때 까지 굽는다. (원본 레시피에는 시간이 안 나와 있는데, 제가 만들때는 한 15분 정도? 였던 듯.)
3. 구워져 나온 2를 잠시 식힌 다음, 망 아래 그릇을 받치고 뒤집어 흘러내리는 캐러멜사과즙을 받는다.
4. 3의 캐러멜 사과즙을 불에 올려 1/2 정도로 졸인 다음, 붓으로 사과 위에 발라 준다. (한 번은 귀찮아서 바르질 않고 그냥 들이부었는데, 그러면 설탕폭탄이 됩니다. 못먹어여;; )

끝!
2011/11/21 23:23 2011/11/21 23:23
차암으로 오랜만의 블로그 포스팅.

이 글의 초판은 "음악과 함께하는 격동의 근현대사"

새벽에 작업한답시고 컴터를 켜고 앉아있는데 일 진도는 안나가고 영 엉뚱한 짓만 하던 중에..
갑자기 업타운의 '다시 만나 줘' 가 듣고싶어지더란 말씀.

들으면서 든 생각이.. '맨날 싸구려 헤드폰으로만 듣다가 (역시 싸구려지만)우퍼 딸린 스피커로 들으니 베이스 쿵쿵 울리는게 다르네. 좋구나~ 근데 왠지..왠지 대학 새내기시절 생각이 나네...'
..해서, 늘 하던것처럼 nowplaying 태그 트윗질을 할까 하다가.. 생각해보니 예전에 특정 시기와 특정 곡을 연관하여 글을 썼던 게 생각나서 보니..

다시 만나 줘..가 없네요?!

그렇잖아도 그 글 쓸때 좀 급히 쓰느라 뭔가 빠진 거 같지만 일단 쓰고보자..싶던 생각이 스쳤던 것도 같은 느낌이..

여튼, 서론이 (늘 그랬듯이) 매우 길었지만, 그때 쓴 글에다 슬쩍 내용추가만 할 까 하다가.. 이러저러한 이유로 새로 쓰는 게 나을 거 같아서..

(그리고 굳이 아직 블로그 죽인 거 아니라는 증빙도 할 겸...)

새로 글을 쓰겠음.

하지만 본 내용은 원래 글에서 거의 긁어다 붙인거라는 점은.. 그냥 넘어갑시다;;

어쨌거나 개인적으로 특정 시기(97년도 봄..이라던가 고3 가을..때라던가 등등등)를 생각하면 떠오르는 곡 혹은 앨범이 있고.. 반대로 특정 곡을 들으면 어떤 시기가 기억나는 경우도 있는데(오늘처럼),

그냥 그때그때 생각 날 때 마다 음악만 듣고 말 게 아니라 정리 해 두는 것도 재미있을 거 같아서.. 써 봅니다.

.시작.


1989년 국민학교5학년 겨울방학 - C.C.Catch 'Backseat of your Cadillac'
- (89년 말인지 90년도 초였는지 모르겠다.. 5학년 겨울방학때였던 건 확실한데. 어쨌던 바로 그 89/90 Winter Season!ㅋ) 당시 Goldstar 휴대용카세트플레이어 번들 테잎에 수록되었던것으로 추정되는 곡. 5학년 겨울방학때 보이스카웃 머시기로 대구 팔공산 갔을때 귀에 내내 꽂고 있었던 터라..
회사 다니기 전까지 이 곡 제목이 도대체 뭔지 모르고 있었는데, 2000몇년도던가 나우누리(...)에서 돌아댕기던 '80년대 유로댄스'라는 곡모음집에서 발견. 감동의 재회.

1990년 국민학교6학년 - 조정현 '그 아픔까지 사랑한거야'
- ....뭔가의 이유 때문에 단체로 벌 받으러 마루에 우루루 나가서 서 있는 동안, 친구 녀석이 이 노래를 부르 던 게 생각남. 근데 그때는 개사해 부르던거였어(...)

1992년 중2 여름 - 서태지형님의 1집
- 설명이 필요없심. 카세트 플레이어에 걸어놓고 테이프 처음부터 끝까지 'Yo! Taiji' 부터 'Missing' 까지 들으며 지냈...

1992년 중2 겨울방학 - 윤종신 2집 'Sorrow'
- 타이틀곡 '너의 결혼식' 보다는 '이별연습'이 더 좋았다. 인순이 누님의 원곡이 있다는건 한참 뒤 노래방에서야 알게 되었(노래방 책은 곡제목정렬이지욤)지만..

1993년 중3 가을 - 015B 4집 'The Fourth Movement'
- 요 앨범도 통째로. 왠지 모르게 진주로 시험보러갔던때가 생각남.

1993년 중3 겨울방학 - Mr.2 '하얀겨울'
- 겨울방학이 되자마자 장만한 486SX 시스템에 딸려온 노래방 프로그램으로 무던히도 불러댔었지.

1994년 고1 봄 - Ace of Base 1집 'The Sign"
- 덥지도 춥지도 않은 날씨에 창문을 열어놓으면 기분좋게 살랑거리는 바람. 침대에 누워 맘에 드는 잡지를 읽으며 듣다가 잠들곤 하던 앨범. 요것도 통째로 사랑해주었다. 명반이지..

1994년 고1 가을 - Crash 1집 'Endless supply of Pain'
- 교실이데아의 '그 부분'을 부른 Crash의 보컬 안흥찬.
교실이데아를 통해 Crash 를 알게되고, Crash 가 부른 Smoke on the water 를 통해 Deep purple 을 알게 된.. 희한한 사슬의 연결고리.

1996년 고3 여름 - 서지원 '내 눈물 모아' 삐삐밴드 2집 '불가능한 작전'
- 고3여름방학 보충수업 후 저녁까지의 자습시간... -ㅂ-

1996년 고3 겨울 - 노땐스 '골든 힛트 일집'
- 신해철,윤상 콤비의 프로젝트 앨범. 첫 앨범 제목이 '골든 힛트 일집' 이다. 그리고 그 이후로 노땐스의 앨범이 나오지 않았지. 뭐, 프로젝트 그룹 같은 거(..같은 거 가 아니라 바로 그건가?)니까.

1997년 봄 - 업타운 '다시 만나 줘'
- 대학교 새내기때.. 미적분 시험 준비인지 과제인지 한답시고 두껍고 무거운 갈쿠리(Calculus) 책을 수원 학교에서 인천 친구 집까지 들고 가서 열심히(나름) 보면서 '뭔 소린지 하나도 머리에 안 들어와!!!' 라고 방심하는 사이..에 머리속을 가득 채우고 있었던 바로 그 곡.

1997년 여름 - 자자 '버스 안에서'
- 이건 좀 이상한데, 이 곡이 포함된 앨범 발매는 97년이 아니라 96년 가을인데.. 이 곡 뜨는데 한참 걸린건가? 아님 내 기억이 뭔가 잘못된건가.. 여튼 이 곡과 연결된 내 기억은 분명 97년도 여름..여름방학때.

1998년 대학2학년 늦가을 - Chet Baker 'As time goes by'
- 요새는 연락이 매우 뜸(..몇년에 한 번 정도...)해진 박정남양께 감사의 말을 전합니다.

1999년 대학3학년 봄 - S#arp 'Lying', 윤종신 7집 '후반(後半)', 신해철의 'Monocrom'
- 대학 2학년 겨울방학때 홍렬이하고 같이 윤종신 7집앨범 콘서트에 갔었다. (지금 얘기지만.. 그때는 그렇게 낯설어보이던 인터컨티넨탈호텔 앞 길이, 10년 후엔 매일 다니는 출근길이 될 줄은..ㅋㅋㅋ) ...앨범 수록곡은 한곡도 모른 상태로. 종신이형님이 빤히 보이는 앞쪽 자리였거든.(..두번째로 비싼 좌석이던가..홍렬이가 산 표라 잘 모르겠심) 그게 미안해서(?)였는지 3학년 봄에 시디까지 구입해서 열심히 들었는데..노래 좋더라. 하긴 종신이형님은 중학교시절부터 지금까지도 듣고 있으니.. 영향력을 미친 기간으로 보면 최장기로구나! 하여튼 이 앨범은 같은 시기 해철옹의 Monocrom 앨범과 함께 두 앨범과의 진하고 끈적한 삼각관계를 구성하던 한 축이었음.
S#arp 'Lying' 은 동대문으로 옷 사러 석호녀석과 갔던 때가 생각남.

1999년 대학3학년 가을 - Smile.dk 'Butterfly'
- 말이 필요없는 DDR 의 명곡. 채보가 널널해 나같은 몸치도 퍼포 넣어가며 플레이 가능(...) 물론 그 시절 얘기임ㅋ

2000년 입사 첫 해 봄 - 플스2판 TTT BGM 中 샤오유 스테이지, 잭2 스테이지, 엔딩
- 첫월급...과 함께 발급된 내 생애 첫 신용카드로 긁어주신 일판 신품 플스2와 TTT(할부는 무려 9개월)
남코의 BGM 코드가 나와 잘 맞다는것을 확인.

2000년 입사 첫 해 여름 - 이박사 'Space Fantasy'
- 테크노뽕짝의 거장 닥터리. 그의 불후의 명곡. 당시 같이 일하던 명진이와 나는 이곡을 들으며 '세상에 이런 곡이'를 연발할 수 밖에 없었다.

2000년 입사 첫 해 가을 - Ridge Racer 5 BGM 中 'Euphoria'
- 그 당시 놀러나갈때 귀에 꽂고있었거든.

2001년 대만출장중 - S#arp 의 'For you'
- S#arp 의 원곡이 아니라, 당시 대만 사무실 칸막이 옆 노래연습하는곳에서 대만 가수(혹은 가수지망생..)이 번안해 연습하던 곡으로 처음 접함. 접한 정도가 아니라 거의 세뇌수준(2달 내내 들었심-ㅂ-)
그래서 그런지 원곡 들어보니 좋더만(...)


2001년 가을 부터 2006년 봄 까지는 Kyoto Jazz Massive 와 Jacques Loussier Trio 의 지배를 받았던 시기. 이 글의 관점에서는 일종의 암흑기라고 해야되나...
KJM 은 처음에 듣기 시작할때 물어보니(적어도 내 주위에선) 아는 사람도 없고 국내에선 음반 구하기도 어렵고.. 해서 어려운가부다..했는데, 나중에 알고보니 몇 차례 내한한 적도 있는 듯. 하지만 한 번도 가보진 못했다.
Jacques Loussier Trio 는 2005년 말에 큰맘먹고 공연 보러..갔는데 혼자 갔어!ㅋ 근데 영화나 공연은 정말 좋아하는 거라면(그리고 안타깝게도 취향이 일치하는 같이 갈만한 사람이 없다면) 혼자 가는 게 좋습니다. 이건 팩트예요 팩트.


2007년 (언제부턴지는 모르겠는데)~초여름 - Casiopea vs The Square Live
- 당시 휴대용 mp3p로 쓰던 PSP 로 줄창 들어댔음... 이거 라이브라 곡들 사이에 끊기지 않고 연결되는 게 많은데, 한 곡이 생각나서 듣기 시작하면 그 곡 이후로 몇곡을 연달아 들어야 하는 일이 종종 발생.

2007년 여름 - 배슬기 '말괄량이(feat. 카를로스 Of 업타운)' (..이라고 벅스 플레이어에 제목이 적혀있음.. 한 자도 안 빼먹고 대소문자까지 그대로 옮김. 왠지 그래야 할 거 같아서...)
- 저 시기 즈음에 모 남성지의 '세련'을 주제로 곡을 추천하는 기사를 읽다 발견..한 곡.

그리고 이 시기 이후로는 다시 암흑기.. 인 것도 같고,
혹은 이 특정 시기와 연결되는 곡은 기억이 몇 년 숙성된 후에야 드러나는 거 같기도 하고..

언젠가 이 글의 세번째판을 쓰게 되는건.. 몇 년 후가 되려나...?

아.. 근데 다 적고 보니 음악과 시기는 있는데 근현대(개인)사는 어디간거냐ㅋ
2011/07/18 07:25 2011/07/18 07:25

에반게리온: 파 를 보았습니다.

분류없음 2009/12/06 02:00 posted by 윤뿌쮸

'어차피 TV판이랑 같은 내용 조금 편집한거 아냐?' 라고 생각하고 넘겼으면 평생 후회할 뻔 했습니다. 농담이 아니라 정말로 보다말고 울 뻔 했거든요(...) '내 생에 에바를 극장에서 볼 수 있게 되다니'(예전 에바를 처음 접했을 당시 매체의 한계로 인한 조악한 화질에 만족할 수 밖에 없었을 때와 대비해서)라던가 TV판에서보다 몇단계 레벨업된 디테일에 대한 감탄이라던가 메카닉물에 뿅가는 (남자의) 본능(?)을 자극하는 부분이라던가..뭐 여튼 여러가지 요소가 복합적으로 작용했습니다만 여튼 영화관에서 보고서 이만큼 만족한 작품은 없었던거 같네요.

어차피 '에바의 현역 팬'인 분들이야 뭐.. 영화 개봉되기 전에 노출된 정보도 찾아다니며 다들 확인하시고 작품 자체도 시사회 예매 오픈날짜까지 챙겨가면서 예약해서 보셨을테지만,
저처럼 에바덕후와 일반인(...) 중간 쯤에 발을 걸치고 계신 분들 중에서 '볼까말까볼까말까'고민하고 계신 분이 있다면 꼭 가서 보시라고 말씀드리고 싶네요.

더 얘기하면 저도 모르는 새에 스포를 흘릴거 같으니 이만하고, 이번 '파' 감상의 재미를 배가시켜주었던 글의 링크를 첨부하니 감상하러 가실 분들께서는 읽어보고 가시는것도 좋지 않을까 싶습니다.

'작품 속 떡밥들에 대한 고찰' 이라는 제목으로 DVDprime 에 올라온 글입니다.

2009/12/06 02:00 2009/12/06 02:00