레이블이 도서인 게시물을 표시합니다. 모든 게시물 표시
레이블이 도서인 게시물을 표시합니다. 모든 게시물 표시

2013년 8월 7일 수요일

셰이더 프로그래밍 입문 틀린내용 업데이트(1쇄/2쇄)

최근에 현정님이 셰이더 프로그래밍 입문 1쇄에서 오타를 하나 찾아주셨는데... 1쇄 2쇄 공통 적용됩니다. 찾아주신 이쁜 현정님께 감사의 말씀 드립니다.


p.154

행기준 행렬

| Tx Bx Nx |  ->  | Tx Bx Nx | 
| Ty By Ny |      | Ty By Ny | 
| Tz By Nz |      | Tz Bz Nz | 


여태까지 찾은 모든 잘못된 내용은 아래 페이지에 있습니다.
셰이더 프로그래밍 입문 정오표

2013년 7월 24일 수요일

셰이더 프로그래밍 입문 GLSL 소스코드(비공식)

제가 출판했던 셰이더 입문 책의 샘플이 HLSL로 되어있는데 GLSL로 바꾸서 본인 블로그에 공개해주신 분이 계시더군요. 혹시라도 GL쪽을 더 많이 하시는 분들을 위해 여기에 링크를 걸어둡니다. 제목에 써놨듯이 비공식이며... 혹시라도 소스코드에 문제가 있더라도 전 모르는 일입니다.. 후후후 -_-

자 그럼 링크입니다.. 꺄아~


http://libsora.so/glsl_example/

2013년 6월 5일 수요일

가슴뭉클해지는 책 리뷰

인터넷으로 가끔 검색해보면서 제 책의 리뷰들을 찾아보는데.. 오늘은 정말 마음이 뿌듯해지는 리뷰를 찾았습니다..

오늘은 평소와 다르게 약간 겸손모드라.. 제가 직접 텍스트를 긁어 붙이기엔 좀 낯이 간지러워서.. 링크 2개로 떼우겠습니다... -_-





p.s. 생각해보니 아직 나쁜 리뷰가 없네요. 1쇄에는 잘못된 내용도 몇개 발견되서 맘이 참 안좋았었는데 말이죠.. 다행히 제 판단이 틀리진 않았나봐요....^_^



2013년 5월 22일 수요일

셰이더 프로그래밍 입문 2쇄 발매

셰이더 프로그래밍 입문의 1쇄가 전부다 팔렸습니다. 블로그에 절반을 공개해놨는데도.. IT 서점 시작이 불황이라는데도.... 잘 팔리는거 보니 기분이 좋네요.... 사주신 분들 모두 감사합니다. 그리고 교재로 채택해주신 대학 및 학원들도 매우 감사합니다. ^_^

2쇄에서 바뀐 내용중 가장 중요한건....

저자소개에 제 사진이 들어갔단 것입니다. 그리고 자기 자랑을 너무 안했다고 아버님이 노해하셔서... 자기자랑을 좀더 했습니다. BCIT 수석 졸업.. 쿨럭쿨럭 -_- 새로 바뀐 저자소개는 한빛미디어 홈페이지에서 즐겁게(?) 감상하실 수 있습니다.

그리고 그보단 덜(?) 중요하지만.... 1쇄에서 있던 오타와 잘못된 내용을 수정하였습니다. 1쇄 정오표는 제 블로그에 이미 올려두었습니다.

자.. 제 사진을 집에 고이 소장하시고 싶으신분들은 2쇄도 한권 씩 사주시면 됩니다.. 그럼 3쇄에는 누드사진을 실을지도.. 쿨럭쿨럭 -_-




2013년 1월 17일 목요일

[또자랑] 제 셰이더 서적에 대한 너무나 좋은 평


제 블로그의 셰이더 입문강좌 페이지에 실린 댓글입니다. 이 댓글 받고 나서... '아, 책 쓴 보람이 있구나.' 생각이 들어.. 또 제자랑겸 여기다도 올립니다. 이런 기회를 제가 놓칠리가 없죠 -_-;

----------------------------------------------------------------------------------------

정헌 2013년 1월 14일 오후 11:00
안녕하세요~ 다시 와보니 바로 윗글에 글 남긴 사람이네요 ㅎㅎㅎ
Appendix A 까지 다 보고 코드로 적용해본다음 후기 올리려 왔답니다

지금으로부터 한.. 7일 전, 처음 이 책을 서점에서 목차를 봤던 기억이 나네요 ㅎㅎ
처음엔 그냥 셰이더 책 또 하나 나왔구나 라고 생각을 하고 지나쳤더랬죠
그런데 저 개인적으로 프로젝트를 진행하던 중이였습니다, 그동안 수학,물리를 공부해서
실질적으로 뭔가 보려고 했지만, 막상 시간이 지나다보니 셰이더를 넣어야겠다는 생각에
봉착하게 됐습니다, 그런데 2,3년 전 셰이더를 본적이 있긴 하지만 다시 시작해볼려고
하니깐 막상 주변엔 셰이더 관련 책이 있긴 하지만 셰이더 책들 분량이 대부분 많아
다시 보기에도 귀차니즘이 승리할 것 같아 그냥 필요한 부분만 보면서 넘어가자라는 식으로 하려고 했으나 원래 성격이 그렇지 못한지라 결국 다른 방법이 없을까 생각을 했죠
생각을 하던중 셰이더 프로그래밍 입문 책의 목차가 머리속에 사~~악 지나가더군요,
그때 드는 생각이 그래! 이거다! 라는 생각이 들어 바로 서점으로 튀어가서
한권 구입했더랬죠 ㅎㅎ
처음엔 챕터8 정도까지만 본 다음 다시 본래 프로젝트로 돌아가려 했지만
8 장을 vs로 코딩하던 중 드는 생각이
"어.. 이 책 뭔가 다르다" 는 생각이 들게 되더군요
처음 해본다면 오랜 시간 투자해야되는 여러가지 DX 설정들과 셰이더와의 상호
상태와 값과 그 의미들이 간단 명료하고 잘 연결 되어있다는 느낌을 받게 되더군요
결국 한챕터 더 보까? 더보까? 더 보까? 하다가 끝까지 보게 됐습니다

인터넷 뒤져가며 두꺼운 셰이더 책 뒤져가며 찾아 설정해야 되는 것들에 대한 시간
절약과 셰이더 소스코드들의 간략하면서도 이해가 잘가는 설명은
처음 앞뒤 뭐 대락 소개나 이런 저런것 빼면 2백 50~60 페이지 정도 되는 분량이
좀 적은거 아닌가? 조금 비싼거 아닌가? 라는 생각을 완전히 뒤바꿔 주더군요

기본적인 탄탄한 노하우가 담겨져 있는 책이라는 느낌이 들었습니다

게임관련 많은책을 보긴했지만 이렇게 직접 사이트에 찾아와 후기를 남기는건 처음이네요

또 질문 하나 하고싶어서 글을 남기기도 한건데요

Q:
대부분 프로그래머는 자신이 익힌 것을 잘 공개하려는 마인드를 갖고 있진 않다고
생각 하곤 합니다, 그런데..
사이트에 보면 이 책의 일부 챕터가 올라와있는것도 볼 수 있고 책 또한 기본적인
내용들을 담았다곤 했다고 하셨지만 노하우가 많이 묻어나온 책이라는걸 느낄 수가
있었습니다, 책을 보면서 이렇게 까지 이 한권의 책으로 도움을 받아도 되나
라는 생각을 하게 되었는데요, 왜 이리 적지 않은 부분을 공개한것인지 궁금하네요 ㅎㅎ
p.s 렌더몽키를 소스코드로 옮기는 과정은 감탄이 나오더군요 ㅎㅎ
스승으로 모셔도 될까요?? ㅎㅎㅎㅎ



2012년 8월 26일 일요일

셰이더 프로그래밍 입문 책 리뷰들

Update: 2012년 11월 10일 기준으로 온라인 책방에 실린 리뷰들을 긁어왔습니다. 앞으로도 종종 업데이트할 생각입니다.

한빛미디어 웹사이트에 달린 제 책의 리뷰 둘입니다. 당연히 좋은 리뷰니까 올립니다 -_-;


교보문고를 보니.... 리뷰가 더 있습니다... 역시 별이 최소 4개입니다 꺄아~ -_-


YES24 에도 있네요.


인터파크에도 4개 있습니다... 평점이 무려 9.5점! -_-v





역시 책은 잘 쓰고 볼일입니다.... (왜 마지막엔 꼭 자뻑일까 -_-)

2012년 7월 8일 일요일

북미취업 가이드 독자 리뷰 2개. (인터파크)

안녕하세요 연두 이유식 좋은거 먹여보겠다고 여전히 책광고에 여념이 없는 포프입니다... 인터파크에 가보니 독자리뷰가 두개나 달려있어서 후다닥 올립니다.. ^_^



좋은 리뷰 모두 감사드립니다. ^_^

2012년 7월 2일 월요일

북미취업 가이드 독자 리뷰 #1 by 슈~♥

슈~♥님이 제 책을 읽으시고 본인 블로그에 남기신 리뷰가 있길래 링크를 퍼옵니다. 제 책이 다른 사람들의 이정표 비스무리한게 된다니 기분이 좋네요. ^^ 리뷰는 아래 링크를 눌러주세요.

슈~♥님의 리뷰 보기


2012년 6월 10일 일요일

북미취업 가이드북, 분야 1위를 달리고 있습니다.

지금 막 인터파크에 들어가보니.... 순위가 멋지군요.


  • 자기계발서 순위: 1 위
  • 이북 전체 순위: 2 위
  • 인터파크 전체순위: 62 위




2012년 6월 5일 화요일

위대한 게임의 탄생 2 에 제 인터뷰가 실렸습니다.

방금 이 책의 편저자이신 박일님이 보내주신 책을 받았습니다.


197페이지엔 제 인터뷰도 실려있습니다.... ^_^/




저는 공짜로 얻었으니.... 여러분들은 사서 보세요.. 훗훗 -_-;

2012년 6월 4일 월요일

7월 10~20일날 쉐이더 책 나옵니다.

원래 한빛 미디어에 5월 출간예정으로 올라와서 기대하고 있었는데 안나와서 확인해보니 일이 많아서 좀 연기되었답니다. 이번엔 7월 10일에 반드시 출판할거라고 하시니 기대해봅시다.

계속 책 출판이 늦어져서 죄송합니다. 생각보다 많이 늦어졌네요. 좀 일찍 나왔으면 다음학기 교재가 될 가능성도 있었을텐데 저도 개인적으로 좀 아쉽습니다.

그래도 더 좋은 책이 나오기 위해 이러는 거라고 생각하고 긍정적으로 기다려주시기 바랍니다. ^_^

업데이트(2012-06-12):
출판사에서 온 이멜을 다시 읽어보니 7/10일 출간.. 7/11일~20일 인쇄배본.. 이라고 하는데.. 이게 무슨 차이인지 헷갈 -_-;

2012년 5월 23일 수요일

북미취업 가이드 책이 나왔습니다.

몇년전에 블로그를 통해 연재했던 북미취업 가이드가 드디어 전자책으로 책이 나왔습니다. 몇 년이 지난 만큼 블로그에 있던 내용을 조금 다듬고 내용도 조금 업데이트했습니다. 편집 및 출판은 연두미디어에서 맡아 주었습니다. 현재 애플 아이북스용으로 올라왔고 기타 국내 온라인 출판사 홈피에도 곧 올라온다 합니다. 전자책 구매 방법은 연두미디어의 홈페이지를 참조하시기 바랍니다.


2012년 3월 28일 수요일

쉐이더 입문강좌 책 계약했습니다

생각해보니 트위터에만 올리고 여기엔 글을 안남겼었군요. 사실 쉐이더 강좌 보시는 분들은 블로그로 더 많이 오실텐데 말이지요..... ㅎㅎ 며칠전에 계약서에 출판 서명했습니다. 여태까지 한빛 출판사에서 편집은 하고 있었는데 멀리 살다보니 이제서야 계약서를 작성했네요.



가장 궁금해 하시는 출판예정일은 5월정도랍니다.



2012년 1월 30일 월요일

[포프의 쉐이더 입문강좌] 06. 만화같은 명암을 입히는 툰쉐이더

이전편 보기

샘플파일 받기

제6장 만화 같은 명암을 입히는 툰쉐이더


이 장에서 새로 배우는 HLSL
  • ceil() - 무조건 올림 함수

이 장에서 새로 사용하는 수학
  • 행렬합치기 - 여러 개의 행렬을 미리 곱해놓은 뒤, 그 결과를 정점변환에 사용해도 결과는 동일함. 단, 속도는 더 빠름.
  • 역행렬 - 반대 방향으로 공간변환을 할 때 유용.


배경
얼마 전에 저희 회사의 아트 디렉터가 했던 말이 있습니다. 프로그래머는 언제나 사실적인 3D 그래픽을 추구하지만, 게이머들의 보통 미적 스타일을 최대로 살린 비사실적인 그래픽에 열광한다고요. 생각해보니  맞는 말이더군요. 프로그래머들은 언제나 수학적으로 옳은 것을 추구하려고 하지만 언제나 미적 스타일을 갖춘 게임들이 흥행을 하니까요. 스트리트 파이터 4, 팀포트리스 2, 보더랜드 등이 그 좋은 예겠죠?

그 동안 현대 3D 그래픽의 주 초점도 사실적인 그래픽을 재현해 내는 것이었습니다. 하지만 그 와중에도 미적인 효과를 살리기 위한 비사실적 렌더링 기법들도 간간이 등장했는데요 여기서 살펴볼 툰쉐이딩(toon shading, 셀 쉐이딩(cell shading)이라고도 합니다) 도 그 중 하나입니다. 툰(toon)이라 하면 만화(cartoon)를 뜻합니다. 만화를 보면 명암처리를 부드럽게 하는 대신에 칼같이 딱딱 끊어서 2~3 단계로 하죠? 뭐, 만화를 안보신 분들은 없을 듯 하니 다 아시겠네요. ^^ 여기서 구현할 쉐이더가 바로 그런 일을 할 겁니다. 일단 결과를 미리 사진으로 보여드리면 대충 감이 오시겠네요.

그림. 6.1. 이장에서 만들어 볼 툰쉐이더

위의 사진을 잘 관찰해 봅시다. 여태까지 사용했던 평범한(?) 난반사광 쉐이더와 뭐가 다르죠? 난반사광이 부드럽게 어두워지는 대신 단계적으로 팍팍 줄어든다는 거죠? 마치 계단을 걸어 내려가는 것처럼요. 그렇다면 이것을 그래프로 그려보면 어떨까요? 일반적인 난반사광의 그래프와 비교해서 보면 좀 더 이해가 쉽겠네요.

그림 6.2  일반 난반사광 그래프와 툰쉐이딩 그래프


위 그래프를 보니 감이 팍팍 오지 않나요? 아니라고요? 으음... 그럼 위 그래프를 표로 간단하게 정리해 보겠습니다..


난반사광의 양 툰쉐이더 값
0 0
0 ~ 0.2 0.2
0.2 ~ 0.4 0.4
0.4 ~ 0.6 0.6
0.6 ~ 0.8 0.8
0.8 ~ 1 1
표 6.1 난반사광의 양과 툰쉐이더 값의 비교

이렇게 비교를 하니 정말 쉽네요. 난반사광의 값을 가져다가 0.2단위로 무조건 올림을 하면 툰쉐이더 값이 나오는군요? 그럼 이 정도만 알면 툰쉐이더를 만드는 건 식은 죽 먹기일 듯 합니다. 곧바로 렌더몽키로 가 볼까요?

기초설정
렌더몽키를 실행한 뒤, DirectX 이펙트를 추가합니다. 새로 생긴 이펙트의 이름을 Default_DirectX_Effect에서 ToonShader로 바꿉니다. matViewProjection이란 행렬이 정의되어 있는 것도 보이시죠? 삭제해 주세요.

그림 6.1에서 주전자 모델을 보여드렸었죠? 주전자는 3D 그래픽 논문에서 즐겨 사용하는 모델 중에 하나입니다. 이리저리 다양한 굴곡이 많아서 쉐이더의 결과를 딱 한눈에 살펴보기 좋다나요? 저희도 주전자 모델을 사용하겠습니다. 렌더몽키의 작업공간 패널에서 Model을 찾으세요. 이 위에 마우스 오른쪽 버튼을 누른 뒤, Change Model > Teapot.3ds를 선택합니다.

툰쉐이딩을 하려면 일단 난반사광을 계산해야겠죠? 그래야 그 결과를 0.2 단위로 올림할 수 있으니까요. 그렇다면 '제4장: 기초적인 조명쉐이더'에서 그랬던 것처럼 빛의 위치와 정점의 법선정보가 필요하겠군요. 우선 빛의 위치를 변수로 선언하겠습니다. ToonShader에 오른쪽 마우스 버튼을 누른 뒤, Add Variable > Float > Float4를 선택하고, 변수의 이름을 gWorldLightPosition으로 바꿉니다. 이 변수의 값은 예전과 마찬가지로 (500, 500, -500, 1)으로 맞춰주세요. 다음은 정점에서 법선정보를 읽어올 차례입니다. Stream Mapping을 더블클릭해서 NORMAL 필드를 추가하면 되겠죠? 데이터형은  FLOAT3, Index는 0으로 해주는 것도 잊지 마세요.

그림 6.1을 다시 한번 봐 보죠. 주전자가 녹색이죠? 주전자의 색을 지정해주는 방법은 여러 가지가 있지만 여기서는 전역변수 하나로 전체 표면의 색을 지정해 주겠습니다. (만약 한 표면 위에서 여러 가지 색상을 사용하시고 싶으시다면 3DS MAX에서 메쉬를 만드실 때, 정점색상(vertex color)를 칠하셔도 됩니다. 그리고 정점쉐이더 입력데이터에서 COLOR0이나 COLOR1 시맨틱을 사용하시면 이 정점정보를 읽어올 수 있습니다.) ToonShader에 마우스 오른쪽 버튼을 누른 뒤, Add Variable > Float > Float3를 선택합니다. 새로 변수가 생기면 이름을 gSurfaceColor로 바꿉니다. 이제 이 변수 위에 마우스를 더블클릭하여 값을 (0, 1, 0)으로 변경합니다. 쉐이더에서 색상을 0~1 사이의 백분율 값으로 표현한다는 거 잊지 않으셨죠?

이제 행렬들을 좀 추가해 주겠습니다. 여태까지 다뤘던 쉐이더 기법에서는 기초설정을 할 때마다 월드행렬, 뷰행렬, 투영행렬을 따로 정의해 줬었죠? 여기서는 조금 다른 방법을 사용하도록 하죠. 3D 그래픽에서 공간변환을 할 때, 행렬을 사용하는 이유 중 하나가 여러 행렬들을 미리 합쳐놓으면(concatenation) 불필요한 연산을 줄일 수 있기 때문입니다. 예를 들면, 정점의 위치를 공간변환 할 때 월드행렬, 뷰행렬, 투영행렬을 차례대로 곱해줘야 하죠? 이러지 말고 미리 월드행렬, 뷰행렬, 투영행렬을 곱해서 새로운 행렬을 하나 구해 놓은 뒤, 그 행렬을 정점에 곱해도 결과는 동일합니다. 그러나 성능상으로 보면 행렬을 3번 곱하는 것보다 1번 곱하는 게 당연히 빠를 테니 행렬을 미리 합치는 방법이 더 낫지요.

여기서도 미리 행렬을 합치겠습니다. 그럼 그 결과 행렬을 건네 받을 전역변수를 하나 추해야겠죠? ToonShader에 오른쪽 마우스 버튼을 눌러 Add Variable > Matrix > Float(4x4)를 선택한 뒤, 변수명을 gWorldViewProjectionMatrix로 바꿉니다. 이제 이 변수 위에 마우스 오른쪽 버튼을 눌러 Variable Semantic > WorldViewProjection을 선택합니다.

자, 그럼 이렇게 행렬을 한 번만 곱하는 건 좋은데 난반사광을 계산하려면 월드행렬이 필요했던 것 같은데요? 빛의 위치가 월드공간에 정의되어 있으니까 빛의 방향벡터를 만들려면 월드공간에서의 정점위치가 필요했었네요. 그럼 당연히 월드행렬을 곱해야겠죠. 그리고 난반사광을 구하려면 역시 월드공간에서의 정점법선도 필요했었으니 역시 월드행렬이 필요하군요. 그렇다면 월드행렬을 전역변수로 전달해줘서 이렇게 행렬곱을 두 번 더 해줘야 할까요? 뭐 그러셔도 상관 없습니다. 틀린 방법은 아니거든요. 하지만 조금만 생각을 더 해보면 행렬곱 1번만으로 똑같은 일을 할 수 있습니다.

정점의 위치와 법선을 월드공간으로 변환하는 이유는 빛의 위치가 월드공간에 정의되어 있어서였습니다. 모든 변수가 동일한 공간에 있어야만 올바른 결과를 구할 수 있으니까요. 그럼 정점의 위치와 법선벡터를 월드공간으로 변환하는 대신에 빛의 위치를 지역공간으로 변환해버리면 어떨까요? 그러면 정점의 위치와 법선에 손을 대지 않아도 모든 매개변수들이 동일한 공간에 있겠죠? 이 방법은 행렬을 1번만 곱하니 아무래도 조금 더 빠르겠네요.

그렇다면 월드공간을 물체공간으로 어떻게 변환할까요? 월드행렬의 역행렬(inverse matrix)을 곱하면 됩니다. 그럼 렌더몽키에 월드행렬의 역행렬도 추가해 보도록 하죠. ToonShader에 마우스 오른쪽 버튼을 누른 뒤, Add Variable > Matrix > Float(4x4)를 선택합니다. 이제 이 변수 이름을 gInvWorldMatrix로 바꿔 주세요. 마지막으로 변수 위에 마우스 오른쪽 버튼을 눌러 Variable Semantic > WorldInverse를 누르면 모든 설정이 마무리 되었습니다.


그림 6.3. 기초설정을 마친 렌더몽키 프로젝트


정점쉐이더
일단 전체 소스코드부터 보여드린 뒤, 한 줄씩 차근차근 설명해드리겠습니다.

struct VS_INPUT
{
   float4 mPosition : POSITION;
   float3 mNormal: NORMAL;
};

struct VS_OUTPUT
{
   float4 mPosition : POSITION;
   float3 mDiffuse : TEXCOORD1;
};

float4x4 gWorldViewProjectionMatrix;
float4x4 gInvWorldMatrix;

float4 gWorldLightPosition;

VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;

   Output.mPosition = mul( Input.mPosition, gWorldViewProjectionMatrix );
 
   float3 objectLightPosition = mul( gWorldLightPosition, gInvWorldMatrix);
   float3 lightDir = normalize(Input.mPosition.xyz - objectLightPosition);
 
   Output.mDiffuse = dot(-lightDir, normalize(Input.mNormal));
 
   return( Output );
 
}



정점쉐이더 입출력데이터 및 전역변수
조명(난반사광)을 계산하려면 법선이 필요하죠? 따라서 정점쉐이더 입력데이터로 위치와 법선이 필요합니다. (정점마다 색을 지정해주셨다면 float3 mColor : COLOR0; 도 추가하셔야 합니다. Stream Mapping에서도 COLOR0을 더해주는 거 잊지 마세요.)

struct VS_INPUT
{
   float4 mPosition : POSITION;
   float3 mNormal: NORMAL;
};

정점출력데이터도 별로 어렵지 않습니다. 난반사광을 계산한 뒤, 픽셀쉐이더에 전달해 주는 게 전부입니다. (마찬가지로 정점마다 색을 지정해주셨다면  float3 mColor: COLOR0;도 추가하셔야 합니다.)  이게 잘 이해가 안 되시는 분들은 '제4장: 기초적인 조명쉐이더'를 다시 한 번 읽어 주세요.

struct VS_OUTPUT
{
   float4 mPosition : POSITION;
   float3 mDiffuse : TEXCOORD1;
};

이제 전역변수로는 위에서 설명 드렸던 행렬 2개와 광원의 위치를 선언해야겠네요.

float4x4 gWorldViewProjectionMatrix;
float4x4 gInvWorldMatrix;

float4 gWorldLightPosition;

이러면 앞서 렌더몽키 프로젝트에 더했던 변수들을 다 처리한 거 같죠? 이제 정점쉐이더 함수를 보겠습니다.

정점쉐이더 함수
우선 정점쉐이더의 가장 중요한 임무를 수행하겠습니다. 정점의 위치를 투영공간으로 가져옵니다. 월드행렬, 뷰행렬, 투영행렬을 하나로 미리 합쳐버렸으니 코드 한 줄로 이런 일을 할 수 있겠네요.

VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;

   Output.mPosition = mul( Input.mPosition, gWorldViewProjectionMatrix );


이제 난반사광의 양을 계산할 차례입니다. 앞서 말씀 드렸듯이 빛의 위치를 지역공간으로 변환한 뒤, 모든 계산을 이 공간에서 해보겠습니다. 우선 빛의 위치를 지역공간으로 변환합니다.

   float3 objectLightPosition = mul( gWorldLightPosition, gInvWorldMatrix);

이제 광원의 위치에서 현재 위치(이미 지역공간에 있습니다)를 가리키는 방향벡터를 만듭니다. 이 방향벡터의 길이를 1로 만드는 것도 잊지 말아야겠죠?

   float3 lightDir = normalize(Input.mPosition.xyz - objectLightPosition);

이제 그 결과와 정점의 법선(역시 지역공간에 존재합니다) 간의 내적을 구하면 난반사광의 양을 구할 수 있습니다.

   Output.mDiffuse = dot(-lightDir, normalize(Input.mNormal));

위에서 법선의 길이를 1로 만들기 위해 normalize()함수를 호출한 거 보이시죠? 보통 정점버퍼로부터 곧바로 가져온 법선은 이미 정규화가 되어있는 게 보통이나 혹시나 해서 normalize()를 한 번 더 호출해 봤답니다.

이제 가볍게 Output을 반환합니다.

   return( Output );
 
}

정점쉐이더 함수는 별로 어려운 게 없었습니다. '제4장: 기초적인 조명쉐이더'에서 다 배웠던 내용이니까요. 그냥 다른 공간을 사용했다는 게 좀 다른 내용이지만 그리 어렵지 않게 이해하시리라 믿습니다. 이제 픽셀쉐이더를 살펴보겠습니다.

픽셀쉐이더
정점쉐이더에서와 마찬가지로 전체 소스코드부터 보여드립니다.

float3 gSurfaceColor;

struct PS_INPUT
{
   float3 mDiffuse : TEXCOORD1;
};

float4 ps_main(PS_INPUT Input) : COLOR
{
   float3 diffuse = saturate(Input.mDiffuse);
 
   diffuse = ceil(diffuse * 5) / 5.0f;
 
   return float4( gSurfaceColor * diffuse.xyz, 1);
 
}

우선 전역변수와 픽셀쉐이더 입력데이터를 정의하죠. 표면의 색상을 전역변수로 선언하고 정점쉐이더에서 계산을 마친 난반사광의 양을 입력데이터로 받겠습니다.

float3 gSurfaceColor;

struct PS_INPUT
{
   float3 mDiffuse : TEXCOORD1;
};

이제 픽셀쉐이더 함수를 봅시다. 우선 mDiffuse에서 저희에게 별 의미가 없는 0 이하의 값을 잘라 냅니다.

float4 ps_main(PS_INPUT Input) : COLOR
{
   float3 diffuse = saturate(Input.mDiffuse);

이제 이 값을 0.2단위로 딱딱 잘라야겠네요. 0.2단위로 무조건 올림을 하면 된다고 했었죠? HLSL에서 무조건 올림을 하는 함수는 ceil()입니다. (영어로 ceiling이 '천장'이라는 뜻이니까, ceil을  '천장으로 올리다' 정도로 생각하시면 이해에 도움이 되실 겁니다. 이 반대로 무조건 내림을 하는 함수로 floor()입니다. 이것은 '바닥으로 내리다' 정도로 이해하세요.) 근데 ceil() 함수는 언제나 바로 위의 정수로만 올림을 한다는군요. 저희는 0.2단위로 올림을 해야 하는데 어쩌죠? 다음과 같이 간단히 곱셈과 나눗셈을 하면 됩니다.

   diffuse = ceil(diffuse * 5) / 5.0f;

위 공식(?)을 자세히 살펴보죠. diffuse가 0~1 사이의 값이니 여기에 5를 곱하면 범위가 0~5가 될 것입니다. 여기에 ceil()을 적용하면 그 결과값이 0, 1, 2, 3, 4, 5중에 하나가 되겠죠. 이제 이 값을 5로 나누면 최종 결과값이 0, 0.2, 0.4, 0.6, 0.8, 1 중에 하나가 될 겁니다. 이게 바로 저희가 원하는 값 맞죠? 그림 6.2와 표 6.1를 다시 봐도 이 값이 맞네요.

그럼 이제 표면의 색을 곱하기만 하면 끝입니다. (빛의 색상은 흰색(1, 1, 1)이라고 가정했습니다. 어떤 수에 1을 곱해도 결과는 바뀌지 않는 거 아시죠?)

   return float4( gSurfaceColor * diffuse.xyz, 1);
}

이제 F5를 눌러 정점쉐이더와 픽셀쉐이더를 각각 컴파일 한 뒤, 미리 보기 창을 보시면 그림 6.1와 똑같은 주전자가 보이죠? 아, 배경색이 다르다고요? 미리 보기 창 안에서 마우스 오른쪽 버튼을 누른 뒤 Clear Color를 선택하시면 배경색을 바꿀 수 있습니다.

선택사항: DirectX 프레임워크
이제 C++로 작성한 DirectX 프레임워크에서 쉐이더를 사용하시고자 하는 분들을 위한 선택적인 절입니다.

우선 '제3장: 텍스처매핑'에서 만들었던 프레임워크의 사본을 만들어 새로운 폴더에 저장합니다. 그 다음은 렌더몽키에서 사용했던 쉐이더와 3D 모델을 DirectX 프레임워크에서 사용할 수 있도록 파일로 저장할 차례입니다.


  1. Workspace 패널에서 ToonShader를 찾아 오른쪽 마우스 버튼을 누릅니다.
  2. 팝업메뉴에서 Export > FX Exporter를 선택합니다.
  3. 위에서 새로 만든 폴더를 찾아 그 안에 ToonShader.fx란 이름으로 파일을 저장합니다.
  4. 이제 Worspace 패널에서 Model을 찾아 오른쪽 마우스 버튼을 누릅니다.
  5. 팝업메뉴에서 Save > Geometry Saver를 선택합니다.
  6. 위에서 새로 만든 폴더를 찾아 그 안에 Teapot.x란 이름으로 파일을 저장합니다.


이제 비주얼 C++ 에서 프레임워크의 솔루션 파일을 엽니다.

우선 예전에 있던 전역변수들부터 살펴보겠습니다. gpTextureMappingShader 변수가 있는데 이 변수가 언급되어 있는 곳을 모두 찾아 gpToonShader로 바꿉니다. gpSphere변수도 똑같은 방법으로 모두 gpTeapot으로 바꿔 줍니다. 텍스처 변수, gpEarthDM도 있네요. 여기서는 텍스처를 전혀 사용하지 않으니 gpEarthDM변수를 사용하는 코드를 모두 찾아 삭제해주세요.

이제 새로 추가해야 할 전역변수들을 알아볼까요? 새로 추가해야 할 전역변수는 빛의 위치와 표면의 색상밖에 없는 것 같군요. 다음의 코드를 추가합니다.

// 광원의 위치
D3DXVECTOR4 gWorldLightPosition = D3DXVECTOR4(500.0f, 500.0f, -500.0f, 1.0f);

// 표면의 색상
D3DXVECTOR4 gSurfaceColor =       D3DXVECTOR4(0, 1, 0, 1);

위 코드에서 전역변수를 선언할 때 렌더몽키에서 사용했었던 값도 그대로 대입해줬습니다.

이제 LoadAssets() 함수로 가서 로딩해올 쉐이더와 모델의 이름을 각각 Toonshader.fx와 Teapot.x로 바꿔줍니다.

bool LoadAssets()
{
    // 텍스처 로딩

    // 쉐이더 로딩
    gpToonShader = LoadShader("ToonShader.fx");
    if ( !gpToonShader )
    {
        return false;
    }

    // 모델 로딩
    gpTeapot = LoadModel("Teapot.x");
    if ( !gpTeapot )
    {
        return false;
    }

    return true;
}

다음은 실제로 장면을 그리는 RenderScene() 함수입니다. 행렬 2개를 새로 전달해줘야 했었죠? 월드/뷰/투영행렬을 합친 행렬과 월드행렬의 역행렬이었습니다. 우선 월드행렬의 역행렬을 구해봅시다. 월드행렬을 구했던 코드 아래에 다음의 라인을 추가합니다.

    // 월드행렬의 역행렬을 구한다.
    D3DXMATRIXA16 matInvWorld;
    D3DXMatrixTranspose(&matInvWorld, &matWorld);

위 코드에서 사용한 D3DXMatrixTranspose() 함수는 전치행렬(transpose matrix)을 구합니다. 여기서 역행렬 대신에 전치행렬을 구한 이유는 월드행렬이 직교행렬이기 때문이죠. 직교행렬의 전치행렬은 역행렬과 같습니다.(이에 대한 증명 및 자세한 설명은 이미 시중에 나와있는 훌륭한 수학책을 참고하시기 바랍니다. 참고로 순수하게 역행렬을 구하시려 한다면 D3DXMatrixInverse() 함수를 쓰시면 됩니다.)

이제 월드/뷰/투영행렬을 서로 곱할 차례입니다. D3DXMatrixMultiply() 함수를 사용하겠습니다.

    // 월드/뷰/투영행렬을 미리 곱한다.
    D3DXMATRIXA16 matWorldView;
    D3DXMATRIXA16 matWorldViewProjection;
    D3DXMatrixMultiply(&matWorldView, &matWorld, &matView);
    D3DXMatrixMultiply(&matWorldViewProjection, &matWorldView, &matProjection);

월드행렬 X 뷰행렬 X 투영행렬 순으로 곱해준 거 보이시죠?

이제 위에서 만들었던 두 행렬을 쉐이더에 전달해 주겠습니다. 예전에 사용했던 SetMatrix() 함수 호출들을 다 지우시고 아래의 코드를 대신 삽입해주세요.


    // 쉐이더 전역변수들을 설정
    gpToonShader->SetMatrix("gWorldViewProjectionMatrix",
        &matWorldViewProjection);
    gpToonShader->SetMatrix("gInvWorldMatrix", &matInvWorld);

마지막으로 광원의 위치와 표면의 색상을 전달해주는 것도 잊지 마셔야겠죠?

    gpToonShader->SetVector("gWorldLightPosition", &gWorldLightPosition);
    gpToonShader->SetVector("gSurfaceColor", &gSurfaceColor);

이제 코드를 컴파일 한 뒤, 프로그램을 실행하시면 빙글빙글 도는 주전자를 보실 수 있을 겁니다. 아무래도 회전을 하니까 손잡이나 주둥이에서 툰쉐이더 효과가 더 잘 나타나죠?

정리
다음은 이 장에서 배운 내용을 짧게 요약해 놓은 것입니다.

  • 툰쉐이더는 비실사 렌더링 기법 중에 하나이다.
  • 툰쉐이딩은 난반사광을 단계적으로 감소시키는 것에 지나지 않는다.
  • 행렬들을 미리 곱해놓으면 공간변환을 더 빨리 할 수 있다.




다음편 보기



2012년 1월 23일 월요일

[포프의 쉐이더 입문강좌] 05. 물체에 색을 입히는 디퓨즈/스페큘러 매핑 Part 2


이전편 보기


픽셀쉐이더
픽셀쉐이더도 전체 소스코드를 보여드린 뒤, 새로 추가된 내용만 설명 드립니다.

struct PS_INPUT
{
   float2 mUV : TEXCOORD0;
   float3 mDiffuse : TEXCOORD1;
   float3 mViewDir: TEXCOORD2;
   float3 mReflection: TEXCOORD3;
};


sampler2D DiffuseSampler;
sampler2D SpecularSampler;


float3 gLightColor;


float4 ps_main(PS_INPUT Input) : COLOR
{
   float4 albedo = tex2D(DiffuseSampler, Input.mUV);
   float3 diffuse = gLightColor * albedo.rgb * saturate(Input.mDiffuse);

   float3 reflection = normalize(Input.mReflection);
   float3 viewDir = normalize(Input.mViewDir);
   float3 specular = 0;
   if ( diffuse.x > 0 )
   {
      specular = saturate(dot(reflection, -viewDir ));
      specular = pow(specular, 20.0f);
   
      float4 specularIntensity  = tex2D(SpecularSampler, Input.mUV);
      specular *= specularIntensity.rgb * gLightColor;
   }


   float3 ambient = float3(0.1f, 0.1f, 0.1f) * albedo;

   return float4(ambient + diffuse + specular, 1);
}

우선 새로 렌더몽키에 추가했던 세 변수를 전역적으로 선언하겠습니다.

sampler2D DiffuseSampler;
sampler2D SpecularSampler;


float3 gLightColor;

그리고 PS_INPUT구조체에 UV좌표를 추가합니다.

  float2 mUV : TEXCOORD0;

이제 픽셀쉐이더 함수의 젤 윗줄에서 디퓨즈맵을 샘플링 해보죠.

   float4 albedo = tex2D(DiffuseSampler, Input.mUV);

이것이 바로 현재 픽셀이 반사하는 색깔입니다. 여기에 난반사광의 양과 빛의 색상을 곱해야 한다고 했죠? 이전에 diffuse변수를 구하던 코드를 다음과 같이 바꿉니다.

   float3 diffuse = gLightColor * albedo.rgb * saturate(Input.mDiffuse);

이제 한번 F5를 눌러서 정점쉐이더와 픽셀쉐이더를 각각 컴파일 한 뒤, 미리 보기 창을 한 번 봐볼까요?

그림 5.3. 디퓨즈맵만 적용한 결과


담벽 텍스처가 보이죠? 푸르스름한 빛도 보이네요. 근데 스페큘러맵을 아직 쓰지 않아서인지 정 반사광이 너무 강하네요. 돌 사이의 틈새까지도 말이지요! 그럼 스페큘러맵을 더해보도록 하죠.

팁: 미리 보기 창에서 물체를 회전하는 법그림 5.3 처럼 틈새에 빛이 들어오게 하려면 물체를 회전하셔야 할 겁니다. 미리 보기 창에서 물체를 회전하시려면 창 안에 왼쪽 마우스 버튼을 누른 채 마우스를 이리저리 움직여보세요. 만약 회전 대신에 이동이나 확대/축소가 된다면 툴바에서 오른쪽 두 번째 아이콘(Overloaded Camera Mode)을 눌러주시면 됩니다.

픽셀쉐이더에서 specular 변수의 거듭제곱을 구하는 코드(pow함수 호출) 바로 밑에서 스페큘러맵을 샘플링 합니다.

      float4 specularIntensity  = tex2D(SpecularSampler, Input.mUV);

이제 이것과 빛의 색상을 specular에 곱해야겠죠? 코드는 다음과 같습니다.

      float4 specularIntensity  = tex2D(SpecularSampler, Input.mUV);
      specular *= specularIntensity.rgb * gLightColor;

위 그림에서 또 다른 문제점 하나는 난 반사광이 사라지는 순간부터 디퓨즈 텍스처의 디테일도 사라진다는 것입니다. 이것은 주변광 값으로 (0.1, 0.1, 0.1)만을 사용했기 때문인데요. 주변광은 그냥 저희가 임의로 정한 빛의 양이므로 여기에도 디퓨즈맵을 곱해주는 것이 맞습니다. ambient변수를 구하는 코드를 찾아 다음과 같이 바꿔주세요.

float3 ambient = float3(0.1f, 0.1f, 0.1f) * albedo;

다시 한번 정점쉐이더와 픽셀쉐이더를 컴파일 한 뒤 미리 보기 창을 볼까요?

그림 5.4. 스페큘러맵과 주변광까지 제대로 적용한 결과



확실한 차이를 볼 수 있죠? 정 반사광이 그렇게 강하지도 않고, 틈새도 완벽하네요. 거기다가 어두운 픽셀에서도 디퓨즈맵의 흔적을 찾아 볼 수 있네요.


선택사항: DirectX 프레임워크
이제 C++로 작성한 DirectX 프레임워크에서 쉐이더를 사용하시려는 분들을 위한 선택적인 절입니다.

우선 저번 장에서 사용했던 프레임워크의 사본을 만들어 새로운 폴더에 저장합니다. 그 다음, 렌더몽키에서 사용했던 쉐이더와 3D 모델을 DirectX 프레임워크에서 사용할 수 있도록 파일로 저장합니다. Sphere.x와 SpecularMapping.fx라는 파일이름을 사용하도록 하겠습니다. 또한 렌더몽키에서 사용했었던 텍스처 2개를 복사해다가 프레임워크 폴더에 저장합니다. Fieldstone_DM.tga와 Fieldstone_SM.tga로 이름을 사용하지요.

이제 비주얼 C++ 솔루션 파일을 연 뒤, 소스코드에서 gpLightingShader 변수가 언급된 곳을 모두 찾아gpSpecularMappingShader로 변경합니다.

그 다음, 전역변수 섹션에 가서 두 텍스처를 저장할 포인터들과 빛의 색상을 저장하는 변수를 선언합니다.

// 텍스처
LPDIRECT3DTEXTURE9        gpStoneDM    = NULL;
LPDIRECT3DTEXTURE9        gpStoneSM    = NULL;


// 빛의 색상
D3DXVECTOR4               gLightColor(0.7f, 0.7f, 1.0f, 1.0f);

렌더몽키에서 사용했던 푸르스름한 빛의 값인 (0.7, 0.7, 1.0)을 그대로 사용하는 거 보이시죠? 위에서 새로운 D3D 자원(텍스처)을 둘 선언했으니 이들을 해제하는 코드를 추가하도록 하죠. CleanUp() 함수에 다음의 코드를 추가합니다.

    // 텍스처를 release 한다.
    if ( gpStoneDM )
    {
        gpStoneDM->Release();
        gpStoneDM = NULL;
    }


    if ( gpStoneSM )
    {
        gpStoneSM->Release();
        gpStoneSM = NULL;
    }

이제 D3D자원들을 로딩할 차례입니다. LoadAssets() 함수에 다음의 코드를 더합니다.

    // 텍스처 로딩
    gpStoneDM = LoadTexture("Fieldstone_DM.tga");
    if ( !gpStoneDM )
    {
        return false;
    }


    gpStoneSM = LoadTexture("Fieldstone_SM.tga");
    if ( !gpStoneSM )
    {
        return false;
    }

쉐이더 파일의 이름을 SpecularMapping.fx로 바꿔주는 것도 잊지 맙시다.

    gpSpecularMappingShader = LoadShader("SpecularMapping.fx");

이제 마지막으로 RenderScene() 함수를 봅시다. 이미 쉐이더가 모든 일을 하고 있으니 간단히 새로운 변수들을 대입해주기만 하면 되겠네요. 예전에 SetMatrix() 함수들을 호출 해주던 곳이 있었죠? 그 아래에 다음의 코드를 추가합시다.

    gpSpecularMappingShader->SetVector("gLightColor", &gLightColor);
    gpSpecularMappingShader->SetTexture("DiffuseMap_Tex", gpStoneDM);
    gpSpecularMappingShader->SetTexture("SpecularMap_Tex", gpStoneSM);

위 코드는 빛의 색상과 두 텍스처맵을 쉐이더에 전달해 줍니다. 텍스처맵을 대입해 줄 때 _Tex 접미사를 붙여줘야 한다는 건 '제3장: 텍스처매핑'에서 설명했었죠?

이제 코드를 컴파일 한 뒤, 프로그램을 실행하면 렌더몽키에서와 동일한 결과를 보실 수 있을 겁니다.

이쯤 되면 느끼셨겠지만 쉐이더를 사용하면 DirectX 프레임워크에서 책임져야 할 그래픽 관련 업무가 현저히 줄어듭니다. D3D 자원을 로딩하는 것과 쉐이더 매개변수, 그리고 렌더상태(render state)를 관리하는 것이 거의 전부입니다.

정리
다음은 이 장에서 배운 내용을 짧게 요약해 놓은 것입니다.

  • 인간이 물체의 색을 볼 수 있는 이유는 물체마다 흡수 및 반사하는 빛의 스펙트럼이 다르기 때문이다.
  • 3D 그래픽에서는 디퓨즈맵과 스페큘러맵을 사용하여 빛의 흡수 및 반사를 재현한다.
  • 스페큘러맵은 각 픽셀의 정 반사광을 조절하는 것을 주 목적으로 한다.
  • 빛의 색도 물체의 최종 색에 기여한다.
  • 텍스처는 색상정보만을 저장하는 것이 아니다. 스페큘러맵의 경우가 그 예이다.


이 장은 예상보다 매우 쉽게 끝나버렸습니다. 저번 장에서 작성했던 조명쉐이더에 살을 붙였기 때문인데요. 이렇게 기본이 되는 쉐이더를 잘 짜놓으면 매우 쉽게 구현할 수 있는 기법들이 많습니다.

하지만 이번 장에서 배운 내용을 절대 가볍게 보시지 말기 바랍니다. 최근 게임에서 3D 물체를 렌더링 할 때 여기서 배웠던 기법을 법선매핑(법선매핑(normal mapping)은 제7장에서 배웁니다)과 혼합하는 것이 거의 표준입니다.


다음편 보기

2012년 1월 16일 월요일

[포프의 쉐이더 입문강좌] 05. 물체에 색을 입히는 디퓨즈/스페큘러 매핑 Part 1

이전편 보기


샘플파일 받기


제5장 물체에 색을 입히는 디퓨즈/스페큘러 매핑

실 세계에서 물체들이 다른 색을 갖는 이유는 물체마다 흡수/반사하는 빛의 스펙트럼(spectrum)이 다르기 때문입니다. 예를 들면 검정색 표면은 모든 스펙트럼을 흡수해서 검정색이고, 하얀색 표면은 모든 스펙트럼을 반사해서 하얀색입니다. 빨간색 표면 같은 경우는 빨강색의 스펙트럼을 반사하고 그 외의 스펙트럼을 흡수하니 빨간색으로 보이는 것이죠.

그렇다면 표면이 빛을 흡수하는 성질을 쉐이더에서 어떻게 표현할까요? 표면 전체가 한가지 색으로 칠해져 있다면 그냥 간단히 전역변수를 사용하면 되겠죠? 하지만 대부분의 경우, 물체의 표면은 다소 복잡한 패턴을 가지고 있습니다. 따라서 각 픽셀마다 색상을 정해줘야겠지요. 그럼 표면에서 반사할 색을 이미지로 그린 뒤, 픽셀쉐이더에서 이 텍스처를 읽어와 조명계산의 결과에 곱하면 되겠죠?

그런데 '제4장: 기초적인 조명쉐이더'에서 빛을 계산할 때, 난 반사광(diffuse light)와 정 반사광(specular light)을 따로 구했었죠? 그렇다면 난 반사광과 정 반사광을 합친 결과에 이 텍스처를 곱해야 할까요? 아니면 따로 해야 할까요? 전에도 말씀드렸듯이 인간이 물체를 지각할 수 있는 이유는 대부분 난 반사광 덕분입니다. (정 반사광은 타이트한 하이라이트를 추가해줄 뿐이지요.) 따라서 위 텍스처를 난 반사광의 결과에만 적용하는 것으로 충분합니다. 이렇게 난 반사광에 적용하는 텍스처를 디퓨즈맵(diffuse map)이라 부릅니다.

그렇다면 정 반사광은 어떻게 할까요? 물론 디퓨즈맵을 정 반사광에 그대로 사용할 수도 있습니다만 다음과 같은 두 가지 이유 때문에 정 반사광용으로 스페큘러맵(specular map)을 따로 만드는 경우가 허다합니다. 첫째, 난 반사광이 반사하는 빛과 정 반사광이 반사하는 빛의 스펙트럼이 다른 경우가 있습니다. 둘째, 각 픽셀이 반사하는 정 반사광의 정도를 조절하는 용도로 스페큘러맵을 사용할 수도 있습니다. 예를 들어, 사람의 얼굴에 정 반사광을 때린다고 생각해 보죠. '제4장: 기초적인 조명쉐이더'에서 봤던 것처럼 얼굴의 전체에서 고르게 정 반사광이 보일까요? 거울을 보시면 알겠지만 이마나 코가 더 반짝거리죠? 그리고 이마나 코에서도 정 반사광이 좀 듬성듬성 보일 겁니다. (이건 모공이나 털 때문에 피부가 매끄럽지 않기 때문입니다.) 스페큘러맵을 잘 칠하면 이런 효과를 낼 수 있습니다. 따라서 이 장에서는 디퓨즈맵과 스페큘러맵을 따로 사용하도록 하겠습니다.

이 외에도 물체의 색에 영향을 미치는 다른 요소가 있습니다. 바로 조명의 색입니다. 흰색 물체에 빨간색 빛을 비추면 물체가 불그스름하게 되죠? 조명의 색은 전역변수로 쉽게 지정할 수 있습니다.

여태까지 말씀 드린 것을 보기 쉽게 설명하면 다음과 같습니다.

난 반사광 = 빛의 색상 X 난 반사광의 양 X 디퓨즈맵의 값정 반사광 = 빛의 색상 X 난 반사광의 양 X 스페큘러맵의 값

그러면 위 내용들을 염두에 두고 디퓨즈/스페큘러매핑 쉐이더를 작성해 볼까요?

기초설정
일단 저번 장에서 사용했던 렌더몽키 프로젝트의 사본을 만들어 새로운 폴더에 저장합니다. 혹시 렌더몽키 프로젝트를 저장해 놓지 않으신 분들은 부록 CD에서 samples\04_lighting\lighting.rfx 파일을 복사해 오시면 됩니다.

이 파일을 렌더몽키에서 연 다음, 쉐이더의 이름을 SpecularMapping으로 바꾸겠습니다. 이름까지 바꾸셨다면 이제 디퓨즈맵과 스페큘러맵으로 사용할 이미지들을 추가해야겠군요. 쉐이더 이름에 마우스 오른쪽 버튼을 누르면 나오는 팝업메뉴에서 Add Texture > Add 2D Texture > Fieldstone.tga파일을 선택합니다. 그러면 Fieldstone이라는 이름의 텍스처가 보이시죠? 이 이름을 DiffuseMap으로 바꾸도록 합시다.

이제 Pass 0에 마우스 오른쪽 버튼을 눌러 Add Texture Object > DiffuseMap을 선택합니다. Texture0이라는 텍스처 개체가 생겼을 것입니다. 이 이름을 DiffuseSampler로 변경합니다.

이제 스페큘러맵을 추가해야 하는데 아무리 렌더몽키 폴더를 뒤져봐도 마땅한 놈이 안 보이는군요. 그래서 제 손으로 직접 스페큘러맵을 만들어서 부록 CD에 넣어놨습니다. 일단 이 스페큘러맵이 어떻게 생겼는지 한번 볼까요? 디퓨즈맵과 같이 비교해서 보면 좋겠군요

그림 5.1. 디퓨즈맵(왼쪽)과 스페큘러맵(오른쪽)

스페큘러맵에서 돌판 사이의 틈새를 검정색으로 칠해 놓은 거 보이시죠? 따라서 이 틈새는 전혀 정 반사광을 반사하지 않을 겁니다. (하지만 난 반사광은 여전히 존재하지요.) 여기서 한가지 기억하실 점은 텍스처가 언제나 색상정보를 가지지는 않는다는 것입니다. 스페큘러맵이 그 좋은 예죠. 스페큘러맵에 저장된 정보는 최종이미지의 색상 값이라기 보다는 각 픽셀이 반사하는 정 반사광의 양입니다. 이와 마찬가지로 픽셀 수준에서 제어하고 싶은 변수가 있다면 이렇게 텍스처를 사용하는 것이 보통입니다. 후에 법선매핑을 다룰 때, 이렇게 텍스처를 이용하는 예를 보실 수 있을 것입니다.

팁: 픽셀수준에서 제어하고 싶은 변수가 있다면 텍스처를 이용합니다.

자, 그럼 부록CD에서 Samples\05_DiffuseSpecularMapping\Fieldstone_SM을 찾아서 렌더몽키 프로젝트에 추가합니다. 추가하는 방법은 간단히 파일을 끌어다 이펙트 이름 위에 놓아주면 됩니다. 텍스처 이름은 SpecularMap으로, 텍스처 개체의 이름은 SpecularSampler로 하겠습니다.

다음은 빛의 색을 추가하도록 하지요. 쉐이더 이름에 오른쪽 버튼을 누른 뒤에 Add Variable > Float > Float3를 선택합니다. 변수명을 gLightColor로 하지요. 이제 이 변수를 더블클릭하면 값을 대입할 수 있습니다. 약간 푸르스름한 빛을 사용한다는 의미로 (0.7, 0.7, 1.0)을 대입하겠습니다.

마지막으로 Stream Mapping을 설정할 차례입니다. '제4장: 기초적인 조명쉐이더'와는 달리 여기서는 텍스처를 사용하니 정점데이터에서 UV 좌표를 읽어와야 합니다. Stream Mapping에 더블클릭을 해서 TEXCOORD0을 추가합니다. 당연히 데이터형은 float2입니다.

여기까지 설정을 마치셨다면 렌더몽키 작업공간이 아래와 같을 겁니다.

그림 5.2. 기초설정을 마친 렌더몽키 프로젝트

정점쉐이더
이제 정점쉐이더를 보도록 하지요. 우선 전체 소스코드부터 보여드린 뒤, 새로 추가된 코드만 설명 드리겠습니다.

float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;

float4 gWorldLightPosition;
float4 gWorldCameraPosition;

struct VS_INPUT
{
   float4 mPosition : POSITION;
   float3 mNormal: NORMAL;
   float2 mUV: TEXCOORD0;
};

struct VS_OUTPUT
{
   float4 mPosition : POSITION;
   float2 mUV: TEXCOORD0;
   float3 mDiffuse : TEXCOORD1;
   float3 mViewDir: TEXCOORD2;
   float3 mReflection: TEXCOORD3;
};

VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;

   Output.mPosition = mul( Input.mPosition, gWorldMatrix );

   float3 lightDir = Output.mPosition.xyz - gWorldLightPosition.xyz;
   lightDir = normalize(lightDir);
 
   float3 viewDir = normalize(Output.mPosition.xyz - gWorldCameraPosition.xyz);
   Output.mViewDir = viewDir;
 
   Output.mPosition = mul( Output.mPosition, gViewMatrix );
   Output.mPosition = mul( Output.mPosition, gProjectionMatrix );
 
   float3 worldNormal = mul( Input.mNormal, (float3x3)gWorldMatrix );
   worldNormal = normalize(worldNormal);

   Output.mDiffuse = dot(-lightDir, worldNormal);
   Output.mReflection = reflect(lightDir, worldNormal);

   Output.mUV = Input.mUV;
 
   return Output;
}



새로 추가해야 할 전역변수가 있던가요? 새로 추가된 변수는 빛의 색상하고 2개의 텍스처 샘플러인데 텍스처 샘플러야 당연히 픽셀쉐이더에서 사용하는 거니까 여기선 선언하지 않아도 되겠네요. 빛의 색상도 픽셀쉐이더에서 그냥 곱하면 되겠는걸요?

그렇다면 정점쉐이더 입출력 구조체는 어떨까요? 새로 추가해야 할 게 하나 생겼죠? 픽셀쉐이더에서 텍스처매핑을 하려면 UV 좌표가 필요하니까요. 정점버퍼에서 UV 좌표를 가져와서 픽셀쉐이더에 전달해 줘야겠네요. 다음의 코드를 정점쉐이더의 입력구조체와 출력구조체 양쪽에 모두 추가합시다.

    float2 mUV: TEXCOORD0;

정점쉐이더 함수에 추가해야 할 코드도 딱 한 줄 뿐입니다. UV좌표를 전달해주는 것이죠.

   Output.mUV = Input.mUV;

정말 간단했죠? 이게 전부랍니다.



2012년 1월 11일 수요일

쉐이더 강좌와 책 출판 업데이트

얼마 전에 쉐이더 강좌를 끝까지 블로그에 연재하겠다고 말씀드렸습니다. 물론 이 결정과 함께 출판사에도 말씀을 드렸지요. 연재를 멈춰야 한다면 출판을 하지 않겠다고... 이 때 제 생각은 '그냥 출판은 안되겠구나...' 였습니다. 한편으로는 블로그 연재도 다 하고 소장용으로 책이 출판되면 좋겠다고 생각을 했으나(설문에 참여해주신 분들 중에 소장용 책이 있었으면 좋겠다라고 해주신 분들도 꽤 되었구요)  아무래도 출판사쪽도 영리를 생각해야하는지라 실현 불가능한 이야기라 생각을 했었지요.

그런데 출판사에서 매우 그럴듯한 새로운 절충안을 들고 나왔습니다. 이 절충안에 따르면 제 블로그에 연재도 끝까지 할 수 있고, 종이책도 나옵니다. 절충안의 대충 자세한 사항은 다음과 같습니다.
  • 제 블로그에 연재는 끝까지 합니다.
  • 단, 출판 전에 연재할 수 있는 분량은 절반입니다. (제6장 까지)
  • 나머지 분량은 책 출판 뒤, 한 4개월 후부터 다시 시작합니다. (제7장부터 제12장... 부록도 할 지도...)
  • 책에 실리는 내용은 블로그 내용과 다릅니다. 출판사에서 출판에 적합하게, 그리고 소장용 가치가 있게 편집을 좀 할거라고 합니다.
  • 블로그에는 제가 예전에 써놓은 내용만 올리며, 출판사에서 수정한 내용은 못올립니다.
이 모든 걸 고려해 봤을 때, 괜찮은 조건 같습니다. 쉐이더 강좌의 연재를 끝내는 게 몇 개월 늦어지는 단점은 있지만, (그동안 게임개발포에버에나 올리죠 뭐 -_-) 그래도 책을 소장할 수 있다는게 독자님들에게 더 나은 조건이라 생각해서 출판사에 그리 하겠다고 전했고, 1월 9일에 원고도 넘겼습니다. 한 4~5월까지는 책이 나올거라고 생각합니다.

출판사는 한빛출판사입니다. (이렇게 제대로 마인드 박힌 출판사 흔치 않습니다. 책 많이 팔아주세요 ^_^)

혹시라도 제 결정이 맘에 안드시는 분들은 맘껏.... 악플 달아주세요 -_-;

2012년 1월 9일 월요일

[포프의 쉐이더 입문강좌] 04. 기초적인 조명쉐이더 Part 3

이전편 보기

정 반사광

배경
정 반사광(specular light)(이것을 반사광이라 부르기도 합니다.)은 난 반사광과는 달리 한 방향으로만 반사되는 빛으로 입사각이 출사각과 같은 것이 특징입니다. 따라서 정 반사광의 효과를 보려면 빛이 반사되는 방향에서 물체를 바라봐야만 합니다. 모니터에 빛이 반사 되서 화면을 보기 힘들었던 기억 있으시죠? 그 때 모니터를 조금 돌리면 조금 살만했던 거도요? 그게 바로 정 반사광입니다.

앞에서 보여 드렸던 난 반사광을 그림에 정 반사광도 추가해 보지요.

그림 4.8 난 반사광과 정 반사광

난 반사광과 마찬가지로 정 반사광을 수학적으로 재현해내는 수학공식이 여럿 있습니다. 여기서는 게임업계에서 널리 사용하는 기법인 퐁(phong) 모델을 사용하겠습니다. 퐁 모델은 반사광과 카메라벡터(카메라에서 현재 위치까지 선을 그은 벡터)가 이루는 각도의 코사인 값을 구하고, 그 결과를 여러번 거듭제곱하면 정 반사광을 구할 수 있다고 합니다. 아래의 그림을 보시죠.

그림 4.9. 정반사광의 예

반사광(R)과 카메라벡터(V)가 이루는 각도의 코사인 값을 구하는 것은 난 반사광에서 했던 것과 별반 차이가 없겠군요. 법선벡터와 입사광 벡터 대신에 반사광 벡터와 카메라벡터를 쓰는 것만 빼면요. 근데 왜 이 결과에 다시 거듭제곱을 할까요? 역시 코사인 그래프를 보면 답이 보입니다.

그림 4.10. 거듭제곱수가 늘어남에 따라 빠르게 줄어드는 코사인 그래프

위 그래프에서 보면 거듭제곱수가 늘어남에 따라 코사인 값이 빠르게 줄어드는 거 보이시죠? 실생활에서 정 반사광을 관찰해봅시다. 정반사광의 폭이 얼마나 되나요? 난 반사광에 비해 상당히 타이트하지 않나요? 바로 이런 타이트한 정 반사광을 재현하기 위해 코사인 값에 거듭제곱을 하는 겁니다.

그러면 거듭제곱은 몇 번이나 해야 할까요? 이건 사실 표면의 재질에 따라 다릅니다. 거친 표면일수록 정 반사광이 덜 타이트할 테니까 거듭제곱 수를 줄여줘야겠죠. 보통 한 20번 정도 거듭제곱을 해주면 대충 괜찮은 결과를 얻으실 수 있습니다.

그럼 이제 쉐이더를 작성해 봅시다.

기초설정
바로 조금 전에 작성했었던 난 반사광 쉐이더에 정 반사광 조명 코드를 추가하도록 하죠. 어차피 이 두 광이 합쳐져야 제대로 된 조명효과니까요.

그림 4.9에서 새로 추가된 것이 뭐가 있었죠? 반사광 벡터하고 카메라 벡터죠? 반사광 벡터야 입사광 벡터를 법선에 대해 반사시킨 것이니(입사각과 출사각이 같습니다) 이미 가지고 있는 정보에서 구할 수 있겠네요. 카메라 벡터는요? 입사광의 벡터를 구했던 것과 마찬가지 방법으로 카메라 위치에서 현재 위치까지 선을 쭈욱~ 그으면 되겠죠? 그러려면 카메라 위치를 전역변수로 만들어야 겠네요. 렌더몽키의 Lighting 쉐이더 위에 마우스 오른쪽 버턴을 눌러 새로운 float4 변수를 추가합시다. 이름은 gWorldCameraPosition이 적당하겠네요. 이제 이 변수 위에 마우스 오른쪽 버튼을 눌러 ViewPosition이라는 변수 시맨틱을 대입합니다.

이 외에 별다른 설정은 없는 것 같군요. 이제 정점쉐이더를 살펴봅시다.

정점쉐이더
마찬가지로 정점쉐이더의 전체 소스코드부터 보여드리겠습니다.

float4x4 gWorldMatrix;
float4x4 gViewMatrix;
float4x4 gProjectionMatrix;


float4 gWorldLightPosition;
float4 gWorldCameraPosition;


struct VS_INPUT
{
   float4 mPosition : POSITION;
   float3 mNormal: NORMAL;
};


struct VS_OUTPUT
{
   float4 mPosition : POSITION;
   float3 mDiffuse : TEXCOORD1;
   float3 mViewDir: TEXCOORD2;
   float3 mReflection: TEXCOORD3;
};


VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;


   Output.mPosition = mul( Input.mPosition, gWorldMatrix );


   float3 lightDir = Output.mPosition.xyz - gWorldLightPosition.xyz;
   lightDir = normalize(lightDir);

   float3 viewDir = normalize(Output.mPosition.xyz - gWorldCameraPosition.xyz);
   Output.mViewDir = viewDir;

   Output.mPosition = mul( Output.mPosition, gViewMatrix );
   Output.mPosition = mul( Output.mPosition, gProjectionMatrix );

   float3 worldNormal = mul( Input.mNormal, (float3x3)gWorldMatrix );
   worldNormal = normalize(worldNormal);


   Output.mDiffuse = dot(-lightDir, worldNormal);
   Output.mReflection = reflect(lightDir, worldNormal);


   return Output;
}


정점쉐이더 입력데이터 및 전역변수
일단 정점쉐이더 입력데이터를 보죠. 새로 필요한 정점정보가 있나요? 아무리 생각해도 별 다른 게 안 떠오르는 거 보니 없는 거 같네요. 난 반사광에 사용했던 입력구조체를 그냥 사용해도 될 거 같습니다.

그렇다면 전역변수는 어떻죠? 방금 전에 추가했던 gWorldCameraPosition을 선언해야겠죠? 다음의 코드를 추가합니다.

float4 gWorldCameraPosition;

정점쉐이더 출력데이터
이제 정점쉐이더 출력데이터를 살펴보도록 하죠. 난 반사광에서 그랬던 것처럼 정점쉐이더에서 정 반사광을 계산한 뒤에 픽셀쉐이더에 전달해 주면 될까요? 불행히도 그렇진 않습니다. 정 반사광을 구하려면 코사인 값에 거듭제곱을 해야 하는데 거듭제곱을 한 뒤 보간(interpolate)을 한 결과와 보간을 한 뒤에 거듭제곱을 한 결과의 차이는 엄청납니다. 따라서 정 반사광 계산은 픽셀 쉐이더에서 해야 하니 이 계산에 필요한 두 방향벡터인 R과  V를 구한 뒤에 픽셀쉐이더에 전달해 주도록 하겠습니다. 다음의 코드를 VS_OUTPUT에 추가합시다.

   float3 mViewDir: TEXCOORD2;
   float3 mReflection: TEXCOORD3;

정점쉐이더 함수
이제 정 반사광을 계산하는데 필요한 두 방향벡터를 구해보죠. 카메라 벡터는 어떻게 구한다고 했었죠? 그냥 카메라 위치로부터 현재위치까지 선을 그으면 된다고 했죠? 입사광의 방향벡터를 구하는 것과 별 다를 바가 없겠네요. 입사광의 방향벡터를 구하는 코드 바로 아래에 다음의 코드를 추가합니다.

   float3 viewDir = normalize(Output.mPosition.xyz - gWorldCameraPosition.xyz);
   Output.mViewDir = viewDir;

이제 정 반사광의 방향벡터를 구할 차례입니다. 이 때, 빛의 입사각과 출사각이 같다고 말씀드렸었죠? 그럼 반사벡터를 구하는 수학 공식이 필요하겠군요. 근데 이런 공식은 굳이 기억하지 않으셔도 됩니다. (저도 수학책 다시 열어봐야 압니다. -_-) 여태까지 그랬던 것처럼 당연히 이런 것을 척척 처리해주는 HLSL 함수가 있겠죠? reflect()라는 함수입니다. reflect는 첫 번째 인자로 입사광의 방향벡터를 두 번째 인자로 반사 면의 법선을 받습니다. Output을 반환하기 바로 전에 다음의 코드를 입력합니다.

   Output.mReflection = reflect(lightDir, worldNormal);

자, 이제 두 벡터를 다 구해봤으니 정점쉐이더에서 할 일은 끝났습니다.

픽셀쉐이더
마찬가지로 픽셀쉐이더의 전체 코드부터 보여드립니다.

struct PS_INPUT
{
   float3 mDiffuse : TEXCOORD1;
   float3 mViewDir: TEXCOORD2;
   float3 mReflection: TEXCOORD3;
};


float4 ps_main(PS_INPUT Input) : COLOR
{
   float3 diffuse = saturate(Input.mDiffuse);

   float3 reflection = normalize(Input.mReflection);
   float3 viewDir = normalize(Input.mViewDir);
   float3 specular = 0;
   if ( diffuse.x > 0 )
   {
      specular = saturate(dot(reflection, -viewDir ));
      specular = pow(specular, 20.0f);
   }


   float3 ambient = float3(0.1f, 0.1f, 0.1f);

   return float4(ambient + diffuse + specular, 1);
}


우선 정점쉐이더 출력데이터에서 가져올 두 벡터를 PS_INPUT 구조체에 추가합니다.

   float3 mViewDir: TEXCOORD2;
   float3 mReflection: TEXCOORD3;

이전에 diffuse를 구했던 코드 바로 밑에 새로운 코드들을 추가하겠습니다. 우선 mReflection과 mViewDir을 다시 한번 정규화시켜 줍니다. 정점쉐이더에서 이미 단위벡터로 만들었던 이 벡터들을 다시 정규화해 주는 이유는 보간기를 거치는 동안 그 값이 흐트러질 수 있기 때문입니다. (보간기가 선형적(linear)으로 보간을 해서 그렇습니다.)

   float3 reflection = normalize(Input.mReflection);
   float3 viewDir = normalize(Input.mViewDir);

이제 이 두 벡터의 내적을 구한 뒤, 거듭제곱을 합니다.

   float3 specular = 0;
   if ( diffuse.x > 0 )
   {
      specular = saturate(dot(reflection, -viewDir ));
      specular = pow(specular, 20.0f);
   }

위에서 난반사광의 양이 0% 이상일 때에만 정 반사광을 계산하는 거 보이시죠? 난 반사광이 존재하지 않는 표면에는 이미 빛이 닿지 않으므로 정 반사광이 존재할 수가 없기 때문입니다. 내적을 구할 때 -viewDir을 사용한 것도 보이시죠? 난 반사광을 구할 때와 마찬가지로 두 벡터의 밑동이 만나야 올바른 내적의 결과를 구할 수 있기 때문입니다.

또한 거듭제곱을 할 때 pow() 함수를 이용한 것도 눈 여겨 봐주시기 바랍니다. 여기서는 20번 거듭제곱을 했는데 각 물체마다 이 값을 다르게 하는 것이 보통입니다. (거듭제곱의 수가 높을 수록 정반사광의 범위가 타이트해집니다. 숫자를 바꿔보면서 실험해보세요.)  따라서 이 값을 float형의 전역변수로 선언해주는 게 보다 나은 방법이 되겠습니다. 이 정도는 독자 분들의 몫으로 남겨두도록 하지요.

이제 결과를 반환할 차례입니다. 일단 정 반사광의 효과만을 보기 위해 specular만을 반환해볼까요? 이전에 있던 return문을 다음과 같이 바꿉니다.

  return float4(specular, 1);

이제 쉐이더를 컴파일한 뒤 실행해보면 다음의 결과를 보실 수 있을 것입니다.

그림 4.11. 난 반사광에 비해 매우 강렬하고 타이트한 하이라이트를 보여주는 정 반사광

이제 정 반사광이 어떤 건지 확실히 보이시죠? 여기에 난 반사광을 더하면 보다 완벽한 조명효과가 되겠네요. return 코드를 다음과 같이 바꿔봅시다.

    return float4(diffuse + specular, 1);

위 코드에서 난 반사광과 정 반사광을 더하면 그 결과가 1이 넘는 경우가 있는데 크게 걱정하지 않으셔도 됩니다. 그런 경우엔 알아서 1이 됩니다. (현재 하드웨어 백버퍼의 포맷이 8비트 이미지이기 때문입니다. 부동소수점 텍스처를 사용하면 1 이상의 값을 저장할 수도 있습니다.)

이제 정점쉐이더와 픽셀쉐이더를 각각 컴파일 하신 뒤 미리 보기 창을 보면 다음과 같은 결과가 보이죠?

그림 4.12. 난 반사광 + 정 반사광

자, 이 정도면 훌륭한 조명효과입니다. 하지만 공의 왼쪽 밑부분이 칠흑같이 어두운 게 좀 망에 안 드는군요. 앞서 말씀 드렸다시피 실제세계에서는 간접광이 저 어두운 부분을 비춰줄 텐데 말이지요. 그럼 아주 간단하게 주변광을 정의해줘서 저 부분을 조금이나마 밝혀볼까요? 주변광을 10%로 선언해서 ambient 변수에 대입해주도록 합시다.

    float3 ambient = float3(0.1f, 0.1f, 0.1f);

그리고 최종 반환 값에 ambient를 추가합니다.

   return float4(ambient + diffuse + specular, 1);


이제 결과가 아래와 같이 바뀔 겁니다.

그림 4.13. 주변광 + 난 반사광 + 정 반사광

선택사항: DirectX 프레임워크
이제 C++로 작성한 DirectX 프레임워크에서 쉐이더를 사용하시고자 하는 분들을 위한 선택적인 절입니다.

우선 '제3장: 텍스처매핑'에서 사용했던 프레임워크의 사본을 만들어 새로운 폴더에 저장합니다. 그 다음, 렌더몽키에서 사용했던 쉐이더와 3D 모델을 DirectX 프레임워크에서 사용할 수 있도록 파일로 저장합니다. Sphere.x와 Lighting.fx라는 파일이름을 사용하도록 하겠습니다.

이제 비주얼 C++에서 솔루션 파일을 엽니다.

자, 그럼 전역변수를 먼저 살펴보겠습니다. 일단 이 장에서는 텍스처를 사용하지 않으니 저번 장에서 선언했던 텍스처 변수, gpEarthDM를 지우겠습니다. 그 다음, 쉐이더 변수의 이름을 gpTextureMappingShader에서 gpLightingShader로 바꿉니다.

이제 새로운 변수들을 선언할 차례입니다. 광원의 위치와 카메라의 위치가 필요했었죠? 이 둘은 모두 월드공간 안에 있었네요. 렌더몽키에서 사용했던 빛의 위치를 다시 사용하겠습니다.

// 빛의 위치
D3DXVECTOR4                gWorldLightPosition(500.0f, 500.0f, -500.0f, 1.0f);

카메라 위치는 예전에 RenderScene() 함수 안에서 사용했던 값을 그대로 가져왔습니다.

// 카메라 위치
D3DXVECTOR4                gWorldCameraPosition( 0.0f, 0.0f, -200.0f, 1.0f );

이제 CleanUp() 함수로 가봅시다. 더 이상 gpEarthDM 텍스처를 사용하지 않으니 이를 해제하는 코드를 지웁니다.

다음은 LoadAssets() 함수 입니다. 우선 gpEarthDM 텍스처를 로딩하는 코드를 삭제합니다. 그리고 쉐이더의 파일명을 Lighting.fx로 바꿉니다. gpTextureMappingShader라는 변수명을gpLightingShader로 바꾸는 것도 잊지 마세요.

    // 텍스처 로딩


    // 쉐이더 로딩
    gpLightingShader = LoadShader("Lighting.fx");
    if ( !gpLightingShader )
    {
        return false;
    }

마지막으로 RenderScene() 함수를 보겠습니다. 일단gpTextureMappingShader 라는 변수명을 모두 찾아gpLightingShader로 바꿉니다. 이제 뷰행렬을 만드는 코드를 보죠. 뷰행렬을 만들 때 사용했던 vEyePt라는 변수가 있었죠? 이 변수의 값이 앞서 정의했던 gWorldCameraPosition의 값과 동일하니 gWolrldCameraPosition의 값을 사용하도록 하지요.

예전에 아래처럼 되어 있던 코드를

    D3DXVECTOR3 vEyePt( 0.0f, 0.0f, -200.0f );

다음과 같이 바꿉니다.

    D3DXVECTOR3 vEyePt( gWorldCameraPosition.x, gWorldCameraPosition.y,
        gWorldCameraPosition.z );

이제gpLightingShader->SetTexture() 코드를 지웁니다. 이 장에서 만든 쉐이더에는 텍스처를 사용하지 않으니 이 코드가 필요 없습니다. 그럼 마지막으로 광원의 위치와 카메라의 위치를 쉐이더에 전달해 줍니다. 이들의 데이터형은 D3DXVECTOR4이므로 쉐이더에서 SetVector()를 호출합니다.

    gpLightingShader->SetVector("gWorldLightPosition", &gWorldLightPosition);
    gpLightingShader->SetVector("gWorldCameraPosition", &gWorldCameraPosition);

이제 코드를 컴파일 한 뒤 실행해보시죠. 아까 렌더몽키에서 보셨던 것과 동일한 결과를 볼 수 있죠?

기타 조명기법
여전히 대부분의 게임이 사용하는 조명기법은 람베르트 + 퐁이지만 최근 들어 다른 조명기법들을 사용하는 게임들이 늘어나고 있습니다. 조명기법을 좀 더 심층적으로 연구하고 싶으신 독자 분들을 위해 몇 가지 기법을 언급하겠습니다.


  • 블린-퐁(Blinn-Phong): 퐁과 거의 비슷한 기법. 현재도 많이 사용함
  • 오렌-네이어(Oren-Nayar): 표면의 거친 정도를 고려한 난 반사광 조명기법
  • 쿡-토런스(Cook-Torrance): 표면의 거친 정도를 고려한 정 반사광 조명기법
  • 구면조화 조명기법(spherical harmonics lighting): 오프라인에서 간접광을 사전 처리한 뒤, 실시간에서 이를 주변광으로 적용할 때 사용할 수 있음


정리
다음은 이 장에서 배운 내용을 짧게 요약해 놓은 것입니다.

  • 람베르트 모델은 난 반사광을 계산하는 기법으로 코사인 함수를 사용한다.
  • 퐁 모델은 정 반사광을 계산하는 기법으로 코사인 값을 거듭제곱 한다.
  • 벡터의 길이를 1로 바꾸면 내적을 구하는 것만으로도 코사인 함수를 대신할 수 있다.
  • 동일한 계산을 어느 쪽에서도 할 수 있다면 픽셀쉐이더 보다는 정점쉐이더에서 한다.
  • 이 장에서 배운 조명보다 훨씬 사실적이고 복잡한 기법들이 존재한다. 그 중 일부는 이미 몇몇 게임에서 쓰이고 있다.


이제 조명기법까지 마쳤으니 쉐이더의 기초는 다 배운 거나 다름없습니다. 다음 장부터는 여태까지 배웠던 지식들을 잘 혼합하여 보다 실용적인 기법들을 구현해 보겠습니다. 제1~4장 중에 잘 이해가 안 되는 내용이 있었다면 다시 한 번 복습을 하신 뒤에 제5장으로 넘어오시기 바랍니다.