요즘 핫한 Node.js 환경설정을 해봅시다.


뭐, 시작하기 앞서 또 잡설이지만,,,

솔직히 이녀석이 웹시장에서 뜰줄은 알고는 있었지만 이정도 일줄은 예상치 못했다.

내가 Node.js 를 처음 접했을 때가 2012년도 였는데 그 때엔 v0 버전대의 알파버전이 나오던 시기였다.

이 녀석을 처음 접했을 때 가장 편했던(?) 점은 역시 자바스크립트를 사용한다는 점이었다.

PHP나 JSP 처럼 새로운 문법을 배울 필요가 없다는 점은 분명 정말 엄청난 메리트였다.

또한 C/C++로 짜여진 네이티브 프로그램을 모듈로 실행시킬수 있다는 점 또한 다른 서버사이드 언어들과의 큰 차별점이지 않나 싶다. 

뭐 처음에는 이정도 감상으로 정말 꽤 괜찮은 물건이 나왔네 하며 감탄을 하였었다.

하지만 요즘은... 쏟아지는 패키지에 모듈에,,, 어디서부터 손대야 할지를 모르겠다.

분명 개발은 쉬워졌지만, 익혀야 할게 너무 늘었다는게 문제다.

게다가 한번 이러한 모듈들이 익히고 나서 계속 써먹을 수 있는게 아닌게 문제다.

나중에 써먹으려고 다시 보면 뭔가 메이저한 업데이트가 있어서 많이 달라져 있다거나, 더 좋은 모듈이 나오거나 해서 완전히 새로 익혀야하는 경우가 쉽상이기 때문이다...

그렇지 않은 모듈 생태계가 어디있겠냐만은 Node.js 쪽은 특히나 심한 것 같다...


위에서 너무 단점만 말한 것 같지만 뭐, 그래도 아무튼 좋은 녀석이고 웹 생태계 변화에 엄청난 공을 한 놈은 틀림없는 사실이다.




1. 왜 NVM인가

node.js를 설치하는 가장 간단한 방법은 역시 apt 패키지 관리자를 사용하는 것이다.


root@glassylife:~# apt-get install nodejs npm

캬~ 넘나 간단한 것.

근데 이방법에는 약간의 문제가 있다.

사실 짚으려는 문제의 원인은 npm을 통해 설치할 모듈에 있는거라

여기서 문제라고 언급하기엔 조금 지나친 감이 있지만...

어떤 것이냐면 다음과 같다.


'고오오올져스한 웹서버'를 만들기 위해 필요한 어떤 노드 모듈 A 가 있다.

근데 얘가 오래전에 만들어진거라 v4 이하의 Node 에서만 동작한다.

참고로 Node.js 최신 버전은 LTS 기준 v6.9.4 이다.

당연히 난 더 이상 업데이트도 안하는 이 망할 모듈을 버리고 더욱 세련된 다른놈을 사용하고 싶다.

그렇지만 내가 원하는 기능을 제공하는 모듈은 저것 하나뿐이다...

어떡하겠는가.

선택지는 셋 중 하나다.

1. 새로 만들거나

2. 모듈을 사용할 수 있는 버전대로 node.js를 다운그레이드하거나

3. 때려치거나...


1번 선택지도 충분히 고려해볼만 하지만 이 포스팅의 목적은 NVM을 사용하는 것이기 때문에 나는 2번 선택지를 고른다.

그래서 node.js 를 v4.0 으로 다운그레이드했다.

좋다 여기까진.


근데 이번엔 '그레에에에잇트한 웹서버'를 만들고 싶다.

여기에 필요한건 단지 최신버전의 node.js 이다.

기껏 다운그레이드 시켜놧더니 최신버전이라니...

어디의 금수저씨는 리소스 제한도 있겠다 서버 한 대 새로 장만하시면 되겠지만 흙수저는 그럴 수 없다.(억지스럽지만 그냥 그렇다 하자)

'고오오올져스한 웹서버'가 돌고있는 동일한 머신에서 '그레에에에잇트한 웹서버' 소스를 작성하고 실행시키려는데 문제가 발생한다.

node.js 패키지는 글로벌로 깔려있어서. 최신버전으로 다시 업그레이드하면 '고오오올져스한 웹서버'는 못돌린다..


역시 서버 한 대를 새로 장만하고 싶은 마음이 굴뚝 같지만 앞서 말했듯이 난 흙수저라 어떻게든 한 대의 자원으로 해결을 봐야 한다

이 때 쓸 수 있는게 바로 NVM(Node Version Manager) 이다.

NVM 통해 각각 다른 버전의 node 로 서버를 돌리면 된다.

사실 예로 든게 억지스러울 순 있다.

NVM의 컨셉이해를 위한 예시니 그러려니하자.

그렇다고 아주 허무맹랑한 예시는 아니다

최근에 겪은건 아니지만 2년전인가 5년전에 확실히 나도 위와 같은 상황에 당면한 경험이 있다.


나는 그 때 NVM을 이용해본 이후로 혹시 모를 상황에 대비해 그냥 습관적으로 설치하게 되었다.




2. NVM 설치

뭐 암튼 각설하고 NVM을 설치해보자.


curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | bash


현재 18 / 01 / 2017 (GMT) 를 기준으로 0.33 버전이 최신인가 보다.

또는 다음 링크에서 최신버전을 확인 및 다운 받을 수 있다.


https://github.com/creationix/nvm#install-script



윗 명령어를 사용하면 알아서 쉘스크립트 실행하여 NVM를 설치 해준다.

완료가 되었다면 putty를 다시 껏다가 키거나 모종의 방법으로 세션을 다시 열자.

또는 그렇게 하지않고 source 명령어로 NVM을 바로 실행시킬 수 있다.


root@glassylife:~# source ~/.bashrc

nvm --version 명령어로 잘 설치되었나 확인!


root@glassylife:~# nvm --version
안 되는 분이 계신다면... 리포트 부탁드린다...
나는 한번에 되서 문제점을 인지할 수가 없당.
여기까지 node version을 컨트롤해주는 NVM설치를 마쳤다.



3. node & npm 설치

nvm --help 명령어로 자세한 사용법을 알아볼 수 있다.

하지만 원하는건 최신버전 및 특정버전의 node를 설치 및 사용하는 것이므로 자세한 사용법은 스스로 찾아 공부하도록 하자.


최신버전 및 특정버전 node.js 를 설치하는 방법은 각각 다음과 같다.

npm은 해당 노드버전이 배포될 때 같이 배포되었던 버전으로(아마도?) 같이 설치되므로 신경쓸 필요가 없다.


root@glassylife:~# nvm install node
root@glassylife:~# nvm install v0.12.18

nvm install node 하면 현재 최신버전의 node를 받게 되고, LTS 버전이 필요하다면 --lts 옵션을 붙여준다.



그리고 nvm으로 설치된 노드 버전들을 다음 명령어로 확인할 수 있다.


root@glassylife:~# nvm ls


다음 명령어로 디폴트 node 버전을 선택할 수 있다.


root@glassylife:~# nvm use v0.12.18

또는 특정 버전으로 node를 실행 시킨다.

root@glassylife:~# nvm run 0.12.18 app.js



이렇게 설치했던 각각의 노드버전들은 ~/.nvm/versions/node 폴더에 잘 정리되어있다. 심심하면 한번 ~/.nvm의 구조를 들여다봐도 좋을 듯 싶다.







Posted by nfyfamraa
,

서버쪽 개발을 하는 사람들을 보면 다들 자기만의 툴사용 방식이 있는데,

나같은 경우 putty로 직접 서버에 붙어서 하는 편이다.


들어가기 앞서 내가 보았던 서버 개발 방식을 몇가지 이야기하려하는데 잡설이므로 스크롤을 내려도 좋다.

조금내려서 1. Vundle 다운로드 항목부터 보시길.


가장 기본적인 방법으로는 로컬컴에서 코딩을 한 후 그 수정본을 ftp로 업로드해서

확인해보는 것이다.

이 방법은 '서브x임' 같이 자신이 평소 사용하던 편집기를 사용할 수 있다는 점을 장점으로 꼽을 수 있다.

그런데 이건 매번 수정할 때마다 업로드를 위해 '파일x라'같은 ftp 툴을 이용해야 한다는 점이 매우 번거롭다.


이보다 좀 더 진화한 방법은, 로컬컴에서도 같은 서버를 만들어 놓고, 로컬에서 테스트를 거치면서 개발을 하다가 메이저한 업데이트가 있을 때에만 서버에 소스를 업로드하여 동기화하는 것이다. 

이 방법은 처음에 언급했던 방법에서 장점은 그대로 보존하고 단점은 보완한 방식이라고 할 수 있다. 

하지만 이러한 방식은 윈도우-리눅스 간의 크로스 플랫폼을 지원하지 않는 서버 프로그램엔 적용할 수 없다는 단점이 있다.

뭐 단점이라고 해봤자 대부분 지원하니까 크게 느껴지진 않을테지만 말이다.

대신 크로스 플랫폼을 지원하는 프로그램이더라도 플랫폼마다 설정방식의 차이가 있어 버그가 발생할 수 있는 경우를 고려해야 한다.

그리고 이 방식은 서버에 직접 붙어서 작업을 하는 것이 아니므로 작업하다 뭔가를 날리거나 하는 등의 실수를 하더라도 안전하다는 장점이 있다.

이런 점에선 내가 사용하는 방법보다 더 좋다고 볼 수 있다.


마지막으로 내가 사용하는 방법은 앞에서 말했듯이 그냥 바로 서버에 붙어 작업하는 것이다.

오로지 콘솔만으로... 문자로만 이루어진 스크린을 앞에 두고 과거 선인들이 느꼈던 고뇌(?)를 몸소 체험해 가면서 말이다.

이러한 방식의 장점은, 개발할 수 있는 장소가 물리적으로 한정되어 있지 않다는 점이다.

위에서 서술했던 두 방법은 평소 자신이 작업하던 환경이 아닌 새로운 곳에서 작업을 하려면 개발환경 세팅을 처음부터 다시해야 한다.

하지만 서버에 직접 붙는다면 처음 한번만 하고나선 나중에 터미널로 붙기만 하면되니, 직장에서든 집에서든 PC방(?)에서든 작업을 이어서 할 수 있다. 

PC방같이 공개장소에서 접속하는 경우 PK관리같은 각별한 보안 관리가 필요하겠지만...

뭐 암튼 그런 장점이 있다.

단점이라면... 그 외 모든것?

ㅋㅋㅋ 그래도 일단 익혀두면 그 성취감과 편리함이 단점들을 모두 커버하는 것 같다.



1. Vundle 다운로드

vi 플러그인 관리자 툴인 Vundle 을 다운로드하자.

먼저 git이 필요하다. git을 설치하자


root@glassylife:~# apt-get update
root@glassylife:~# apt-cache search git | grep '^git '
root@glassylife:~# apt-get install git

일단 나는 root 로 접속했기 때문에 그냥 했는데 일반계정으로 접속했다면


NormalAccount@glassylife:~# sudo apt-get update

와 같이 실행하려는 명령어 앞에 sudo를 붙이면 된다.

만에 하나 sudo를 사용해도 권한에러가 발생한다면 구글에 "sudoers"라는 키워드를 이용해 검색해보라.


일반 유저래도 흙수저 은수저가 있다. sudo를 사용할 수 있는 은수저가 되기 위해선 sudoers 라는 그룹에 속해 있어야 한다.

update는 로컬과 원격의 패키지 저장소의 데이터를 싱크시켜주는데 패키지를 설치할 때 습관적으로 한번씩 해주자.

패키지 저장소라고는 했는데, 말에 조금 어폐가 있지만 딱히 적당한 표현이 생각나질 않아서 일단 그냥 두고 나중에 수정해야겠다.

여기서 말하는 패키지 저장소는 실제로 패키지의 바이너리 데이터를 보관하는 게 아니라 패키지의 바이너리 데이터가 어디에 있는지, 버전은 몇인지와 같은 메타데이터를 관리하는 저장소이다.


apt-cache search 는 로컬 패키지 저장소에 찾으려는 패키지가 있는지 검색한다. 

없으면 패키지가 있는 원격저장소를 직접 찾아 등록시켜줘야한다.

대부분 잘 될것이기 때문에 이 명령어는 사용하지 않아도 되지만, 혹시라도 다음과정에서 git 패키지 설치가 안 되는 분은 로컬 저장소에서 git을 검색해 있는지 확인해 보고 없다면 갓-구글에게 "restore ubuntu default repo"나 이와 비스무리한 키워드로 검색해보길 바란다.

다음으로 apt-get install git 명령어로 git을 설치한다.


이제 git으로 Vundle 소스를 clone 한다. 


root@glassylife:~# git clone https://github.com/VundleVim/Vundle.vim.git ~/.vim/bundle/Vundle.vim




2. Vundle 설정 및 플러그인 설치

별거 없다. Vundle 깃헙에 있는 Quick Start 항목을 그대로 따라한다.

Vundle 소스를 받은 후 ~/.vimrc 파일을 수정해준다. 없으면 만들어 준다.


root@glassylife:~# vi ~/.vimrc


일종의 vim 스크립트 파일인데 다음과 같은 코드를 넣어줘야 vi를 사용할 때 Vundle이  작동한다.


set nocompatible " be iMproved, required filetype off " required " set the runtime path to include Vundle and initialize set rtp+=~/.vim/bundle/Vundle.vim call vundle#begin() " alternatively, pass a path where Vundle should install plugins "call vundle#begin('~/some/path/here') " let Vundle manage Vundle, required Plugin 'VundleVim/Vundle.vim' " The following are examples of different formats supported. " Keep Plugin commands between vundle#begin/end. " plugin on GitHub repo Plugin 'tpope/vim-fugitive' " plugin from http://vim-scripts.org/vim/scripts.html Plugin 'L9' " Git plugin not hosted on GitHub Plugin 'git://git.wincent.com/command-t.git'

" The sparkup vim script is in a subdirectory of this repo called vim. " Pass the path to set the runtimepath properly. Plugin 'rstacruz/sparkup', {'rtp': 'vim/'} " All of your Plugins must be added before the following line call vundle#end() " required filetype plugin indent on " required " To ignore plugin indent changes, instead use: "filetype plugin on " " Brief help " :PluginList - lists configured plugins " :PluginInstall - installs plugins; append `!` to update or just :PluginUpdate " :PluginSearch foo - searches for foo; append `!` to refresh local cache " :PluginClean - confirms removal of unused plugins; append `!` to auto-approve removal " " see :h vundle for more details or wiki for FAQ " Put your non-Plugin stuff after this line


나중에 플러그인을 추가할 때 여기에서 플러그인을 눈치껏 추가해주면 된다. 

소스 중간쯤에 보이는 Plugin 'plugin_name' 과 같은 형식으로 추가해주면 된다.

그럼 플러그인 추가 방법도 알았겠다 NERDTree를 추가해보자


참고로 NERDTree 플러그인은 pwd를 기준으로 디렉토리 구조를 비주얼라이징 해주거나 파일을 열 수 있도록 도와준다.

코딩을 하다보면 여기저기에 나눠진 소스들을 확인하고 수정하게 되는데,

이 때 소스경로를 직접 입력하지 않고도 파일에 접근 할 수 있게 도와준다.

써보면 안다 얼마나 편한지를...


다음과 같이 rstacruz/sparkup 다음줄에 밑의 소스 2줄을 추가해준다.

참고로 쌍따옴표(")는 주석이다.


...
Plugin 'rstacruz/sparkup', {'rtp': 'vim/'}
" NERDTree Plugin
Plugin 'scrooloose/nerdtree'


코드를 넣었으면 저장을 하고, 닫는다.

그리고 다음 명령어로 플러그인 설치를 한다.


root@glassylife:~# vim +PluginInstall +qall


또는 vim의 명령어 모드에서 :PluginInstall 로도 설치할 수 있다.


이제 vim에 들어가서 명령어 모드에서 :NERDTreeToggle 를 입력해 보자

다음과 같이 왼쪽에 뭔가 불쑥 튀어 나온다. 오오...

나는 현재 빈 폴더에서 vi 를 실행시켜서 특이할만한게 없다.



이미지나 직접 확인해 볼 수 있듯이 사용설명을 보려면 '?'을 입력하면 된다.

그 외에 기본적인 사용 방법은 


Ctrl+h,j,k,l 로 윈도우를 옮겨다니거나

:q 로 윈도우를 닫을 수 있다.


3. Vi 설정

매번 명령어를 입력해 NERDTree를 불러오기 귀찮으니까 단축키로 등록해 놓자.

F7키에 등록 시켜놓는게 일반적이다.


다시 ~/.vimrc 파일을 열고 맨 끝에 다음 소스를 추가한다.


...
" map NERT Tree to key 'F7'
nmap <F7> :NERDTreeToggle<CR>


추가한 김에 간단한 vi 설정도 넣자...

마찬가지로 맨 끝에 넣는다


...
" vim settings
set number           " show line number
set ts=2             " tab size is 2 spaces
set autoindent       " use auto indent
set cindent          " use C style indent
set smartindent      " use smart indent
set shiftwidth=2     " shifting size is 2 spaces
set paste            " turn off autoindent when you paste code
set hlsearch         " hilight searching results
set noexpandtab      " turn off auto replacing tab with spaces

뭐... 그럭저럭 쓸만한데 이쁘지가 않다.

color scheme을 지정해보자


다시 .vimrc로 돌아가서.. 플러그인을 추가해준다.

NERDTree 플러그인 추가해줬던 줄 밑에 다음 소스를 추가해 준다


...
Plugin 'scrooloose/nerdtree'
" solarized color scheme
Plugin 'altercation/vim-colors-solarized'


그리고 맨 끝에 다음과 같이 설정을 추가해 준다...


...
" setting soarized color scheme
set t_Co=256
set background=dark
highlight Normal ctermbg=NONE
highlight nonText ctermbg=NONE
let g:solarized_termcolors=256
hi Comment term=bold cterm=bold ctermfg=2
colorscheme solarized


다시 다음 명령어로 플러그인을 설치해주고...


root@glassylife:~# vim +PluginInstall +qall


vi을 열면...! 



알록달록한게 매우 맘에 든다ㅋㅋㅋ 매우 흡족.




4. Plugin 검색

이제 능동적으로 필요한 플러그인을 찾고 설치해보자

플러그인 검색은 vi의 명령어 모드에서 :PluginSearch 를 입력한다.



무슨 플러그인 4000개 씩이나...


그럼 왼쪽에 윈도우가 하나 생기는데 vi에서 일반적으로 단어 찾는 단축키인 / 를 이용해 적당한 검색 키워드를 입력한다.



taglist 라고 검색하니 'taglist-plus'라는 플러그인을 찾을 수 있었다.

이제 이걸 그대로 NERDTree 추가했듯이 ~/.vimrc 파일에 넣어준다.

그리고 :PluginInstall 해주는거 까먹지 말고!!





Posted by nfyfamraa
,

이번엔 셰이더를 적용해 봅시다.

뭔가 초장부터 중간보스를 만난 느낌이네요.



들어가기 앞서 기본 개념인 버텍스와 픽셀에 대해서 짚어봅시다.

3D 그래픽을 조금이라도 접해본 사람들은 다 알만한 개념이긴 한데 원문에서 이 개념을 설명하기 위해 한 강의를 투자하였기 때문에 간단히 짚고 갑니다.


버텍스: 3차원 그래픽 공간에서 한 점을 의미하는 가장 기본적인 개념입니다. 카르테시안 좌표계에서 x, y, z 요소로 나타낼 수 있죠.

픽셀: 물리적으로 화면에서 한 점을 나타내는 최소 단위입니다. 하나의 픽셀은 RGBA를 조합한 한가지 색만을 표현할 수 있습니다.



쉐이더

쉐이더도 일종의 프로그램입니다. 일반적인 C/C++ 네이티브 프로그램과 다른점은 쉐이더는 CPU가 아니라 GPU에서 처리되는 프로그램이라는 점이죠(그치만 쿠다를 사용한다면?!). 쉐이더는 여러가지가 있는데 책 "OpenGL Super Bible 6th" 에서 발췌한 렌더링 파이프라인 다이어그램에서 그 중 몇가지를 볼 수 있습니다.




이미지에서 볼 수 있듯이 렌더링 파이프라인에서 가장 처음과 끝에 버텍스 쉐이더와 프레그먼트 쉐이더가 위치합니다. 3D를 렌더링하는데 빠져서는 안 될 엄청 중요한 놈들이죠. 이것들 외에 추가로 사용할 수 있는 쉐이더가 몇 개 더 있는데요 테설레이션 쉐이더와 지오메트리 쉐이더입니다. 렌더링 파이프라인에서 얘네들이 없이도 충분히 3D를 화면을 렌더링 할 수 있지만, 사용한다면 더욱 정교하게 픽셀을 관리하여 멋진 효과를 줄 수 있죠. 이러한 쉐이더들은 프로그래밍으로 직접 컨트롤 할 수 있는데 반해, 버텍스 패치(Vertex fetch), 레스터라이제이션(Rasterization), 프레임버퍼 오퍼레이션(Buffer operations)은 직접 컨트롤 할 수 없습니다. 


예제 코드를 작성해보기 전에 쉐이더간의 기본동작에 대해 알아봅시다.

파이프라인에서 한 쉐이더에서 다음 쉐이더로 넘어갈 때 변수를 이용해서 데이터를 전달할 수 있습니다. 이 때 변수들은 다음의 특정 규칙을 따라야 합니다.


1. 변수의 이름과 타입은 앞 쉐이더의 것과 뒷 쉐이더의 것이 일치해야 합니다(뭐 당연한거 같네요).

2. 변수는 파이프라인상에서 바로 다음에 위치한 쉐이더에게만 전달할 수 있습니다. 예를 들어 버텍스 쉐이더, 테설레이션, 프레그먼트 쉐이더를 사용한다고 할 때 버텍스 쉐이더에서 프레그먼트 쉐이더로 데이터를 전달하고 싶다면, 먼저 버텍스 쉐이더에서 테설레이션 쉐이더로 변수를 전달한 다음 다시 테설레이션에서 프레그먼트 쉐이더로 변수를 전달해야 합니다.


또한 CPU에서 보내지는 버텍스 데이터들은 버퍼를 통해 전달되는데, 버텍스 쉐이더에서 밖에 접근할 수 없습니다. 반면 쉐이더간 데이터전달에 사용되는 유니폼 변수들은 어느 쉐이더에서든 접근이 가능합니다(위의 2번 조건과 함께 생각해보면 각 쉐이더가 실행될 때 유니폼 변수들이 같은 메모리 공간을 사용하는거 같아 보이네요. 같은 메모리 주소를 사용하니까 변수 생존 기간이 한 쉐이더에서 다음 쉐이더까지인가 봅니다).


후 드디어 코딩입니다. 쉐이딩 언어로는 GLSL을 사용합니다. OpenGL Shading Language 의 약자에요ㅋ


예제프로그램에서는 쉐이더 소스를 파일로 작성하고, 메인 루틴에서 실행중에 동적으로 이 쉐이더 소스를 읽어와 컴파일하고 렌더링 파이프 라인에 적용합니다. 먼저 이러한 동작을 하는 C++ 모듈을 만들어 보죠.


프로젝트에 Core라는 폴더를 만들고 그 안에 Shader_Loader.h 와 Shader_Loader.cpp 파일을 작성합니다.


// Shader_Loader.h
#pragma once

#include <GL/glew.h>
#include <GL/freeglut.h>
#include <iostream>

namespace Core
{

    class Shader_Loader
    {
    private:

        std::string ReadShader(char *filename);
        GLuint CreateShader(GLenum shaderType,
            std::string source,
            char* shaderName);

    public:

        Shader_Loader(void);
        ~Shader_Loader(void);
        GLuint CreateProgram(char* VertexShaderFilename,
            char* FragmentShaderFilename);

    };
}


// Shader_Loader.cpp
#include "Shader_Loader.h" 
#include <iostream>
#include <fstream>
#include <vector>

using namespace Core;

Shader_Loader::Shader_Loader(void){}
Shader_Loader::~Shader_Loader(void){}

std::string Shader_Loader::ReadShader(char *filename)
{

    std::string shaderCode;
    std::ifstream file(filename, std::ios::in);

    if(!file.good())
    {
        std::cout<<"Can't read file "<<filename<<std::endl;
        std::terminate();
    }

    file.seekg(0, std::ios::end);
    shaderCode.resize((unsigned int)file.tellg());
    file.seekg(0, std::ios::beg);
    file.read(&shaderCode[0], shaderCode.size());
    file.close();
    return shaderCode;
}

GLuint Shader_Loader::CreateShader(GLenum shaderType, 
                                   std::string source, char* shaderName)
{

    int compile_result = 0;

    GLuint shader = glCreateShader(shaderType);
    const char *shader_code_ptr = source.c_str();
    const int shader_code_size = source.size();

    glShaderSource(shader, 1, &shader_code_ptr, &shader_code_size);
    glCompileShader(shader);
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compile_result);

    //check for errors
    if (compile_result == GL_FALSE)
    {

        int info_log_length = 0;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &info_log_length);
        std::vector<char> shader_log(info_log_length);
        glGetShaderInfoLog(shader, info_log_length, NULL, &shader_log[0]);
        std::cout << "ERROR compiling shader: " << shaderName << std::endl << &shader_log[0] << std::endl;
        return 0;
    }
    return shader;
}

GLuint Shader_Loader::CreateProgram(char* vertexShaderFilename,
                                    char* fragmentShaderFilename)
{

    //read the shader files and save the code
    std::string vertex_shader_code = ReadShader(vertexShaderFilename);
    std::string fragment_shader_code = ReadShader(fragmentShaderFilename);

    GLuint vertex_shader = CreateShader(GL_VERTEX_SHADER, vertex_shader_code, "vertex shader");
    GLuint fragment_shader = CreateShader(GL_FRAGMENT_SHADER, fragment_shader_code, "fragment shader");

    int link_result = 0;
    //create the program handle, attatch the shaders and link it
    GLuint program = glCreateProgram();
    glAttachShader(program, vertex_shader);
    glAttachShader(program, fragment_shader);

    glLinkProgram(program);
    glGetProgramiv(program, GL_LINK_STATUS, &link_result);
    //check for link errors
    if (link_result == GL_FALSE)
    {

        int info_log_length = 0;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &info_log_length);
        std::vector<char> program_log(info_log_length);
        glGetProgramInfoLog(program, info_log_length, NULL, &program_log[0]);
        std::cout << "Shader Loader : LINK ERROR" << std::endl << &program_log[0] << std::endl;
        return 0;
    }
    return program;
}


gl 라이브러리 함수 및 Shader_Loader 클래스의 설명입니다.


GLuint: 

쉐이더와 프로그램 핸들들을 갖습니다. GLuint 변수는 기본적으로 이러한 엔티티(쉐이더와 프로그램 핸들들)를 갖는 빈 객체로 존재합니다. OpenGL 에서는 '프로그램' 이라는 용어를 좀 헷갈리게 사용하는데요, OpenGL에서 정의하는 '프로그램'이라는 용어는 쉐이더들(vertex, fragment, tessellation, geometry)을 담을 수 있는 컨테이너를 의미합니다.


glCreateShader(GLenum shader_type):

인자로 전달받는 shader_type 으로 비어있는 쉐이더 객체를 생성하고 핸들을 반환합니다.


glShaderSource(GLuint shader, GLsizei count, 

                     const GLchar **shader_code, const GLint *legth):

shader 객체에 shader_code 를 로드합니다. *GLchar 은 캐릭터형 배열이며 이 배열의 갯수를 count 로 알려줍니다. length 는 각 *GLchar 의 길이 정보를 갖는 배열입니다. 예제에선 소스코드를 한개씩 전달하였으므로 count 를 1로 전달하였죠.


glCompileShader(GLuint shader):

소스코드를 컴파일합니다.


glGetShaderiv(GLuint shader, GLenum pname, GLint *prarams):

에러를 확인하고 콘솔로 검출해냅니다


glCreateProgram():

프로그램 객체를 생성하고 그 핸들을 반환합니다.


glAttachShader(GLuint program, GLuint shader):

프로그램에 쉐이더를 붙입니다.


glLinkProgram(GLuint program):

프로그램 객체를 링킹합니다.


glGetProgramiv(GLuint program, GLenum pname, GLint *params):

glGetShaderiv과 마찬가지로 에러를 확인하고 콘솔로 검출해냅니다.


glUseProgram(GLuint program):

렌더링 루프에서 program 객체를 이용하여 렌더링 하로독 지정합니다.


Shader_Loader: 

생성자 함수...


~Shader_Loader: 

소멸자 함수...


CreateShader: 

쉐이더를 생성하고 컴파일.


ReadShader: 

쉐이더 파일의 소스를 읽는 메서드


CreateProgram: 

내부적으로 ReadShader, CreateShader 메서드를 호출 하여 버텍스와 프레그먼트 쉐이더를 불러오고 프로그램에 담은 후 링킹하는 메서드



버텍스 쉐이더와 프레그먼트 쉐이더

쉐이더에서 가장 중요한 것은 각 쉐이더들은 버텍스를 다루는데 자신만의 고유의 역할을 갖는다는 것입니다. 버텍스 쉐이더는 x,y,z 3차원 좌표로 이루어진 버텍스들이 2차원 화면에서는 어떤 위치에 그려질지 정사영하는 일을, 프래그먼트 쉐이더는 각 픽셀들이 어떤 색상을 갖는지 정하는 일을 하는 식이죠.


버텍스 쉐이더에서 여러분이 꼭 기억해야할 변수는 gl_Position 입니다. 이 변수는 미리정의 되어 있으며 현재 버텍스의 작업이 다 끝난 후의 스크린상의 점의 위치를 가리킵니다.


프레그먼트 쉐이더의 가장 큰 역할은 각각의 프레그먼트들의 색상을 결정하는 것입니다. 이 쉐이더에서 반환값은 색상버퍼뿐이며, 이 색상버퍼에 표현되지 않은 오브젝트들은 전부 검정색으로 표현됩니다.


다음으로 버텍스 쉐이더의 코드를 작성해 보죠. Core 폴더와 같이 Shaders 폴더도 하나 만들고 Vertex_Sahder.glsl 과 Fragment_Shader.glsl 파일을 작성합니다.


// Vertex_Shader.glsl
#version 330 core

void main(void)
{
    const vec4 vertices[3] = vec4[3](vec4( 0.25, -0.25, 0.5, 1.0),
                                     vec4(-0.25, -0.25, 0.5, 1.0),
                                     vec4( 0.25, 0.25, 0.5, 1.0));
    gl_Position = vertices[gl_VertexID];
}


// Fragment_Shader.glsl
#version 330 core
out vec4 color;

void main(void)
{
    color = vec4(0.0, 1.0, 0.0, 1.0);
}


모든 쉐이더는 먼저 OpenGL 의 버전을 명시해주어야 합니다. 전 3.3버전이므로 330 이라고 적었습니다. 원글에서는 430 이라고 적었네요. 버전 다음에 나오는 core라는 단어는 해당 버전의 glsl 핵심 함수들을 사용할 것임을 나타냅니다. 


다음으로 main 함수로 넘어가봅시다. 3D 그래픽에서 삼각형을 그리기 위해서는 꼭짓점을 나타내는 세 개의 버텍스가 필요합니다. OpenGL의 버텍스 쉐이더에서는 이 버텍스들을 스크린상의 어디에 나타낼 것인지 계산을 한 후 gl_Position 변수에 담아 다음 쉐이더로 보내죠.



위 이미지처럼 스크린은  x, y 값의 범위가 모두 -1~1을 갖는 좌표계로 표현 됩니다. (0, 0)은 화면의 정중앙을 나타내죠. 그래서 200x400 해상도의 스크린을 예로 들면 소스코드의 삼각형은 스크린상에서 꼭지점 (125, 150), (75, 150), (125, 150) 으로 표현됩니다. 소스코드에서 사용된 이 좌표표현 방법을 NDC(Nomalized Device Coordinates) 라고 합니다.


실제로 NDC는 x, y, z, w 네가지 요소로 표현이 됩니다. 네 번째 원소인 w는 W를 표현하는데 위 소스의 경우 W가 1.0으로 NDC가 버텍스의 좌표를 다루고 있다는 것을 나타냅니다(W가 뭘 의미하는지는 잘 모르겠네요). NDC 변환이 끝난 후 Window Transformation 또는 Screen Transformation 변환을 합니다. 이 두 변환작업은 장면(scene)을 OpenGL의 viewport(윈도우창 안쪽의 OpenGL 로 그려질 부분)에 맞추기 위해 하는데, 그래픽카드에서 진행되므로 신경쓸 필요 없습니다. 여기까지 모든 변환을 마친 최종 좌표값들은 모든 모양들이 픽셀/프레그먼트로 전환되기 위해 raterization 과정을 거칩니다. 그러면 위 삼각형은 다음 이미지와 같은 형태를 갖게되겠죠.



glVertexID는 현재 처리되고 있는 버텍스의 ID를 나타냅니다. 

프래그먼트 쉐이더에서는 color 변수에 결과값을 내어야합니다. 


마지막으로 main 파일을 조금 손 보도록 하죠.


// main.cpp
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <fstream>
#include <vector>

#include "Core/Shader_Loader.h"

using namespace Core;

GLuint program;

void renderScene(void)
{
    glClearColor(1.0, 0.0, 0.0, 1.0);//clear red
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    //use the created program
    glUseProgram(program);

    //draw 3 vertices as triangles
    glDrawArrays(GL_TRIANGLES, 0, 3);

    glutSwapBuffers();
}

void Init()
{

    glEnable(GL_DEPTH_TEST);

    //load and compile shaders
    Core::Shader_Loader shaderLoader;
    program = shaderLoader.CreateProgram("Shaders\\Vertex_Shader.glsl",
        "Shaders\\Fragment_Shader.glsl");
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}

int main(int argc, char **argv)
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowPosition(100, 100);
    glutInitWindowSize(800, 600);
    glutCreateWindow("Drawing my first triangle");
    glewInit();

    Init();

    // register callbacks
    glutDisplayFunc(renderScene);
    glutMainLoop();
    glDeleteProgram(program);
    return 0;

}


수정한 main 파일에서는 CreateProgram 메서드를 호출하여 쉐이더 소스를 불러와 컴파일 하였습니다. 그리고 나서 glUseProgram 를 호출하여 렌더링 루프에서 사용할 프로그램을 지정하였고 glDrawArrays 를 호출하여 얼마나 많을 버텍스들을 사용하고 어떻게 그려낼 것인지 지정하였습니다. 마지막으로 새로 등장한 glDrawArrays 에 대해 알아보죠.


glDrawArrays(GLenum mode, GLint first, GLsizei count):

mode 는 드로잉 모드 또는 points, lines, triangles 등의 모형을 가리킵니다. first 는 버텍스 배열내에서, 그리기 시작할 버텍스의 인덱스를 나타내며 count 는 그릴 버텍스의 갯수를 나타냅니다.




후 여기까지 오다니... 힘드네요 힘들어 ㅋㅋ

다음은 이번 강의의 원문 링크와 현재까지의 프로젝트 폴더 구조입니다.


* 원문 링크

http://in2gpu.com/2014/10/20/building-blocks-vertex-pixel/

http://in2gpu.com/2014/10/29/shaders-basics/

http://in2gpu.com/2014/11/24/creating-a-triangle-in-opengl-shader/


* 프로젝트 폴더 구조




그럼 다음에 봅시다~~


아, 위 코드를 실행 했을 때 에러가 없는데도 제대로 동작하지 않는 경우가 있을 수 있습니다. 튜토리얼 작성자 말로는 쉐이더 코드가 일부 하드웨어와 맞지 않아 생기는 현상 같다고 하네요. 이번 강의는 쉐이더의 기본개념을 공부하기 위한 강의이고 실제 필드에선 저렇게 버텍스 쉐이더 안에서 버텍스를 정의하지 않으니 제대로 동작하지 않더라도 짜증내지말고 그냥 다음 강의로 넘어갑시다.ㅎㅎㅎ

Posted by nfyfamraa
,